摘要:基于注解@Aspect实现Spring AOP切面编程。
基于注解@Aspect实现Spring AOP
Spring AOP使用的是动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象在特定的切点做了增强处理,包含了目标对象的全部方法,并且回调原对象的方法。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类实现一个接口,她的核心是InvocationHandler接口和Proxy类。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类。注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
如果想要实现拦截private方法,可以使用原生 AspectJ 编译时织入。Spring使用纯Spring AOP(只能拦截public/protected/包)都是无法被拦截private方法的,因为子类无法覆盖;包级别能被拦截的原因是,如果子类和父类在同一个包中是能覆盖的。cglib动态代理可以拦截 public/protected/包级别方法(即这些方法都是能代理的)。因为AOP底层是动态代理,jdk动态代理是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截;cglib是生成被代理类的子类,private的方法照样不会出现在子类里,也不能被拦截。关于Spring AOP的更多基本概念请参考《Spring AOP 面向切面编程之AOP是什么》。
AspectJ是一个面向切面的框架,它扩展了java语言,定义了AOP语法,能够在编译期提供代码的织入。这里创建一个基于注解@Aspect的demo,实现AOP切面。定义测试API getUserById:
@Autowired
private UserService userService;
@GetMapping(value ="/getUserById", produces = "application/json; charset=utf-8")
public User getUserById(Long userId) {
User user = userService.getUserById(userId);
return user;
}
编写实现类:
package com.eg.wiener.service;
import com.eg.wiener.dto.User;
public interface UserService {
User getUserById(Long userId);
}
=== 我是分割线 ===
package com.eg.wiener.service.impl;
import com.eg.wiener.dto.User;
import com.eg.wiener.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class UserServiceImpl implements UserService {
private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
@Override
public User getUserById(Long userId) {
logger.info("--------**测试bean增强**-----------");
User user = new User();
user.setId(userId);
user.setAddress("测试地址是 " + UUID.randomUUID().toString());
logger.info("类信息: {}", user.getClass());
return user;
}
}
在切面UserServiceAspect中使用@Pointcut注解声明切点表达式,命名为userPointcut(),然后在各类通知类型中引用此切点表达式。
package com.eg.wiener.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserServiceAspect {
private static Logger logger = LoggerFactory.getLogger(UserServiceAspect.class);
private final String POINT_CUT = "execution(* com.eg.wiener.service.UserService.getUserById(..))";
@Pointcut(POINT_CUT)
public void userPointcut() {
}
@Before("userPointcut()")
public void beforeTest() {
System.out.println("before ...");
}
@After("userPointcut()")
public void afterTest() {
System.out.println("After ...");
}
@AfterReturning("userPointcut()")
public void afterReturningTest() {
System.out.println("AfterReturning ...");
}
@Around("userPointcut()")
public Object aroundTest(ProceedingJoinPoint pj) {
long start = System.currentTimeMillis();
Object obj = new Object();
try {
System.out.println("Around start ...");
obj = pj.proceed(pj.getArgs());
System.out.println("Around end ...耗时:" +
(System.currentTimeMillis() - start));
} catch (Exception e) {
logger.error("------ e -----,", e);
} catch (Throwable throwable) {
logger.error("----- throwable ------,", throwable);
}
return obj;
}
}
POINT_CUT具体到了函数getUserById,表示切入点就是userService中的函数getUserById,不拦截其它接口。org.aspectj.lang.ProceedingJoinPoint为JoinPoint的子类,重点看一下其中的如下两个方法:
public Object proceed() throws Throwable;
public Object proceed(Object[] args) throws Throwable;
执行proceed() 意为调用下一个advice或者执行目标方法,返回结果为目标方法返回值,因此,如有必要,可以在这里修改方法的返回值。proceed(Object[] args)中的args为目标方法的参数,可以通过修改参数改变方法入参。
在编写环绕通知的时候,如果被增强函数有返回值,我们需要返回执行结果;否则,API无法返回我们的业务数据,导致竹篮打水一场空。启动项目,请求getUserById,控制台打印结果如下:
Around start ...
before ...
2021-02-17 10:55:21.259 INFO 9000 --- [nio-8087-exec-4] c.e.wiener.service.impl.UserServiceImpl : --------测试bean增强-----------
2021-02-17 10:55:21.259 INFO 9000 --- [nio-8087-exec-4] c.e.wiener.service.impl.UserServiceImpl : 类信息: class com.eg.wiener.dto.User
AfterReturning ...
After ...
Around end ...耗时:1
可以看到五种通知类型和具体实现类的执行顺序为:around、before、实现类、afterReturning、after和around。
使用环绕通知监控API执行时间。在环绕通知中记录API执行完毕所耗费的时间,可以方便的监控和统计哪些API性能低,需要重构,以保证系统稳健运行。
既然AOP已经生效了,那么,问题来了,究竟AspectJ是如何在没有修改UserServiceImpl类的情况下为UserServiceImpl类增加新功能的呢?这是注解@AspectJ在内存中临时为方法生成一个AOP对象,这个AOP对象在特定的切点做了增强处理,包含了目标对象的全部方法,并且回调原对象的方法,即运行的就是经过增强之后的AOP对象。
小结
AspectJ在编译时就增强了目标对象,Spring AOP的动态代理则是在每次运行时动态的增强,生成AOP代理对象,区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。