Java EE CDI系列(三)——Interceptor拦截器
前言
CDI
的全称是Contexts And Dependency Injection
,是JavaEE 6标准中一个规范,将依赖注入IOC/DI上升到容器级别, 它提供了Java EE
平台上服务注入的组件管理核心。CDI规范中包含了对拦截器的定义,提供了类似于Spring的AOP功能的工具,让我们可以对托管类进行“非侵入性”、“高灵活性”的方法增强。(拦截器其实最早在JavaEE 5就已经提出来了,只是说CDI规范将拦截器并入的时候再次做了增强。)目前网上很多文章都只是简单介绍了CDI拦截器的使用方式,对于其进阶的注解以及发展背景则很少有提及,本篇文章将尽量系统全面地对CDI拦截器的使用和进阶用法进行演示,希望对各位读者有所帮助。
一、什么是拦截器
拦截器可以理解为是在我们调用某个业务方法之前或者之后,对调用方法的请求进行拦截,然后可以进行其他业务逻辑判断和增强。比如说,我们现在需要对系统中和订单有关的模块方法进行常规日志的打印,手动给每个方法加上日志输出确实是一种解决方案,但假如说后续我们不需要这个日志了,或者说我们对日志的格式需要重新做一下调整,那么我们就不得不花费大量精力人工对每个方法的日志进行修改。
很明显,上述的日志方案我们并不满意,有没有一种更加灵活的方案可以让我们实现这个效果呢?答案自然是有的,CDI规范中提供的拦截器就可以用来解决上面这个问题。我们可以定义一个拦截器,只需要加上一个注解就可以在业务方法的前后进行日志输出的功能增强,即使后面不再需要这个功能了,那么我们只需要修改一下配置文件就可以撤销日志输出的操作,可以说是十分强大的功能了。
二、如何定义一个拦截器
拦截器的定义十分的简单,下面我们以上一小节的案例来做一个代码实现
步骤一:自定义功能注解
定义的@LogEnable
注解表示具有日志输出的功能,其中元注解@InterceptorBinding
表示@LogEnable
注解背后绑定着拦截器
@InterceptorBinding
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogEnable {
}
步骤二:定义拦截器
拦截器类上需要有@Interceptor
注解和@LogEnable
注解,前者表示该类是一个拦截器,后者表示该拦截器和@LogEnable
注解绑定在了一起。
在LogingInterceptor
类中的方法上,我们使用了@AroundInvoke
注解,表明该方法将在拦截点的前后进行增强。在方法中,我们接收一个InvocationContext类型的参数,该参数就是我们所拦截方法的上下文,我们需要手动调用一下proceed()
方法来确保被拦截的方法可以被正常调用。
@LogEnable
@Interceptor // 该注解表明该类是拦截器
public class LogingInterceptor {
/**
* 实现类似于Spring AOP环绕增强的效果
* @param ctx
* @return
* @throws Exception
*/
@AroundInvoke
public Object printLog(InvocationContext ctx) throws Exception {
System.out.println("=======方法执行前打印日志=========");
Object proceed = ctx.proceed();
System.out.println("=======方法执行后打印日志=========");
return proceed;
}
}
步骤三:在业务方法中使用@LogEnable注解
@LogEnable
public class PasswordServiceImpl implements PasswordService {
@Override
public void verifyPassword() {
System.out.println("密码检查无误...");
}
}
步骤四:在resources/META-INF/bean.xml文件中,配置拦截器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
<interceptors>
<class>com.services.cdi.interceptor.LogingInterceptor</class>
</interceptors>
</beans>
步骤五:部署项目进行测试
从截图中可以看到,@LogEnable
注解生效了,我们只在密码检查方法上加了一个注解,但是却可以实现对原有方法的日志增强,且日志的打印输出并没有和原有的方法进行强耦合。
三、拦截器的进阶知识点
(一)拦截器的排序
当一个业务类/方法上同时使用了2个或者更多数量的拦截器,那么这些拦截器会以什么样的顺序来进行执行呢?
一般情况下,会以bean.xml
中定义的拦截器顺序来执行,在拦截器列表中的排序越靠前,说明该拦截器会被越优先调用。我们可以在上一小节的案例基础上来新增一个拦截器,用于校验调用方是否有足够的权限来调用该方法。我们可以参考下面的步骤来进行验证:
步骤一:新增自定义注解@RightCheck
和拦截器RightInterceptor
自定义注解
@InterceptorBinding
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RightCheck {
}
拦截器类
@Interceptor
@RightCheck
public class RightInterceptor {
@AroundInvoke
public Object checkRight(InvocationContext ic) throws Exception {
System.out.println("=========用户权限合法,放行==========");
return ic.proceed();
}
}
步骤二:在业务类/方法上使用@RightCheck
注解
@LogEnable
@RightCheck
public class PasswordServiceImpl implements PasswordService {
@Override
public void verifyPassword() {
System.out.println("密码检查无误...");
}
}
步骤三:配置bean.xml
文档
需要注意,这里的拦截器顺序是1、RightInterceptor, 2、 LogingInterceptor
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
<interceptors>
<class>com.services.cdi.interceptor.RightInterceptor</class>
<class>com.services.cdi.interceptor.LogingInterceptor</class>
</interceptors>
</beans>
步骤四:部署项目进行验证
从控制台的输出结果可知,拦截器的执行顺序确实按照我们定义的顺序一样去执行了。
除了使用bean.xml
进行拦截器的注册外,还有其他方式进行注册吗?
答案是有的,CDI允许我们使用@Priority
注解来注册和定义拦截器的优先级,且如果@Priority
注解和bean.xml
文件都注册了同一个拦截器,那么将会以@Priority
注解定义的优先级为准。但CDI规范中,更加推荐我们使用bean.xml
的方式来注册拦截器,原因是xml文档的维护虽然不如直接使用注解方便,但是胜在直观简洁,我们可以比较快速并清晰地对拦截器的对象和顺序进行了解。
(二)实现多个拦截器的定向绑定与@NonBinding注解的使用
实现多个拦截器的定向绑定
一个自定义注解下可能存在有多个拦截器,如果某些场景下我们只想要让其中某个拦截器生效,我们可以结合自定义注解中的成员变量来解决这个问题。比如@LogEnable
注解下绑定有LogingInterceptor1
、LogingInterceptor2
共两个拦截器,如果正常使用自定义注解的话,那么这两个拦截器方法都会被执行。我们希望在检查密码方法的时候,只进行LogingInterceptor2
拦截器的调用,那么我们可以这么做:
1、给原自定义注解新增成员变量
@InterceptorBinding
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogEnable {
String interceptorName() default "LogingInterceptor1"; // 表示拦截器名称
}
2、给LogingInterceptor2
类配置interceptorName的值
@Interceptor
@LogEnable(interceptorName = "LogingInterceptor2")
public class LogingInterceptor2 {
@AroundInvoke
public Object printLog(InvocationContext ic) throws Exception {
System.out.println("=========LogingInterceptor2在方法前打印日志=========");
Object result = ic.proceed();
System.out.println("=========LogingInterceptor2在方法后打印日志=========");
return result;
}
}
3、在业务方法上使用匹配的自定义注解
@LogEnable(interceptorName = "LogingInterceptor2")
@RightCheck
public class PasswordServiceImpl implements PasswordService {
@Override
public void verifyPassword() {
System.out.println("密码检查无误...");
}
}
4、部署项目验证结果
可以看到,虽然我们@LogEnable
注解下一共绑定了2个拦截器,但是我们可以通过成员变量的方式来标识当前自定义注解将会启用哪个拦截器。
@NonBinding注解的引入
上面的方案虽然可以解决我们的问题,但是我们知道,在实际应用中,我们很多自定义的注解中会有和具体的业务逻辑相关的成员变量,比如下面的自定义注解,很明显我们不希望在定位拦截器的时候,让logLevel
、conversionPattern
这些无关的成员变量也作为定位依据。
@InterceptorBinding
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogEnable {
String interceptorName() default "LogingInterceptor1";
String logLevel() default "DEBUG";
String conversionPattern() default "yyyy-MM-dd";
}
@NonBinding
注解就可以帮助我们解决这个问题,@NonBinding
注解用于标识注解中哪些成员变量不作为拦截器的绑定条件。比如我们只想让成员变量interceptorName作为拦截器的绑定条件,其他条件作为普通的成员变量,那么可以如同下面一样对原注解进行修改:
@InterceptorBinding
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogEnable {
String interceptorName() default "LogingInterceptor1";
@Nonbinding String logLevel() default "DEBUG";
@Nonbinding String conversionPattern() default "yyyy-MM-dd";
}
(三)@Interceptors注解的使用
CDI提供了@Interceptors
注解来简化我们对拦截器的使用,我们之前对于拦截器的使用流程,都是自定义注解+拦截器+注册拦截器来实现的。但如果使用@Interceptors
注解,我们则可以直接省略自定义注解和注册拦截器这两个步骤,直接在方法上标明这个方法将会被哪些拦截器拦截。以下面的代码为例,定义了拦截器后,直接在要拦截的业务类/方法上面注明该类/方法将被哪些拦截器拦截。
@Interceptors({LogingInterceptor.class, RightInterceptor.class})
public class PasswordServiceImpl implements PasswordService {
@Override
public void verifyPassword() {
System.out.println("密码检查无误...");
}
}
验证结果如下:拦截器可以正常执行,执行的顺序为我们在注解中定义的顺序。
注意事项:
(1)需要注意的是,当@Interceptor注解应用在类上的时候,该类中的所有被调用的方法都会被拦截器拦截到。
(2)@Interceptor
注解虽然十分的方便简单,但拦截器的指定是硬编码写在代码中的,不是特别灵活。譬如现在有几百个类都使用这个注解来添加日志拦截器,那我一旦想要取消,就得逐个类去删除了。所以CDI更加推荐我们使用自定义注解这种方式来绑定拦截器。
四、常见问题Q&A
(一)越早执行的拦截器是越早结束吗?
不是。越早开始的拦截器往往是最后才结束运行的。可以参考下方的拦截器执行顺序图,最终方法将依次执行拦截器A的前置方法 -> 拦截器B的前置方法 -> 业务方法 -> 拦截器B的后置方法 -> 拦截器A的后置方法。
(二)CDI的拦截器和Spring AOP的区别
整篇文章看下来,相信大家或多或少会对CDI拦截器的作用和Spring AOP进行对比,事实上二者确实在功能上有一定的相似之处:可以对指定的方法进行低耦合的增强,但二者还是有一定的区别的:
Spring AOP的耦合度更低,而且可以实现批量增强某组方法。spring aop支持我们使用切面表达式来一次性增强满足条件的所有方法,且被增强的方法是无感知的!而相比于CDI的拦截器的话,其实还是要写一些注解在被增强的类/方法上的,而且也做不到将某个拦截器批量应用某组方法上。
Spring AOP的拦截点更加多样,Spring AOP除了支持前置/后置/环绕通知之外,还支持异常抛出通知和最终通知。而CDI的拦截器只支持三种拦截点:业务方法的环绕增强、Bean生命周期回调以及EJB Bean的超时方法回调。其中后面两种拦截点比较少会用到,所以本篇文章就没怎么提及。当然了,无论是AOP还是CDI的拦截器,其实都足以满足大多数场景的使用了。
相对来说,CDI拦截器的学习曲线要高于Spring AOP,而且CDI属于JAVA EE中定义的规范,在标准的JAVA EE项目中,建议优先使用CDI的拦截器来做方法增强。
(三)有什么方法可以快捷地看到业务方法上对应的拦截器
很遗憾ヽ(ー_ー)ノ,除非使用的是业务类/方法上使用了@Interceptors
注解来定义拦截器,否则并没有直接的途径可以看那些自定义注解所绑定的拦截器。最好的方式可能还是全局查询来找一下...
五、小拓展
JavaEE 5的Interceptor和JavaEE 6的Interceptor有什么区别呢?
在 Java EE 5 (EJB 3.0)中,Interceptor就开始被引入了,我们可以通过简单的注释实现托管Bean一些简单的AOP操作。在 Java EE 6 中,拦截器被并入到了CDI 1.0之中,在更高级别进行了抽象,以便它可以更通用地应用于平台中更广泛的规范集。从下面的SDK文档中,我们可以大致看到这段期间的进展:
1、新增了@Interceptor
、AroundTimeout
注解,提升了拦截器使用的灵活性
2、增强了InvocationContext接口
JavaEE 5
JavaEE 6
参考资源:
- javaee6 SDK文档:https://docs.oracle.com/javaee/6/api/javax/interceptor/InvocationContext.html
- javaee5 SDK文档:https://docs.oracle.com/javaee/5/api/javax/interceptor/InvocationContext.html
-
JBOSS
官方文档:https://docs.jboss.org/weld/reference/latest/en-US/html/ -
J2EE
官方文档:https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3.0.html#introduction