SpringBoot中的两种字段自动填充方式
creatby,updateby等字段自动填充
每个字段在插入数据库,或者更新时都要在serviceimpl层对creatby,updateby等字段进行填充,这个太繁琐了,以下两种方法可以实现字段的自动填充。本项目使用第一种。
方法一:
首先创建一个
AutoFillInterceptor
类。下面会对代码逐行分析。
以下代码也可以直接复制粘贴,但前提是你的实体类中的自动填充的字段和下面四个静态常量名字一样。
@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class AutoFillInterceptor implements Interceptor {private static final String CREATE_BY = "createdBy";private static final String UPDATE_BY = "updatedBy";private static final String CREATE_TIME = "createdAt";private static final String UPDATE_TIME = "updatedAt";@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0];SqlCommandType sqlCommandType = ms.getSqlCommandType();Object parameter = args[1];if(parameter != null && sqlCommandType!=null){if(SqlCommandType.INSERT.equals(sqlCommandType)){if(parameter instanceof MapperMethod.ParamMap){MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameter;ArrayList list= (ArrayList) paramMap.get("list");list.forEach(v->{setFieldValByName(CREATE_TIME, LocalDateTime.now(), v);setFieldValByName(UPDATE_TIME, LocalDateTime.now(), v);});paramMap.put("list", list);}else{// 单条插入的情况// 设置创建人和创建时间字段值setFieldValByName(CREATE_TIME, LocalDateTime.now(), parameter);setFieldValByName(UPDATE_TIME, LocalDateTime.now(), parameter);}}else if(SqlCommandType.UPDATE.equals(sqlCommandType)){// 更新操作// 设置更新人和更新时间字段值setFieldValByName(UPDATE_TIME, LocalDateTime.now(), parameter);}}// 继续执行原始方法return invocation.proceed();}private void setFieldValByName(String fieldName, Object fieldVal, Object parameter) {MetaObject metaObject = SystemMetaObject.forObject(parameter);if (metaObject.hasSetter(fieldName)) {metaObject.setValue(fieldName, fieldVal);}}@Overridepublic void setProperties(Properties properties) {Interceptor.super.setProperties(properties);}@Overridepublic Object plugin(Object target) {return Interceptor.super.plugin(target);}
}
代码结构与作用
这是一个实现了MyBatis拦截器(Interceptor
接口)的类AutoFillInterceptor
,用于在执行SQL操作(INSERT或UPDATE)时,自动填充一些通用字段,比如创建时间(createdAt
)、更新时间(updatedAt
)等。
在企业级项目中,通常需要记录数据的创建时间和修改时间,这个拦截器就是为了解决这种需求,在新增和修改数据时自动填充这些字段。下面我们来逐行分析代码。
代码逐行解析
@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
:Spring的注解,将这个类注册为一个Spring Bean,便于管理。@Intercepts
:MyBatis的注解,声明这是一个拦截器,并指定要拦截的目标。@Signature
:定义拦截器的具体拦截方法。type = Executor.class
:表示拦截MyBatis的Executor
类。method = "update"
:表示拦截update
方法,这个方法用于执行更新操作(包括INSERT、UPDATE、DELETE)。args = {MappedStatement.class, Object.class}
:指定update
方法的参数类型,即SQL映射信息MappedStatement
和参数对象Object
。
public class AutoFillInterceptor implements Interceptor {
AutoFillInterceptor
类实现了MyBatis的Interceptor
接口,用于实现自定义的拦截逻辑。
private static final String CREATE_BY = "createdBy";private static final String UPDATE_BY = "updatedBy";private static final String CREATE_TIME = "createdAt";private static final String UPDATE_TIME = "updatedAt";
- 这几行定义了一些常量,分别表示字段名称,如创建者、修改者、创建时间和修改时间。这些常量将在拦截逻辑中用来自动填充字段。
@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0];SqlCommandType sqlCommandType = ms.getSqlCommandType();Object parameter = args[1];
intercept
方法是拦截器的核心逻辑。Object[] args = invocation.getArgs()
:获取拦截方法的参数。MappedStatement ms = (MappedStatement) args[0]
:获取MappedStatement
,包含了有关SQL语句的信息。SqlCommandType sqlCommandType = ms.getSqlCommandType()
:获取SQL的操作类型(INSERT、UPDATE、DELETE)。Object parameter = args[1]
:获取参数对象,通常是用户要插入或更新的数据。
if(parameter != null && sqlCommandType != null){
- 检查参数是否为空,并确认操作类型是否非空,确保有必要继续执行后续操作。
if(SqlCommandType.INSERT.equals(sqlCommandType)){if(parameter instanceof MapperMethod.ParamMap){MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameter;ArrayList list= (ArrayList) paramMap.get("list");list.forEach(v -> {setFieldValByName(CREATE_TIME, LocalDateTime.now(), v);setFieldValByName(UPDATE_TIME, LocalDateTime.now(), v);});paramMap.put("list", list);} else {// 单条插入的情况// 设置创建人和创建时间字段值setFieldValByName(CREATE_TIME, LocalDateTime.now(), parameter);setFieldValByName(UPDATE_TIME, LocalDateTime.now(), parameter);}}
if (SqlCommandType.INSERT.equals(sqlCommandType))
:如果当前SQL是INSERT操作:if (parameter instanceof MapperMethod.ParamMap)
:判断参数是否是MapperMethod.ParamMap
类型,这通常用于批量插入。ArrayList list = (ArrayList) paramMap.get("list")
:从参数Map中获取名为list
的参数,这是批量插入的数据集合。list.forEach(v -> {...})
:对每个元素进行操作,调用setFieldValByName
方法设置createdAt
和updatedAt
为当前时间。
else
部分:处理单条插入的情况,直接给parameter
对象设置创建时间和更新时间。
else if(SqlCommandType.UPDATE.equals(sqlCommandType)){// 更新操作// 设置更新人和更新时间字段值setFieldValByName(UPDATE_TIME, LocalDateTime.now(), parameter);}
else if (SqlCommandType.UPDATE.equals(sqlCommandType))
:如果当前SQL是UPDATE操作:- 使用
setFieldValByName
方法将updatedAt
字段设置为当前时间。
- 使用
}// 继续执行原始方法return invocation.proceed();}
- 最终通过
invocation.proceed()
调用被拦截的方法,继续执行原始的数据库操作。
private void setFieldValByName(String fieldName, Object fieldVal, Object parameter) {MetaObject metaObject = SystemMetaObject.forObject(parameter);if (metaObject.hasSetter(fieldName)) {metaObject.setValue(fieldName, fieldVal);}}
setFieldValByName
方法用于设置对象中指定字段的值:MetaObject metaObject = SystemMetaObject.forObject(parameter)
:创建MetaObject
,用于操作传入对象的元数据。if (metaObject.hasSetter(fieldName))
:检查对象是否有对应字段的setter方法。metaObject.setValue(fieldName, fieldVal)
:如果有setter方法,则设置字段的值。
@Overridepublic void setProperties(Properties properties) {Interceptor.super.setProperties(properties);}@Overridepublic Object plugin(Object target) {return Interceptor.super.plugin(target);}
}
setProperties
和plugin
方法是Interceptor
接口的默认实现,plugin
方法用于生成代理对象。
总结
- 这个拦截器的作用是自动填充
createdAt
和updatedAt
字段,以便在执行INSERT和UPDATE操作时自动记录创建和更新时间。 - 主要拦截
Executor
的update
方法,通过判断SQL类型来确定是INSERT还是UPDATE操作,从而设置相应字段。 - 使用了MyBatis的
MetaObject
工具类来动态操作参数对象的字段值。
通过这个拦截器,开发者不需要在业务代码中手动设置createdAt
和updatedAt
,大大减少了重复代码,也保证了这些公共字段的一致性和正确性。
方法二:
这个方法是使用自定义注解来写的,所以要在需要填充的sql上加上这个注解。这个可能更加灵活更加简单把。
公共字段自动填充
技术点:枚举、注解、AOP、反射
创建时间、修改时间、创建人、修改人这4个公共字段。
为mapper方法加注解AutoFill,标识需要进行公共字段自动填充
自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值。
在Mapper的方法上接入AutoFill注解。
public enum OperationType {更新操作UPDATE,插入操作INSERT
}@Target(ElementType.METHOD)当前注解加在什么位置
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {//数据库操作类型:UPDATE INSERTOperationType value();
}
补充注解基本知识
public @interface MyAnnotation {// 定义注解的成员String value(); // 这是一个名为"value"的成员int count() default 1; // 这是一个名为"count"的成员,带有默认值
}@MyAnnotation(value = "Hello", count = 3)
public class MyClass {// 类的代码
}
对于AutoFillAspect类
切点、execution表达式
/*** 自定义切面,实现公共字段自动填充处理逻辑*/
@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();}}}}
使用
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);
自定义切面:实现公共字段的自动填充
这段代码使用了 Spring AOP(面向切面编程)来实现对数据库操作时,自动填充一些公共字段,例如创建时间、更新时间、创建人、更新人等。接下来,我们逐行解析这段代码,以帮助你理解各个部分的功能和实现逻辑。
代码结构概览
@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("开始进行公共字段自动填充...");...}
}
这段代码定义了一个切面 AutoFillAspect
,它会在符合条件的数据库操作方法执行之前,通过前置通知 (@Before
) 自动对某些公共字段进行填充。
注解解释
@Aspect
:表示当前类是一个切面类,用于定义通知和切入点。@Component
:把这个切面类注册为 Spring 容器中的一个组件。@Slf4j
:用来启用日志功能,以方便调试和记录信息。
切入点定义
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
解释:
@Pointcut
:用于定义一个切入点,描述哪些方法需要被切面逻辑拦截。execution(* com.sky.mapper.*.*(..))
:匹配com.sky.mapper
包下的所有类和所有方法,(..)
表示任意参数类型和数量。&& @annotation(com.sky.annotation.AutoFill)
:表示只拦截被@AutoFill
注解标记的方法。
通过这种定义,只有符合指定包下的类且有 @AutoFill
注解的方法,才会被切面逻辑拦截。
前置通知(Before Advice)
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {log.info("开始进行公共字段自动填充...");...
}
@Before("autoFillPointCut()")
:这是前置通知,表示在切入点所匹配的方法执行之前,执行autoFill()
方法。JoinPoint joinPoint
:JoinPoint
是一个连接点,表示被拦截的方法,允许获取到目标方法的一些信息,比如方法名和参数等。
获取注解和方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
:获取当前拦截的方法的签名信息,转换为MethodSignature
类型。AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
:获取方法上的@AutoFill
注解对象。OperationType operationType = autoFill.value();
:获取注解中指定的数据库操作类型(例如 INSERT 或 UPDATE)。
获取方法参数
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {return;
}
Object entity = args[0];
Object[] args = joinPoint.getArgs();
:获取当前被拦截的方法的参数。if (args == null || args.length == 0)
:如果没有参数,直接返回。Object entity = args[0];
:假设第一个参数是实体对象,用于操作数据库。这里有一个约定,即实体对象总是第一个参数。
准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
LocalDateTime now = LocalDateTime.now();
:获取当前时间,用于填充创建时间和更新时间。Long currentId = BaseContext.getCurrentId();
:获取当前操作用户的 ID,用于填充创建人和更新人信息。
根据操作类型进行赋值
插入操作(INSERT)
if (operationType == OperationType.INSERT) {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();}
}
if (operationType == OperationType.INSERT)
:如果数据库操作类型是插入(INSERT)。- 通过反射的方式获取实体类中的
setCreateTime
、setCreateUser
、setUpdateTime
和setUpdateUser
方法。 invoke()
方法用于调用这些 setter 方法并传入相应的值,完成公共字段的赋值。
更新操作(UPDATE)
else if (operationType == OperationType.UPDATE) {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();}
}
else if (operationType == OperationType.UPDATE)
:如果操作类型是更新(UPDATE)。- 这里只需填充更新相关的字段,即更新时间和更新人。
小结
这段代码实现了对数据库操作的公共字段自动填充,具体如下:
- 定义一个切面
AutoFillAspect
,用于拦截特定包中的方法,并且方法需要用@AutoFill
注解进行标记。 - 使用 AOP 的前置通知在方法执行前进行字段自动填充。
- 通过反射机制获取实体对象的方法并进行赋值,根据操作类型填充不同的字段。
这使得代码变得更加简洁和可维护,减少了重复的公共字段赋值逻辑,也方便对创建时间、更新时间等公共属性的一致性管理。