aop实现公共字段自动填充

简介:苍穹外卖p31-p34

遇到问题

问题分析

当进行Insert操作时,每次都需要对创建时间、创建者id、修改时间和修改者id进行赋值;当进行Update操作时,每次都需要对修改时间和修改者id进行复制;

缺点

代码冗余,易出错,可读性下降,测试变复杂,增加性能开销,维护成本增加

在这里插入图片描述

四种解决方法

为了减少冗余,建议采取以下措施:
1、封装操作逻辑:将重复的赋值逻辑抽象到一个方法中,确保每次插入和更新时都能调用这些方法。
2、使用 ORM 框架的生命周期回调:如前述,利用 ORM 框架的功能自动管理这些字段,避免手动赋值。
3、服务层管理:通过服务层统一管理业务逻辑,在插入和更新时设置相关字段。
4、AOP 或拦截器:使用切面编程来处理这些通用的赋值逻辑,避免在每个业务逻辑中重复代码。
在这里插入图片描述

AOP 或拦截器方法具体实现

一、创建在定义注解

在这里插入图片描述

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,DELETE
    OperationType value();
}

OperationType为枚举类:类型枚举

package com.sky.enumeration;

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

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

    /**
     * 插入操作
     */
    INSERT

}

二、创建切面类

在这里插入图片描述

package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
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//这也是一个Bean,需要Spring管理
@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];//获取当前被拦截方法的第一个参数,因为实体不是固定的,所以使用Object接收;约定:被拦截方法的第一个参数必须是实体类型的

        //准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();//从ThreadLocal中获取当前用户的id(当前用户在登录时,拦截器类获取token令牌,解析并将当前用户id存进ThreadLocal中)

        //根据当前不同的操作类型,为对应的属性通过反射来赋值
        if (operationType == OperationType.INSERT){
            //为4个公共字段赋值
            try {
                //entity是Object类型,没有get,set方法,必须通过反射获得方法
                Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser");

                //通过反射为对象属性赋值
                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 {
                //entity是Object类型,没有get,set方法,必须通过反射获得方法
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser");

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

在这里插入图片描述

三、在需要进行拦截的方法上加上自定义注解AutoFill

    /**
     * 插入员工数据
     * @param employee
     */
    @Insert(value = "Insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
            "value " +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    @AutoFill(value = OperationType.INSERT)//在需要进行自动填充的方法中加入注解,并写明操作类型
    void addEmployee(Employee employee);

四种解决方法具体说明

为了减少冗余,可以采用以下措施,每种方法都可以提高代码的可维护性和可读性。以下是针对每个措施的具体说明:

1. 封装操作逻辑

实现步骤

  • 创建一个工具类或服务类,专门负责处理创建时间、修改时间、创建者 ID 和修改者 ID 的赋值逻辑。
  • 定义一个方法,例如 setAuditFields(entity, isNew),该方法接受要操作的实体和一个布尔值 isNew(表示是否为新创建的记录)。

示例代码

public class AuditUtil {
    public static void setAuditFields(MyEntity entity, boolean isNew) {
        if (isNew) {
            entity.setCreatedTime(new Date());
            entity.setCreatedBy(currentUserId());
        }
        entity.setModifiedTime(new Date());
        entity.setModifiedBy(currentUserId());
    }
}

调用方式
在插入或更新前调用该方法:

AuditUtil.setAuditFields(entity, true); // 对于插入
AuditUtil.setAuditFields(entity, false); // 对于更新

2. 使用 ORM 框架的生命周期回调

实现步骤

  • 在实体类中使用 ORM 框架(如 Hibernate 或 JPA)的注解来标记生命周期回调方法。这些方法会在插入或更新实体时被自动调用。

示例代码

@Entity
public class MyEntity {
    @Column(name = "created_time")
    private Date createdTime;

    @Column(name = "modified_time")
    private Date modifiedTime;

    @PrePersist
    public void prePersist() {
        this.createdTime = new Date();
        this.modifiedTime = new Date();
    }

    @PreUpdate
    public void preUpdate() {
        this.modifiedTime = new Date();
    }
}

3. 服务层管理

实现步骤

  • 在服务层定义通用的方法,用于插入和更新操作。在这些方法中集中管理相关字段的赋值逻辑。

示例代码

@Service
public class MyEntityService {
    public void save(MyEntity entity) {
        setAuditFields(entity, true);
        myEntityRepository.save(entity);
    }

    public void update(MyEntity entity) {
        setAuditFields(entity, false);
        myEntityRepository.save(entity);
    }

    private void setAuditFields(MyEntity entity, boolean isNew) {
        if (isNew) {
            entity.setCreatedTime(new Date());
            entity.setCreatedBy(currentUserId());
        }
        entity.setModifiedTime(new Date());
        entity.setModifiedBy(currentUserId());
    }
}

4. AOP 或拦截器

实现步骤

  • 使用 AOP(面向切面编程)来创建一个切面,拦截所有需要的服务方法,在执行前后加入赋值逻辑。

示例代码

@Aspect
@Component
public class AuditAspect {
    @Before("execution(* com.example.service.MyEntityService.save(..))")
    public void beforeSave(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args[0] instanceof MyEntity) {
            MyEntity entity = (MyEntity) args[0];
            entity.setCreatedTime(new Date());
            entity.setCreatedBy(currentUserId());
        }
    }

    @Before("execution(* com.example.service.MyEntityService.update(..))")
    public void beforeUpdate(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args[0] instanceof MyEntity) {
            MyEntity entity = (MyEntity) args[0];
            entity.setModifiedTime(new Date());
            entity.setModifiedBy(currentUserId());
        }
    }
}

总结

通过上述四种方法,可以有效地减少代码中的冗余,确保创建和修改时间、创建者 ID 和修改者 ID 等字段的一致性和正确性。这不仅提高了代码质量,还减少了未来维护和修改的成本。选择具体的实现方式可以根据项目的需求、团队的技术栈和约定进行调整。

当然可以更详细地讨论 AOP(面向切面编程)。以下是对 AOP 相关概念的深入解析,包括示例和应用场景。

AOP(面向切面编程)

对 AOP 相关概念的深入解析

1. 横切关注点

横切关注点可以理解为那些影响多个模块或组件的功能,而不直接属于任何单一模块。例如:

  • 日志记录:在不同的服务中可能都需要记录日志。
  • 安全性:访问控制、用户认证等通常需要跨多个模块处理。
  • 事务管理:在数据库操作中,涉及到多个方法时,需要确保事务的一致性。

2. 切面(Aspect)

切面是 AOP 的核心,它封装了与横切关注点相关的代码。切面包括两部分:

  • 切入点(Pointcut):定义哪些连接点会被切面影响。
  • 通知(Advice):定义在切入点触发时执行的逻辑。
示例
@Aspect
public class LoggingAspect {
    
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}

    @Before("serviceLayer()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature());
    }
}

在这个示例中,serviceLayer() 定义了切入点,即所有 com.example.service 包下的方法,logBefore() 是前置通知,在方法执行前记录日志。

3. 连接点(Join Point)

连接点是程序执行过程中的某个特定位置。在 Spring AOP 中,连接点通常是方法调用。AOP 框架会在这些连接点插入切面逻辑。

4. 代理(Proxy)

AOP 实现通常依赖于代理模式。代理可以是:

  • 静态代理:在编译时生成代理类。
  • 动态代理:在运行时创建代理对象。
示例(动态代理)

使用 Spring AOP 时,框架会自动创建代理。例如,使用 JDK 动态代理或 CGLIB 代理来增强目标类的功能。

5. AOP 框架

Spring AOP
  • 集成在 Spring 框架中,适用于在 Spring 管理的 Bean 上使用 AOP。
  • 主要通过注解(如 @Aspect, @Before)或 XML 配置进行切面定义。
AspectJ
  • 更强大的 AOP 框架,支持编译时和运行时织入。
  • 提供了更复杂的切入点表达式和更全面的功能。

6. 使用场景

以下是一些常见的 AOP 使用场景:

  • 日志管理:统一处理系统的日志记录,避免在每个方法中重复代码。
  • 性能监控:监控方法的执行时间,以便进行性能优化。
  • 异常处理:集中管理业务逻辑中的异常处理,提供统一的错误响应。
  • 安全控制:在方法调用前验证用户权限,确保只有授权用户能够访问特定功能。
  • 事务管理:在数据库操作中管理事务的开始和提交,避免重复代码。

7. 优缺点

优点
  • 提高代码可重用性:将横切关注点提取到切面中,减少重复代码。
  • 增强模块化:使业务逻辑和横切关注点分开,便于管理和维护。
  • 提高可维护性:修改横切关注点的逻辑只需更新切面,而不是逐个查找和修改。
缺点
  • 学习曲线陡峭:初学者可能需要时间来理解 AOP 的概念和实现方式。
  • 调试困难:由于切面逻辑是动态插入的,可能导致调试时难以追踪问题。
  • 性能开销:在高频调用的场景下,AOP 可能引入一定的性能开销。

8. 结论

AOP 是一种有效的编程技术,通过将横切关注点从业务逻辑中分离出来,使得代码更加整洁和可维护。在现代软件开发中,特别是在微服务架构和企业应用中,AOP 得到了广泛应用。掌握 AOP 可以帮助开发者更好地组织代码,提高系统的可维护性和扩展性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

╰つ゛木槿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值