从AOP的入门程序到现在,我们一直都在使用切入点表达式来描述切入点。下面我们就来详细的介绍一下切入点表达式的具体写法。
切入点表达式:
- 描述切入点方法的一种表达式
- 作用:主要用来决定项目中的哪些方法需要加入通知
- 常见形式:
- execution(……):根据方法的签名来匹配
- @annotation(……) :根据注解匹配
首先我们先学习第一种最为常见的execution切入点表达式。
3.3.1 execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?的表示可以省略的部分
- 访问修饰符:可省略(比如: public、protected)
- 包名.类名: 可省略
- throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
示例
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
- * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
- .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式的语法规则:
- 方法的修饰符可以省略
- 返回值可以使用*号代替(任意返回值类型)
- 包名可以使用*号代替,代表任意包(一层包使用一个*)
- 使用..配置包名,标识此包以及此包下的所有子包
- 类名可以使用*号代替,标识任意类
- 方法名可以使用*号代替,表示任意方法
- 可以使用..配置参数,任意参数
切入点表达式示例
省略方法的修饰符号
execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
使用*代替返回值类型
execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
使用*代替包名(一层包使用一个*)
execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer))
使用..省略包名
execution(* com..DeptServiceImpl.delete(java.lang.Integer))
使用*代替类名
execution(* com..*.delete(java.lang.Integer))
使用..省略参数
execution(* com..*.*(..))
注意事项:
- 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
切入点表达式的书写建议:
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
//业务类 @Service public class DeptServiceImpl implements DeptService { public List<Dept> findAllDept() { //省略代码... } public Dept findDeptById(Integer id) { //省略代码... } public void updateDeptById(Integer id) { //省略代码... } public void updateDeptByMoreCondition(Dept dept) { //省略代码... } //其他代码... }
//匹配DeptServiceImpl类中以find开头的方法 execution(* com.itheima.service.impl.DeptServiceImpl.find*(..))
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
execution(* com.itheima.service.DeptService.*(..))
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包
execution(* com.itheima.*.*.DeptServiceImpl.find*(..))
3.3.2 @annotation
已经学习了execution切入点表达式的语法。那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。而在之前我们是将两个切入点表达式组合在了一起完成的需求,这个是比较繁琐的。
我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。
实现步骤:
- 编写自定义注解
- 在业务类要做为连接点的方法上添加自定义注解
自定义注解:MyLog
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog { }
业务类:DeptServiceImpl
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override @MyLog //自定义注解(表示:当前方法属于目标方法) public List<Dept> list() { List<Dept> deptList = deptMapper.list(); //模拟异常 //int num = 10/0; return deptList; } @Override @MyLog //自定义注解(表示:当前方法属于目标方法) public void delete(Integer id) { //1. 删除部门 deptMapper.delete(id); } @Override public void save(Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.save(dept); } @Override public Dept getById(Integer id) { return deptMapper.getById(id); } @Override public void update(Dept dept) { dept.setUpdateTime(LocalDateTime.now()); deptMapper.update(dept); } }
切面类
@Slf4j @Component @Aspect public class MyAspect6 { //针对list方法、delete方法进行前置通知和后置通知 //前置通知 @Before("@annotation(com.itheima.anno.MyLog)") public void before(){ log.info("MyAspect6 -> before ..."); } //后置通知 @After("@annotation(com.itheima.anno.MyLog)") public void after(){ log.info("MyAspect6 -> after ..."); } }
重启SpringBoot服务,测试查询所有部门数据,查看控制台日志:
到此我们两种常见的切入点表达式我已经介绍完了。
- execution切入点表达式
-
- 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
- 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
- annotation 切入点表达式
-
- 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了
3.4 连接点
讲解完了切入点表达式之后,接下来我们再来讲解最后一个部分连接点。我们前面在讲解AOP核心概念的时候,我们提到过什么是连接点,连接点可以简单理解为可以被AOP控制的方法。我们目标对象当中所有的方法是不是都是可以被AOP控制的方法。而在SpringAOP当中,连接点又特指方法的执行。
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
- 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
- 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
示例代码:
@Slf4j @Component @Aspect public class MyAspect7 { @Pointcut("@annotation(com.itheima.anno.MyLog)") private void pt(){} //前置通知 @Before("pt()") public void before(JoinPoint joinPoint){ log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ..."); } //后置通知 @Before("pt()") public void after(JoinPoint joinPoint){ log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ..."); } //环绕通知 @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { //获取目标类名 String name = pjp.getTarget().getClass().getName(); log.info("目标类名:{}",name); //目标方法名 String methodName = pjp.getSignature().getName(); log.info("目标方法名:{}",methodName); //获取方法执行时需要的参数 Object[] args = pjp.getArgs(); log.info("目标方法参数:{}", Arrays.toString(args)); //执行原始方法 Object returnValue = pjp.proceed(); return returnValue; } }
重新启动SpringBoot服务,执行查询部门数据的功能: