首页 > 技术文章 > 自定义注解-(属性、类 方法)

dongl961230 2020-08-25 16:32 原文

大家对注解应该已经不会陌生了,但是往往在开发中已有的注解满足不了我们的业务需求时,就需要我们自定义注解来完成我们的工作;

从注解使用位置可以分为 属性、方法和类 下面就分别从这两个方面提供一些例子,来分析一下如何自定义注解:

一、属性注解

一般在实体类中使用的字段注解有:@NotNull 、 @Range(min = 20 , max = 99) 等都可以对实体类的字段值进行验证,举个例子:如果我们有需求传入的字段信息需要是一个集合中的元素,这时就没有注解可以满足了、这里我们需要自定义注解;

1、定义一个 @CheckField 注解,通过 @interface声明一个注解:

package com.dongl.utils.annotation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * @author D-L
 * @date 2020-08-24
 *
 * @Constraint 通过使用validatedBy来指定与注解关联的验证器
 *
 * @Retention 用来说明该注解类的生命周期。
 *
 * RetentionPolicy.SOURCE: 注解只保留在源文件中
 * RetentionPolicy.CLASS : 注解保留在class文件中,在加载到JVM虚拟机时丢弃
 * RetentionPolicy.RUNTIME: 注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解。
 */
@Target({ ElementType.FIELD}) //只允许用在类的字段上
@Retention(RetentionPolicy.RUNTIME) //注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解
@Constraint(validatedBy = VerificationField.class)
public @interface CheckField {
    /**
     * 合法的参数值
     * */
    String[] fieldValues();

    /**
     * 提示信息
     * */
    String message() default "参数不为指定值";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

2、参数验证逻辑类:

package com.dongl.utils.annotation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;

/**
 * @author D-L
 * @date 2020-08-24
 *
 * 需要实现 ConstraintValidator 泛型接口
 *
 * 第一个泛型参数类型 CheckField:注解,第二个泛型参数 Object:校验字段类型。
 * 需要实现 initialize 和 isValid 方法,isValid 方法为校验逻辑,initialize 方法初始化工作
 */
public class VerificationField implements ConstraintValidator<CheckField, Object> {
    /**
     * 从注解中获取合法的参数值
     */
    private List<String> paramValues;

    @Override
    public void initialize(CheckField checkField) {
        //初始化时获取注解上的值
        paramValues = Arrays.asList(checkField.fieldValues());
    }

    public boolean isValid(Object o, ConstraintValidatorContext context) {
        if (paramValues.contains(o)) {
            return true;
        }
        //自定义的参数列表中不存在
        return false;
    }
}

3、建一个实体类,使用注解@CheckField

package com.dongl.bean.mybean;


import com.dongl.utils.annotation.CheckField;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotNull;
import java.util.Date;

public class User {
    /**主键*/
    private Long id;
    /**姓名*/
    @NotNull
    private String name;
    /**性别*/
    @CheckField(fieldValues = {"男", "女"})
    private String sex;
    /**出生日期*/
    private Date birthDay;
    /**年龄*/
    @Range(min = 20 , max = 99)
    private Short age;
    /**详细地址*/
    private String address;

    public User() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public Date getBirthDay() {
        return birthDay;
    }

    public void setBirthDay(Date birthDay) {
        this.birthDay = birthDay;
    }

    public Short getAge() {
        return age;
    }

    public void setAge(Short age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", birthDay=" + birthDay +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

4、测试方法:

package com.dongl.controller;

import com.dongl.bean.mybean.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * @author D-L
 * @Classname AnnotationController
 * @Version 1.0
 * @Description   测试自定义注解 controller
 * @Date 2020/8/24
 */

@RestController
@RequestMapping("Annotation")
public class AnnotationController {

    @PostMapping(value = "user")
    public String test(@Validated @RequestBody User user) {
        System.out.println(user);
        return "do something you like ------";
    }
}

二、方法、类注解

<一> 权限注解(注解+拦截器

1、自定义一个注解

package com.dongl.utils.permission;


import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author D-L
 * @Classname CheckPermission
 * @Version 1.0
 * @Description  自定义注解 检查权限
 * @Date 2020/8/24
 */
@Target({ ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
    /**
     * 用户名
     */
    String userName() default "";

    /**
     * 用户code
     */
    String userCode() default "";


    /**
     * 提示信息
     * */
    String message() default "当前登录人权限不足";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

2、定义一个类继承HandlerInterceptorAdapter,并重写方法

拦截器适配器HandlerInterceptorAdapter HandlerInterceptor

在HandlerInterceptorAdapter中主要提供了以下的方法:

  • preHandle:在方法被调用前执行。在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器。如果返回false,则中断执行,也就是说我们想调用的方法 不会被执行,但是你可以修改response为你想要的响应。
  • postHandle:在方法执行后调用。
  • afterCompletion:在整个请求处理完毕后进行回调,也就是说视图渲染完毕或者调用方已经拿到响应。
package com.dongl.utils.permission;

import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author D-L
 * @Classname AnnotationController
 * @Version 1.0
 * @Description  拦截器类
 * @Date 2020/8/24
 *
 *
 * 在HandlerInterceptorAdapter中主要提供了以下的方法:
 * preHandle:在方法被调用前执行。在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器。如果返回false,则中断执行,也就是说我们想调用的方法 不会被执行,但是你可以修改response为你想要的响应。
 * postHandle:在方法执行后调用。
 * afterCompletion:在整个请求处理完毕后进行回调,也就是说视图渲染完毕或者调用方已经拿到响应。
 */
public class CheckPermissionInterceptor extends HandlerInterceptorAdapter {
    /**
     * 处理器处理之前调用
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        CheckPermission permission = findCheckPermission(handlerMethod);

        //获取注解中的值
        String userName = null; String userCode = null;
        if(permission != null) {
            userName = permission.userName();
            userCode = permission.userCode();
        }

        //如果没有添加权限注解则直接跳过允许访问
        if (StringUtils.isEmpty(userName) && StringUtils.isEmpty(userCode)) {
            return true;
        }

        /**这里校验是否有权限一般会到数据库中 或者redis中获取权限配置数据  查看此用户是否具有此权限
        这里为了测试 就直接写死了*/
        if (! "admin".equals(userName) && "admin".equals(userCode)) {
            System.out.println("拦截器1--------------------");
            response.getOutputStream().write(permission.message().getBytes());
            return false;
        }
        return true;
    }

    /**
     * 在方法执行后调用
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //在方法执行后调用
    }

    /**
     * 在整个请求处理完毕后进行回调
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //在整个请求处理完毕后进行回调,也就是说视图渲染完毕或者调用方已经拿到响应
       
        //日志肯定是在afterCompletion之后记录的,否则中途失败了,也记录了,那就扯淡了。
        // 一定是程序正常跑完后,我们记录下那些对数据库做个增删改的操作日志进数据库。
        // 所以我们只需要继承HandlerInterceptorAdapter,并重写afterCompletion一个方法即可,
        // 因为preHandle默认是true。
    }

    /**
     * 处理器处理之前调用
     */
    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    }

    /**
     * 根据handlerMethod返回注解信息
     *
     * @param handlerMethod 方法对象
     * @return PermissionCheck注解
     */
    private CheckPermission findCheckPermission(HandlerMethod handlerMethod) {
        //在方法上寻找注解
        CheckPermission permission = handlerMethod.getMethodAnnotation(CheckPermission.class);
        if (permission == null) {
            //在类上寻找注解
            permission = handlerMethod.getBeanType().getAnnotation(CheckPermission.class);
        }
        return permission;
    }
}

2、实现WebMvcConfigurer配置拦截器

package com.dongl.utils.permission;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author D-L
 * @Classname AnnotationController
 * @Version 1.0
 * @Description   实现WebMvcConfigurer配置拦截器
 * @Date 2020/8/24
 *
 * WebMvcConfigurerAdapter 抽象类是对WebMvcConfigurer接口的简单抽象(增加了一些默认实现),
 * 但在在SpringBoot2.0及Spring5.0中WebMvcConfigurerAdapter已被废弃 。
 * 官方推荐直接实现WebMvcConfigurer或者直接继承WebMvcConfigurationSupport
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //多个拦截器会按照顺序进行校验拦截  内部维护了一个list集合 registrations
        //private final List<InterceptorRegistration> registrations = new ArrayList<>();
        registry.addInterceptor(new CheckPermissionInterceptor());
        //registry.addInterceptor(new CheckPermissionHandler());
    }
}

3、功能测试

package com.dongl.controller;

import com.dongl.bean.mybean.User;
import com.dongl.utils.permission.CheckPermission;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * @author D-L
 * @Classname AnnotationController
 * @Version 1.0
 * @Description   测试自定义注解 controller
 * @Date 2020/8/24
 */

@RestController
@RequestMapping("Annotation")
public class AnnotationController {

    @CheckPermission(userCode = "admin" ,userName = "admin")
    @PostMapping(value = "user")
    public String test(@Validated @RequestBody User user) {
        System.out.println(user);
        return "do something you like ------";
    }
}

<二> 缓存注解(注解+切面)

因为缓存注解需要在方法执行之前有返回值,所以没有通过拦截器处理这个注解,而是通过使用切面在执行方法之前对注解进行处理。如果注解没有返回值,将会返回方法中的值。

1、自定义注解

package com.dongl.utils.cache;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * @author D-L
 * @Classname CustomCacheAspect
 * @Version 1.0
 * @Description   自定义注解 
 * @Date 2020/8/24
 */
@Target({ ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCache {
    /**
     * 缓存的key值
     * */
    String key();
}

2、自定义切面类

package com.dongl.utils.cache;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
 * @author D-L
 * @Classname CustomCacheAspect
 * @Version 1.0
 * @Description   测试自定义注解 切面类
 * @Date 2020/8/24
 */
@Aspect
@Component
public class CustomCacheAspect {
    /**
     * 在方法执行之前对注解进行处理
     *
     * @param joinPoint
     * @param customCache 注解
     * @return 返回中的值
     */
    @Around("@annotation(com.dongl.utils.cache.CustomCache) && @annotation(customCache)")
    public Object dealProcess(ProceedingJoinPoint joinPoint, CustomCache customCache) {
        Object result = null;

        if (customCache.key() == null) {
            //TODO throw error
        }
        //TODO 业务场景会比这个复杂的多,会涉及参数的解析如key可能是#{id}这些,数据查询
        //TODO 这里做简单演示,如果key为testKey则返回 do something you like ---
        if ("testKey".equals(customCache.key())) {
            return "do something you like ---";
        }

        //执行目标方法  开始执行业务代码
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        //如果没返回值 接口也不会有返回值
        return result;
    }
}

3、测试方法

package com.dongl.controller;

import com.dongl.utils.cache.CustomCache;
import org.springframework.web.bind.annotation.*;

/**
 * @author D-L
 * @Classname AnnotationController
 * @Version 1.0
 * @Description   测试自定义注解 controller
 * @Date 2020/8/24
 */

@RestController
@RequestMapping("Annotation")
public class AnnotationController {

    @GetMapping("cache")
    @CustomCache(key = "testKey")
    public Object testCustomCache() {
        return "don't hit cache -------------";
    }
}

推荐阅读