
在spring aop还没出现之前,想要在目标方法之前先后加上日志打印的功能,我们一般是这样做的:
@Service
public class TestService {
public void doSomething1() {
beforeLog();
System.out.println("==doSomething1==");
afterLog();
}
public void doSomething2() {
beforeLog();
System.out.println("==doSomething1==");
afterLog();
}
public void doSomething3() {
beforeLog();
System.out.println("==doSomething1==");
afterLog();
}
public void beforeLog() {
System.out.println("打印请求日志");
}
public void afterLog() {
System.out.println("打印响应日志");
}
}
如果加了新doSomethingXXX方法,就需要在新方法前后手动加beforeLog和afterLog方法。
原本相安无事的,但长此以往,总有会出现几个刺头青。
刺头青A说:每加一个新方法,都需要加两行重复的代码,是不是很麻烦?
刺头青B说:业务代码和公共代码是不是耦合在一起了?
刺头青C说:如果有几千个类中加了公共代码,而有一天我需要删除,是不是要疯了?
spring大师们说:我们提供一套spring的aop机制,你们可以闭嘴了。
下面看看用spring aop(偷偷说一句,还用了aspectj)是如何打印日志的:
@Service
public class TestService {
public void doSomething1() {
System.out.println("==doSomething1==");
}
public void doSomething2() {
System.out.println("==doSomething1==");
}
public void doSomething3() {
System.out.println("==doSomething1==");
}
}
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(public * com.sue.cache.service.*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void beforeLog() {
System.out.println("打印请求日志");
}
@After("pointcut()")
public void afterLog() {
System.out.println("打印响应日志");
}
}
增加了LogAspect
类,在类上加了@Aspect
注解。先在类中使用@Pointcut
注解定义了pointcut方法,然后将beforeLog和afterLog方法移到这个类中,分别加上@Before
和@After
注解。
改造后,业务方法在TestService类中,而公共方法在LogAspect类中,是分离的。如果要新加一个业务方法,直接加就好,LogAspect类不用改任何代码,新加的业务方法就自动拥有打印日志的功能,是不是很神奇?

spring aop其实是一种横切的思想,通过动态代理技术将公共代码织入到业务方法中。
这里出于5毛钱的友情,有必要温馨提醒一下。aop是一种思想,不是spring独有的,目前市面上比较出名的有:
- aspectj
- spring aop
- jboss aop
我们现在主流的做法是将spring aop和aspectj结合使用,spring借鉴了AspectJ的切面,以提供注解驱动的AOP。
此时,一个黑影一闪而过。
刺头青D问:你说的“横切”,“动态代理”,“织入” 是什么东东?
几个重要的概念
根据上面spring aop的代码,用一张图聊聊几个重要的概念:

- 连接点(Joinpoint) 程序执行的某个特定位置,如某个方法调用前,调用后,方法抛出异常后,这些代码中的特定点称为连接点。
- 切点(Pointcut) 每个程序的连接点有多个,如何定位到某个感兴趣的连接点,就需要通过切点来定位。
- 通知(Advice) 增强是织入到目标类连接点上的一段程序代码。
- 切面(Aspect) 切面由切点和通知组成,它既包括了横切逻辑的定义,也包括了连接点的定义,SpringAOP就是将切面所定义的横切逻辑织入到切面所制定的连接点中。
- 目标对象(Target) 需要被增强的业务对象
- 代理类(Proxy) 一个类被AOP织入增强后,就产生了一个代理类。
- 织入(Weaving) 织入就是将增强添加到对目标类具体连接点上的过程。
还是那个刺头青D说(旁边:这位仁兄比较好学):spring aop概念弄明白了,挺简单的。@Pointcut
注解的execution
表达式刚刚看得我一脸懵逼,可以再说说吗,我请你吃饭?
切入点表达式
@Pointcut
注解的execution
切入点表达,看似简单,里面还是有些内容的。为了更直观一些,还是用张图来总结一下:

该表达式的含义是:匹配访问权限是public,任意返回值,包名为:com.sue.cache.service,下面的所有类所有方法和所有参数类型。图中所有用*表示,比如图中类名用.*表示的是所有类。如果具体匹配某个类,比如:TestService,则表达式可以换成:
@Pointcut("execution(public * com.sue.cache.service.TestService.*(..))")
其实spring支持9种表达式,execution
只是其中一种。

有哪些入口?
先说说我为什么会问这样一个问题?
spring aop有哪些入口?说人话就是在问:spring中有哪些场景需要调用aop生成代理对象,难道你不好奇吗?
入口1
AbstractAutowireCapableBeanFactory类的createBean方法中,有这样一段代码:

它通过BeanPostProcessor提供了一个生成代理对象的机会。具体逻辑在AbstractAutoProxyCreator类的postProcessBeforeInstantiation方法中:
说白了,需要实现TargetSource
才有可能会生成代理对象。该接口是对Target
目标对象的封装,通过该接口可以获取到目标对象的实例。
不出意外,这时,又会冒出一个黑影。
刺头青F说:这里生成代理对象有什么用呢?
有时我们想自己控制bean的创建和初始化,而不需要通过spring容器,这时就可以通过实现TargetSource
满足要求。只是创建单纯的实例还好,如果我们想使用代理该怎么办呢?这时候,入口1的作用就体现出来了。
入口2
AbstractAutowireCapableBeanFactory类的doCreateBean方法中,有这样一段代码:

它主要作用是为了解决对象的循环依赖问题,核心思路是提前暴露singletonFactory到缓存中。
通过getEarlyBeanReference方法生成代理对象:

它又会调用wrapIfNecessary方法:

这里有你想看到的生成代理的逻辑。
这时。。。。,你猜错了,黑影去吃饭了。。。
入口3
AbstractAutowireCapableBeanFactory类的initializeBean方法中,有这样一段代码:

它会调用到AbstractAutoProxyCreator类postProcessAfterInitialization方法:

该方法中能看到我们熟悉的面孔:wrapIfNecessary方法。从上面得知该方法里面包含了真正生成代理对象的逻辑。
这个入口,是为了给普通bean能够生成代理用的,是spring最常见并且使用最多的入口。
下面为了加深印象,用一张图总结一下:

jdk动态代理 vs cglib
我猜你们对jdk动态代理和cglib是知道的(即使猜错了也不会少块肉😃),但为了照顾一下新朋友,还是有必要把这两种生成代理的方式拿出来说说。
jdk动态代理
jdk动态代理是通过反射技术实现的,生成代理的代码如下:
public interface IUser {
void add();
}
public class User implements IUser{
@Override
public void add() {
System.out.println("===add===");
}
}
public class JdkProxy implements InvocationHandler {
private Object target;
public Object getProxy(Object target) {
this.target = target;
return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target,args);
after();
return result;
}
private void before() {
System.out.println("===before===");
}
private void after() {
System.out.println("===after===");
}
}
public class Test {
public static void main(String[] args) {
User user = new User();
JdkProxy jdkProxy = new JdkProxy();
IUser proxy = (IUser)jdkProxy.getProxy(user);
proxy.add();
}
}
首先要定义一个接口IUser,然后定义接口实现类User,再定义类JdkProxy实现InvocationHandler
接口,重写invoke方法,该方法中实现额外的逻辑。当然,别忘了在getProxy方法中,用Proxy.newProxyInstance
方法创建一个代理对象。
jdk动态代理三个要素:
- 定义一个接口
- 实现InvocationHandler接口
- 使用Proxy创建代理对象
cglib
cglib底层是通过asm字节码技术实现的,生成代理的代码如下:
public class User {
public void add() {
System.out.println("===add===");
}
}
public class CglibProxy implements MethodInterceptor {
private Object target;
public Object getProxy(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = method.invoke(target,objects);
after();
return result;
}
private void before() {
System.out.println("===before===");
}
private void after() {
System.out.println("===after===");
}
}
public class Test {
public static void main(String[] args) {
User user = new User();
CglibProxy cglibProxy = new CglibProxy();
IUser proxy = (IUser)cglibProxy.getProxy(user);
proxy.add();
}
}
这里不需要定义接口,直接定义目标类User,然后实现MethodInterceptor
接口,重写intercept
方法,该方法中实现额外的逻辑。当然,别忘了在getProxy方法中,通过Enhancer创建代理对象。
cglib两个要素:
- 实现MethodInterceptor接口
- 使用Enhancer创建代理对象
spring中如何用的?
DefaultAopProxyFactory类的createAopProxy方法中,有这样一段代码:

它里面包含:
- JdkDynamicAopProxy jdk动态代理生成类
- ObjenesisCglibAopProxy cglib代理生成类
JdkDynamicAopProxy类的invoke方法生成的代理对象。而ObjenesisCglibAopProxy类的父类:CglibAopProxy,它的getProxy方法生成的代理对象。
哪个更好?
我猜,不是刺头青,是你,可能会来自灵魂深处的一问:jdk动态代理和cglib哪个更好?
其实这个问题没有标准答案,要看具体的业务场景:
- 没有定义接口,只能使用cglib,不说它好不行。
- 定义了接口,需要创建单例或少量对象,调用多次时,可以使用jdk动态代理,因为它创建时更耗时,但调用时速度更快。
- 定义了接口,需要创建多个对象时,可以使用cglib,因为它创建速度更快。
随着jdk版本不断迭代更新,jdk动态代理创建耗时不断被优化,8以上的版本中,跟cglib已经差不多。所以spring官方默认推荐使用jdk动态代理,因为它调用速度更快。
五种通知

前置通知
该通知在方法执行之前执行,只需在公共方法上加@Before注解,就能定义前置通知:
@Before("pointcut()")
public void beforeLog(JoinPoint joinPoint) {
System.out.println("打印请求日志");
}
后置通知
该通知在方法执行之后执行,只需在公共方法上加@After注解,就能定义后置通知:
@After("pointcut()")
public void afterLog(JoinPoint joinPoint) {
System.out.println("打印响应日志");
}
环绕通知
该通知在方法执行前后执行,只需在公共方法上加@Round注解,就能定义环绕通知:
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("打印请求日志");
Object result = joinPoint.proceed();
System.out.println("打印响应日志");
return result;
}
结果通知
该通知在方法结束后执行,能够获取方法返回结果,只需在公共方法上加@AfterReturning注解,就能定义结果通知:
@AfterReturning(pointcut = "pointcut()",returning = "retVal")
public void afterReturning(JoinPoint joinPoint, Object retVal) {
System.out.println("获取结果:"+retVal);
}
异常通知
该通知在方法抛出异常之后执行,只需在公共方法上加@AfterThrowing注解,就能定义异常通知:
@AfterThrowing(pointcut = "pointcut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("异常:"+e);
}
spring aop给这五种通知,分别分配了一个xxxAdvice类。在ReflectiveAspectJAdvisorFactory类的getAdvice方法中可以看得到:

下面用一张图总结一下对应关系:

这五种xxxAdvice类都实现了Advice接口,但是有些差异。
下面三个xxxAdvice类实现了MethodInterceptor
接口:

而另外两个类:AspectJMethodBeforeAdvice
和 AspectJAfterReturningAdvice
没有实现上面的接口,这是为什么?(这里留点悬念,后面的文章会揭晓谜题,敬请期待。)
一个猝不及防,依然是那个刺头青D,放下碗冲过来问了句:这五种通知的执行顺序是怎么样的?

多个切面正常情况

多个切面异常情况

当有多有切面时,按照可以通过@Order(n)指定执行顺序,n值越小越先执行。
为什么使用链式调用?
这个问题没人问,是我自己想聊聊(旁白:因为我长得帅,有点自恋了)。
先看看spring是如何使用链式调用的,在ReflectiveMethodInvocation的proceed方法中,有这样一段代码:

下面用一张图捋一捋上面的逻辑:

图中包含了一个递归的链式调用,为什么要这样设计呢?
假如不这样设计,我们代码中是不是需要写很多if…else,根据不同的切面和通知单独处理?
而spring巧妙的使用责任链模式消除了原本需要大量的if…else判断,让代码的扩展性更好,很好的体现了开闭原则:对扩展开放,对修改关闭。
缓存中存的原始还是代理对象?
我们知道spring中为了性能考虑是有缓存的,通常说包含了三级缓存:

singletonFactories(三级缓存)
AbstractAutowireCapableBeanFactory类的doCreateBean方法中,有这样一段代码:

其实之前已经说过,它是为了解决循环依赖问题。这次要说的是addSingletonFactory方法:

它里面保存的是singletonFactory对象,所以是原始对象。
earlySingletonObjects(二级缓存)
AbstractBeanFactory类的doGetBean方法中,有这样一段代码:

在调用getBean方法获取bean实例时,会调用getSingleton尝试先从缓存中看能否获取到,如果能获取到则直接返回。

这段代码会先从一级缓存中获取bean,如果没有再从二级缓存中获取,如果还是没有则从三级缓存中获取singletonFactory,通过getObject方法获取实例,将该实例放入到二级缓存中。
答案的谜底就聚焦在getObject方法中,而这个方法又是在哪来定义的呢?
其实就是上面的getEarlyBeanReference方法,我们知道这个方法生成的是代理对象,所以二级缓存中存的是代理对象。
singletonObjects(一级缓存)
DefaultSingletonBeanRegistry类的getSingleton方法中,有这样一段代码:

此时的bean创建、注入和初始化完成了,判断是如果新的单例对象,则会加入到一级缓存中,具体代码如下:

出于一块钱的友谊,有必要温馨提醒一下:这里是DefaultSingletonBeanRegistry类的getSingleton方法,跟上面说的AbstractBeanFactory类getSingleton方法不一样。
几个常见的坑
我们几乎每天都在用spring aop。
“什么?我怎么不知道?” 你可能会问。
如果你每天在用spring事务的话,就是每天在用spring aop,因为spring事务的底层就用到了spring aop。
注意事务失效的场景。

0 条评论