最近主要负责公司的 dubbo 服务改造。在改造过程中,涉及到很多核心系统的编码。改造的系统涉及到核心系统,并且改造的系统一多,难免会产生一点胡思乱想。下面我就分享一下我在项目改造过程中的一点胡乱的想法。需要对大家有帮助:
1、统一的打包方式对于之前项目中使用 restful
进行交互,项目的发布就没有版本这个概念。但在使用 dubbo 服务化就依赖版本这个概念。在项目中我们打包的方式和项目的版本的是相互绑定的。
这对于运维来说其实是一种负担。每次修改版本的时候都开发修改 build 脚本。而且运维也需要在CI/CD
的脚本也需要修改。
fintech-notice-client
${project.basedir}/src/main/resources
**/env/**/*.properties
org.springframework.boot
spring-boot-maven-plugin
在运行的的 pom.xml
文件 的 build 标签添加 finalName
指定你需要发布项目的名称。打包以后就会生成 fintech-receipt-client.jar
文件。这样发布就不需要依赖版本这个概念了。
下面是我理解的我们项目的依赖图:
在项目中,我们定义版本的方式都是各个项目中都定义一个版本号比如:
xxx-client :定义版本 1.0.0
xxx-service:定义版本 1.0.0
… 等等
在我的思维里,编码过程中,重复就是不好的
。需要项目的依赖需要统一的版本管理。其实在我们的项目中,也有一个最顶级的项目父 POM。但是它没有发挥出它应有的作用。在这里我们需要一个 maven 的插件flatten-maven-plugin
:
org.codehaus.mojo
flatten-maven-plugin
1.0.0
flatten
process-resources
flatten
它的作用就是统一的包管理。只需要在项目父 POM 中如下定义:
它的子项目中只需要如下定义就可以了:
这个主要是在查看 dubbo 源码的时候,发现它的包管理是基于此。哈哈,拿来主义。
服务化就涉及到 Jar 包依赖,并且我们项目的 Jar 包依赖是需要上传到 maven 私服的。在上传过程中我们只需要上传依赖方关心的 Jar 包即可。比如在 query 服务当中只需要暴露 query-manage-api.jar
这个 Jar 包。
由于这个项目需要依赖它的父项目的 qeury.xxx.pom
。所以其它项目没有必要上传到私服上去。以查询服务为例,以下项目的就不需要上传到私服上去:
不需要上传到私服的 pom 中添加maven-deploy-plugin
这个 maven 插件即可。
org.apache.maven.plugins
maven-deploy-plugin
2.8.2
true
4、上传类的源码到私服
在项目上传到私服的过程当中,如果我们不把类的源码上传到私服中就只会显示类相关信息。并不会显示项目的注释相关内容。如果在提供的 xxx-api 接口里面已经定义好了类描述以及字符描述和方法描述。把类的源信息上传到私服可以有效的降低接口的使用方和提供方的沟通成本。下面就是需要在顶级项目 pom 中需要添加 maven-source-plugin
这个 maven 插件:
org.apache.maven.plugins
maven-source-plugin
attach-sources
jar
这样在执行 mvn deploy 的时候就会把源码 Jar 包打包到 maven 私服上面去了。
5、保证服务新老逻辑兼容之前项目中有一个 xxx-manage-api
项目,在进行 dubbo 服务化的时候既要保证原有暴露的 restful 服务能够继续对外提供服务又要进行 dubbo 服务化暴露新的接口出去。所以就在 xxx-manage-api
项目中重新创建了一个包 facade 把需要暴露的 dubbo 服务接口添加到新包中。
这样 Controller 调用 manage 接口提供的服务,而暴露的 dubbo 服务调用 facade 包提供的服务。 Facade 的实现直接调用原来 manage 的实现就可以了
。
统一规范的接口响应,因为之前暴露的使用的是 restful 服务,可以使用 spring mvc 的统一异常处理机制
。但是现在暴露的是 dubbo 接口,因为消费方要依赖提供方提供的 API Jar 包响应对象是强类型的。所以需要统一规范接口的响应,这样就可以通过 Spring AOP 来进行统一异常处理
。
最佳实践:普通响应对象与分页响应对象且分页响应对象继承与普通响应对象,这样在做 dubbo 服务的统一异常处理时就可以统一返回分页响应对象:
普通响应对象
@Data
@NoArgsConstructor
public class CommonResponse implements Serializable {
private static final long serialVersionUID = 7873005749765413353L;
/**
* 业务响应状态码
*/
private String code;
/**
* 返回码描述信息
*/
private String msg;
/**
* 业务数据
*/
private T data;
public CommonResponse(T data) {
this("200", "Success", data);
}
public CommonResponse(String code, String msg, T data){
this.code = code;
this.msg = msg;
this.data = data;
}
public static CommonResponse success(T data){
return new CommonResponse(data);
}
public static CommonResponse error(String code, String message){
return new CommonResponse(code, message, null);
}
@Override
public String toString(){
return ToStringBuilder.reflectionToString(this);
}
}
下面是分页响应对象:
分页响应对象
@Data
public class CommonPageResponse extends CommonResponse {
/**
* 页码(从第1页开始)
*/
private Integer pageNum;
/**
* 每页条数
*/
private Integer pageSize;
/**
* 总记录数
*/
private Long totalElements;
private CommonPageResponse(String bizCode, String message, T data){
super(bizCode, message, data);
}
public CommonPageResponse(int pageNum, Integer pageSize, Long totalElements, T data) {
this(CommonConstants.SUCCESS_CODE, CommonConstants.SUCCESS_MESSAGE, data);
this.pageNum = pageNum;
this.pageSize = pageSize;
this.totalElements = totalElements;
}
public static CommonPageResponse error(String code, String message){
return new CommonPageResponse(code, message, null);
}
public static CommonPageResponse zero(int pageNum, int pageSize) {
return new CommonPageResponse(pageNum, pageSize, 0L, null);
}
public static CommonPageResponse success(int pageNum, Integer pageSize, Long totalElements, T data) {
return new CommonPageResponse(pageNum, pageSize, totalElements, data);
}
}
并且为了保证不影响之前的 restful 的统一异常处理,所以需要依赖 Spring MVC 的提供的拦截器机制 HandlerInterceptor 来判断这个请求是否是 web 请求。 这样在 AOP 里面如果是 web 请求就不需要进行异常处理。如果不是 web 请求才需要进行 dubbo 的统一异常处理。
判断服务是否是 web 请求的拦截器
/**
* 设置服务上下文 Interceptor
*
* @see ServiceContextInterceptor
*/
public class ServiceContextInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ServiceContext serviceContext = new ServiceContext();
serviceContext.setWeb(true);
ServiceContextHolder.set(serviceContext);
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ServiceContextHolder.cleanUp();
}
}
下面就是 Dubbo 暴露服务统一异常处理,实现了MethodInterceptor
这个接口。
@Slf4j
public class ServiceExceptionInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if(isWeb()) {
return invocation.proceed();
}
try {
return invocation.proceed();
} catch (BizException e) {
String code = e.getBizCode();
String message = e.getMessage();
if(StringUtils.isBlank(message)){
message = ReturnCodeEnum.getValueByCode(code);
}
log.error("bizException error and bizCode is :{}, message is: {}", code, message, e);
return CommonPageResponse.error(code, message);
} catch (Exception e) {
log.error("system inner error : {}", e);
return CommonPageResponse.error(ReturnCodeEnum.FAIL.getCode(), ReturnCodeEnum.FAIL.getMsg());
}
}
private boolean isWeb(){
ServiceContext serviceContext = ServiceContextHolder.get();
if(serviceContext == null) {
return false;
}
return serviceContext.isWeb();
}
}
最终通过 Spring 的 AOP 配置完成整个过程:
/**
* Dubbo 暴露服务统一异常处理
*
* @see ServiceExceptionConfig
*/
@Configuration
public class ServiceExceptionConfig {
@Bean
public Advisor serviceExceptionAdvisor(){
DefaultBeanFactoryPointcutAdvisor advisor = new DefaultBeanFactoryPointcutAdvisor();
ServiceExceptionInterceptor serviceExceptionInterceptor = new ServiceExceptionInterceptor();
advisor.setAdvice(serviceExceptionInterceptor);
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("within(cn.carlzone.fintech.query.manage.support.*)");
advisor.setPointcut(pointcut);
return advisor;
}
}
下面就是接口定义:
public interface ConfigQueryManage {
CommonResponse<List> queryProductInfo(PayTypeRequest request);
}
7、开关式编程
在进行上线过程中不可能是一帆风顺的,所以我们要做到项目发布的可回滚。就是在项目发布过程中如果新逻辑上线有问题还能够把项目回滚到之前的逻辑。这种就是基于开关的编程模式。
同样的在服务进行 dubbo 的过程当中,我们既要保证 dubbo 服务上线有问题代码逻辑还能够回滚到之前 restful 调用的 feign 模式。因为我们项目中使用到了 apollo 这个分布式配置管理,能够很好的在线更改配置值 。所以这里就可以很方便的使用开关式编程来进行项目回滚。
开关式编程其实需要利用要设计模式中的适配器模式,适配器模式的这里不是我讲的重点。我就不展开了,如果读者不清楚,可以自行了解。
用过 Spring Cloud 的同学都应该知道 Feign 是基于接口的调用。在这里我就抽取出一个 Client 服务端这个概念,就是用来实现不同的客户端调用。即可以是基于 Feign 的实现,又可以是 Dubbo 的实现。 Client 这个接口我是直接把 Feign 中的代码 copy 过来,去掉不需要的注解:
远程调用客户端抽象接口
public interface SettleClient {
BizResultDTO settleReceipt(ReceiptSettleReqVO request);
}
下面就是 Feign 和 Dubbo 不同的实现
远程调用 Feign 实现
@Slf4j
@ProfilerLog
@Service("settleClientFeign")
public class SettleClientFeign implements SettleClient {
@Resource
private FintechSettleService fintechSettleService;
@Override
public BizResultDTO settleReceipt(BizRequest request) {
log.info("SettleClientFeign#settleReceipt request param is {}", JSON.toJSONString(request));
BizResultDTO response = fintechSettleService.settleReceipt(request);
log.info("SettleClientFeign#settleReceipt response is {}", JSON.toJSONString(response));
return response;
}
}
接着就是 Dubbo 调用 的实现:
@Slf4j
@ProfilerLog
@Service("settleClientDubbo")
public class SettleClientDubbo implements SettleClient {
@Resource
protected ConversionService genericConversionService;
@Reference
private ISettlementService settlementService;
@Override
public BizResultDTO settleReceipt(BizRequest request) {
log.info("SettleClientDubbo#doSign request param is {}", JSON.toJSONString(request));
RpcRequest remoteRequest = genericConversionService.convert(request, RpcRequest.class);
BizResultDTO response = execute(BizResponse.class,
() -> settlementService.settleReceipt(remoteRequest));
log.info("SettleClientDubbo#doSign response is {}", JSON.toJSONString(response));
if(SuccessCodeConstants.FINTECH_SETTLE_SUCCESS_CODE.equals(response.getBizCode())) {
return BizResultDTO.buildBizResult(response.getBizCode(), response.getMessage(), response.getData());
}
throw new BizException(ReturnCodeConstants.FAIL);
}
}
...
之前的代码是直接调用 Feign 接口,抽象出来一个 Client 的有以下两点好处
在 Client 里面打印请求与响应日志 在 Client 里面处理远程调用的异常业务调用方其实跟本不应该关心远程调用的请求与响应日志还有就是异常处理,这些都应该包装到 Client 里面。调用日志与异常处理在 Client 端也符合面向对象的单一职责原则
,业务调用方只需要进行接口调用即可。
为什么之前 Client 的接口定义是把 Feign 的接口定义直接 copy 过来,下面就是巧妙的地方 :对于业务方远程调用是调用 Feign 还是 Dubbo 这里其实有一个选择的过程。所以这里就利用了适配器模式来进行选择。
@Service("settleClientAdapter")
public class SettleClientAdapter implements SettleClient {
@Resource
private SettleClient settleClientDubbo;
@Resource
private SettleClient settleClientFeign;
@Value("${project.runtime.supportDubbo:N}")
private String supportDubbo;
@Override
public BizResultDTO settleReceipt(ReceiptSettleReqVO request) {
return getClient().settleReceipt(request);
}
private SettleClient getClient(){
if("Y".equals(supportDubbo)) {
return this.settleClientDubbo;
} else {
return this.settleClientFeign;
}
}
}
其实这个适配类也可以不实现这个 Client,只不过实现了它表达的语义更加明确!
业务方调用这个适配的类,然后可以基于 apolle 来进行动态配置,来选择进行不同的客户端调用。这样就可以做到开关模式开发。
注意:基于开关模式进行开发会造成代码膨胀,所以需要定义的清理不需要的代码逻辑。
因为之前使用 restful 暴露服务,也不需要把 Jar 包上传到私服上去。所以大家也没有注意到 maven 里面版本这个概念。但是在进行 dubbo 服务化之后消费方需要引用服务方提供的 API Jar 包。我们 maven 私服的 maven-releases 仓库没有禁止重复同上传同一版本的代码。
这样在修改代码之后,开发有可能会把同一个版本上传到 maven 私服这样就会把原来的版本进行覆盖。然后消费方在发布的时候就会拉取到新的代码,这样有可能会有问题。所以可以通过以下方式来进行 maven 私有仓库不允许重复部署:
浏览器登录nexus管理界面 点击设置图标 --> Repository --> Repositories --> maven-releases Hosted --> 选择‘Disable redeploy策略