餐饮智能互联平台技术要点——Spring AOP(公共字段填充)

问题分析

新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表中都会有这些字段,如下:

序号 字段名 含义 数据类型
1 create_time 创建时间 datetime
2 create_user 创建人id bigint
3 update_time 修改时间 datetime
4 update_user 修改人id bigint

而针对于这些字段,我们的赋值方式为:

1). 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。

2). 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。

目前,在我们的项目中处理这些字段都是在每一个业务方法中进行赋值操作,如下:

//设置当前记录的创建时间、修改时间、创建人、修改人
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
//设置创建时间、修改时间、创建人、修改人
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(BaseContext.getCurrentId());
category.setUpdateUser(BaseContext.getCurrentId());

如果都按照上述的操作方式来处理这些公共字段, 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐,那能不能对于这些公共字段在某个地方统一处理,来简化开发呢?

答案是可以的,我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。

实现思路

在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:

序号 字段名 含义 数据类型 操作类型
1 create_time 创建时间 datetime insert
2 create_user 创建人id bigint insert
3 update_time 修改时间 datetime insert、update
4 update_user 修改人id bigint insert、update

实现步骤

1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

3). 在 Mapper 的方法上加入 AutoFill 注解

技术点: 枚举、注解、AOP、反射

代码开发

自定义注解 AutoFill

核心代码

进入到sky-server模块,创建com.sky.annotation包。

package com.sky.annotation;

import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型:UPDATE INSERT
    OperationType value();
}

@Target

@Target(ElementType.METHOD):这是元注解,用于指定注解的使用范围。在这里,@AutoFill注解可以用于标注方法。这意味着只有被@AutoFill注解标记的方法才会触发自动填充处理。

@Retention

@Retention(RetentionPolicy.RUNTIME):这是另一个元注解,用于指定注解的保留策略。RetentionPolicy.RUNTIME表示该注解会在运行时保留,这意味着您可以在运行时通过反射来访问和处理这个注解。

@interface

public @interface AutoFill:这是一个自定义注解的定义,使用@interface关键字来声明。这个注解被命名为AutoFill,即为标记自动填充的注解。

其中OperationType已在sky-common模块中定义。

package com.sky.enumeration;

/**
 * 数据库操作类型
 */
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT
}

自定义切面 AutoFillAspect

核心代码

在sky-server模块,创建com.sky.aspect包。

package com.sky.aspect;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 前置通知,在通知中进行公共字段的赋值
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        /////////////////////重要////////////////////////////////////
        //可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解
        log.info("开始进行公共字段自动填充...");

    }
}

@Pointcut

@Pointcut:这是一个Spring AOP的注解,用于定义切入点。它通常用于定义一个方法,这个方法的名称可以自定义,以便在通知(Advice)中引用这个切入点。

execution

execution(* com.sky.mapper..(..))":这部分定义了切入点的表达式,指定了匹配的方法执行规则。具体来说:

  • execution:这是指定切入点表达式的关键字。
    • com.sky.mapper..(..):这部分表示匹配com.sky.mapper包及其子包中的所有类的所有方法。
  • *(..):这部分表示匹配任意方法名以及任意参数。

@annotation

@annotation(com.sky.annotation.AutoFill):这部分表示切入点的条件,它要求被切入的方法必须带有@AutoFill注解。

这个切入点将用于确定哪些方法需要进行自动填充处理,以便在切面(Aspect)中执行相应的操作。

完善自定义切面 AutoFillAspect 的 autoFill 方法

核心代码

package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 前置通知,在通知中进行公共字段的赋值
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

        //获取到当前被拦截的方法上的数据库操作类型
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
        OperationType operationType = autoFill.value();//获得数据库操作类型

        //获取到当前被拦截的方法的参数--实体对象
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){
            return;
        }

        Object entity = args[0];

        //准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //根据当前不同的操作类型,为对应的属性通过反射来赋值
        if(operationType == OperationType.INSERT){
            //为4个公共字段赋值
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setCreateTime.invoke(entity,now);
                setCreateUser.invoke(entity,currentId);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){
            //为2个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

JoinPoint

JoinPoint 是 Spring AOP 中的一个概念,用于表示正在执行的程序的当前点,通常是代表方法调用的点。它包含了与当前方法调用相关的信息,例如方法名称、方法参数、目标对象等等。

JoinPoint 不仅仅用于表示方法调用点,还可以表示其他程序执行点,如异常抛出点或者字段访问点。在 Spring AOP 中,JoinPoint 主要用于传递给通知(Advice),以便通知能够获取和操作方法调用的上下文信息。

通常,JoinPoint 可以在通知中作为参数来使用,以获取关于方法调用的信息。例如,在前置通知中,您可以使用 JoinPoint 来访问方法的参数,方法的名称和目标对象等信息,并在方法执行前执行一些逻辑。

以下是一些 JoinPoint 常用的属性和方法:

  • getArgs(): 获取方法调用的参数数组。
  • getSignature(): 获取方法的签名,包括方法名、返回类型等信息。
  • getTarget(): 获取目标对象,也就是方法所属的对象。
  • getThis(): 获取代理对象(如果有代理的话),通常是当前对象的引用。
  • toLongString(): 返回长格式的字符串表示,包含有关方法和目标对象的详细信息。
  • toString(): 返回简短格式的字符串表示,通常包含方法名称和目标对象的类名。

总之,JoinPoint 是 Spring AOP 中用于表示方法调用点的对象,它提供了丰富的方法和属性,以便通知能够获取和操作与方法调用相关的上下文信息。

在Mapper接口的方法上加入 AutoFill 注解

CategoryMapper为例,分别在新增和修改方法添加@AutoFill()注解,也需要EmployeeMapper做相同操作

package com.sky.mapper;

@Mapper
public interface CategoryMapper {
    /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    @AutoFill(value = OperationType.INSERT)
    void insert(Category category);
    /**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);

}

同时,将业务层为公共字段赋值的代码注释掉。

1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。

2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。

AOP常用概念

切面(Aspect)

AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。

连接点(Join Point)

哪些方法需要被AOP增强,这些方法就叫做连接点。

通知(Advice)

AOP在特定的切入点上执行的增强处理,有:

  • before 前置通知
  • after 后置通知
  • afterReturning 返回通知
  • afterThrowing 异常通知
  • around 环绕通知

切入点(Pointcut)

实际真正被增强的方法,称为切入点。
切入点是一个表达式,它定义了在哪些方法调用点应用通知。切入点表达式可以使用通配符和逻辑运算符来匹配多个方法。

织入(Weaving)

织入是将通知与切入点匹配的方法连接起来的过程。Spring AOP 在运行时执行织入,将通知织入到方法调用链中。

AOP实现原理

它是基于代理设计模式,而代理设计模式又分为静态代理和动态代理,静态代理比较简单就是一个接口,分别由一个真实实现和一个代理实现,而动态代理分为基于接口的JDK动态代理和基于类的CGLIB的动态代理。

JDK 动态代理

当目标对象实现了接口时,Spring AOP 使用 JDK 动态代理。它通过 Java 反射机制生成代理类,代理类实现了目标接口,并在方法调用时调用通知。

CGLIB 代理

当目标对象没有实现接口时,Spring AOP 使用 CGLIB 代理。CGLIB 通过继承目标类来创建代理对象,然后通过方法的重写来调用通知。

posted @ 2023-10-10 15:10  岸南  阅读(79)  评论(0)    收藏  举报