Spring AOP - 你真的会用么?
Spring AOP - 用法篇
定义
AOP(Aspect Oriented Programming),面向切面编程,是对 面向对象编程OOP的升华。
AOP的基本单元是切面(Aspect)。
OOP的特性是 封装、继承、多态。它使得我们的代码重用性变高。这也带来了一个问题,就是多个类对重复的事情都需要进行封装,比如,想要对类中的方法做日志,那么就需要在每个类中都去添加记录日志相关的代码,这显然是不够优雅的。
当然可以将记录日志的逻辑重新封装在一个类中,然后在需要记录日志的类中去 创建一个 日志类的对象。这样确实能够解决每个类的重复代码问题,但是 日志类
和 使用日志的类
之间出现了耦合。
这时候就出现了AOP,对程序进行横向切割,将程序中的各个功能模块分离出来(比如将日志记录模块分离出来,在需要用的时候织入进我们的业务代码中)。
基础概念
- 目标对象(Target): 被增强的方法所在的对象。
- 代理对象(Proxy): 对目标对象进行增强后的对象,客户端实际调用的对象。
- 连接点(JoinPoint): 目标对象中可以被增强的方法。
- 切入点(PointCut): 目标对象中实习被增强的方法。
- 通知/增强(Advice): 增强部分的代码逻辑。
- 切面(Aspect): 增强和切入点的组合。
- 织入(Weaving): 将通知和切入点组合动态组合的过程。
入门示例
基于XML文件配置的方式
略
基于注解的方式
引入依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
通知类
通知类需要交给Spring管理,并且加上@Aslect
注解
@Slf4j
@Aspect
@Component
public class UserAdvice {
// 前置通知,指定了service包及其子包下的所有类中返回值为void类型的方法作为切点
@Before("execution(void com.hss.spring.aop.demo.service..*.*(..))")
public void beforeAdvice() {
log.info("operate start time:{}",new Date());
}
// 后置通知,指定了service包及其子包下的所有类中返回值为void类型的方法作为切点
@After("execution(void com.hss.spring.aop.demo.service..*.*(..))")
public void afterAdvice() {
log.info("operate end time:{}",new Date());
}
}
目标类
@Service
public class UserServiceImpl implements UserService {
@Override
public void login() {
System.out.println("user login...");
}
@Override
public void register() {
System.out.println("user register...");
}
// 注意返回值,这个int返回值的方法不会被作为切入点
@Override
public int userCount() {
System.out.println("get user count...");
return 0;
}
}
Controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@GetMapping("/login")
public String login() {
userService.login();
return "success";
}
@GetMapping("/register")
public String register() {
userService.register();
return "success";
}
@GetMapping("/count")
public String userCount() {
userService.userCount();
return "success";
}
}
测试
分别请求三个链接,结果如下图:
可以看到,两个 void返回类型的方法都成功加入了日志,而返回值为 int 的方法由于没有作为切点,所以没有加上日志功能。
切点表达式的配置
语法:execution([访问修饰符]返回值类型 包名.类名.方法名(参数))
- 访问修饰符可以不写
- 返回值类型、某一级包名、类名、方法名可以使用 * 来表示任意
- 包名与类名之间使用 . 表示层级关系,使用双点 .. 表示该包及其子包下的类
- 参数列表可以使用两个点.. 表示任意参数
举例:
//表示访问修饰符为public、无返回值、在com.hss.aop.demo.service.impl包下的UserServiceImpl类的无参方法login
execution(public void com.hss.aop.demo.service.impl.UserServiceImpl.login())
//表述com.hss.aop.demo.service.impl 包下的TargetImpl类的任意方法
execution(* com.hss.aop.demo.service.impl.UserServiceImpl.*(..))
//表示com.hss.aop.demo.service.impl包下的任意类的任意方法
execution(* com.hss.aop.demo.service.impl.*.*(..))
//表示com.hss.aop.demo.service 包及其子包下的任意类的任意方法
execution(* com.hss.aop.demo.service..*.*(..))
//表示任意包中的任意类的任意方法
execution(* *..*.*(..))
通知类型
- 前置通知(@Before):目标方法执行前执行
- 后置通知(@AfterReturning):目标方法执行后执行,若目标方法执行出现异常时,后置通知不执行
- 环绕通知(@Around):目标方法执行前后执行,目标方法异常时,环绕后方法不执行
- 异常通知(@AfterThrowing):目标方法抛出异常时执行
- 最终通知(@After):不论目标方法是否发生异常,最终都会执行
通知方法被调用时,Spring还可以为其传递一些必要的参数:
- JointPoint:连接点对象,任何通知都可以使用,可以获取当前目标对象,目标方法参数等信息。
- ProceedingJoinPoint:JoinPoint的子类对象,主要用于环绕通知中执行 proceed()来执行目标方法。
- Throwable:异常对象,使用在异常通知中,需要在配置文件中指出异常对象名称
@Before("execution(void com.hss.spring.aop.demo.service..*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
Object target = joinPoint.getTarget();
System.out.println(signature);
System.out.println(target);
log.info("operate start time:{}",new Date());
}
JointPoint 的 方法:
切点标志符
例如上面用到的切点表达式:execution([访问修饰符]返回值类型 包名.类名.方法名(参数))
其中 execution
就是切点标志符。
其他的切点标志符:@annotation
这种标志符个人认为比较灵活,可以自定义一些注解,然后在想通过切面增强的方法上面加上注解,就可以实现AOP。
例如:
@Component
@Aspect
public class ProductAspect {
@Before("@annotation(com.hss.spring.aop.annotation.Action)")
public void before() {
System.out.println("执行方法前");
}
@After("@annotation(com.hss.spring.aop.annotation.Action)")
public void after() {
System.out.println("执行方法后");
}
}
这里的前置通知和最终通知都是在加了@Action
注解的方法进行增强。
@Action注解代码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Action {
AOPTypeEnum type();
}
AOPTypeEnum 代码:
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum AOPTypeEnum {
SHOE_ADD(1,"增加商品-鞋子",OperationTypeEnum.ADD_TYPE),
SHIRT_ADD(2,"增加商品-上衣",OperationTypeEnum.ADD_TYPE),
PANT_ADD(3,"增加商品-裤子",OperationTypeEnum.ADD_TYPE),
SHOE_UPDATE(1,"更新商品-鞋子",OperationTypeEnum.UPDATE_TYPE),
SHIRT_UPDATE(2,"更新商品-上衣",OperationTypeEnum.UPDATE_TYPE),
PANT_UPDATE(3,"更新商品-裤子",OperationTypeEnum.UPDATE_TYPE),
SHOE_DELETE(1,"删除商品-鞋子",OperationTypeEnum.DELETE_TYPE),
SHIRT_DELETE(2,"删除商品-上衣",OperationTypeEnum.DELETE_TYPE),
PANT_DELETE(3,"删除商品-裤子",OperationTypeEnum.DELETE_TYPE),
;
private Integer resourceId;
private String desc;
private OperationTypeEnum operationTypeEnum;
}
OperationTypeEnum 代码:
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum OperationTypeEnum {
ADD_TYPE(1,"增加"),
UPDATE_TYPE(2,"更新"),
DELETE_TYPE(3,"删除"),
;
private Integer code;
private String desc;
}
ServiceImpl 代码:
@Service
public class ProductServiceImpl implements ProductService {
@Override
@Action(type = AOPTypeEnum.SHOE_ADD)
public void addShoe(String shoeName) {
System.out.println("新增鞋子:"+shoeName);
}
}
Controller 代码:
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
ProductService productService;
@GetMapping("/shoe/add/{shoeName}")
public String addShoe(@PathVariable("shoeName") String shoeName) {
productService.addShoe(shoeName);
return "success";
}
}
还有一个值得注意的地方是,我们在注解内部可以定义一个属性,这个属性就可以用来在通知方法中去做if
的判断。比如说:对于查询的方法,我只需要对其进行记录日志的增强,我就可以在service中对应的方法的@Action
注解中传入一个 代表查询 的属性;而对于编辑或者删除方法,我除了要记录日志外,还需要将具体执行的请求参数之类的写到MySQL表中做记录,这时就可以向@Action
注解中传入一个表示修改的属性。
所以就可以通过@Action
注解中的属性,在通知方法中判断具体要执行的增强,是不是非常的灵活!!
其他的切点标志符:within、args、bean等等
这些还没用到,用到的时候再来补充吧。
转载自:https://juejin.cn/post/7244492473933365304