Java服务_SpringAOP&Filter&Interceptor比较与实战

Java服务_SpringAOP&Filter&Interceptor比较与实战

一、SpringAOP

1.SpringAOP概述

1.1 AOP与SpringAOP

AOP与Spring并不是一个强绑定的关系,AOP是指面向切面编程,是一种编程思想;SpringAOP是指Spring框架提供的一套AOP的快捷实现封装。AOP本省不是某种语言或某个框架特有的,它实现的是将横向逻辑与业务逻辑解耦,实现对业务代码无侵入,从而让我们更专注于业务逻辑本身,本质是在不改变原有业务逻辑的情况下增强横切逻辑,在软件开发过程中,像权限、监控、日志、事务、异常重试等非业务主链路逻辑和业务代码混夹在一起就不够优雅,可以用AOP来实现。

SpringAOP提供了三种AOP的快捷实现方式:

1)Spring1.2 基于接口的配置:Spring最早的AOP实现是完全基于接口,虽然兼容,但已经不推荐了。

2)Spring2.0+ schema-based 配置 :Spring2.0之后,提供了 schema-based 配置,也就是xml的方式配置。

3)Spring2.0+ @Aspect配置:Spring2.0之后,也提供了 @Aspect 基于注解的实现方式,也就是本文的主角,也是目前最方便、最广泛使用的方式!

1.2 @Aspect简单案例快速入门

@Aspect注解方式,它的概念像@Aspect、@Pointcut、@Before、@After、@Around等注解都是来自于 AspectJ,但是功能的实现是纯SpringAOP自己实现的,主要有两大核心:

1)定义切入点:使用@Pointcut切点表达式。

2)定义切入时机和增强处理逻辑:五种通知Advice注解对切入点执行增强处理,包括:@Before、@After、@AfterRunning、@AfterThrowing、@Around。

通过对切入点的定义就可以知道对哪些方法进行增强,通过对切入时机和增强逻辑的定义就可以知道在源方法的前面还是后面增加什么增强逻辑。

pom.xml依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

AOP实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @Aspect和@Component定义一个切面类
@Aspect
@Component
public class MethodLogAspect {
// 核心一:定义切点(使用execution方式)
@Pointcut("execution(* com.jd.bdaa.arch.dao.mapper.drive.*.*(..))")
public void mapperMethod() {
}

// 核心二:定义切入时机和增强处理(这是5种通知中的前置通知)
@Before("mapperMethod()")
public void printLog(JoinPoint joinPoint) {
System.out.println("前置通知:" + joinPoint);
}
}

经过上述处理,在com.jd.bdaa.arch.dao.mapper.drive这个包下的所有类中的所有方法执行前,都会先执行printLog()方法。

1.3 切面类中切点表达式的配置方式

切点表达式就是用来指定对哪些方法进行增强的表达式,切点表达式有如下三种配置方式:

1)内置配置

定义切入时机和增强处理时,直接在 @Before@AfterReturning 等通知注解中指定切点表达式。切入时机和增强处理又叫做通知

1
2
3
4
5
6
7
8
9
10
// @Aspect和@Component定义一个切面类
@Aspect
@Component
public class MethodLogAspect {
// 一步定义完成切点、切入时机和增强处理
@Before("execution(* com.jd.bdaa.arch.dao.mapper.drive.*.*(..))")
public void printLog(JoinPoint joinPoint) {
System.out.println("前置通知:" + joinPoint);
}
}

2)注解配置

在切面类中,先定义一个方法并使用 @Pointcut 注解来指定切点表达式。然后在定义通知时,在通知注解中指定定义表达式的方法签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @Aspect和@Component定义一个切面类
@Aspect
@Component
public class MethodLogAspect {
// 核心一:定义切点(使用execution方式)
@Pointcut("execution(* com.jd.bdaa.arch.dao.mapper.drive.*.*(..))")
public void mapperMethod() {
}

// 核心二:定义切入时机和增强处理(这是5种通知中的前置通知)
@Before("mapperMethod()")
public void printLog(JoinPoint joinPoint) {
System.out.println("前置通知:" + joinPoint);
}
}

3)公共配置

也可以将切点的定义和通知的定义分别放在两个不同的类中,实现切点表达式的共用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CommonPointCut {
@Pointcut("execution(* com.jd.bdaa.arch.dao.mapper.drive.*.*(..))")
public void mapperMethod() {
}
}

@Aspect
@Component
public class MethodLogAspect {
@Before("com.jd.bdaa.arch.aop.CommonPointCut.mapperMethod()")
public void printLog(JoinPoint joinPoint) {
System.out.println("前置通知:" + joinPoint);
}
}

1.4 切点表达式类型

切点表达式就是用来指定对哪些方法进行增强的表达式,SpringAOP目前支持如下十种切点表达式:

1.4.1 execution

execution也是最常用的切点表达式类型,可以匹配方法、类、包。

表达式模式:

1
execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)

表达式解释:

  • modifier:匹配修饰符,public, private 等,省略时匹配任意修饰符

  • ret-type:匹配返回类型,使用 * 匹配任意类型

  • declaring-type:匹配目标类,省略时匹配任意类型

    • .. 匹配包及其子包的所有类
  • name-pattern:匹配方法名称,使用 * 表示通配符

    • * 匹配任意方法
    • set* 匹配名称以 set 开头的方法
  • param-pattern:匹配参数类型和数量

    • () 匹配没有参数的方法
    • (..) 匹配有任意数量参数的方法
    • (*) 匹配有一个任意类型参数的方法
    • (*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
  • throws-pattern:匹配抛出异常类型,省略时匹配任意类型

使用示例:

1
2
3
4
5
6
7
8
9
10
11
// 匹配public方法
execution(public * *(..))

// 匹配名称以set开头的方法
execution(* set*(..))

// 匹配AccountService接口或类的方法
execution(* com.xyz.service.AccountService.*(..))

// 匹配service包及其子包的类或接口
execution(* com.xyz.service..*(..))

还可以使用&&、|| 和 ! 来组合多个切点表达式,表示多个表达式“与”、“或”和“非”的逻辑关系,提升匹配效率,示例如下

1
2
3
4
@Pointcut("!execution(* com.jd.bdaa.arch.dao.mapper.drive.DriveDataSourceRouteMapper.*(..))" +
"&& !execution(* com.jd.bdaa.arch.dao.mapper.drive.DriveDataSourceRouteStrategyAppkeyMapper.*(..))" +
"&& !execution(* com.jd.bdaa.arch.dao.mapper.drive.DriveDataSourceRouteStrategyMapper.*(..))" +
"&& execution(* com.jd.bdaa.arch.dao.mapper.drive.*.*(..))")
1.4.2 within

匹配指定类型。匹配指定类的任意方法,不能匹配接口。

表达式模式:

1
within(declaring-type)

使用示例:

1
2
3
4
5
6
7
8
// 匹配service包的类
within(com.xyz.service.*)

// 匹配service包及其子包的类
within(com.xyz.service..*)

// 匹配AccountServiceImpl类
within(com.xyz.service.AccountServiceImpl)
1.4.3 this

匹配代理对象实例的类型,匹配在运行时对象的类型。

注意:基于 JDK 动态代理实现的 AOP,this 不能匹配接口的实现类,因为代理类和实现类并不是同一种类型,详情参阅《Spring中的AOP和动态代理

表达式模式:

1
this(declaring-type)

使用示例:

1
2
3
4
5
6
7
8
// 匹配代理对象类型为service包下的类
this(com.xyz.service.*)

// 匹配代理对象类型为service包及其子包下的类
this(com.xyz.service..*)

// 匹配代理对象类型为AccountServiceImpl的类
this(com.xyz.service.AccountServiceImpl)
1.4.4 target

匹配目标对象实例的类型,匹配 AOP 被代理对象的类型。

表达式模式:

1
target(declaring-type)

使用示例:

1
2
3
4
5
6
7
8
// 匹配目标对象类型为service包下的类
target(com.xyz.service.*)

// 匹配目标对象类型为service包及其子包下的类
target(com.xyz.service..*)

// 匹配目标对象类型为AccountServiceImpl的类
target(com.xyz.service.AccountServiceImpl

三种表达式匹配范围如下:

表达式匹配范围 within this target
接口
实现接口的类
不实现接口的类
1.4.5 args

匹配方法参数类型和数量,参数类型可以为指定类型及其子类。

使用 execution 表达式匹配参数时,不能匹配参数类型为子类的方法。

表达式模式:

1
args(param-pattern)

使用示例:

1
2
3
4
5
// 匹配参数只有一个且为Serializable类型(或实现Serializable接口的类)
args(java.io.Serializable)

// 匹配参数个数至少有一个且为第一个为Example类型(或实现Example接口的类)
args(cn.codeartist.spring.aop.pointcut.Example,..)
1.4.6 bean

通过 bean 的 id 或名称匹配,支持 * 通配符。

表达式模式:

1
bean(bean-name)

使用示例:

1
2
3
4
5
// 匹配名称以Service结尾的bean
bean(*Service)

// 匹配名称为demoServiceImpl的bean
bean(demoServiceImpl)
1.4.7 @annotation

匹配方法是否含有注解,当方法上使用了指定注解,该方法会被匹配,在接口方法上使用注解不匹配。这也是SpringAOP最常用的一种切点表达式,使用该类型通常还需要开发者另外新建一个对应的注解类。

使用示例:

1
2
// 匹配使用了Demo注解的方法
@annotation(cn.codeartist.spring.aop.pointcut.Demo)

还需要创建一个对应的注解类:

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Demo {
}
1.4.8 @within

匹配指定类型是否含有注解,当定义类时使用了注解,该类的方法会被匹配,但在接口上使用注解不匹配。

使用示例:

1
2
// 匹配使用了Demo注解的类
@within(cn.codeartist.spring.aop.pointcut.Demo)
1.4.9 @target

匹配目标对象实例的类型是否含有注解,当运行时对象实例的类型使用了注解,该类的方法会被匹配,在接口上使用注解不匹配。

使用示例:

1
2
// 匹配对象实例使用了Demo注解的类
@target(cn.codeartist.spring.aop.pointcut.Demo)
1.4.10 @args

匹配方法参数类型是否含有注解。当方法的参数类型上使用了注解,该方法会被匹配。

使用示例:

1
2
3
4
5
// 匹配参数只有一个且参数类使用了Demo注解
@args(cn.codeartist.spring.aop.pointcut.Demo)

// 匹配参数个数至少有一个且为第一个参数类使用了Demo注解
@args(cn.codeartist.spring.aop.pointcut.Demo,..)
1.4.11 切点表达式组合

使用 &&、|| 和 ! 来组合多个切点表达式,表示多个表达式“与”、“或”和“非”的逻辑关系。

这可以用来组合多种类型的表达式,来提升匹配效率。

1
2
3
4
5
// 匹配doExecution()切点表达式并且参数第一个为Account类型的方法
@Before("doExecution() && args(account,..)")
public void validateAccount(Account account) {
// 自定义逻辑
}

1.5 通知类型与语法

通知(Advice)就是切入点和增强逻辑。SpringAOP共有五种切入点类型:

@Before:前置通知,在被切的方法执行前执行;

@After:后置通知,在被切的方法return后执行,无论被切方法是否异常都会执行;

@AfterRunning:返回通知,在被切的方法return后执行,带有返回值,如果被切方法抛出异常则不会执行;

@AfterThrowing:异常通知,只被切的方法抛异常时执行,否则不执行;

@Around:环绕通知,这是功能最强大的Advice,可以自定义执行顺序。

这五种切入点的执行顺序如下:

方法语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Before("pointCut()")
public void 方法名(JoinPoint joinPoint) {...}

@After("pointCut()")
public void 方法名(JoinPoint joinPoint) {...}

@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {...}

@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {...}

@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {...}

增强逻辑的方法名可以随意自定义,但是出参数和入参类型是固定写法。

上述JoinPoint类型入参连接点非常重要,因为Spring只支持方法类型的连接点,所以在Spring中joinPoint指的就是被增强的源方法。JoinPoint连接点类型中有三个常用的方法:

1)getSignature()获取签名:

1
MethodSignature signature = (MethodSignature) joinPoint.getSignature();

通过signature实例可以获取名称getName() 和参数类型getParameterTypes()。

2)getTarget()获取目标类:

1
Class<?> clazz = joinPoint.getTarget().getClass();

如果被增强的类是被别的切面切过的类,可以使用AopUtils.getTargetClass获取一个数组,再从数组中找你期望的类。

1
2
import org.springframework.aop.support.AopUtils;
Class<?>[] targets = AopUtils.getTargetClass(joinPoint.getTarget()).getInterfaces();

3)getArgs()获取入参值

1
Object[] args = joinPoint.getArgs()

基于上述3个方法,可以轻松获取到被增强的类名、方法名、方法参数类型、方法参数值等。printMethod方法如下:

1
2
3
4
5
6
private void printMethod(JoinPoint joinPoint) throws NoSuchMethodException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Class<?> clazz = joinPoint.getTarget().getClass();
Method method = clazz.getMethod(signature.getName(), signature.getParameterTypes());
System.out.printf("[MethodLogAspect]切面打印 -> [className]:%s -> [methodName]:%s -> [methodArgs]:%s%n", clazz.getName(), method.getName(), Arrays.toString(joinPoint.getArgs()));
}

2.定义驱动AOP实战实例

比较多,选两个比较典型的,公共配置自动同步生产AOP、调用外部系统监控AOP

2.1 公共配置自动同步生产AOP

定义驱动中的逻辑表是需要区分预发和线上的,所以分了两个mysql库,这个设置其实不太合理,但是已经这样用了,那么有一些公共配置项目就需要在预发环境配置好了之后直接同步到生产环境,如数据源、数据集之类的,这使用就需要用到这个预发自动同步生产AOP,将预发环境的增删改方法增强生产环境增删改逻辑。

这个AOP采用的是@annotation类型的切点表达式,所以需要先注册一个注解类:

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface PreInvokeProd {

}

创建切面类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@ConditionalOnProperty(prefix = "config", name = "env", havingValue = "PRE")
@Slf4j
@Component
@Aspect
public class PreInvokeProdAspect {

@Autowired
private UmpInfo umpInfo;

@Autowired
private DriveMetaProdService driveMetaProdService;

@Around("@annotation(com.jd.bdaa.arch.annotation.PreInvokeProd)")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String interfaceName = joinPoint.getTarget().getClass().getInterfaces()[0].getSimpleName();
String methodName = joinPoint.getSignature().getName();
try {
Object res = joinPoint.proceed();
log.info("execute preInvokeProd class {}, {}, {}, {}", interfaceName, methodName, JsonUtil.toJsonString(joinPoint.getArgs()), res);
driveMetaProdService.getMethod(interfaceName + ":" + methodName).invoke(driveMetaProdService.getService(interfaceName), joinPoint.getArgs());
return res;
} catch (Throwable e) {
if (e instanceof CommonException || e instanceof com.jd.bdaa.arch.error.exception.CommonException
|| e instanceof BizHandleException || e instanceof ParamException || e instanceof ExternalApiException) {
throw e;
}
log.error("execute preInvokeProd failed, service:{}, method:{}. exception:", interfaceName, methodName, e);
UmpUtil.makeAlarm(umpInfo, PARAM_HANDLER_ERROR, "预发同步生产报错。uuid = " + MDC.get(Constants.LOG_UUID_KEY));
throw new CommonException("数据同步异常");
}
}
}

在目标增强方法上添加对应注解即可生效:

1
2
3
4
5
6
7
8
9
10
/**
* 删除数据源
*/
@Transactional
@PreInvokeProd
@Override
public Boolean deleteDataSource(String erp, String dataSource) throws Exception {
DriveEzdDataSourcePO dataSourcePO = driveEzdDataSourceService.getDataSourceByDatasource(dataSource);
return deleteDataSource(erp, dataSourcePO);
}

2.2 调用外部系统监控AOP

定义驱动依赖调用了多个下游系统,如ezd服务等,那么调用了哪些外部系统,调用参数等都希望统一打印到日志中,辅助跨服务联调,在故障排查、责任划分时有清晰明了的依据,这就需要这样一个AOP进行统一的日志打印。

使用execution类型的切点表达式,并通过逻辑运算符指定某几个类中的方法,指定类上的方法直接就增强了,不需要另外在添加什么代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Aspect
@Component
@DependsOn("duccConfig")
public class JsfMethodAspect {

private static final Logger logger = LoggerFactory.getLogger(MapperMethodAspect.class);
private static final String UMP_KEY = "drive_local_cache_aop_jsf";

@Autowired
private DuccConfig duccConfig;

@Pointcut("!execution(* com.jd.bdaa.arch.external.MenuOpenQueryExternalService.*(..))" +
"&& !execution(* com.jd.bdaa.arch.external.EasyDataExternalService.*(..))" +
"&& execution(* com.jd.bdaa.arch.external.*.*(..))")
public void jsfMethod() {
}

@Before("jsfMethod()")
public void beforeMapperMethod(JoinPoint joinPoint) {
if (duccConfig.getRouteLocalCacheEnable()) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String clzName = signature.getDeclaringType().getSimpleName();
DriveRouteContext routeContext = DriveRouteMonitorUtil.getDriveRouteContextFromSystem();
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
StringBuffer stackTraceBuffer = new StringBuffer();
for (int i = 0; i < stackTrace.length; i++) {
if (stackTrace[i].toString().startsWith("com.jd.bdaa")) {
stackTraceBuffer.append(stackTrace[i]).append("-->");
}
}
if (Objects.isNull(routeContext)) {
// 代表配置链路
if (logger.isDebugEnabled()) {
logger.debug("JsfMethodAspect get mapper execute info. keys={clzName,methodName,stackTracePath}", clzName, signature.getName(), stackTraceBuffer);
}
return;
}
// 代表查询路由链路
if (logger.isDebugEnabled()) {
logger.debug("JsfMethodAspect get mapper execute info. keys={clzName,methodName,stackTracePath,alias,resAppkey,serviceId,uuid}", clzName, signature.getName(),
stackTraceBuffer, routeContext.getAlias(), routeContext.getResAppKey(), routeContext.getServiceId(), routeContext.getUuid());
}
String detail = String.format("JsfMethodAspect get mapper execute info. %s ,%s ,%s ,%s ,%s ,%s ,%s ", clzName, signature.getName(),
stackTraceBuffer, routeContext.getAlias(), routeContext.getResAppKey(), routeContext.getServiceId(), routeContext.getUuid());
UmpUtil.alarm(UMP_KEY, detail);
}
}
}

二、Filter过滤器

1.Filter过滤器简介

Filter过滤器是在Servlet的规范中定义的,依赖于Servlet容器的组件,但不依赖于任何框架。

Filter过滤器只可以获取到请求中的Request和Response,无法获取到处理请求的方法的信息。

Filter过滤器是通过回调函数实现的。

2.代码示例

以下仅为简单示例case,要真正使用还有很多复杂配置,还有xml、注解等不同配置方式。

创建过滤器,同时配置过滤资源路径:

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@WebFilter(value = "/name") //这里我们只拦截name请求,记得要在启动类配ServletComponentScan
public class MyFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
log.info("before filter");
// 请求放行
chain.doFilter(request, response);
log.info("after filter");
}
}

创建Controller,也就是过滤器起作用的目标资源路径:

1
2
3
4
5
6
7
8
9
10
@Slf4j
@RestController
public class MyController {

@GetMapping("/name")
public String getName() {
log.info("getName");
return "sticki";
}
}

3.多个过滤器执行顺序

当对同一个资源路径配置了多个过滤器时,可以使用多种方法控制过滤器的执行顺序。如在web.xml配置中谁定义在前,谁先执行;如Order注解配置优先级,值小的先执行;如通过Ordered接口指定优先级。

三、Interceptor拦截器

1.Interceptor拦截器简介

Interceptor拦截器是Spring框架的组件,依赖于Spring,但不依赖于Servlet。

Interceptor拦截器可以通过DI(依赖注入)的方式获取到Spring中存在的Bean,也可以获取到请求的上下文信息,如被拦截请求方法和类信息等。

Interceptor拦截器通过反射实现,本质是动态代理,也是运用了AOP的编程思想。

2.代码示例

以下仅为简单示例case,要真正使用还有很多复杂配置,还有xml、注解等不同配置方式。

创建拦截器,注意拦截器的preHandle()方法返回值为true时,请求才可以通过拦截器,返回值为false时,请求不能通过拦截器,即被拦截器拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
public class MyInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("before interceptor");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
log.info("after interceptor");
}
}

创建拦截器配置类,用于配置需要拦截的资源路径:

1
2
3
4
5
6
7
8
9
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 只拦截 age 的请求
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/age");
}
}

创建Controller,也就是过滤器起作用的目标资源路径:

1
2
3
4
5
6
7
8
9
10
@Slf4j
@RestController
public class MyController {

@GetMapping("/age")
public String getAge() {
log.info("getAge");
return "18";
}
}

3.多个拦截器执行顺序

当对同一个资源路径配置了多个拦截器时,可以使用多种方法控制拦截器的执行顺序。如在拦截器配置类对同一个资源路径先添加哪个拦截器,哪个先执行;如使用InterceptorRegistrationd的order()方法配置优先级,值小的先执行。

四、过滤器、拦截器、SpringAOP、ControllerAdvcie对比与执行顺序

1.执行顺序

除了上述所说的过滤器、拦截器、SpringAOP,Spring中还有一个封装好组件也是通过AOP对Controller进行增强,那就是ControllerAdvice,该组件常用于全局异常处理(配合自定义异常效果更佳)、数据绑定、数据预处理等操作,我的全局异常处理博文中提有专门提过。

那么这四者都可以用于对Controller进行增强,其执行顺序如下:Filter过滤器 > Interceptor拦截器 > ControllerAdvice > AOP > Controller。

注意,当抛出的异常被ControllerAdvice捕获之后,Interceptor拦截器不会再有后置处理了,但是Filter过滤器还是有后置处理的

2.过滤器、拦截器与RPC浅析

上文中所提到的Filter和Interceptor是指一般大家常用的基于Servlet或Spring已经定义封装好的工具组件,也就是只能用于WebService中的组件,或者说是只能用于Controller层组件。

RPC(远程过程调用)的使用范围和实现方式非常之广,RPC按通信协议可以分为基于HTTP的、基于TCP等;按报文协议可以分为基于XML文本的、基于JSON文本的,二进制的;按是否跨平台语言,可以分为平台专用的,平台中立的。WebService只是RPC的一种实现方式,一般应用于基于HTTP的、XML文本的、跨平台的、功能完善的业务系统。

那么对于WebService类型以外的RPC同样需要类似的Filter和Interceptor功能组件,归根结底它们都是AOP罢了,只不过在WebService中有一套已经封装好的组件,并定义好执行顺序等规则。那么对于任意的RPC协议,甚至是同一个微服务内部,任意研发人员都可以自己实现一套’过滤器’或’拦截器’。

如阿里的dobbo、hsf、京东的jsf等RPC协议都封装支持了Filter过滤器组件,并提供了一些可以直接使用的Filter实现类。

3.京东jsf协议过滤器使用实战

创建并实现一个日志打印Filter类,从其上游接口所属包也可以看出这是一个京东完全自建的过滤器类,从其方法体中也可以看出就是一个自行实现的AOP:

1
2
3
4
5
6
7
8
package com.jd.jsf.gd.filter;

import com.jd.jsf.gd.msg.RequestMessage;
import com.jd.jsf.gd.msg.ResponseMessage;

public interface Filter {
ResponseMessage invoke(RequestMessage var1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class JsfLogMessageInterceptor extends AbstractFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JsfLogMessageInterceptor.class);
private ObjectMapper objectMapper = new ObjectMapper();

public JsfLogMessageInterceptor() {
}

public ResponseMessage invoke(RequestMessage requestMessage) {
Object[] originalArgs = requestMessage.getInvocationBody().getArgs();
String args = null;

try {
args = this.objectMapper.writeValueAsString(originalArgs);
if (args.length() > 100) {
args = args.substring(0, 100) + "...";
}
} catch (Exception var22) {
LOGGER.warn("Failed to process args json", var22);
}

Map<Byte, Object> headers = requestMessage.getMsgHeader().getAttrMap();
String className = requestMessage.getClassName();
String methodName = requestMessage.getMethodName();
String alias = requestMessage.getAlias();
long receiveTime = requestMessage.getReceiveTime();
String receiveTimeStr = DateUtils.dateToStr(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
LOGGER.info("Received JSF message. keys={className,methodName,alias,receiveTime,receiveTimeStr,headers,args}", new Object[]{className, methodName, alias, receiveTime, receiveTimeStr, headers, args});
long startTime = System.currentTimeMillis();
ResponseMessage response = this.getNext().invoke(requestMessage);
long endTime = System.currentTimeMillis();
String invokedTime = DateUtils.dateToStr(new Date(endTime), "yyyy-MM-dd HH:mm:ss.SSS");
String result = null;
if (response != null) {
try {
result = this.objectMapper.writeValueAsString(response.getResponse());
if (result.length() > 100) {
result = result.substring(0, 100) + "...";
}
} catch (Exception var21) {
LOGGER.warn("Failed to process response json", var21);
}
}

boolean isProvider = RpcContext.getContext().isProviderSide();
if (isProvider) {
AppInfo providerAppInfo = this.getProviderAppInfo();
AppInfo consumerAppInfo = this.getConsumerAppInfo();
LOGGER.info("JSF invoked. keys={className,methodName,alias,receiveTime,receiveTimeStr,invokedTime,costTime,headers,args,response,isProvider,appIdProvider,appNameProvider,appIdConsumer,appNameConsumer}", new Object[]{className, methodName, alias, receiveTime, receiveTimeStr, invokedTime, endTime - startTime, headers, args, result, isProvider, providerAppInfo.getAppId(), providerAppInfo.getAppName(), consumerAppInfo.getAppId(), consumerAppInfo.getAppName()});
} else {
LOGGER.info("JSF invoked. keys={className,methodName,alias,receiveTime,receiveTimeStr,invokedTime,costTime,headers,args,response}", new Object[]{className, methodName, alias, receiveTime, receiveTimeStr, invokedTime, endTime - startTime, headers, args, result});
}

if (response != null && response.isError()) {
LOGGER.error("Response JSF error. className: " + className + ", methodName: " + methodName, response.getException());
}

return response;
}

private AppInfo getProviderAppInfo() {
String appIdCurrent = String.valueOf(JSFContext.get("appId"));
String appNameCurrent = String.valueOf(JSFContext.get("appName"));
String appInsIdCurrent = String.valueOf(JSFContext.get("appInsId"));
AppInfo appInfo = new AppInfo(appIdCurrent, appNameCurrent, appInsIdCurrent);
return appInfo;
}

private AppInfo getConsumerAppInfo() {
String appIdConsumer = String.valueOf(RpcContext.getContext().getAttachment(".appId"));
String appNameConsumer = String.valueOf(RpcContext.getContext().getAttachment(".appName"));
String appInsIdConsumer = String.valueOf(RpcContext.getContext().getAttachment(".appInsId"));
AppInfo appInfo = new AppInfo(appIdConsumer, appNameConsumer, appInsIdConsumer);
return appInfo;
}
}

在jsf配置文件中为生产方和消费方配置自定义的Filter实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--生产者-->
<jsf:provider id="metaDataService" interface="com.jd.bdaa.arch.provider.service.MetaDataService" alias="${jsf.alias.meta}"
ref="metaDataServiceImpl" server="meta-data-center-jsf-server"
filter="jsfMdcInterceptor, jsfLogMessageInterceptor"/>

<!--消费者-->
<jsf:consumer id="dataAssetMetaService"
interface="com.jd.bdaa.arch.meta.provider.service.DataAssetMetaService"
protocol="jsf"
alias="${jsf.metaDataPull.alias}"
serialization="hessian"
timeout="10000"
filter="jsfMdcInterceptor, jsfLogMessageInterceptor"
/>

参考文献

【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码)