SpringBoot之AOP面向切面编程

  • A+
所属分类:Java片段

什么是AOP

aop全称Aspect Oriented Programming,面向切面,AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。其与设计模式完成的任务差不多,是提供另一种角度来思考程序的结构,来弥补面向对象编程的不足。

通俗点讲就是提供一个为一个业务实现提供切面注入的机制,通过这种方式,在业务运行中将定义好的切面通过切入点绑定到业务中,以实现将一些特殊的逻辑绑定到此业务中。

举个栗子,项目中有记录操作日志的需求、或者流程变更是记录变更履历,无非就是插表操作,很简单的一个save操作,都是一些记录日志或者其他辅助性的代码。一遍又一遍的重写和调用。不仅浪费了时间,又将项目变得更加的冗余,实在得不偿失。

  所以就需要面向切面aop就出场了

AOP 相关名词

要理解SpringBoot整合aop的实现,就必须先对面向切面实现的一些aop的名称有所了解

  • 切面(Aspect):一个关注点的模块化。以注解@Aspect的形式放在类上方,声明一个切面。

  • 连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候都可以是连接点。

  • 通知(Advice):通知增强,需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用。

    主要包括5个注解:Before,After,AfterReturning,AfterThrowing,Around。

    @Before:在切点方法之前执行。

    @After:在切点方法之后执行

    @AfterReturning:切点方法返回后执行

    @AfterThrowing:切点方法抛异常执行

    @Around:属于环绕增强,能控制切点执行前,执行后,用这个注解后,程序抛异常,会影响@AfterThrowing这个注解

SpringBoot之AOP面向切面编程

  • 切点(Pointcut):其实就是筛选出的连接点,匹配连接点的断言,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。如果说通知定义了切面的动作或者执行时机的话,切点则定义了执行的地点。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
  • 引入(Introduction):在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去。
  • 目标对象(Target Object):被一个或者多个切面所通知的对象。也被称做被通知(adviced)对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
  • AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

    其中重要的名词有:切面(Aspect)切入点(Pointcut)

整合AOP

以处理业务逻辑日志为例,新增日志处理的面向切面处理

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

定义日志注解

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2021/02/22 22:09
 * @Created by 墨云
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
    String value() default "";
}

定义日志实体类

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2021/2/28 23:07
 * @Created by moyun
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("com_sys_log")
public class SysLog implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String className;

    private String methodName;

    private String params;

    private Long excTime;

    private String remark;

    private String createDate;

}

定义日志切面

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2021/2/28 23:19
 * @Created by moyun
 */
@Aspect //使用@Aspect
@Component
public class SysLogAsp {

    @Autowired
    private SysLogService sysLogService;

    /**
     *定义切点, 这里我们使用注解的形式
     * 当然,我们也可以通过切点表达式直接指定需要拦截的package,需要拦截的class 以及 method
     * 切点表达式:   execution(...)
     */
    @Pointcut("@annotation(com.kjyfx.annotation.SysLog)")
    public void logPointCut() {}


    /**
     * 添加环绕通知@Around
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        long beginTime = System.currentTimeMillis();
        Object result = point.proceed();
        long time = System.currentTimeMillis() - beginTime;
        try {
            //实现保存日志逻辑
            saveLog(point, time);
        } catch (Exception e) {

        }
        return result;
    }


    /**
     * 保存日志
     * @param joinPoint
     * @param time
     */
    private void saveLog(ProceedingJoinPoint joinPoint, long time) {

        // 获取方法的关键信息,类,包
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        SysLogEntity sysLogEntity = new SysLogEntity();
        sysLogEntity.setExcTime(time);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        sysLogEntity.setCreateDate(dateFormat.format(new Date()));
        SysLog sysLog = method.getAnnotation(SysLog.class);
        if(sysLog != null) {
            //注解上的描述
            sysLogEntity.setRemark(sysLog.value());
        }
        //请求的 类名、方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        sysLogEntity.setClassName(className);
        sysLogEntity.setMethodName(methodName);
        //请求的参数
        Object[] args = joinPoint.getArgs();
        try {
            List<String> list = new ArrayList<String>();
            for (Object o : args) {
                list.add(o.toString());
            }
            sysLogEntity.setParams(list.toString());
        } catch (Exception e){

        }
        sysLogService.save(sysLogEntity);
    }
}

控制类添方法上添加@SysLog注解

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2021/2/28 23:07
 * @Created by moyun
 */
@RestController
@RequestMapping("/sysUser")
public class SysUserController {

    @Autowired
    private SysUserService userService;


    @RequestMapping("/getUserById/{id}")
    @SysLog("根据Id获取用户")
    public BaseResponse getUserById(@PathVariable Long id){
        SysUser user = userService.getById(id);
        return BaseResponse.renderSuccess("成功!",user);
    }

}

通过postman访问本地接口,成功获取到数据

SpringBoot之AOP面向切面编程

此时查看数据库系统日志表,则会看到对应的日志信息

SpringBoot之AOP面向切面编程

切入点表达式

定义切入点的时候需要一个包含名字和任意参数的签名,还有一个切入点表达式,如execution(public * com.example.aop...(..))

切入点表达式的格式:execution([可见性]返回类型[声明类型].方法名(参数)[异常])

其中[]内的是可选的,其它的还支持通配符的使用:

*:匹配所有字符

..:一般用于匹配多个包,多个参数

+:表示类及其子类

运算符有:&&,||,!

切入点表达式关键词用例
execution:用于匹配子表达式。
//匹配com.cjm.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意
@Pointcut(“execution(* com.cjm.model...(..))”)
public void before(){}

within:用于匹配连接点所在的Java类或者包。
//匹配Person类中的所有方法
@Pointcut(“within(com.cjm.model.Person)”)
public void before(){}
//匹配com.cjm包及其子包中所有类中的所有方法
@Pointcut(“within(com.cjm..*)”)
public void before(){}

this:用于向通知方法中传入代理对象的引用。
@Before(“before() && this(proxy)”)
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}

target:用于向通知方法中传入目标对象的引用。
@Before(“before() && target(target)
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}

args:用于将参数传入到通知方法中。
@Before(“before() && args(age,username)”)
public void beforeAdvide(JoinPoint point, int age, String username){
//处理逻辑
}

@within :用于匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配。
@Pointcut(“@within(com.cjm.annotation.AdviceAnnotation)”)
- 所有被@AdviceAnnotation标注的类都将匹配
public void before(){}

@target :和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。
@Pointcut(“@target(com.cjm.annotation.AdviceAnnotation)”)
public void before(){}

@args :传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注。
@Before(“@args(com.cjm.annotation.AdviceAnnotation)”)
public void beforeAdvide(JoinPoint point){
//处理逻辑
}

@annotation :匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。
@Pointcut(“@annotation(com.cjm.annotation.AdviceAnnotation)”)
public void before(){}

bean:通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。
@Pointcut(“bean(person)”)
public void before(){}

  • 云说Java
  • 关注公众号获取更多资源
  • weinxin
  • Java栈记
  • 关注公众号获取更多资源
  • weinxin
墨云

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: