目录
前言
Aop小案例
过程分析
解决方案
今天在写系统权限校验时发现某个含有切点注解的方法进不去切面类,单元测试了下,发现在单个类内的方法调用是不能够进入切
面中的,这就说明在内部方法调用时并未使用代理对象进行代理。
为了验证这个现象,我们写个Aop的小例子,注意本案例是基于SpringBoot 2.1.3版本
首先自定义一个注解,用于标识切点:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TestCheckPoint {
}
然后定义一个切面类:
@Component
@Aspect
public class TestAspect {
private static final Logger logger = LoggerFactory.getLogger(TestAspect.class);
@Before("@annotation(tp)")
public void beforMethod(JoinPoint joinPoint, TestCheckPoint tp) {
logger.info("Before method...");
}
@After("@annotation(tp)")
public void afterMethod(JoinPoint joinPoint, TestCheckPoint tp) {
logger.info("After method...");
}
}
为了演示,在controller层模拟调用方法的过程,controller如下:
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private HelloServiceImpl helloService;
@RequestMapping("/say")
public void sayHello() {
helloService.sayHello();
}
}
HelloServiceImpl如下,注意它是有实现接口的:
@Service
public class HelloServiceImpl implements HelloService {
private static final Logger logger = LoggerFactory.getLogger(HelloServiceImpl.class);
/**
* 切面测试
*/
@TestCheckPoint
public void sayHello() {
logger.info("Say hello world");
}
}
然后启动项目,访问http://localhost:7640/say,可以看到Aop正常生效:
然后修改在helloServiceImpl新增一个方法,并在该方法内调用本类的sayHello方法,同时修改controller层方法调用:
/**
* 测试本类中对切面的调用
*/
public void sayHelloWithoutAsp() {
this.sayHello();
}
此时再启动,会发现Aop始终无法拦截sayHello方法即进不到切面类内部。
到这里我们分析一下整个流程,首先controller层调用helloServiceImpl的sayHelloWithoutAsp方法,然后sayHelloWithoutAsp方法内部再调用被自定义注解所标识的sayHello方法。过程很简单,那为什么两种方式会产生不一样的结果呢?
我们对比下这两种的区别,可以发现仅仅是调用sayHello的对象不同,先来看第二种,由于是在本类的内部方法之间进行调用,所以肯定是调用的当前类this对象,断点调试下,可以发现这个this对象并不是代理对象,而只是当前类的普通对象,因此不存在方法增强。
接下来看下第一种调用方式中的helloServiceImpl,可以发现该类在被Spring注入时已经完成代理操作,返回的是一个cglib代理对象,因此其调用的sayHello方法自然而然可以对其进行增强,简单来说呢就是调用者首先调用代理对象,然后执行一系列前置操作,接着调用目标对象的方法,最后执行一系列后置操作,完成整个业务逻辑。对比上述第二种方式的直接调用目标对象的方法,那肯定得不到正确结果呀。
看到这里,你会不会对这个代理对象的前缀产生疑惑呢?怎么是个CGLIB代理对象呢?反正我最开始很纳闷,这个目标对象明明实现了HelloService接口,按理说Spring应该采用JDK动态代理呀,怎么会去使用CGLIB动态代理呢?
这就涉及到了另外一个配置参数,proxy-target-class,如果将该参数置为true,那么则强制使用基于类的代理进行创建,反之则基于接口的方式创建,而另一个坑是因为SpringBoot 2.x以后该参数被默认置为true了,(在Spring中默认是为false的,本人没经过测试,有兴趣可自行测试),因此上述HelloServiceImpl对象在创建代理时被强制使用了CGLIB代理也是能够理解的了。
在SpringBoot中如果你想启用jdk动态代理,可在yaml中去掉强制使用cglib代理的配置,如下:
spring:
# 默认为true,表示基于类的代理,如果为false表示基于jdk接口的代理
aop:
proxy-target-class: true
当你改为false之后,你会发现controller层中的helloService会注入失败,那是因为基于接口形式的代理必须要以其实现的接口类型注入即HelloService而不能是原先的HelloServiceImpl,否则会一直报注入失败的问题,这个坑一开始也困扰着我,修改完毕后让我们再次断点查看,发现此时的helloService已经变成jdk代理对象啦~
既然了解了整个过程,那么我们只需要使得在内部方法调用时拿到当前类的代理对象,然后再用这个代理对象去执行目标方法不就ok啦?这里就提供一种简单的方式来实现,运用AopContext类来获取当前类的代理对象,当然这里有个前提,必须要在启动类上加上@EnableAspectJAutoProxy(exposeProxy = true),后面的参数即代表在进行内部方法间的调用时如果也想能够进行代理,那么必须将该参数置为true,否则会报错,默认为false,当然在该注解上也可以进行proxy-target-class参数的配置,形如:@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
同时在HelloServiceImpl方法内部加上获取当前类的代理对象方法,如下:
/**
* 测试本类中对切面的调用
*/
public void sayHelloWithoutAsp() {
getHelloServiceImpl().sayHello();
}
/**
* 强制获取代理对象,必须开启exposeProxy配置,否则获取不到当前代理对象
* @return
*/
private HelloServiceImpl getHelloServiceImpl() {
return AopContext.currentProxy() != null ? (HelloServiceImpl) AopContext.currentProxy() : this;
}
重启项目,就能得到和第一种方式一模一样的效果啦