目录
前言
基本理解与应用举例
业务应用
业务失败重试机制
基本模版代码
具体业务处理
使用及测试展示
使用框架实现业务失败重试机制
业务前置检查流程模版
基本代码依赖
模版代码准备
业务应用前置检查
业务实现
测试及结果展示
Thrift远程调用处理模版
基本常规业务处理模版
分页处理模版
滚动处理模版
业务使用模版定义
业务模拟服务
具体模拟业务
测试及展示
前言
模版模式应该是工作中最常用的设计模式之一,直白的讲就是如果的一些处理方式是有一定的模版流程处理的,那么在应用中使用该模式在合适不过了。对于其基本的业务应用,我简单写了以下三个基本的通用模版(业务失败重试机制、业务前置检查流程模版、Thrift远程调用处理模版)来展示,有问题的可以留言纠正,谢谢!
基本理解与应用举例
GoF《设计模式》一书中,定义如下:
? ? ? Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
基本结合业务使用可以翻译为:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。算法骨架,其实就是指的我们定义的业务处理流程模版。
应用举例平时在看源码的时候基本都可以看到,比方说以下的几个举例:
- spring 中 Hibernate 比如开启事务、获取 Session、关闭 Session 等规范化流程的设计和实现;
- HttpServlet的service()模板方法,定义了整个HTTP请求的执行流程,doGet()、doPost()是模板中可以由子类来定制的部分;
- Java IO类库中,比如InputStream、OutputStream、Reader、Writer的设计都使用了模板模式
业务应用
实际中只有业务上有很固定的流程基本都可以使用模版模式进行定义和解决问题,这边的就我自身来看,具体的业务的一定不方便拿出来举例,所以暂时想了以下三个业务场景较为公用的拿出来与大家分享,有需要纠正的请大家留言纠正。
业务失败重试机制
我们在实际业务处理中经常会遇到调用远程方法如果因为超时或一些其他原因失败的话,最好在进行重试调用一次,而重试简单的来看主要的模版流程基本有按业务实际调用失败后规定休眠一段时间后再次重试一次,如果超过重试次数则返回报错,如果中间重试成功则按成功的处理,所以我们可以简单的写一个业务失败重试机制的模版
基本模版代码
package org.zyf.javabasic.designpatterns.strategy.template.retry;
import lombok.extern.log4j.Log4j2;
/**
* @author yanfengzhang
* @description 模板方式处理重试逻辑
* @date 2022/3/15 00:13
*/
@Log4j2
public abstract class RetryTemplate<R> {
/**
* 默认重试次数
*/
private static final int DEFAULT_RETRY_TIME = 1;
/**
* 定义重试次数
*/
private int retryTime = DEFAULT_RETRY_TIME;
/**
* 重试的睡眠时间
*/
private int sleepMills = 0;
/**
* 业务处理的睡眠时间
*
* @return 睡眠时间
*/
public int getSleepMills() {
return sleepMills;
}
/**
* 获取重试次数
*
* @return 重试次数
*/
public int getRetryTime() {
return retryTime;
}
/**
* 规定业务处理的睡眠时间
*
* @param sleepMills 睡眠时间(毫秒)
* @return 重试模版
*/
public RetryTemplate<R> setSleepMills(int sleepMills) {
if (sleepMills < 0) {
throw new IllegalArgumentException("sleepMills should equal or bigger than 0");
}
this.sleepMills = sleepMills;
return this;
}
/**
* 规定业务处理的重试次数
*
* @param retryTime 业务处理的重试次数
* @return 重试模版
*/
public RetryTemplate<R> setRetryTime(int retryTime) {
if (retryTime <= 0) {
throw new IllegalArgumentException("retryTime should bigger than 0");
}
this.retryTime = retryTime;
return this;
}
/**
* 重试的业务执行代码
* 失败时请抛出一个异常
*/
protected abstract Object doBiz() throws Exception;
/**
* 重试的主要业务逻辑
*
* @return 实际业务处理返回内容
* @throws InterruptedException 中断异常
*/
public R execute() throws InterruptedException {
for (int i = 0; i < retryTime; i++) {
try {
return (R) doBiz();
} catch (Exception e) {
log.warn("业务执行出现异常,e: ", e);
Thread.sleep(sleepMills);
}
}
return null;
}
}
具体业务处理
package org.zyf.javabasic.designpatterns.strategy.template.retry;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Random;
/**
* @author yanfengzhang
* @description
* @date 2022/3/15 00:20
*/
@Service
@Slf4j
public class RetryBizService {
private static Map<Integer, String> aphorisms = Maps.newHashMap();
static {
aphorisms.put(1, "知人者智,自知者明。胜人者有力,自胜者强。——老子");
aphorisms.put(2, "要知道对好事的称颂过于夸大,也会招来人们的反感轻蔑和嫉妒。——培根");
aphorisms.put(5, "业精于勤,荒于嬉;行成于思,毁于随。——韩愈");
aphorisms.put(7, "最大的骄傲于最大的自卑都表示心灵的最软弱无力。——斯宾诺莎");
aphorisms.put(9, "知之者不如好之者,好之者不如乐之者。——孔子");
}
/**
* 获取名言警句
*
* @return 名言警句
*/
public String getAphorisms() throws Exception {
int randomNumber = new Random().nextInt(10);
String rersult = aphorisms.get(randomNumber);
if (StringUtils.isBlank(rersult)) {
throw new Exception("抱歉!系统异常,暂无数据可进行返回!randomNumber=" + randomNumber);
}
return rersult;
}
}
使用及测试展示
测试代码
package org.zyf.javabasic.designpatterns.strategy.template.retry;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.zyf.javabasic.ZYFApplication;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @author yanfengzhang
* @description
* @date 2022/3/2022/3/15 00:23
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ZYFApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
public class RetryTemplateTest {
@Autowired
private RetryBizService retryBizService;
@Test
public void testRetryBizService() {
String aphorisms = null;
try {
aphorisms = (String) new RetryTemplate() {
@Override
protected Object doBiz() throws Exception {
return retryBizService.getAphorisms();
}
}.setRetryTime(3).setSleepMills(200).execute();
} catch (Exception e) {
log.warn("[RetryBizService名言警句展示] 系统系统进行重试调用失败!");
}
if (StringUtils.isEmpty(aphorisms)) {
log.error("[RetryBizService名言警句展示异常,请稍后重试]");
return;
}
log.info("测试成功,得到的名言警句为:{}", aphorisms);
}
}
测试结果展示
使用框架实现业务失败重试机制
但是实际中,以上的实现还是较为简单,比如像高并发等场景基本没有进行考虑,所以实际应用中我建议使用guava的重试模版进行处理比较好
引入pom
<!-- 引入对应的guava重试机制相关功能 -->
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
使用及测试展示
package org.zyf.javabasic.designpatterns.strategy.template.retry;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.zyf.javabasic.ZYFApplication;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @author yanfengzhang
* @description
* @date 2022/3/2022/3/15 00:23
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ZYFApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
public class RetryTemplateTest {
@Autowired
private RetryBizService retryBizService;
@Test
public void testGuavaRetrying() {
Callable<String> callable = () -> {
/*业务逻辑*/
return retryBizService.getAphorisms();
};
/*定义重试器*/
Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
/*如果结果为空则重试*/
.retryIfResult(Objects::isNull)
/*发生IO异常则重试*/
.retryIfExceptionOfType(Exception.class)
/*发生运行时异常则重试*/
.retryIfRuntimeException()
/*等待*/
.withWaitStrategy(WaitStrategies.incrementingWait(100, TimeUnit.MILLISECONDS,
100, TimeUnit.MILLISECONDS))
/*允许执行4次(首次执行 + 最多重试3次)*/
.withStopStrategy(StopStrategies.stopAfterAttempt(4))
.build();
try {
/*执行*/
String aphorisms = retryer.call(callable);
if (StringUtils.isEmpty(aphorisms)) {
log.error("[RetryBizService名言警句展示异常,请稍后重试]");
return;
}
log.info("测试成功,得到的名言警句为:{}", aphorisms);
} catch (RetryException | ExecutionException e) {
/*重试次数超过阈值或被强制中断*/
log.warn("[RetryBizService名言警句展示] 系统系统进行重试调用失败!");
}
}
}
业务前置检查流程模版
平时我们对于业务的基本入参可能会有一些指定的业务检查流程需要检查,比方依次有基础参数检查、业务逻辑检查、冲突关联检查、验证码校验、二次弹窗校验等,于是将其整合一下即可有业务前置检查流程模版。
基本代码依赖
package org.zyf.javabasic.designpatterns.strategy.template.check;
import lombok.Builder;
import lombok.Data;
/**
* @author yanfengzhang
* @description 校验基本返回结构
* @date 2022/3/16 23:29
*/
@Data
@Builder
public class CheckResponse {
/**
* 成功标志:true-校验通过,false-校验未通过
*/
private boolean pass;
/**
* 校验码
*/
private Integer code;
/**
* 未通过原因
*/
private String errorMsg;
}
模版代码准备
package org.zyf.javabasic.designpatterns.strategy.template.check;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author yanfengzhang
* @description 业务数据校验模版
* @date 2022/3/16 23:35
*/
@Slf4j
@Service
public abstract class BizCheckTemplate<P> {
/**
* 校验全流程模版
*
* @param param 基本入参
* @return 校验结果
* @throws Exception 业务异常
*/
public CheckResponse checkProcess(P param) {
/*1.基础参数校验*/
CheckResponse checkResponse = checkParam(param);
if (!checkResponse.isPass()) {
return checkResponse;
}
/*2.业务逻辑校验*/
checkResponse = checkBiz(param);
if (!checkResponse.isPass()) {
return checkResponse;
}
/*3.冲突关联校验*/
checkResponse = checkConflict(param);
if (!checkResponse.isPass()) {
return checkResponse;
}
/*4.验证码校验*/
checkResponse = checkVerifyCode(param);
if (!checkResponse.isPass()) {
return checkResponse;
}
/*5.二次弹窗校验*/
checkResponse = checkTwicePopup(param);
if (!checkResponse.isPass()) {
return checkResponse;
}
return checkResponse;
}
protected abstract CheckResponse checkParam(P param);
protected abstract CheckResponse checkBiz(P param);
protected abstract CheckResponse checkTwicePopup(P param);
protected abstract CheckResponse checkVerifyCode(P param);
protected abstract CheckResponse checkConflict(P param);
}
业务应用前置检查
假如有一个申诉的业务系统,其需要进行以上前置的检查处理,我们定义其基本检查的实现类如下:
申诉提交检查
package org.zyf.javabasic.designpatterns.strategy.template.check;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author yanfengzhang
* @description 申诉内容创建检查
* @date 2022/3/16 20:24
*/
@Service
@Slf4j
public class AppealCreateCheck extends BizCheckTemplate<AppealMatters> {
@Autowired
private AppealCheckService appealCheckService;
/**
* 申诉内容参数检查
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkParam(AppealMatters param) {
if (!param.getType().equals(1)) {
/*为了用于测试*/
return CheckResponse.builder().pass(false).errorMsg("新增申诉内容类型不符合要求!").build();
}
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉内容业务逻辑检查
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkBiz(AppealMatters param) {
/*业务检查1:申诉的内容编码是唯一的,当前重复申诉*/
/*业务检查2:申诉的内容项不得包含非法指定的相关内容*/
/*业务检查3:申诉人对应的申诉项中和申诉人当前享有的权益不匹配*/
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉二次弹窗检查(不涉及直接返回通过)
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkTwicePopup(AppealMatters param) {
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉验证码校验(不涉及直接返回通过)
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkVerifyCode(AppealMatters param) {
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉关联冲突校验
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkConflict(AppealMatters param) {
return appealCheckService.checkConflict(param);
}
}
申诉修改检查
package org.zyf.javabasic.designpatterns.strategy.template.check;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author yanfengzhang
* @description 申诉内容修改检查
* @date 2022/3/16 23:43
*/
@Service
@Slf4j
public class AppealUpdateCheck extends BizCheckTemplate<AppealMatters> {
@Autowired
private AppealCheckService appealCheckService;
/**
* 申诉内容参数检查
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkParam(AppealMatters param) {
if (!param.getType().equals(2)) {
/*为了用于测试*/
return CheckResponse.builder().pass(false).errorMsg("修改申诉内容类型不符合要求!").build();
}
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉内容业务逻辑检查
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkBiz(AppealMatters param) {
/*业务检查1:申诉的内容编码不能修改*/
/*业务检查2:申诉的内容项已进行处理的不能在修改,新增的申诉项不得包含非法指定的相关内容*/
/*业务检查3:新增的申诉项中,申诉人对应的申诉项中和申诉人当前享有的权益不匹配*/
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉二次弹窗检查
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkTwicePopup(AppealMatters param) {
/*二次弹窗检查检查1:申诉的内容存在修改次数,请确认是否修改*/
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉验证码校验
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkVerifyCode(AppealMatters param) {
/*验证码校验检查1:修改操作需本人进行验证码确认*/
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉关联冲突校验
*
* @param param 请求参数信息
* @return 校验结果
*/
@Override
protected CheckResponse checkConflict(AppealMatters param) {
return appealCheckService.checkConflict(param);
}
}
申诉删除检查
package org.zyf.javabasic.designpatterns.strategy.template.check;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author yanfengzhang
* @description 申诉内容删除检查
* @date 2022/3/16 23:44
*/
@Service
@Slf4j
public class AppealDeleteCheck extends BizCheckTemplate<Long> {
@Autowired
private AppealCheckService appealCheckService;
/**
* 申诉内容参数检查
*
* @return 校验结果
* @id id 请求参数信息
*/
@Override
protected CheckResponse checkParam(Long id) {
if (!checkPosInteger(id)) {
return CheckResponse.builder().pass(false).errorMsg("申诉内容id不合法!").build();
}
/*id必须为有效ID*/
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉内容业务逻辑检查
*
* @return 校验结果
* @id id 请求参数信息
*/
@Override
protected CheckResponse checkBiz(Long id) {
/*业务检查1:申诉的内容必须存在*/
/*业务检查2:申诉的内容项全部处理完成的不能删除*/
/*业务检查3:申诉人当前享有的权益存在需要进行续费的暂时不能删除申诉内容*/
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉二次弹窗检查
*
* @return 校验结果
* @id id 请求参数信息
*/
@Override
protected CheckResponse checkTwicePopup(Long id) {
/*二次弹窗检查检查1:申请确认是否删除*/
/*二次弹窗检查检查2:相关未享受的权益删除后不再享有,申请确认是否删除*/
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉验证码校验
*
* @return 校验结果
* @id id 请求参数信息
*/
@Override
protected CheckResponse checkVerifyCode(Long id) {
/*验证码校验检查1:修改操作需本人进行验证码确认*/
return CheckResponse.builder().pass(true).build();
}
/**
* 申诉关联冲突校验(不涉及直接返回通过)
*
* @return 校验结果
* @id id 请求参数信息
*/
@Override
protected CheckResponse checkConflict(Long id) {
return CheckResponse.builder().pass(true).build();
}
/**
* ID信息合法性:必须为正整数
*
* @param id id信息
* @return true-合法;false-非法
*/
public static boolean checkPosInteger(Long id) {
return null != id && id > 0L;
}
}
公用检查
package org.zyf.javabasic.designpatterns.strategy.template.check;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* @author yanfengzhang
* @description 申诉基本校验检查(公用)
* @date 2022/3/16 23:40
*/
@Service
@Slf4j
public class AppealCheckService {
/**
* 公用的创建和修改必要参数检查
*
* @param param 必要参数信息
* @return 检查结果
*/
public CheckResponse checkParam(AppealMatters param) {
if (Objects.isNull(param)) {
return CheckResponse.builder().pass(false).errorMsg("创建消息不能为空!").build();
}
if (StringUtils.isBlank(param.getCode())) {
return CheckResponse.builder().pass(false).errorMsg("申诉编码不能为空!").build();
}
if (Objects.isNull(param.getType())) {
return CheckResponse.builder().pass(false).errorMsg("申诉类型不能为空且必须为规定的类型!").build();
}
return CheckResponse.builder().pass(true).build();
}
/**
* 公用的创建和修改关联冲突校验
*
* @param param 请求参数信息
* @return 检查结果
*/
public CheckResponse checkConflict(AppealMatters param) {
/*关联冲突检查1:申诉的内容对应的申诉类型在当前系统指定暂停办理中,请进行替换*/
/*关联冲突检查2:申诉的内容项直接存在相同申诉问题,请重新归类提交*/
/*关联冲突检查3:申诉的内容项与实际享有权益存在冲突,请确认避免后续被系统锁定*/
/*关联冲突检查4:申诉人当前存在其他非法内容,则本次提交需解决之前的内容*/
return CheckResponse.builder().pass(true).build();
}
}
业务实现
package org.zyf.javabasic.designpatterns.strategy.template.check;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author yanfengzhang
* @description 申诉相关业务逻辑
* @date 2022/3/16 22:02
*/
@Service
@Slf4j
public class AppealService {
@Autowired
private AppealCheckService appealCheckService;
@Autowired
private AppealCreateCheck appealCreateCheck;
@Autowired
private AppealUpdateCheck appealUpdateCheck;
@Autowired
private AppealDeleteCheck appealDeleteCheck;
/**
* 申诉内容留档
*
* @param appealMatters 申诉内容
*/
public void saveAppealMatters(AppealMatters appealMatters) throws Exception {
/*1.基本必要参数检查*/
CheckResponse checkResponse = appealCheckService.checkParam(appealMatters);
if (!checkResponse.isPass()) {
throw new Exception(checkResponse.getErrorMsg());
}
/*2.实际业务操作*/
if (checkPosInteger(appealMatters.getId())) {
checkResponse = appealCreateCheck.checkProcess(appealMatters);
if (!checkResponse.isPass()) {
throw new Exception(checkResponse.getErrorMsg());
}
/*业务处理,省略*/
}
checkResponse = appealUpdateCheck.checkProcess(appealMatters);
if (!checkResponse.isPass()) {
throw new Exception(checkResponse.getErrorMsg());
}
/*业务处理,省略*/
}
/**
* 申诉内容留档
*
* @param appealMattersId 申诉内容id
*/
public void deleteAppealMatters(Long appealMattersId) throws Exception {
/*1.基本必要参数检查*/
CheckResponse checkResponse = appealDeleteCheck.checkProcess(appealMattersId);
if (!checkResponse.isPass()) {
throw new Exception(checkResponse.getErrorMsg());
}
/*业务处理,省略*/
}
/**
* ID信息合法性:必须为正整数
*
* @param id id信息
* @return true-合法;false-非法
*/
public static boolean checkPosInteger(Long id) {
return null != id && id > 0L;
}
}
测试及结果展示
package org.zyf.javabasic.designpatterns.strategy.template.check;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.zyf.javabasic.ZYFApplication;
import java.util.Arrays;
import java.util.List;
/**
* @author yanfengzhang
* @description
* @date 2022/3/17 00:15
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ZYFApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
public class BizCheckTemplateTest {
@Autowired
private AppealService appealService;
@Test
public void testAppealService() throws Exception {
List<String> appealItems = Arrays.asList("隔壁搬来的邻居真他妈的吵!", "小区的健身房器械就不能在多一些练腿的?");
AppealMatters appealMatters = AppealMatters.builder()
.code("shvhf2736gbbnvhbb")
.declarant("zyf")
.type(2)
.appealItems(appealItems).build();
log.info("测试新建");
appealService.saveAppealMatters(appealMatters);
log.info("测试更新");
appealMatters.setId(7L);
appealMatters.setType(2);
appealService.saveAppealMatters(appealMatters);
log.info("测试删除");
appealService.deleteAppealMatters(7L);
}
}
结果展示
Thrift远程调用处理模版
平时可能在使用调用thrift接口的时候往往会使用分页获取调用或者使用滚动的方式进行处理,我们针对这样的业务场景页也共用的定义一套业务模版去处理,具体如下:
基本常规业务处理模版
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import com.google.common.collect.Lists;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Objects;
/**
* @author yanfengzhang
* @description 业务thrift调用基本类
* * TC : thrift请求
* * TR : thrift返回
* * FR : 业务实际真正要的处理结果
* @date 2022/3/18 23:18
*/
@Data
@Slf4j
public abstract class ThriftInvokeCommand<TC, TR, FR> {
/**
* thrift请求
*/
protected TC command;
/**
* 每个命令最多翻滚多少页
*/
private int maxPageCount = ConfigUtils.getThriftScrollMaxPageLimit();
/**
* 每个命令的最大重试个数
*/
private int maxRetryCount = ConfigUtils.getThriftMaxInvokeRetryCount();
/**
* 每个命令的休眠时间
*/
private int sleepMs = ConfigUtils.getThriftScrollRate();
/**
* 每个命令的查询页数
*/
private int pageSize = ConfigUtils.getThriftScrollPageSize();
public ThriftInvokeCommand(TC command) {
this.command = command;
}
/**
* 核心处理逻辑:获取指定结果
*/
public List<FR> getResult() {
/*执行的页数*/
int currPageNo = 0;
/*command 设置 pageSize*/
setPageSize(command, pageSize);
/*调用接口的失败次数*/
int errorCount = 0;
TR thriftResult = null;
List<FR> rList = Lists.newArrayList();
while (true) {
try {
/*1.前置处理*/
preHandle();
/*2.业务转发调用*/
thriftResult = invoke(command);
/*3.从thriftResult提取结果并转换为实际需要的*/
rList.addAll(convertThriftResult(thriftResult));
/*4.后置处理*/
afterHandle(thriftResult);
} catch (Exception e) {
log.error("", e);
errorCount++;
}
/*thrift调用无结果则直接停止循转*/
if (Objects.isNull(thriftResult) || 0 == getResultSize(thriftResult)) {
break;
}
if (errorCount >= maxRetryCount || currPageNo >= maxPageCount) {
String msg = errorCount >= maxRetryCount ? "重试次数过多,已达最大尝试次数:" + maxRetryCount : "循环次数过多,已达最大尝试次数:" + maxPageCount;
log.warn("ThriftInvokeCommand 处理警告:{}", msg);
break;
}
currPageNo++;
/*5.停顿,降低调用频率*/
sleepInvoke();
}
return rList;
}
/**
* thrift调用前置处理
*/
protected abstract void preHandle();
/**
* thrift调用后置处理
*
* @param thriftResult thrift调用结果
*/
protected abstract void afterHandle(TR thriftResult);
/**
* 核心逻辑, 通过命令获取结果
*
* @param command thrift调用请求
* @return thrift调用结果
*/
protected abstract TR invoke(TC command) throws Exception;
/**
* command 设置 pageSize
*/
protected abstract void setPageSize(TC command, int pageSize);
/**
* 从 thriftResult 获取 结果长度,若是 0 ,那么 break 退出
*/
protected abstract int getResultSize(TR thriftResult);
/**
* 从 thriftResult 提取结果
*/
protected abstract List<FR> convertThriftResult(TR thriftResult);
/**
* 休眠调整处理
*/
private void sleepInvoke() {
if (sleepMs <= 0) {
return;
}
try {
Thread.sleep(sleepMs);
} catch (InterruptedException ignored) {
}
}
public ThriftInvokeCommand setPageSize(int pageSize) {
this.pageSize = pageSize;
return this;
}
public ThriftInvokeCommand setSleepMs(int sleepMs) {
this.sleepMs = sleepMs;
return this;
}
}
分页处理模版
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
/**
* @author yanfengzhang
* @description 用于利用分页形式滚动调用thrift
* @date 2022/3/18 23:25
*/
public abstract class ThriftInvokePageCommand<TC, TR, FR> extends ThriftInvokeCommand<TC, TR, FR> {
private int pageNo = 1;
public ThriftInvokePageCommand(TC command) {
super(command);
}
@Override
public void preHandle() {
/*command 设置 pageNo*/
setPageNo(command, pageNo);
}
@Override
public void afterHandle(TR thriftResult) {
/*页数加 1*/
pageNo++;
}
/**
* command 设置 pageNo
*/
protected abstract void setPageNo(TC command, int pageNo);
}
滚动处理模版
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
/**
* @author yanfengzhang
* @description 用于利用scroll形式滚动调用thrift
* @date 2022/3/18 23:23
*/
public abstract class ThriftInvokeScrollCommand<TC, TR, FR> extends ThriftInvokeCommand<TC, TR, FR> {
private Long scrollId = 0L;
public ThriftInvokeScrollCommand(TC command) {
super(command);
}
@Override
public void preHandle() {
/*command 设置 ScrollId*/
setScrollId(command, scrollId);
}
@Override
public void afterHandle(TR thriftResult) {
/*从 thriftResult 获取 scrollId*/
scrollId = getNextScrollId(thriftResult);
}
/**
* command 设置 ScrollId
*/
protected abstract void setScrollId(TC command, Long scrollId);
/**
* 从 thriftResult 获取 scrollId
*/
protected abstract Long getNextScrollId(TR thriftResult);
}
业务使用模版定义
假设获取电影资源thrift有两套模版基本如下
获取电影thrift接口分页处理
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.assertj.core.util.Lists;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author yanfengzhang
* @description 获取电影thrift接口分页处理
* @date 2022/3/18 23:32
*/
@Slf4j
public class GetMoviesByConditionPage extends ThriftInvokePageCommand<MoviesConditions, MoviesResult, MovieBasicInfo> {
/**
* 实际thrift接口(此处只做模拟)
* 实际该处应该类似为:private MoviesInfoThriftService.Iface moviesInfoThriftService;
*/
private MoviesInfoThriftService moviesInfoThriftService;
/**
* 实际分页调用构造器
*
* @param moviesConditions 请求查询分页请求
* @param moviesInfoThriftService 实际分页调用电影ThriftService
*/
public GetMoviesByConditionPage(MoviesConditions moviesConditions, MoviesInfoThriftService moviesInfoThriftService) {
super(moviesConditions);
this.moviesInfoThriftService = moviesInfoThriftService;
}
/**
* 设置实际页号
*
* @param moviesConditions 请求查询分页请求
* @param pageNo 页号
*/
@Override
protected void setPageNo(MoviesConditions moviesConditions, int pageNo) {
if (Objects.isNull(moviesConditions)) {
return;
}
moviesConditions.setPageNo(pageNo);
}
/**
* 设置实际页大小
*
* @param moviesConditions 请求查询分页请求
* @param pageSize 页大小
*/
@Override
protected void setPageSize(MoviesConditions moviesConditions, int pageSize) {
if (Objects.isNull(moviesConditions)) {
return;
}
moviesConditions.setPageSize(pageSize);
}
/**
* 获取实际数据大小
*
* @param moviesResult 请求查询分页请求
* @return 实际数据大小
*/
@Override
protected int getResultSize(MoviesResult moviesResult) {
if (Objects.isNull(moviesResult)) {
return 0;
}
return moviesResult.getMovieInfos().size();
}
/**
* 实际业务调用thrift接口
*
* @param moviesConditions 请求查询分页请求
* @return 实际业务调用thrift接口返回
* @throws Exception 业务异常
*/
@Override
protected MoviesResult invoke(MoviesConditions moviesConditions) throws Exception {
if (Objects.isNull(moviesConditions)) {
return null;
}
return moviesInfoThriftService.getMoviesByConditionPage(moviesConditions);
}
/**
* 获取实际业务需要的结果(进行实际的转换)
*
* @param moviesResult 实际业务调用thrift接口返回
* @return 实际业务需要的电影基本信息
*/
@Override
protected List<MovieBasicInfo> convertThriftResult(MoviesResult moviesResult) {
if (Objects.isNull(moviesResult) || CollectionUtils.isEmpty(moviesResult.getMovieInfos())) {
return Lists.newArrayList();
}
return moviesResult.getMovieInfos().stream().map(movieInfo -> MovieBasicInfo.builder()
.id(movieInfo.getId())
.name(movieInfo.getName())
.desc(movieInfo.getDesc())
.director(movieInfo.getDirector())
.actor(movieInfo.getActor())
.duration(movieInfo.getDuration())
.year(movieInfo.getYear())
.score(movieInfo.getScore()).build()).collect(Collectors.toList());
}
}
获取电影thrift接口滚动处理
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.assertj.core.util.Lists;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author yanfengzhang
* @description 获取电影thrift接口滚动处理
* @date 2022/3/20 17:37
*/
@Slf4j
public class GetMoviesByConditionScroll extends ThriftInvokeScrollCommand<MoviesConditions, MoviesResult, MovieBasicInfo> {
/**
* 实际thrift接口(此处只做模拟)
* 实际该处应该类似为:private MoviesInfoThriftService.Iface moviesInfoThriftService;
*/
private MoviesInfoThriftService moviesInfoThriftService;
/**
* 实际分页调用构造器
*
* @param moviesConditions 请求查询分页请求
* @param moviesInfoThriftService 实际分页调用电影ThriftService
*/
public GetMoviesByConditionScroll(MoviesConditions moviesConditions, MoviesInfoThriftService moviesInfoThriftService) {
super(moviesConditions);
this.moviesInfoThriftService = moviesInfoThriftService;
}
/**
* @param moviesConditions 请求查询分页请求
* @param scrollId 当前滚动ID
*/
@Override
protected void setScrollId(MoviesConditions moviesConditions, Long scrollId) {
if (Objects.isNull(moviesConditions)) {
return;
}
moviesConditions.setScrollId(scrollId);
}
/**
* @param thriftResult 请求查询分页请求
* @return 下一个滚动ID
*/
@Override
protected Long getNextScrollId(MoviesResult thriftResult) {
if (Objects.isNull(thriftResult)) {
return 0L;
}
return thriftResult.getNextScrollId();
}
/**
* 设置实际页大小
*
* @param moviesConditions 请求查询分页请求
* @param pageSize 页大小
*/
@Override
protected void setPageSize(MoviesConditions moviesConditions, int pageSize) {
if (Objects.isNull(moviesConditions)) {
return;
}
moviesConditions.setPageSize(pageSize);
}
/**
* 获取实际数据大小
*
* @param moviesResult 请求查询分页请求
* @return 实际数据大小
*/
@Override
protected int getResultSize(MoviesResult moviesResult) {
if (Objects.isNull(moviesResult)) {
return 0;
}
return moviesResult.getMovieInfos().size();
}
/**
* 实际业务调用thrift接口
*
* @param moviesConditions 请求查询分页请求
* @return 实际业务调用thrift接口返回
* @throws Exception 业务异常
*/
@Override
protected MoviesResult invoke(MoviesConditions moviesConditions) throws Exception {
if (Objects.isNull(moviesConditions)) {
return null;
}
return moviesInfoThriftService.getMoviesByConditionScroll(moviesConditions);
}
/**
* 获取实际业务需要的结果(进行实际的转换)
*
* @param moviesResult 实际业务调用thrift接口返回
* @return 实际业务需要的电影基本信息
*/
@Override
protected List<MovieBasicInfo> convertThriftResult(MoviesResult moviesResult) {
if (Objects.isNull(moviesResult) || CollectionUtils.isEmpty(moviesResult.getMovieInfos())) {
return Lists.newArrayList();
}
return moviesResult.getMovieInfos().stream().map(movieInfo -> MovieBasicInfo.builder()
.id(movieInfo.getId())
.name(movieInfo.getName())
.desc(movieInfo.getDesc())
.director(movieInfo.getDirector())
.actor(movieInfo.getActor())
.duration(movieInfo.getDuration())
.year(movieInfo.getYear())
.score(movieInfo.getScore()).build()).collect(Collectors.toList());
}
}
业务模拟服务
前置准备数据
查询电影资源条件集合
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import lombok.Builder;
import lombok.Data;
/**
* @author yanfengzhang
* @description 查询电影资源条件集合
* @date 2022/3/18 23:33
*/
@Data
@Builder
public class MoviesConditions {
/**
* 综合排序类型:1-热播榜,2-好评榜,3-新上线,4-爱奇艺自制(热播),5-腾讯自制(热播),6-优酷自制(热播)
*/
private int sortType;
/**
* 播放类型:1-剧情,2-喜剧,3-动作,4-爱情,5-惊悚,6-犯罪,7-悬疑,8-战争,9-科幻,10-动画,11-恐怖
* 12-家庭,13-传记,14-冒险,15-奇幻,16-武侠,17-历史,18-运动,19-歌舞,20-音乐,21-记录
* 22-伦理,23-西部
*/
private int playType;
/**
* 电影产地归类:1-内地,2-中国香港,3-美国,4-欧洲,5-日本,6-韩国,7-英国,8-法国,9-德国,10-泰国
* 11-印度,12-意大利,13-西班牙,14-加拿大,15-澳大利亚,16-中国台湾,17-拉丁美洲,18-越南
* 19-俄罗斯,20-其他
*/
private int productPlace;
/**
* 电影源:1-院线,2-蓝光,3-奥斯卡,4-自制电影,5-独播,6-烂片,7-网络电影,8-巨制
*/
private int source;
/**
* 付费类型:1-免费,2-付费,3-VIP
*/
private int paymentType;
/**
* 年份归类:1-2022年,2-2021年,3-2020年,4-2019年,5-20211-2018年
* 6-2000-2010年,7-90年代,8-80年代,9-更早
*/
private int years;
/**
* 页号
*/
private int pageNo;
/**
* 页大小
*/
private int pageSize;
/**
* 当前滚动ID
*/
private Long scrollId;
}
电影资源信息(只是样板)
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import lombok.Builder;
import lombok.Data;
import java.util.List;
/**
* @author yanfengzhang
* @description 电影资源信息(只是样板)
* @date 2022/3/18 23:33
*/
@Data
@Builder
public class MovieInfo {
/**
* ID编号
*/
private Long id;
/**
* 电影名称
*/
private String name;
/**
* 电影简介
*/
private String desc;
/**
* 电影导演
*/
private String director;
/**
* 电影演员
*/
private String actor;
/**
* 电影综合评分
*/
private Integer score;
/**
* 电影时长
*/
private Integer duration;
/**
* 归属排序:1-热播榜,2-好评榜,3-新上线,4-爱奇艺自制(热播),5-腾讯自制(热播),6-优酷自制(热播)
*/
private int belong;
/**
* 播放类型:1-剧情,2-喜剧,3-动作,4-爱情,5-惊悚,6-犯罪,7-悬疑,8-战争,9-科幻,10-动画,11-恐怖
* 12-家庭,13-传记,14-冒险,15-奇幻,16-武侠,17-历史,18-运动,19-歌舞,20-音乐,21-记录
* 22-伦理,23-西部
*/
private List<Integer> playTypes;
/**
* 电影产地归类:1-内地,2-中国香港,3-美国,4-欧洲,5-日本,6-韩国,7-英国,8-法国,9-德国,10-泰国
* 11-印度,12-意大利,13-西班牙,14-加拿大,15-澳大利亚,16-中国台湾,17-拉丁美洲,18-越南
* 19-俄罗斯,20-其他
*/
private int productPlace;
/**
* 电影源:1-院线,2-蓝光,3-奥斯卡,4-自制电影,5-独播,6-烂片,7-网络电影,8-巨制
*/
private List<Integer> sources;
/**
* 付费类型:1-免费,2-付费,3-VIP
*/
private int paymentType;
/**
* 上映年份
*/
private int year;
}
基本电影信息
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import lombok.Builder;
import lombok.Data;
/**
* @author yanfengzhang
* @description 基本电影信息
* @date 2022/3/18 23:35
*/
@Data
@Builder
public class MovieBasicInfo {
/**
* ID编号
*/
private Long id;
/**
* 电影名称
*/
private String name;
/**
* 电影简介
*/
private String desc;
/**
* 电影导演
*/
private String director;
/**
* 电影演员
*/
private String actor;
/**
* 电影综合评分
*/
private Integer score;
/**
* 电影时长
*/
private Integer duration;
/**
* 上映年份
*/
private int year;
}
获取的电影集合
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import lombok.Builder;
import lombok.Data;
import java.util.List;
/**
* @author yanfengzhang
* @description 获取的电影集合
* @date 2022/3/18 23:35
*/
@Data
@Builder
public class MoviesResult {
/**
* 总数目
*/
private long total;
/**
* 具体电影信息
*/
public List<MovieInfo> movieInfos;
/**
* 电影ID集合
*/
public List<Long> movieIdList;
/**
* 下一个滚动ID
*/
private Long nextScrollId;
}
具体模拟业务
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Lists;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* @author yanfengzhang
* @description 模拟实际电影thrift接口
* @date 2022/3/18 23:39
*/
@Service
@Slf4j
public class MoviesInfoThriftService {
private static Map<Integer, String> descWords = Maps.newHashMap();
static {
descWords.put(1, "人生若无悔,那该多无趣啊。——《一代宗师》");
descWords.put(2, "冬天冷,是因为为了让我们懂得周围人的温暖,是多么的珍贵。——《熔炉》");
descWords.put(3, "你要尽全力保护你的梦想。那些嘲笑你梦想的人,他们注定失败,他们想把你变成和他们一样。我坚信,只要心中有梦想,我就会与众不同。你也是。——《当幸福来敲门》");
descWords.put(5, "成功的含义不在于得到什么,而是在于你从那个奋斗的起点走了多远。——《心灵捕手》");
descWords.put(6, "让朋友低估你的优点,让敌人高估你的缺点。——《教父》");
descWords.put(7, "所有大人都曾是小孩,虽然只有少数人记得。——《小王子》");
descWords.put(8, "记住,希望是好事,也许是人间至善,而美好的事物永不消逝。——《肖申克的救赎》");
descWords.put(9, "当你不能再拥有的时候,唯一可以做的,就是令自己不要忘记。——《东邪西毒》");
descWords.put(10, "我们要学会珍惜我们生活的每一天,因为,这每一天的开始,都将是我们余下生命之中的第一天。除非我们即将死去。——《美国美人》");
}
public MoviesResult getMoviesByConditionPage(MoviesConditions moviesConditions) {
if (!checkPosInteger(moviesConditions.getPageSize())) {
return null;
}
List<MovieInfo> movieInfos = Lists.newArrayList();
List<Long> movieIdList = Lists.newArrayList();
/*模拟数据,返回第一页数据和指定的页面大小*/
if (moviesConditions.getPageNo() == 1) {
for (int i = 1; i <= moviesConditions.getPageSize(); i++) {
movieIdList.add((long) i);
movieInfos.add(MovieInfo.builder()
.id((long) i)
.name("张彦峰喜欢的电影" + i)
.desc(descWords.get(i))
.actor("张彦峰")
.director("张彦峰")
.belong(2)
.score(new Random().nextInt(10) % (10 - i + 1) + i).build());
}
}
if (moviesConditions.getPageNo() == 2) {
for (int i = 6; i <= moviesConditions.getPageSize() + 5; i++) {
movieIdList.add((long) i);
movieInfos.add(MovieInfo.builder()
.id((long) i)
.name("张彦峰喜欢的电影" + i)
.desc(descWords.get(i))
.actor("张彦峰")
.director("张彦峰")
.belong(2)
.score(new Random().nextInt(10) % (10 - i + 1) + i).build());
}
}
return MoviesResult.builder().total(5).movieIdList(movieIdList).movieInfos(movieInfos).build();
}
public MoviesResult getMoviesByConditionScroll(MoviesConditions moviesConditions) {
if (!checkPosInteger(moviesConditions.getPageSize()) || moviesConditions.getScrollId() < 0) {
return null;
}
List<MovieInfo> movieInfos = Lists.newArrayList();
List<Long> movieIdList = Lists.newArrayList();
/*模拟数据,返回第一页数据和指定的页面大小*/
if (moviesConditions.getScrollId() == 0) {
for (int i = 1; i <= moviesConditions.getPageSize(); i++) {
movieIdList.add((long) i);
movieInfos.add(MovieInfo.builder()
.id((long) i)
.name("张彦峰喜欢的电影" + i)
.desc(descWords.get(i))
.actor("张彦峰")
.director("张彦峰")
.belong(2)
.score(new Random().nextInt(10) % (10 - i + 1) + i).build());
}
return MoviesResult.builder().total(5).movieIdList(movieIdList).movieInfos(movieInfos).nextScrollId(1L).build();
}
if (moviesConditions.getScrollId() == 1) {
for (int i = 6; i <= moviesConditions.getPageSize() + 5; i++) {
movieIdList.add((long) i);
movieInfos.add(MovieInfo.builder()
.id((long) i)
.name("张彦峰喜欢的电影" + i)
.desc(descWords.get(i))
.actor("张彦峰")
.director("张彦峰")
.belong(2)
.score(new Random().nextInt(10) % (10 - i + 1) + i).build());
}
}
return MoviesResult.builder().total(5).movieIdList(movieIdList).movieInfos(movieInfos).nextScrollId(-1L).build();
}
/**
* ID信息合法性:必须为正整数
*
* @param id id信息
* @return true-合法;false-非法
*/
public static boolean checkPosInteger(Integer id) {
return null != id && id > 0L;
}
}
测试及展示
测试代码
package org.zyf.javabasic.designpatterns.strategy.template.thrift;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.zyf.javabasic.ZYFApplication;
import java.util.List;
/**
* @author yanfengzhang
* @description 测试按页获取远程数据
* @date 2022/3/20 17:21
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ZYFApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
public class ThriftInvokeCommandTest {
@Autowired
private MoviesInfoThriftService moviesInfoThriftService;
@Test
public void testGetMoviesByCondition() {
log.info("测试分页获取数据:");
MoviesConditions moviesConditions1 = MoviesConditions.builder()
.pageNo(1)
.pageSize(2)
.build();
List movieBasicInfos1 = new GetMoviesByConditionPage(moviesConditions1, moviesInfoThriftService)
.setPageSize(5)
.setSleepMs(100)
.getResult();
log.info("按实际条件返回的基本结果为:{}", movieBasicInfos1);
log.info("测试滚动获取数据:");
MoviesConditions moviesConditions2 = MoviesConditions.builder()
.pageNo(1)
.pageSize(2)
.scrollId(0L)
.build();
List<MovieBasicInfo> movieBasicInfos2 = new GetMoviesByConditionScroll(moviesConditions2, moviesInfoThriftService)
.setPageSize(5)
.setSleepMs(100)
.getResult();
log.info("按实际条件返回的基本结果为:{}", movieBasicInfos2);
}
}
|