横切关注点
在应用中分布于多处的与业务逻辑代码无关的处理逻辑,最经典的就是日志记录,记录每个请求以及请求的时间。这些代码和业务逻辑混合在一起造成了我们的代码结构混杂以及难以维护的后果。
如果能把这些关注点全部抽取出来那不是很好么,因此想到之前java的动态代理和cglib代理,其实这并不是很难的事情。
代理类拦截方法,记录日志,然后调用业务逻辑方法。
而这件事情,而专门干这件事情的就是AOP(面向切面)。
面向切面编程
将系统散布于各处的相同处理逻辑抽取出来这就是面向切面。
连接点
其实就是一个点,类比动态代理就是所要调用的真正的业务方法,但并不仅限方法,也可以是修改一个字段的,或者说throw时。它是一个处理时机。用记录日志来说,我们想要在系统中比较敏感的地方记录日志,比如说用户中心的一些操作,那么所有用户中心模块下的类方法都是需要被切面作用(记录日志)的连接点。
切点
上面说了,用户中心模块下的所有方法都需要被切面作用,而切点就定义了这些点,和其他模块的类方法分离出来。这就像切蛋糕一样,将一个完整的蛋糕(程序)切成(切成)了好几块,而只有一个蛋糕(用户模块)是你关注的,或者说属于你的(被切面作用)。简单说切点就是定义了切面能够切入的点(在蛋糕的哪个地方开始切,也就是何处)。
通知
通知就是整个切面中需要做的东西了,也就是我们的记录日志,但是想想,我们记录用户的敏感操作是要在用户操作前记录,还是操作后,或者操作前后都需要?因此通知同时也定义了切面时机。
介于一件事情的时机,我们定义了五种通知。
before-----在方法调用前。
after-----在方法调用后,不管方法成功与否。
after-returning-----在方法成功执行后。
after-throwing-----在方法抛出异常时。
around-----在方法的前后都需要,around和before和after的不同是,after和before是单独的分离成了两个方法,而around方法是单独的around方法中包含了before,after以及业务方法。
切面
整个切面编程最重要的就是切面的概念,它由切点和通知组成,因此,可以将它看成一个概念,口语化来说,切面定义了在何处何时干些什么事情。其中何处指的是切点,何时干些什么指的是通知。
引入
引入允许我们像现有的类添加新方法和属性,这很像C#中的扩展方法,一个类在不破坏原有的结构上,引入新的方法,在外界看来这些方法就是他自身的,但是我们知道,其实真正的实现是另外一个实现类。只需要做一些小手脚。
织入
上文说过,切面由切点作用于连接点,那么如何作用进去,创建新的代理类,这就是织入作的事情,和引入不同的是,引入是添加原来没有的方法和属性,而织入是在原有的方法上面将通知编织进去。
spring前提:
spring自身的aop实现是基于java的动态代理,而java动态代理是面向接口代理的,也就是说在运行期,spring会根据代理接口动态的把切面织入到代理接口的实现类当中,该实现类拦截了你所定义的切点。
以及因为spring aop自身是面向接口,所以当面向类代理的时候,同样的我们需要采用AspectJ的CGLib代理来实现。
spring自身对aop的实现并不是很好,他只支持方法连接点,而对于构造函数(因为构造函数比较特殊,所以单独算一个点)以及成员变量的修改,spring aop集成了AspectJ来实现。
不仅仅在这一方面,spring aop中大量的与AspectJ交织在一起,下文会提到。
1.编程式spring动态代理织入切面
spring-aop默认使用java的面向接口代理,我们用ProxyFactoryBean来创建代理实现类,我们用来看如下代码。
必不可少的代理接口。
public interface Cake {
//切蛋糕方法
void cut();
}
public class CakeImpl implements Cake {
public void cut(){
System.out.println("切蛋糕了。。");
}
}
spring遵守AOP联盟规范,实现了相关的接口,这里就不具体展开了。
public class CakeAdvisor implements MethodBeforeAdvice,AfterReturningAdvice,ThrowsAdvice {
public void afterThrowing(Exception ex){
System.out.println("蛋糕切坏了。。。");
}
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("我要水果多的那一块蛋糕。。。");
}
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("切好蛋糕拿走之前。。。");
}
}
测试代码
ProxyFactoryBean bean = new ProxyFactoryBean();
bean.setTarget(new CakeImpl());//设置具体的实现类
bean.setInterfaces(Cake.class);//设置接口
bean.addAdvice(new CakeAdvisor());//设置通知
//在get的时候java动态生成代理接口的实现类返回,所以强转的是接口,而不是实现类。这也是我们说的基于接口的代理。
Cake ck = (Cake)bean.getObject();
ck.cut();
输出
我要水果多的那一块蛋糕。。。
切蛋糕了。。
切好蛋糕拿走之前。。。
然后bean.getObject(),断点调试验证是真的用了java的动态代理技术。
ok,最底层调试我们看到InvocationHandler就应该就清楚了,到底Spring自身是如何实现aop的了。那么最后无论我们执行任何方法必然都是委托给了invoke。
2.xml申明式spring动态代理织入切面
<!--aop配置顶点节点-->
<aop:config>
<!-- 切点,这里切点定义的无论是接口(Cake)还是实现类(CakeImpl)都没有太大的关系,因为是根据接口动态生成实现类的。因此只要代码中用接口去接受动态实现类就可以了。 -->
<aop:pointcut id="ckPoint" expression="execution(* demo.entity.Cake.*(..))" />
<!-- 切面 -->
<aop:aspect id="ckAspect" ref="ckAspect">
<!-- <aop:类型通知 对应了五种通知类型,method指向切面的方法 -->
<aop:before method="before" pointcut-ref="ckPoint"/>
<aop:after method="after" pointcut-ref="ckPoint"/>
<aop:around method="around" pointcut-ref="ckPoint"/>
<aop:after-throwing method="throwEx" pointcut-ref="ckPoint"/>
<aop:after-returning method="afterReturning" pointcut-ref="ckPoint"/>
</aop:aspect>
</aop:config>
aop:config 标签包裹了所有面向切面的内容,是一个顶点节点,同时spring会为每个aop:aspect切面在运行时生成一个面向接口的动态实现类。
上文说过,spring集成了AspectJ框架,来完善spring的aop不足。
而最突出这一点的就是spring借助了AspecJ的切点表达式,在这个配置文件中就是<aop:pointcut>标签中的expression中的值,因此此时我们必须引入Aspect包了。
这里需要说明一点的是,around方法比较特殊,就像最开始说的,它包含了前置和后置以及具体调用的业务逻辑方法,这里我为了区分明显一点,所以选择了打印输出,但是实际上你完全可以换成前置和后置方法。
同时around方法你可以看到我们传入了ProceedingJoinPoint类的实例,该类在 org.aspectj.lang 包下,属于AspectJ,
但是实际上他真正的类型是 org.springframework.aop.aspectj 包下的 MethodInvocationProceedingJoinPoint 实例,同时,你找到这个类可以看到他实现了AspectJ的ProceedingJoinPoint
从这一点也可以看出spring-aop和Aspectj紧紧的结合在了一起。
下面是切面类。
@Component
public class CkAspect{
public void before(){
System.out.println("before 我要水果多的那一块蛋糕。。。");
}
public void after(){
System.out.println("after 蛋糕切好后了。。。");
}
public void afterReturning(){
System.out.println(" afterReturning 切好蛋糕拿走之前。。。");
}
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before...");
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
throw throwable;
}
System.out.println("around after...");
return result;
}
public void throwEx(){
System.out.println("蛋糕切坏了。。");
}
}
另外既然还是动态代理,那么下面getBean依旧会跟到JdkDynamicAopProxy的invoke方法中。
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
//面向接口,因此用接口来接受,而且get的时候才返回动态实现类。
Cake cake = (Cake)applicationContext.getBean("cakeImpl");
cake.cut();
测试代码打印如下。
before 我要水果多的那一块蛋糕。。。
around before...
切蛋糕了。。
afterReturning 切好蛋糕拿走之前。。。
around after...
after 蛋糕切好后了。。。
其实你把配置中的通知类型位置换一下,输出就会不一样,不过这到底是为什么暂时还不清楚,有清楚这个问题的原因的道友请务必留言让我请求一下!感谢!
好了,上面所做的全部都是用的java基于接口的代理,因此我们的测试代理中一直用的都是接口来接受,但是如果我们真的要用实现类来接受怎么办呢?
这时候就要用AspectJ的CGlib代理来实现,AspectJ会在编译时会把我们的切面织入到实现类的子类当中,生成一个子类。而这时,我们的实现类就是代理类的子类,所以就可以用子类来接受了。
spring配置文件中加上下面这句。
<!--默认为false,表示是用面向接口代理,设为true之后用aspectj的CGLib基于类的代理-->
<aop:aspectj-autoproxy proxy-target-class="true" />
测试代码改为
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
//我们换成了实现类来接受,但是要知道,真正的类型是实现类的子类。
CakeImpl cake = (CakeImpl)applicationContext.getBean("cakeImpl");
cake.cut();
输出
before 我要水果多的那一块蛋糕。。。
around before...
切蛋糕了。。
afterReturning 切好蛋糕拿走之前。。。
around after...
after 蛋糕切好后了。。。
然后我们再来看下这张图。
以上是对于spring实战的部分梳理笔记。
欢迎指错改正~