Spring AOP常见错误(上)
this调用的当前类方法无法被拦截
问题
假设当前开发负责电费充值的类,同时记录下进行充值的时间(此时需要使用到AOP
),并提供电费充值接口:
@Service
public class ElectricService {
public void charge() throws Exception {
System.out.println("Electric charging ...");
this.pay();
public void pay() throws Exception {
System.out.println("Pay with alipay ...");
// 模拟支付耗时
Thread.sleep(1000);
}
}
@Aspect
@Service
@Slf4j
public class AopConfig {
@Around("execution(* com.spring.puzzle.class5.example1.ElectricService.pay()) ")
public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("Pay method time cost(ms): " + (end - start));
}
}
@RestController
public class HelloWorldController {
@Autowired
ElectricService electricService;
@RequestMapping(path = "charge", method = RequestMethod.GET)
public void charge() throws Exception{
electricService.charge();
};
}
但是在访问接口后,计算时间的切面并没有被执行,即在类的内部通过this
方式调用的方法没被AOP增强的
原因
通过Debug可知this
对应的是普通的ElectricService
对象,而在控制器类装配的electricService
对象是被Spring增强后的Bean:
先补充关于Spring AOP
的基础知识:
-
Spring AOP
的实现-
Spring AOP
的底层是动态代理,而创建代理的方式有两种:-
JDK
动态代理只能对实现了接口的类生成代理,而不能针对普通类 -
CGLIB
可针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法来实现代理对象
-
-
-
如何使用
Spring AOP
?- 添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 添加注解:对于非
Spring Boot
程序,除了添加依赖项外还常会使用@EnableAspectJAutoProxy
来开启AOP功能
具体看下创建代理对象的过程:创建代理对象的关键由AnnotationAwareAspectJAutoProxyCreator
完成的,它本质上是一种BeanPostProcessor
,所以它的执行是在完成原始Bean构建后的初始化Bean中:
// AbstractAutoProxyCreator#postProcessAfterInitialization
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// *需使用AOP时,该方法把创建的原始的Bean对象wrap成代理对象作为Bean返回
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
// AbstractAutoProxyCreator#wrapIfNecessary
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
...
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// *创建代理对象的关键
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
...
}
// AbstractAutoProxyCreator#createProxy
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {
...
// 创建代理工厂
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
// 将通知器(advisors)、被代理对象等信息加入到代理工厂
if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
...
// 通过该代理工厂来获取代理对象
return proxyFactory.getProxy(getProxyClassLoader());
}
只有通过上述工厂才创建出一个代理对象,而之前直接使用this
使用的还是普通对象
解决方式
方法的核心在于引用被动态代理创建出来的对象,有以下两种方式:
- 使用被
@Autowired
注解的对象替换this
:
@Service
public class ElectricService {
@Autowired
ElectricService electricService;
public void charge() throws Exception {
System.out.println("Electric charging ...");
electric.pay();
}
public void pay() throws Exception {
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
}
}
- 直接从
AopContext
获取当前的Proxy
:AopContext
是通过一个ThreadLocal
将Proxy
和线程绑定,这样就可随时拿出当前线程绑定的Proxy
(前提是在@EnableAspectJAutoProxy
里加配置项exposeProxy = true
,表示将代理对象放入到ThreadLocal
)
@Service
public class ElectricService {
public void charge() throws Exception {
System.out.println("Electric charging ...");
ElectricService electric = ((ElectricService) AopContext.currentProxy());
electric.pay();
}
public void pay() throws Exception {
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
}
}
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
直接访问被拦截类的属性抛空指针异常
问题
在使用charge
方法进行支付时会用到一个管理员用户付款编号,此时新增几个类:
// 包含用户的付款编号信息
public class User {
private String payNum;
public User(String payNum) {
this.payNum = payNum;
}
public String getPayNum() {
return payNum;
}
public void setPayNum(String payNum) {
this.payNum = payNum;
}
}
@Service
public class AdminUserService {
public final User adminUser = new User("202101166");
// 用于登录系统
public void login() {
System.out.println("admin user login...");
}
}
在电费充值时需管理员登录并使用其编号进行结算:
@Service
public class ElectricService {
@Autowired
private AdminUserService adminUserService;
public void charge() throws Exception {
System.out.println("Electric charging ...");
this.pay();
}
public void pay() throws Exception {
adminUserService.login();
String payNum = adminUserService.adminUser.getPayNum();
System.out.println("User pay num : " + payNum);
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
}
}
由于安全需管理员在登录时记录一行日志以便于以后审计管理员操作:
@Aspect
@Service
@Slf4j
public class AopConfig {
@Before("execution(* com.spring.puzzle.class5.example2.AdminUserService.login(..)) ")
public void logAdminLogin(JoinPoint pjp) throws Throwable {
System.out.println("! admin login ...");
}
}
结果在执行到接口中的electricService.charge()
时不仅没打印日志,还执行String payNum = adminUserService.adminUser.getPayNum()
报NPE
,对pay
方法进行分析后发现加入AOP
后adminUserService
对象已经是代理对象了,但是它的adminUser
属性是null
原因
增强后的类实际是AdminUserService
的子类,它会重写所有public
和protected
方法,并在内部将调用委托给原始的AdminUserService
实例(以CGLIB
的 Proxy
的实现类CglibAopProxy
为例来看具体的流程)
public Object getProxy(@Nullable ClassLoader classLoader) {
...
// ①创建并配置enhancer
Enhancer enhancer = createEnhancer();
...
// ②获取Callback:包含DynamicAdvisedInterceptor,即MethodInterceptor
Callback[] callbacks = getCallbacks(rootClass);
...
// ③生成代理对象并创建代理,即设置enhancer的callback值
return createProxyClassAndInstance(enhancer, callbacks);
}
...
}
第三步会执行到CglibAopProxy
子类ObjenesisCglibAopProxy
的createProxyClassAndInstance
方法中:Spring会默认尝试使用objenesis
方式实例化对象,如失败则再尝试使用常规方式实例化对象
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
// 创建代理类class
Class<?> proxyClass = enhancer.createClass();
Object proxyInstance = null;
// 一般为true
if (objenesis.isWorthTrying()) {
try {
// 创建实例
proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
}
...
}
if (proxyInstance == null) {
// 尝试普通反射方式创建实例
try {
Constructor<?> ctor = (this.constructorArgs != null ?
proxyClass.getDeclaredConstructor(this.constructorArgTypes) :
proxyClass.getDeclaredConstructor());
ReflectionUtils.makeAccessible(ctor);
proxyInstance = (this.constructorArgs != null ?
ctor.newInstance(this.constructorArgs) : ctor.newInstance());
}
...
}
((Factory) proxyInstance).setCallbacks(callbacks);
return proxyInstance;
}
objenesis
方式最后使用了JDK
的ReflectionFactory.newConstructorForSerialization
方法完成代理对象的实例化,这种方式创建出来的对象不会初始化类成员变量
解决方式
在 AdminUserService
里写getAdminUser
方法,从内部访问获取变量:
@Service
public class AdminUserService {
public final User adminUser = new User("202101166");
public User getAdminUser() {
return adminUser;
}
public void login() {
System.out.println("admin user login...");
}
}
@Service
public class ElectricService {
@Autowired
private AdminUserService adminUserService;
public void charge() throws Exception {
System.out.println("Electric charging ...");
this.pay();
}
public void pay() throws Exception {
adminUserService.login();
String payNum = adminUserService.getAdminUser().getPayNum(); // 原来该步骤处报NPE
System.out.println("User pay num : " + payNum);
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
}
}
既然代理类的类属性不会被初始化,为啥可通过AdminUserService
的getUser
方法获取到代理类实例的属性?当代理类方法被调用后会被Spring拦截,进入到DynamicAdvisedInterceptor#intercept
方法,在此方法中获取被代理的原始对象(原始对象的类属性是被实例化过且存在的)
根据原因分析,还可以有另一种解决方式:修改启动参数spring.objenesis.ignore=true
参考
极客时间-Spring 编程常见错误 50 例
https://github.com/jiafu1115/springissue/tree/master/src/main/java/com/spring/puzzle/class5