> 文章列表 > Java自定义注解实现持久化时自动设置创建人字段

Java自定义注解实现持久化时自动设置创建人字段

Java自定义注解实现持久化时自动设置创建人字段

先简单介绍一下自定义注解相关的一些前置知识,完整代码可通过目录直接跳转查看

1. 注解的定义

1.1 元注解

元注解 的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation(元注解)类型,它们被用来提供对其它 annotation类型作说明

标准的元注解:
  • @Target
  • @Retention
  • @Documented
  • @Inherited

下面对这四个元注解进行详细介绍:

@Target:标明作用范围,取值在java.lang.annotation.ElementType 中进行定义

public enum ElementType {/** 类,接口(包括注解类型)或枚举的声明 */TYPE,/** 属性的声明 */FIELD,/** 方法的声明 */METHOD,/** 方法形式参数声明 */PARAMETER,/** 构造方法的声明 */CONSTRUCTOR,/** 局部变量声明 */LOCAL_VARIABLE,/** 注解类型声明 */ANNOTATION_TYPE,/** 包的声明 */PACKAGE
}

@Retention:修饰自定义注解的生命周期,取值在java.lang.annotation.RetentionPolicy中定义:

public enum RetentionPolicy {/*** Annotations are to be discarded by the compiler.* (注解将被编译器忽略掉)*/SOURCE,/*** Annotations are to be recorded in the class file by the compiler* but need not be retained by the VM at run time.  This is the default* behavior.* (注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为)*/CLASS,/*** Annotations are to be recorded in the class file by the compiler and* retained by the VM at run time, so they may be read reflectively.* (注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到)* @see java.lang.reflect.AnnotatedElement*/RUNTIME
}

@Documented:指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中

@Inherited:是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解,@Inherited注解只对那些@Target被定义为 ElementType.TYPE 的自定义注解起作用

介绍完元注解之后,我们再来看一下自定义注解的实现代码。

1.2 基于Spring AOP开发

1.2.1 AspectJ

AspectJ 是一个基于 Java 语言的 AOP 框架。在 Spring 2.0 以后,新增了对 AspectJ 框架的支持。在 Spring 框架中建议使用 AspectJ 框架开发 AOP,在使用 AspectJ 配置切面时,切面不需要实现一些特定的接口。

1.2.1.1 AspectJ术语
  • Joinpoint(连接点): 类里面可以被增强的方法,这些方法称为连接点,连接点具有以下方法

    joinPoint.getTarget();     获取目标对象
    joinPoint.getSignature().getName();   获取目标方法名
    joinPoint.getArgs();    获取目标方法参数列表
    joinPoint.getThis();    获取代理对象
    
  • **Pointcut(切入点): ** 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义,切入点指示符规则稍后详细介绍

    方式一:

    @Before("within(com.baomidou.mybatisplus.extension.service.IService+)")
    public void before(JoinPoint joinPoint) {System.out.println("进入切面");
    }
    

    方式二:

    /*** 使用Pointcut定义切点*/
    @Pointcut("execution(* com.dwp.spring.springAop.dao.UserDao.addUser(..))")
    private void myPointcut(){}/*** 应用切入点函数*/
    @After(value="myPointcut()")
    public void after(){System.out.println("最终通知....");
    }
    
  • Advice(通知/增强): 所谓通知是指拦截到Joinpoint之后所要做的事情,分为以下五种类型

    • before 目标方法执行前执行,前置通知
    • after 目标方法执行后执行,后置通知
    • after returning 目标方法返回时执行 ,后置返回通知
    • after throwing 目标方法抛出异常时执行 异常通知
    • around 在目标函数执行中执行,可控制目标函数是否执行,环绕通知
  • Introduction(引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field.

  • Target(目标对象): 代理的目标对象(要增强的类)

  • Weaving(织入): 是把增强应用到目标的过程,把advice 应用到 target的过程

  • Proxy(代理): 一个类被AOP织入增强后,就产生一个结果代理类

1.2.1.2 切入点指示符:

为了方法通知应用到相应过滤的目标方法上,SpringAOP提供了匹配表达式,这些表达式也叫切入点指示符.

a. 通配符

在定义匹配表达式时,通配符几乎随处可见,如*、… 、+ ,它们的含义如下:

  • … :匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包

    //任意返回值,任意名称,任意参数的公共方法
    execution(public * *(..))
    //匹配com.dwp.dao包及其子包中所有类中的所有方法
    within(com.dwp.dao..*)
    
  • + :匹配给定类的任意子类

    //匹配实现了DaoUser接口的所有子类的方法
    within(com.dwp.dao.DaoUser+)
    
  • * :匹配任意数量的字符

    //匹配com.dwp.service包及其子包中所有类的所有方法
    within(com.dwp.service..*)
    //匹配以set开头,参数为int类型,任意返回值的方法
    execution(* set*(int))
    

b. 类型签名表达式

为了方便类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字。其语法格式如下:

within(<type name>)

示例:

//匹配com.dwp.dao包及其子包中所有类中的所有方法
@Pointcut("within(com.dwp.dao..*)")//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.dwp.dao.UserDaoImpl)")//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.dwp.dao.UserDaoImpl+)")//匹配所有实现UserDao接口的类的所有方法
@Pointcut("within(com.dwp.dao.UserDao+)")

c. 方法签名表达式

如果想根据方法签名进行过滤,关键字execution可以帮到我们,语法表达式如下

//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))

对于给定的作用域、返回值类型、完全限定类名以及参数匹配的方法将会应用切点函数指定的通知,这里给出模型案例:

//匹配UserDaoImpl类中的所有方法
@Pointcut("execution(* com.dwp.dao.UserDaoImpl.*(..))")//匹配UserDaoImpl类中的所有公共的方法
@Pointcut("execution(public * com.dwp.dao.UserDaoImpl.*(..))")//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public int com.dwp.dao.UserDaoImpl.*(..))")//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法
@Pointcut("execution(public * com.dwp.dao.UserDaoImpl.*(int , ..))")

d. 其他指示符

  • bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;

    //匹配名称中带有后缀Service的Bean。
    @Pointcut("bean(*Service)")
    private void myPointcut1(){}
    
  • this :用于匹配当前AOP代理对象类型的执行方法;请注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配

    //匹配了任意实现了UserDao接口的代理对象的方法进行过滤
    @Pointcut("this(com.dwp.spring.springAop.dao.UserDao)")
    private void myPointcut2(){}
    
  • target :用于匹配当前目标对象类型的执行方法;

    //匹配了任意实现了UserDao接口的目标对象的方法进行过滤
    @Pointcut("target(com.dwp.spring.springAop.dao.UserDao)")
    private void myPointcut3(){}
    
  • @within:用于匹配所以持有指定注解类型内的方法;请注意与within是有区别的, within是用于匹配指定类型内的方法执行;

    //匹配使用了MarkerAnnotation注解的类(注意是类)
    @Pointcut("@within(com.dwp.spring.annotation.MarkerAnnotation)")
    private void myPointcut4(){}
    
  • @annotation(com.dwp.spring.MarkerMethodAnnotation) : 根据所应用的注解进行方法过滤

    //匹配使用了MarkerAnnotation注解的方法(注意是方法)
    @Pointcut("@annotation(com.dwp.spring.annotation.MarkerAnnotation)")
    private void myPointcut5(){}
    

最后说明一点,切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或者&&、||、!),如下例:

//匹配了任意实现了UserDao接口的目标对象的方法并且该接口不在com.dwp.dao包及其子包下
@Pointcut("target(com.dwp.spring.springAop.dao.UserDao) !within(com.dwp.dao..*)")
private void myPointcut6(){}//匹配了任意实现了UserDao接口的目标对象的方法并且该方法名称为addUser
@Pointcut("target(com.dwp.spring.springAop.dao.UserDao)&&execution(* com.dwp.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut7(){}

1.3 完整代码

自定义注解工程:

pom.xml引入依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Insert.class:

import java.lang.annotation.*;/*** 修饰在方法上,用于数据持久化时自动给创建人属性字段赋值* <p>* 从Keycloak中获取操作用户信息,如果实体类存在指定名称的属性,则会自动为其赋值** @author deng.weiping* @since 0.0.1*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Insert {/*** 需赋值的字段名称,默认值:createBy*/String name() default "createBy";}

InsertAspect.class:

import com.dwp.sdw.util.ReflectUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;@Aspect
@Component
@Slf4j
public class InsertAspect {/*** 前置通知*/@Before("@annotation(an)")public void before(JoinPoint joinPoint, Insert an) {invokeMethod(joinPoint, an);}/*** 获取请求参数** @param point* @return*/private static void invokeMethod(JoinPoint point, Insert an) {Object[] objs = point.getArgs();if (objs.length > 0) {for (Object obj : objs) {ReflectUtil.setSpecifiedFieldValue(obj, an.name());}}}}

ReflectUtil.class:

import lombok.extern.slf4j.Slf4j;import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;@Slf4j
public class ReflectUtil {/*** 查找指定属性并赋值** @param bean      类对象* @param fieldName 字段名*/public static void setSpecifiedFieldValue(Object bean, String fieldName) {try {Class clazz = bean.getClass();while (clazz != Object.class) {Field[] fields = clazz.getDeclaredFields();List<Field> results = Arrays.stream(fields).filter(field -> fieldName.equals(field.getName())).collect(Collectors.toList());if (!results.isEmpty()) {results.forEach(field -> setFieldValue(bean, field));//最小匹配break;}//当前类未匹配上指定字段,尝试向上从父类中查找指定字段clazz = clazz.getSuperclass();}} catch (Exception e) {log.warn("The specified field was not matched");}}/*** 通过反射给类的属性赋值** @param bean  类对象* @param field 类属性*/public static void setFieldValue(Object bean, Field field) {field.setAccessible(true);try {/*** 具体值根据实际情况自行获取* 这里使用Keycloak*/field.set(bean, KeycloakUtils.getUsername());} catch (IllegalAccessException e) {log.warn("Failed to set a value for the property:{}", e);}}
}

2 注解的使用

项目工程

  1. pom.xml引入自定义注解依赖
<dependency><groupId>com.dwp</groupId><artifactId>sdw-annotation</artifactId><version>0.0.1</version>
</dependency>
  1. 在方法上加上注解
@Insert
public void save(Student stu) {studentMapper.insert(stu);
}