在前面我们已经提到了前置、后置、环绕、最终、异常等增强形式,它们的增强对象都是针对方法级别的,而引介增强,则是对类级别的增强,我们可以通过引介增强为目标类添加新的属性和方法,更为诱人的是,这些新属性或方法是可以根据我们业务逻辑需求而动态变化的。怎么来理解这一点?我们先展示一个用引介增强解决的现实需求问题,现在先来看看我们的一个需求:
我们要设计一个定时任务,在每天特定流量高峰时间里,判断人们某个网络请求在服务端程序业务逻辑处理上的耗时,一般地,我们web后端处理请求处理按如下时序图执行:
现在,即需要我们统计从接收请求后到发出相应前这个过程的耗时。
最直接的解决方法就是,在Controller层的每一个方法起始到结束,分别记录一次时间,用结束减起始来获得结果,但这存在两个问题:
1. 根据springMVC一个方法对应一种请求,如果我们的controller中有很多方法,而我们需要为每一个方法都进行统计,这意味者我们需要在controller中嵌入大量重复冗杂的代码,并且这些代码和我们controller层逻辑没有任何关系,如果日后我们根据需求要添加其他任务,就又要修改我们的controller,这不符合开闭原则和职责分明原则。
2. 因为我们的需求是动态的,我们还可能需要在controller中每次调用都去判断当前时刻是否处于需要统计耗时的时刻。
下面看看我们如何用引介增强来解决同样的问题:
public class UserController {
public void login(String name){
System.out.println("I'm "+name+" ,I'm logining");
try {
Thread.sleep(100);//模拟登陆过程中进行了数据库查询。各种业务逻辑处理的复杂工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public interface TaskActiver {//通过此接口来监控是是否激活定时器任务
void startTask();//满足特定条件,开启任务
void stopTask();//不满足特定条件,停止任务
}
public class MyTaskController extends DelegatingIntroductionInterceptor implements TaskActiver{
/* 1. DelegatingIntroductionInterceptor是引介增强接口IntroductionInterceptor的实现类,我们通过扩展此接口自定义引介增强类 2. 引介增强类要实现我们的代理接口TaskActiver,这是和其他增强不同的地方。 */
private ThreadLocal<Boolean> isTime = new ThreadLocal<Boolean>();//这是一个线程安全的变量。
public MyTaskController(){//定时任务控制器构造函数
isTime.set(false);//任务默认处于关闭状态
}
@Override
public void startTask() {//通过定时器监控达到特定时间,启动任务。
isTime.set(true);
}
@Override
public void stopTask() {//在任务完成后关闭任务,等待下次定时器到时启动
isTime.set(false);
}
/** * //覆盖父类的invoke方法,当程序运行到特定方法时,我们先拦截下来, * 然后启动我们的任务,再调用相应的方法,在方法调用完后,完成并关闭我们的任务 */
@Override
public Object invoke(MethodInvocation invocation) throws Throwable{
Object obj = null;
if(isTime.get()){//到了特定时刻,即我们的定时器处于任务激活状态。
MyTask.getTarget().set(invocation.getMethod().getName());
//通过反射获取运行期当前任务的名称,并初始化我们的任务配置,在这里我们的任务是记录时间
MyTask.beginRecord();//任务开始,记录开始时间
obj = super.invoke(invocation);//调用原目标对象方法
MyTask.endRecord();//结束任务,结合开始时间计算耗时,并将统计结果保存到日志记录文本中
}else{
obj = super.invoke(invocation);
}
return obj;
}
}
/********************下面是我们的任务操作类***********************/
//这里是记时操作,如果以后我们有其他类似业务需求,可以修改此类,而不是像我们开始提到的那样直接写在controller中
/* MyTask是单例类,为确保每个线程有自己独立的时间记录和方法名,将下面两个静态变量定义为ThreadLocal类型。 */
private static ThreadLocal<Long>beginTime = new ThreadLocal<Long>() ;//记录开始时间
private static ThreadLocal<String> target = new ThreadLocal<String>();//记录调用方法
public static void beginRecord(){
System.out.println("记时开始");
beginTime .set(System.currentTimeMillis());
}
public static void endRecord(){
System.out.println("记时结束");
System.out.println("调用"+target+"方法耗时:" +( System.currentTimeMillis() - beginTime.get())+ "毫秒");
}
}
- 首先谈谈ThreadLocal是什么东东,当我们在多线程中共享一个对象时,对象里的成员属性是有状态的,只要一个线程对成员属性进行了修改,在其他线程也会生效,如果我们想每个线程都共享一个对象,但不共享里面的成员属性,就可以使用ThreadLocal, 它会为我们的每一个线程创建一个当前成员属性的拷贝,即使我们任一线程对其进行了修改,也不会影响到其他线程。这就好像几个朋友(线程)一起住在(共享)一间大房子(对象)但每个人都有自己独立的(一模一样的)房间(成员属性)即使我们把自己的房间拆了,也不会影响到其他人。
- 对于以上三个ThreadLocal变量中的beginTime和target我们可能较好理解,但为什么我们要把isTime也射程ThreadLocal型?这是考虑我们可能要利用MyTaskController是单例的,如果我们要调度其他定时任务(而不是这里简单地记录前后耗时),为确保不同引介增强的“定时状态“不互相共享,我们必须确保它是线程安全的。
- 那么可能又问:为什么不干脆直接将MyTaskControoler设成prototype?这首先要明确,MyTaskController是一个代理类,由CGLib创建,而CGLib创建代理的性能是很低的,如果每次注入(如getBean())获取代理实例,都返回一个新的实例,将会严重影响性能。
<bean id="myTaskController" class="test.aop4.MyTaskController"/>
<bean id="proxyFactory" class="org.springframework.aop.framework.ProxyFactoryBean" >
<!-- 使用CHLib代理,配合引介增强通过创建子类来生成代理,默认值为false会报错 -->
<property name="proxyTargetClass" value="true" />
<property name="interfaces" value="test.aop4.TaskActiver" /><!--注册代理接口,我们的代理增强类必须实现此接口-->
<property name="targetClass" value="test.aop4.UserController"/><!--这是我们的目标对象,通过指定目标对象,我们可以在注入时,直接将当前代理工厂Bean强制转换为我们的UserController。从而生成我们的代理子类-->
<property name="interceptorNames" value="myTaskController" /><!--字面意思是拦截器名称,实现效果类似于拦截器,把我们目标类的所有行为(方法)都拦截下来,织入我们的增强(体现在invoke函数的重写中)-->
</bean>
public static void main(String args[]) throws InterruptedException{
ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:test/aop4/aop.xml");
UserController userController = (UserController) ac.getBean("proxyFactory");//通过代理工厂来生成我们的目标类
SimpleDateFormat sdf = new SimpleDateFormat("HHmm");
TaskActiver taskActiver = (TaskActiver) userController;
userController.login("zenghao");//这里方便测试,没有开启任务,先在正常情况下模拟用户登陆请求
while(true){//这里方便测试,一般情况,我们应另开一个线程单独来运行我们的定时任务部分
//定时任务部分开始
Integer now = Integer.valueOf(sdf.format(new Date()));//以时分格式获取当前时间
if(now > 0000&& now < 0030 ){
//在特定时间内开启任务,咱们不妨假设这个时间就是双十一晚的12点到12点半。淘宝要实施监控统计数据
taskActiver.startTask();
}else if(now <= 0000 && now >= 0030){//在特定时间外关闭任务
taskActiver.stopTask();
}
//定时任务部分结束
//下面模拟正常的web请求,假设每隔5秒中,服务器接受一个web请求。
Thread.sleep(5 * 1000);
userController.login("zenghao");
}
}
I’m zenghao ,I’m logining ——————-这是任务没开始前的web请求
记时开始
I’m zenghao ,I’m logining —————-这是满足特定时间开启任务后的web请求
记时结束
调用java.lang.ThreadLocal@17d8986方法耗时:101毫秒
记时开始
记时结束
调用java.lang.ThreadLocal@17d8986方法耗时:0毫秒 ——-代理增强类中使用ThreadLocal变量的时间也被记录下来,
记时开始
I’m zenghao ,I’m logining —————-这是满足特定时间开启任务后的web请求
记时结束
调用java.lang.ThreadLocal@17d8986方法耗时:101毫秒
TaskActiver taskActiver = (TaskActiver) userController;
即将我们的代理接口引用指向了我们的目标对象,这个时候我们操纵我们的代理接口,实际上由多态特性,我们操纵的是实现了此接口的增强类MyTaskController,于是,核心部分来了:我们的增强类”涵盖“了我们的目标对象(注意到我们的taskActiver是由userController强转而来的),但他又有了许多新的特性,比如我们在MyTaskController中定义的isTime变量,或者简介调用了MyTask中的方法(实际上可以完全不要MyTask类,直接将里面的变量方法定义在MyTaskController中)。总之,我想说的是,我们通过引介增强,可以: 除了上述方法外,我们还可以通过@AspectJ和基于Schema的配置来实现引介增强。不过通过测试,通过以上两种方法,由于不能实现重写invoke方法的横切逻辑,增强效果大大减弱,它仅能为目标类添加冬天添加新的实现接口,却不能无侵入式地修改原目标类方法的调用效果,比如我再调用login方法,不能通过引介增强来实现耗时统计的功能了,除非使用环绕增强加上一定的业务逻辑处理。下面我们先看用@AspectJ配置来分析说明
使用注解@DeclareParents,相当于IntroductionInterceptor,该注解有两个属性:
- value:定义切点
- defaultImpl:指定默认的接口实现类。
下面是代码示例:
相对之前例子没有变化
public class UserController {
public void login(String name){
System.out.println("I'm "+name+" ,I'm logining");
try {
Thread.sleep(100);//模拟登陆过程中进行了数据库查询。各种业务逻辑处理的复杂工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
相对之前例子,代理接口没有变化,而代理接口实现(增强)类则不再继承DelegatingIntroductionInterceptor类。
/*********************代理接口*********************/
public interface TaskActiver {//通过此接口来监控是是否激活定时器任务
void startTask();
void stopTask();
}
/*********************代理接口实现类*********************/
public class MyTaskController implements TaskActiver{
private ThreadLocal<Boolean> isTime = new ThreadLocal<Boolean>();
public MyTaskController(){
isTime.set(false);
}
@Override
public void startTask() {//通过定时器监控达到特定时间,启动任务。
System.out.println("任务开启");
isTime.set(true);
}
@Override
public void stopTask() {//在任务完成后关闭任务,等待下次定时器到时启动
System.out.println("任务关闭");
isTime.set(false);
}
}
@Aspect
public class MyTaskAspect {
//value指向我们的目标对象,defaultImpl指向我们的代理接口实现类
@DeclareParents(value = "test.aop5.UserController",defaultImpl = MyTaskController.class)//注解在我们的代理接口上
public TaskActiver taskActiver;//声明代理接口
}
<aop:aspectj-autoproxy proxy-target-class="true"/><!-- 使切面注解@AspectJ生效 -->
<bean id="userController" class="test.aop5.UserController" /><!-- 测试需要注入的Bean-->
<bean class="test.aop5.MyTaskAspect" /><!--注册切面,使IOC容器自动配置AOP-->
public static void main(String args[]) throws InterruptedException{
ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:test/aop5/aop.xml");
UserController userController = (UserController) ac.getBean("userController");
TaskActiver taskActiver = (TaskActiver) userController;//①强制转换成功,这是java多态中的向上转型。
System.out.println(taskActiver instanceof UserController);//②
System.out.println(userController instanceof TaskActiver);//③
taskActiver.startTask();//④
userController.login("zenghao");//⑤
}
运行测试代码后,控制台打印信息:
true
true
任务开启
I’m zenghao ,I’m logining
接下来我们分析测试方法中注释编号相应的代码行:
1. ①中使用了向上转型,向上转型会使父类(taskTactiver)丢失了子类(userController)的方法,但这里的关键是,taskActiver成为了userController的父类,这是通过引介增强实现的,即我们为UserController添加了新的实现接口taskActiver
2. ②结果为true是显然的,因为我们的taskActiver就是由userController转型而来
3. ③恰恰验证了我们①中的结论:taskActiver成为了userController的父类
4. ④我们通过父类接口调用其增强实现类中的startTask方法,这点可以通过打印信息“任务开启”来说明。在这里,我们也得到一个结论,我们为目标类引介新的代理接口,实际上是让目标类新增了新的代理接口的增强实现类中的功能
5. ⑤模拟我们的web请求,这里不再像我们开始例子那样,能够实现耗时统计,尽管我们在④中开启了任务(事实上,从增强实现类的定义来看,这是显然的结果),也就是说,我们只是为目标对象添加了新的接口,但要使用相应接口的功能还要通过向上转型来完成,而向上转型后父接口也丢失了目标对象的属性方法,因而两者是相对独立的。从这个角度讲,我个人觉得接引增强的这种配置实现实际意义并不大。
使用这种方法我们主要需要配置<aop:declare-parents>
标签属性。同上例的实现效果,使用基于Schema的配置,可去除切面类MyTaskAspect,同时重新配置xml文件:
<bean id="userController" class="test.aop6.UserController" />
<aop:config proxy-target-class="true">
<aop:aspect>
<aop:declare-parents types-matching="test.aop6.UserController" implement-interface="test.aop6.TaskActiver" default-impl="test.aop6.MyTaskController" />
</aop:aspect>
</aop:config>
运行同样的测试文件,我们会得到相同的结果。
在这里,我们要注意:
1. 一定要将proxy-target-class配置成true,否则引介增强配置失败,会报异常:java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to test.aop6.UserController
2. types-matching指向我们的目标对象类,这里可以使用我们的AspctJ统配符如+、*、和..等通配符
3. implement-interface指向我们的代理接口
4. default-impl指向我们实现了代理接口的增强类。这里使用全限定名的方式
5. 里的还有另一个属性delegate-ref,它指向我们注册好的另外一个Bean名称
6. 因为我们引介增强是类级别的,不用专门配置切面,即也无需在<aop:aspect>
中声明ref=”切面Bean名称”
本博文提到的示例源码请到我的github仓库https://github.com/jeanhao/spring的aop分支下载。