苍穹外卖项目指北:公共字段的自动填充(AOP)

概要

在开发苍穹外卖员工管理模块,我们常可见新增和更新用户数据时需要指定一些字段

setUpdateUser()
setUpdateTime()
setCreateUser()
setCreateTime()

这些字段会在项目代码中被反复使用,我们若要使项目代码简洁明了,就需要想办法进行公共字段的自动填充(AOP)。

技术名词解释

  • AOP

实现

如何实现公共字段的自动填充?我们要在基本不改变原有代码的基础上,对代码功能进行增强,那么,这就需要使用到AOP的编程思想。

什么是AOP

我们熟知的OOP,也就是面向对象编程思想,秉持着万物皆为对象。那么AOP思想,也就是我们所说的面向切面编程,与OOP类似的,AOP也是一种编程思想。

工作流程

                Spring启动
                    |
                    ↓
          扫描@Component、@Aspect
                    |
                    ↓
            找到切面类和目标类
                    |
                    ↓
          判断哪些方法匹配切入点
                    |
                    ↓
            创建代理对象 Proxy
                    |
                    ↓
------------------------------------------------
                    |
              用户调用方法
                    |
                    ↓
              调用代理对象
                    |
                    ↓
          执行AOP增强逻辑(Advice)
                    |
                    ↓
             调用真实业务方法
                    |
                    ↓
                 返回结果
                    |
                    ↓
             执行后置增强逻辑
------------------------------------------------

AOP通知类型

  1. 前置通知 – @Before
  2. 后置通知 – @After
  3. 环绕通知 – @Around
  4. 返回后通知 – @AfterReturning
  5. 抛出异常后通知 – @AfterThrowing

一个简单的流程

了解了AOP的基本流程以及AOP的通知类型后,我们就来瞧瞧我们如何使用简单AOP

  1. 引入AOP依赖项
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 创建切面类(Aspect)
@Aspect
@Component
public class MyAspect {

}

对于@Aspect标识这是一个切面类
对于@Component标识这个类交给Spring管理

  1. 在切面类中定义切入点
@Pointcut("execution(* com.example.service.*.*(..))")
public void pointCut(){}
  1. 编写通知逻辑
  • 前置通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.blog.dao.BookDao.update())")
    private void pt() {
    }

    @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }
}
  • 后置通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.blog.dao.BookDao.update())")
    private void pt() {
    }

    @After("pt()")
    public void after(){
        System.out.println("after advice ...");
    }
}
  • 环绕通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.blog.dao.BookDao.update())")
    private void pt() {
    }

    @Around("pt()")
    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }
}

对于环绕通知,像上述使用后,我们得到的结果为

around before advice …
around after advice …

只得到了增强方法的结果,原始方法并没有被执行
所以,我们在实际使用中,应在方法中添加ProceedingJoinPoint,同时在需要的位置使用proceed()调用原始方法。

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.blog.dao.BookDao.update())")
    private void pt() {
    }

    @Around("pt()")
    public void around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}

特别注意的是,函数的返回类型很重要。上述案例中的类型是void,返回的是NULL,但是当有返回类型时,我们应该按照返回类型去设计返回值。

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(int com.blog.dao.BookDao.select())")
    private void pt() {
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        Object res = pjp.proceed();
        System.out.println("around after advice ...");
        return res;
    }
}
  • 返回后通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(int com.blog.dao.BookDao.select())")
    private void pt() {
    }

    @AfterReturning("pt()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
}
  • 抛出异常后通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(int com.blog.dao.BookDao.select())")
    private void pt() {
    }

    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

对于AOP,我们就先了解到这,更多的,我们将来再进行讨论。

苍穹外卖中的实现

对于苍穹外卖中,想要通过AOP对公共字段赋值,首先就是要先识别出操作类型(Insert / Update),那么我们采用的方式就是通过注解去标识。

首先定义枚举类:

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

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

    /**
     * 插入操作
     */
    INSERT

}

然后创建自定义注解:

/**
 * 自定义注解,标识某个方法需要进行公共字段的填充
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {

    OperationType value();
}

随后,编写切面类:

@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) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        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);

                // 通过反射为属性赋值
                setCreateUser.invoke(entity, currentId);
                setUpdateUser.invoke(entity, currentId);
                setCreateTime.invoke(entity, now);
                setUpdateTime.invoke(entity, now);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            // 为两个字段赋值
            try {

                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

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

我们来详细看一下切面类,依照上述的简单流程,我们首先需要编写切入点(指定需要增强代码的地方) ,由于指定整个mapper的所有方法过于宽泛,所有的方法都会被拦截,所以我们使用

@annotation(com.sky.annotation.AutoFill)

接着,编写通知类,因为我们需要实现公共字段的填充,在mapper语句执行前进行填充,所以我们采取前置通知。
编写通知类,我们需要实现两种不同的操作,因此,我们需要先获取操作类型,通过反射来获取:

// 获取当前拦截的方法的参数
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();

由于我们使用AOP去做代码增强,所以会对 employeeMapper 中的 insert / update 方法将会进行拦截,比如employeeMapper.insert(employee)中的 employee 被拦截了,我们要将其获取出来(我们约定在第一个位置放这个实体对象,所以取args[0]),然后通过反射去将其值赋给公共字段。

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

随后就是利用反射进行赋值啦~~

为什么我们不直接写entity.setCreateUser(currentId);,反而要采用反射呢?

对于Java编译器来说,直接使用实体类去赋值,编译器只知道entity的类型是Object,不关心取到的到底是什么实体类,对于Object来说,里面没有setCurrentId()这个方法,会直接报错。

而反射,是在运行时动态查找和调用方法,所以没有这种问题。

那我们直接对类型强转不就好了?eg:((Employee) entity).setCreateUser(currentId);
主要的问题就是,我们的AOP设计是对于整个mapper层的,对于需要增强的代码所涉及的实体类不确定(Employee,Category等),因此还是使用反射进行赋值。

Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
setCreateUser.invoke(entity, currentId);

小结

在苍穹外卖的实现中,核心思路是通过自定义注解 @AutoFill 标识目标方法,利用切面类统一拦截 Mapper 层的 Insert / Update 操作,再通过反射机制动态为实体对象填充公共字段,从而将重复代码从业务逻辑中彻底抽离,实现代码的简洁与易维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值