likes
comments
collection
share

Spring AOP - 你真的会用么?

作者站长头像
站长
· 阅读数 10

Spring AOP - 用法篇

定义

AOP(Aspect Oriented Programming),面向切面编程,是对 面向对象编程OOP的升华。

AOP的基本单元是切面(Aspect)。

OOP的特性是 封装、继承、多态。它使得我们的代码重用性变高。这也带来了一个问题,就是多个类对重复的事情都需要进行封装,比如,想要对类中的方法做日志,那么就需要在每个类中都去添加记录日志相关的代码,这显然是不够优雅的。

当然可以将记录日志的逻辑重新封装在一个类中,然后在需要记录日志的类中去 创建一个 日志类的对象。这样确实能够解决每个类的重复代码问题,但是 日志类使用日志的类 之间出现了耦合

这时候就出现了AOP,对程序进行横向切割,将程序中的各个功能模块分离出来(比如将日志记录模块分离出来,在需要用的时候织入进我们的业务代码中)。

Spring AOP - 你真的会用么?


基础概念

  • 目标对象(Target): 被增强的方法所在的对象。
  • 代理对象(Proxy): 对目标对象进行增强后的对象,客户端实际调用的对象。
  • 连接点(JoinPoint): 目标对象中可以被增强的方法。
  • 切入点(PointCut): 目标对象中实习被增强的方法。
  • 通知/增强(Advice): 增强部分的代码逻辑。
  • 切面(Aspect): 增强和切入点的组合。
  • 织入(Weaving): 将通知和切入点组合动态组合的过程。

Spring AOP - 你真的会用么?

入门示例

基于XML文件配置的方式

基于注解的方式

Spring AOP - 你真的会用么?

引入依赖:

<dependency>  
<groupId>org.aspectj</groupId>  
<artifactId>aspectjweaver</artifactId>  
<version>1.9.6</version>  
</dependency>

通知类

通知类需要交给Spring管理,并且加上@Aspect注解

@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";  
    }   
}

测试

分别请求三个链接,结果如下图:

Spring AOP - 你真的会用么? 可以看到,两个 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 的 方法:

Spring AOP - 你真的会用么?


切点标志符

例如上面用到的切点表达式: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注解中的属性,在通知方法中判断具体要执行的增强,是不是非常的灵活!!

其他的切点标志符:withinargsbean等等

这些还没用到,用到的时候再来补充吧。