远程调用代码封装杂谈
上周处理了一个线上问题,经过排查发现是RPC远端调用超时,框架抛出的超时异常没有被捕捉,导致数据进入中间态,无法推进后续处理。好在影响不大,及时修复掉了。
关于这部分的代码规范,之前也有所思考,正好有这个契机做一下整理。
讨论背景和范围
做应用分层架构时,有一种实践方式是将代表外部服务的类如UserService,包装成一个UserServiceClient类,上层业务调用统一使用UserServiceClient,是一种简单的代理模式。
本文的讨论实例,即UserService、UserServiceClient以及其实现UserServiceClientImpl,形式化的定义如下:
// 远程RPC接口
public interface UserService {
/**
* 用户查询
*/
ResultDTO<UserInfo> query(QueryRequest param);
/**
* 用户创建
*/
ResultDTO<String> create(CreateRequest param);
}
// 本地接口
public interface UserServiceClient {
/**
* 用户查询
*/
Result<UserInfo> query(QueryReequest param);
/**
* 用户创建
*/
Result<String> create(CreateRequest param);
}
// 本地接口实现
public classe UserServiceClientImpl implement UserServiceClient {
@Autorwire
private UserService userSerivce;
/**
* 用户查询
*/
@override
Result<UserInfo> query(QueryReequest param) {
// 包装调用代码片段
}
/**
* 用户创建
*/
@override
Result<String> create(CreateRequest param) {
// 包装调用代码片段
}
}
一、不做任何处理/不封装
Client类没有任何的处理,仅仅是对Servie类的调用及原样返回。
// 本地接口实现
public classe UserServiceClientImpl implement UserServiceClient {
@Autorwire
private UserService userSerivce;
/**
* 用户查询
*/
@override
Result<UserInfo> query(QueryReequest param) {
return userSerivce.query(param);
}
/**
* 用户创建
*/
@override
Result<String> create(CreateRequest request) {
return userSerivce.create(param);
}
}
非常不推荐,原因可以和后续的几种形式中对比来看。
这种写法实际上跟lombok提供的@Delegate注解是一样的,这个注解一样不推荐。
@Component
public class UserServiceClient {
@Autowired
@Delegate
private UserService userService;
}
二、结果统一再封装
RPC调用的目标可能是不同的系统,调用的封装结果也有所不同。为了便于上层业务处理,减少对外部的感知,可以定义一个通用的Result类来包装。
// 本地接口实现
public classe UserServiceClientImpl implement UserServiceClient {
@Autorwire
private UserService userSerivce;
/**
* 用户查询
*/
@override
Result<UserInfo> query(QueryReequest param) {
ResultDTO<UserInfo> rpcResult = userSerivce.query(param);
Result<UserInfo> result = new Result<>();
// 封装调用结果
result.setSuccess(result.isSuccess());
result.setData(result.getData());
// 错误码、错误堆栈等填充,略
return result;
}
/**
* 用户创建
*/
@override
Result<String> create(CreateRequest request) {
// 略
}
}
三、只取结果不封装
上层处理时,对封装的结果判断会比较冗余。如果在Client就能区分使用意图,可以将非预期的结果封装成业务异常,预期结果直接返回。
特定场景的返回结果可以用不同的业务异常区分。
// 本地接口实现
public classe UserServiceClientImpl implement UserServiceClient {
@Autorwire
private UserService userSerivce;
/**
* 用户查询
*/
@override
UserInfo query(QueryReequest param) {
ResultDTO<UserInfo> rpcResult = userSerivce.query(param);
if(rpcResult == null) {
throw new BizException("调用结果为空!");
}
if(rpcResult != null && rpcResult.isSuccess()) {
return rpcResult.getData();
}
if("XXX".equals(rpcResult.getErrorCode())) {
throw new XXXBizException("调用结果失败,异常码XXX");
} else {
throw new BizException("调用结果失败");
}
}
/**
* 用户创建
*/
@override
String create(CreateRequest request) {
// 略
}
}
四、对调用处增加异常处理
RPC调用会发生系统间交互,难免会出现超时,很多框架直接抛出超时异常。除此以外,被调用的业务系统接口可能由于历史原因或者编码问题,可能会直接把自己的异常抛给调用者。为了保证自己系统的稳定性,需要对异常进行捕获。
如何捕获异常?并不是简单的catch(Exception e)
就能搞定。在阿里巴巴出品的《Java开发手册》中提到,要用Throwable来捕获,原因是:
【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable
类来进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出
NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹
配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即
使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
这样,一个完善的Client就完成了:
// 本地接口实现
public classe UserServiceClientImpl implement UserServiceClient {
@Autorwire
private UserService userSerivce;
/**
* 用户查询
*/
@override
UserInfo query(QueryReequest param) {
try {
ResultDTO<UserInfo> rpcResult = userSerivce.query(param);
} catch (Throwable t) {
if(t instanceof XXXTimeoutException) {
// 已知的特殊调用异常处理,如超时异常需要做自动重试,特殊处理
throw new BizException("超时异常")
}
throw new BizException("调用异常", t)
}
if(rpcResult == null) {
throw new BizException("调用结果为空!");
}
if(rpcResult != null && rpcResult.isSuccess()) {
return rpcResult.getData();
}
if("XXX".equals(rpcResult.getErrorCode())) {
throw new XXXBizException("调用结果失败,异常码XXX");
} else {
throw new BizException("调用结果失败");
}
}
/**
* 用户创建
*/
@override
String create(CreateRequest request) {
// 略
}
}
用拦截器封装异常
对于外部调用,以及内部调用,都可以用拦截器做统一的处理。对于捕获的异常的处理以及日志的打印在拦截器中做,会让代码编写更加简洁。
示例如下:
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class RpcInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String invocationSignature = method.getDeclaringClass().getSimpleName() + "." + method.getName();
// 排除掉java原生方法
for(Method m : methods) {
if(invocation.getMethod().equals(m)) {
return invocation.proceed();
}
}
Object result = null;
Objectp[] params = invocation.getArguments();
try {
result = invocation.proceed();
} catch( Throwable e) {
// 接各种异常,区分异常类型
// 处理异常、打印日志
} finally {
// 打印结果日志, 打印时也要处理异常
}
return result;
}
设置代理
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConitionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.Bean;
import org.springframework.context.ComponentScan;
import org.springframework.context.Configuration;
@Configuration
public class Interceptor {
@Bean
public RpcInterceptor rpcSerivceInterceptor() {
RpcInterceptor rpcSerivceInterceptor = new RpcInterceptor();
// 可以注入一些logger什么的
return rpcSerivceInterceptor;
}
@Bean
public BeanNameAutoProxyCreator rpcServiceBeanAutoProxyCreator() {
BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
// 设置代理类的名称
beanNameAutoProxyCreator.setBeanNames("*RpcServiceImpl");
// 设置拦截链名字
beanNameAutoProxyCreator.setInterceptorName("rpcSerivceInterceptor");
return beanNameAutoProxyCreator;
}
}
如果对上层的返回结果需要统一封装,也可以在拦截器里做。
全局异常处理注解
使用@RestControllerAdvice和@ExceptionHandler两个注解,统一处理应用中发生的所有异常。
@RestControllerAdvice
public class UnionExceptionHandler {
public UnionExceptionHandler() {
}
@ExceptionHandler(XXException.class)
public ResponseEntity<Result> handle(Exception ex, WebRequest request) throws Exception {
Result error = ((BizzExceptionInterface)ex).getError();
log.warn("XXException, code={}, message={}," ,error.getC(), error.getM());
return ResponseEntity.ok().header(HEADER_BIZZ_CODE, error.getC()).body(error);
}
}
作者:五岳
出处:http://www.cnblogs.com/wuyuegb2312
对于标题未标注为“转载”的文章均为原创,其版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。