简介:苍穹外卖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 可以帮助开发者更好地组织代码,提高系统的可维护性和扩展性。