SpringAOP失效问题

前言

有些时候我们会发现事务失效。其实事务也是基于AOP实现的,现在我们复现问题并解决。

AOP示例

原始的登录逻辑。逻辑很简单,当用户是admin时正常登录,如果非admin则非法登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class LoginService {

public void login(String userName) {
if (isAdmin(userName)) {
System.out.println(userName + "正常登录");
return;
}
System.out.println(userName + "非法登录");
}

protected boolean isAdmin(String userName) {
return "admin".equalsIgnoreCase(userName);
}
}

由于我们要在登录逻辑前后加入日志功能,所以我们需要编写一个环绕通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Aspect
@Component
public class LoginAspect {

@Around(value = "execution(* com.example.demojava.aop2.LoginService.login(..))")
public Object loginAspect(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
String userName = (String) args[0];
System.out.println("log:" + userName + "前置增强");
Object proceed = joinPoint.proceed();
System.out.println("log:" + userName + "后置增强");
return proceed;
}
}

接下来写个测试类看下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(SpringRunner.class)
@SpringBootTest
class LoginServiceTest {

@Autowired
LoginService loginService;

@Test
void loginAdmin() {
loginService.login("admin");
}

@Test
void loginTest() {
loginService.login("test");
}
}

结果输出如下

1
2
3
log:test前置增强
test非法登录
log:test后置增强

AOP失效情形

接下来假如我们又有了一个新需求,就是要对不合法用户做些特殊的处理,比如说统计下不合法用户调用登陆接口的次数。由于直接修改原有的登录逻辑有很多弊端,所以我们还是选择通过AOP来实现该功能。这可以通过编写一个返回增强来实现:

1
2
3
4
5
6
7
8
9
@AfterReturning(value = "execution(* com.example.demojava.aop2.LoginService.isAdmin(..))", returning = "result")
public void isAdminAspect(JoinPoint joinPoint, Object result) {

if (!(boolean)result) {
Object[] args = joinPoint.getArgs();
String userName = (String) args[0];
System.out.println("非法用户" + userName + "增强逻辑");
}
}

接下来我们还是用上一节的测试类来测试一下,我们直接看结果:

1
2
3
log:test前置增强
test非法登录
log:test后置增强

AOP为何会失效

之所以会出现上述AOP失效的现象,归根到底是由于AOP的实现机制导致的。Spring AOP采用代理的方式实现AOP,我们编写的横切逻辑被添加到动态生成的代理对象中,只要我们调用的是代理对象,则可以保证调用的是被增强的代理方法。而在代理对象中,不管你的横切逻辑是怎样的,也不管你增加了多少层的横切逻辑,有一点可以确定的是,你终归会调用目标对象的同一方法来调用原始的业务逻辑。

如果目标对象中的原始方法依赖于其他对象,那么Spring会注入所依赖对象的代理对象,从而保证依赖的对象的横切逻辑能够被正常织入。而一旦目标对象调用的是自身的其他方法时,问题就来了,这种情况下,目标对象调用的并不是代理对象的方法,故被调用的方法无法织入横切逻辑。

如上图所示,method1和method2方法是同个类中的方法,当外部通过代理对象调用method1时,最终会调用目标对象的method1方法,而在目标对象的method1方法中调用method2方法时,最终调用的是目标对象的method2方法,而不是代理对象的method2方法,故而针对method2的AOP增强失效了。

如何避免AOP失效

方案一

要解决上述Spring AOP失效的问题,有两个方法,一个是将isAdmin方法跟login方法写在不同的类里,这样一来,当login方法调用isAdmin方法时,Spring会注入相应的代理对象,从而可以调用到isAdmin方法的代理逻辑。另一个方法是在调用isAdmin方法时先获取当前上下文的代理对象,再通过该代理对象调用被增强了的isAdmin方法,这样一来也能解决AOP失效的问题。实际上Spring AOP为我们提供了获取当前上下文代理对象的方法,使用起来非常方便,首先需要在AOP配置里暴露代理对象,在Spring Boot中可以通过注解@EnableAspectJAutoProxy(exposeProxy = true)进行配置:

1
2
3
4
5
6
7
8
9
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class DemoJavaApplication {

public static void main(String[] args) {
SpringApplication.run(DemoJavaApplication.class, args);
}

}

然后修改login方法,通过AopContext获取当前上下文代理对象,再通过该代理对象调用isAdmin方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class LoginService {

public void login(String userName) {
LoginService currentProxy = (LoginService) AopContext.currentProxy();
if (currentProxy.isAdmin(userName)) {
System.out.println(userName + "正常登录");
return;
}
System.out.println(userName + "非法登录");
}

protected boolean isAdmin(String userName) {
return "admin".equalsIgnoreCase(userName);
}
}

最后运行测试类看下效果

1
2
3
4
log:test前置增强
非法用户test增强逻辑
test非法登录
log:test后置增强

这样就解决了上述AOP失效的问题。

方案二

springboot通过实现ApplicationContext获取代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
public class SpringContextUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}

//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}

//通过name获取 Bean.
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}

//通过class获取Bean.
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}

//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}

}

修改login方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class LoginService {

public void login(String userName) {
LoginService loginService = SpringContextUtil.getBean(this.getClass());
if (loginService.isAdmin(userName)) {
System.out.println(userName + "正常登录");
return;
}
System.out.println(userName + "非法登录");
}

protected boolean isAdmin(String userName) {
return "admin".equalsIgnoreCase(userName);
}
}

接下来我们再次测试以下

1
2
3
4
log:test前置增强
非法用户test增强逻辑
test非法登录
log:test后置增强

可以看到问题已经解决了。

验证结果

以上我们猜想并解决了问题,下边来验证下对象的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class LoginService {

public void login(String userName) {
LoginService loginService = SpringContextUtil.getBean(this.getClass());

System.out.println("当前对象为" + this.getClass().getName());
System.out.println("代理对象为" + loginService.getClass().getName());

if (loginService.isAdmin(userName)) {
System.out.println(userName + "正常登录");
return;
}
System.out.println(userName + "非法登录");
}

protected boolean isAdmin(String userName) {
return "admin".equalsIgnoreCase(userName);
}
}

结果输出

1
2
3
4
5
6
log:test前置增强
当前对象为com.example.demojava.aop2.LoginService
代理对象为com.example.demojava.aop2.LoginService$$EnhancerBySpringCGLIB$$97517af5
非法用户test增强逻辑
test非法登录
log:test后置增强

由日志可以看出失效调用时的this为target对象,而后边的调用为cglib生成的代理对象。

最后

本文到此结束,感谢阅读。如果您觉得不错,请关注公众号【当我遇上你】,您的支持是我写作的最大动力