注解的概念

在Spring Boot中,注解是一种用于提供元数据的特殊标记。它们可以应用于类、方法、字段或参数上,用于配置应用程序的行为或告诉Spring框架如何处理特定的组件。

Spring Boot提供了许多注解,用于简化配置和自动化常见任务。以下是一些常用的Spring Boot注解及其概念:

  1. @SpringBootApplication:这是一个组合注解,用于标记主应用程序类。它包含了@Configuration@EnableAutoConfiguration@ComponentScan注解,用于启用自动配置和组件扫描。
  2. @RestController:用于标记一个类,表示它是一个RESTful风格的控制器。它结合了@Controller@ResponseBody注解,简化了编写RESTful API的过程。
  3. @RequestMapping:用于映射HTTP请求到控制器的处理方法。可以用于类级别和方法级别,用于指定请求的URL路径和HTTP方法。
  4. @Autowired:用于自动装配依赖。当Spring容器中存在匹配类型的bean时,它会自动将其注入到标记了@Autowired的字段、构造函数或方法参数中。
  5. @Component:用于标记一个类为Spring组件。Spring会自动扫描并将其实例化为bean,可以通过@Autowired进行依赖注入。
  6. @Configuration:用于标记一个类为配置类。配置类通常包含用于配置bean的方法,这些方法使用@Bean注解进行标记。
  7. @EnableAutoConfiguration:用于启用Spring Boot的自动配置机制。它会根据项目的依赖和配置,自动配置Spring应用程序的各种组件。

这只是一小部分Spring Boot注解的概念,Spring Boot提供了更多的注解用于不同的场景和需求。通过使用这些注解,可以简化配置和开发过程,提高开发效率。相当于一个说明文件,告诉应用程序某个被注解的类或属性是什么,要怎么处理。注解对于它所修饰的代码并没有直接的影响。

注解的使用范围

1)为编译器提供信息:注解能被编译器检测到错误或抑制警告。

2)编译时和部署时的处理: 软件工具能处理注解信息从而生成代码,XML文件等等。

3)运行时的处理:有些注解在运行时能被检测到。

自定义注解的步骤

第一步:定义注解

第二步:配置注解

第三步:解析注解

注解的基本语法

最基本的注解定义

public @interface MyAnnotation {
      public String name();
      int age();
      String sex() default "女";
}

在自定义注解中,其实现部分只能定义注解类型元素!
说明:

  • 访问修饰符必须为public,不写默认为public;
  • 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型以及一维数组;
  • 该元素的名称一般定义为名词,如果注解中只有一个元素,名字起为value最好;
  • ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;
  • default`代表默认值,值必须定义的类型一致;
  • 如果没有默认值,代表后续使用注解时必须给该类型元素赋值。

常用的元注解

元注解:专门修饰注解的注解。

@Target

@Target是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。其注解的源码如下

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**

* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
  */
       ElementType[] value();
  }

从源码可以看出它使用一个枚举类型元素,接下来看这个枚举类型的源码

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration(类、接口(包括注释类型)或枚举声明) */
    TYPE,

    /** Field declaration (includes enum constants) (字段声明(包括枚举常量))*/
    FIELD,

    /** Method declaration(方法声明) */
    METHOD,

    /** Formal parameter declaration(形式参数声明) */
    PARAMETER,

    /** Constructor declaration(构造函数声明) */
    CONSTRUCTOR,

    /** Local variable declaration(局部变量声明) */
    LOCAL_VARIABLE,

    /** Annotation type declaration(注释类型声明) */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

因此,我们可以在使用@Target时指定注解的使用范围,示例如下:

//@MyAnnotation被限定只能使用在类、接口或方法上面
@Target(value = {ElementType.METHOD,ElementType.TYPE})
public @interface MyAnnotation {
    public String name();
    int age();
    String sex() default "女";
}

@Retention

@Retention注解,用来修饰自定义注解的生命力。
a.如果一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;

b.如果一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到,是默认的;

c.如果一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME
@Retention注解源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

里面也是一个枚举类型元素,其源码如下

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,

    /**
     * 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.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

使用此注解修饰自定义注解生命力的示例如下:

//设置注解的生命力在运行期
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    public String name();
    int age();
    String sex() default "女";
}

@Documented

@Documented注解,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

@Inherited

@Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类(继承关系)的声明部分也能自动拥有该注解。该注解只对@Target被定义为ElementType.TYPE的自定义注解起作用。下面对其进行详细说明:
1)打开其源码,可以看到

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

(1)该注解作用于整个程序运行中(@Retention(RetentionPolicy.RUNTIME);

(2)该注解只能修饰注解(@Target({ElementType.ANNOTATION_TYPE})),它是一个元注解。

此注解的中文翻译是继承的意思,那么究竟是什么意思呢?通过示例进行演示

第一:先定义两个注解@HasInherited 和 @NoInherited,前者注解包含@Inherited 注解,后者反之

也就是说,被 @Inherited 注解修饰的注解,如果作用于某个类上,其子类是可以继承的该注解的。否则,若一个注解没有被 @Inherited注解所修饰,那么其作用范围只能是当前类,其子类是不能被继承的。

自定义注解举例

创建注解

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.aidex.common.enums.BusinessType;
import com.aidex.common.enums.OperatorType;

/**
 * 自定义操作日志记录注解
 *
 * @author wcy
 *
 */
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{
    /**
     * 模块
     */
    public String title() default "";

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    public OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;

    /**
     * 是否保存响应的参数
     */
    public boolean isSaveResponseData() default true;
}

创建aop切面

@Aspect
@Component
public class LogAspect {
    private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    private static final String LOG_IGNORE_PROPERTIES = "createBy,createDate,updateBy,updateDate,version,updateIp,delFlag,remoteAddr,requestUri,method,userAgent";

    /** 排除敏感属性字段 */
    public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };

    // 配置织入点
    @Pointcut("@annotation(com.aidex.common.annotation.Log)")
    public void logPointCut() {
    }

    @Around("@annotation(com.aidex.common.annotation.Log)")
    public Object loggingAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        // 定义返回对象、得到方法需要的参数
        Object resultData = null;
        Object[] args = joinPoint.getArgs();
        long takeUpTime = 0;
        resultData = joinPoint.proceed(args);
        long endTime = System.currentTimeMillis();
        takeUpTime = endTime - startTime;
        handleLog(joinPoint, null, resultData, takeUpTime);
        return resultData;
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null, 0);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, long takeUpTime) {
        try {
            // 获得注解
            Log controllerLog = getAnnotationLog(joinPoint);
            if (controllerLog == null) {
                return;
            }

            // 获取当前的用户
            LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());

            // *========数据库日志=========*//
            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
            operLog.setOperIp(ip);
            operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
            if (loginUser != null) {
                operLog.setOperName(loginUser.getUsername());
            }

            if (e != null) {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            operLog.setTakeUpTime(takeUpTime);
            //设置数据变更
            setLogContent(operLog);
            // 保存数据库
            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
        } catch (Exception exp) {
            // 记录本地异常日志
            LOG.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    private void setLogContent(SysOperLog operLog) {
        if (ContextHandler.get(BaseEntity.LOG_TYPE) != null) {
            String logType = (String) ContextHandler.get(BaseEntity.LOG_TYPE);
            if (StringUtils.isNotBlank(logType)) {
                if ("insert".equals(logType)) {

                    // 异步保存日志
                } else if ("update".equals(logType)) {

                } else if ("delete".equals(logType)) {

                }
            }
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param log     日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setOperatorType(log.operatorType().ordinal());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息,传入到数据库中。
            setRequestValue(joinPoint, operLog);
        }
        // 是否需要保存response,参数和值
        if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
            operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数,放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception {
        String requestMethod = operLog.getRequestMethod();
        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
            String params = argsArrayToString(joinPoint.getArgs());
            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
        } else {
            Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
            operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter()), 0, 2000));
        }
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(Log.class);
        }
        return null;
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray) {
        String params = "";
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (StringUtils.isNotNull(o) && !isFilterObject(o)) {
                    try {
                        String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter());
                        params += jsonObj.toString() + " ";
                    } catch (Exception e) {
                    }
                }
            }
        }
        return params.trim();
    }

    /**
     * 忽略敏感属性
     */
    public PropertyPreExcludeFilter excludePropertyPreFilter()
    {
        return new PropertyPreExcludeFilter().addExcludes(EXCLUDE_PROPERTIES);
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象,则返回true;否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}

结合SpEl实现注解动态参数传值

在自定义注解后,使用注解时其参数必须是固定不变的。那么能不能实现动态传参呢?答案是肯定的。
这时便可以结合SpEL表达式+注解方式实现。简单来说,就是在查阅文档时,需要传入用户的编号,把用户编号作为权限注解的参数,在切面中去获取用户编号去验证此用户是否有查阅权限,如果有则放行,反之则抛出异常,进行全局异常捕获后指定返回的信息。相比于拦截器,个人更推荐这种方式。具体实现如下:

自定义注解

/**
 * 资源访问权限注解
 */
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceAccessPermission {
    /**
     * 用户编号,可支持SpEL表达式
     *
     * @return
     */
    String value();
}

创建SpEL表达式解析器

/**
 * SpEL表达式解析器,根据传入的类型进行解析
 *
 * @param <T>
 */
public class ExpressionEvaluator<T> extends CachedExpressionEvaluator {
    private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
    private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
    private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);

    /**
     * 创建表达式上下文
     *
     * @param object
     * @param targetClass
     * @param method
     * @param args
     * @return
     */
    public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) {
        Method targetMethod = getTargetMethod(targetClass, method);
        ExpressionRootObject root = new ExpressionRootObject(object, args);
        return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
    }

    /**
     * 获取目标对象的方法
     *
     * @param targetClass
     * @param method
     * @return
     */
    private Method getTargetMethod(Class<?> targetClass, Method method) {
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
        Method targetMethod = this.targetMethodCache.get(methodKey);
        if (targetMethod == null) {
            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            this.targetMethodCache.put(methodKey, targetMethod);
        }
        return targetMethod;
    }

    /**
     * 根据条件表达式获SpEL取值
     *
     * @param conditionExpression
     * @param elementKey
     * @param evalContext
     * @param clazz
     * @return
     */
    public T getValueByConditionExpression(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
        return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
    }
}

@Getter
@ToString
@AllArgsConstructor
class ExpressionRootObject {
    private final Object object;
    private final Object[] args;
}

配置切面,进行权限认证

/**
 * 资源访问权限切面处理
 */
@Component
@Aspect
@Slf4j
public class ResourceAccessPermissionAspect {
    private ExpressionEvaluator<String> evaluator = new ExpressionEvaluator<>();

    @Pointcut("@annotation(com.zxh.test.annotation.ResourceAccessPermission)")
    private void pointCut() {

    }

    @Before("pointCut()")
    public void doPermission(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        Object[] args = joinPoint.getArgs();
        ResourceAccessPermission permission = method.getAnnotation(ResourceAccessPermission.class);
        if (args == null) {
            return;
        }
        // 读取参数,如果以#开头则按照EL处理,否则按照普通字符串处理
        String userNo = permission.value();
        if (StrUtil.startWith(userNo, "#")) {
            Object pointTarget = joinPoint.getTarget();
            Class<?> aClass = pointTarget.getClass();
            // SpEL表达式的方式读取对应参数值
            EvaluationContext evaluationContext = evaluator.createEvaluationContext(pointTarget, aClass, method, args);
            AnnotatedElementKey methodKey = new AnnotatedElementKey(method, aClass);
            //根据条件读取注解中的参数值
            userNo = evaluator.getValueByConditionExpression(userNo, methodKey, evaluationContext, String.class);
        }
        log.info("参数:{}", userNo);
        // TODO 按照业务自定义逻辑处理

        /**
         * 这里为了演示,假设用户编号不等于3时显示无权限,实际需要去查询用户与权限分配表
         * 不符合要求的就抛出指定的异常,再捕获全局异常返回相应提示信息即可
         */
        if (!userNo.equals("3")) {
            throw new RuntimeException("无权访问");
        }
    }
}

接口调用

/**
     * 这里将查询条件中的用户编号作为参数传入注解
     *
     * @param queryBody
     * @return
     */@PostMapping("/read")
@ResourceAccessPermission("#queryBody.userNo")
    public String read(@RequestBody QueryBody queryBody) {
        return "查询成功";
    }
最后修改:2023 年 08 月 12 日
如果觉得我的文章对你有用,请随意赞赏