详解Java中自定义注解的使用

2023-03-20 11:03:44 注解 自定义 详解

什么是注解

在早期的工作的时候 ,自定义注解写的比较多,可大多都只是因为 这样看起来 不会存在一堆代码耦合在一起的情况,所以使用了自定义注解,这样看起来清晰些,

Annontation是Java5开始引入的新特征,中文名称叫注解。

它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观、更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。

Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。

一般我们自定义一个注解的操作是这样的:

public @interface MyAnnotation {
}

如果说我们需要给他加上参数,那么大概是这样的

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface MyAnnotation {
    public int age() default 18;
    String name() ;
    String [] books();
}

我们可以关注到上面有些我们不曾见过的注解,而这类注解,统称为元注解 ,我们可以大概来看一下

@Document

是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。

@Target

是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的,不定义说明可以放在任何元素上。

上面这个 Target这玩意有个枚举,可以清晰的看出来,他的 属性

使用枚举类ElementType来定义

public enum ElementType {
    
    TYPE,
    
    FIELD,
    
    METHOD,
    
    PARAMETER,
    
    CONSTRUCTOR,
    
    LOCAL_VARIABLE,
    
    ANNOTATION_TYPE,
    
    PACKAGE
}

@Retention

即用来修饰自定义注解的生命周期。

使用了RetentionPolicy枚举类型定义了三个阶段

public enum RetentionPolicy {
    
    SOURCE,

    
    CLASS,

    
    RUNTIME
}

@Inherited

允许子类继承父类中的注解

注解的注意事项

1.访问修饰符必须为public,不写默认为public;

2.该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组

3.该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作);

4.()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;

5.default代表默认值,值必须和第2点定义的类型一致;

6.如果没有默认值,代表后续使用注解时必须给该类型元素赋值。

注解的本质

所有的Java注解都基于Annotation接口。但是,手动定义一个继承自Annotation接口的接口无效。要定义一个有效的Java注解,需要使用@interface关键字来声明注解。Annotation接口本身只是一个普通的接口,并不定义任何注解类型。

public interface Annotation {  
    boolean equals(Object obj);
    
    int hashCode();
    
    String toString();
    
    Class<? extends Annotation> annotationType();
}

在Java中,所有的注解都是基于Annotation接口的,但是手动定义一个继承自Annotation接口的接口并不会创建一个有效的注解。要定义有效的注解,需要使用特殊的关键字@interface来声明注解类型。Annotation接口本身只是一个普通的接口,而不是一个定义注解的接口。因此,使用@interface声明注解是定义Java注解的标准方法。

public @interface MyAnnotation1 {
}
public interface MyAnnotation2 extends Annotation  {
}
// javap -c TestAnnotation1.class
Compiled from "MyAnnotation1.java"                                                                 
public interface com.spirimark.corejava.annotation.MyAnnotation1 extends java.lang.annotation.Annotation {}
​
// javap -c TestAnnotation2.class
Compiled from "MyAnnotation2.java"                                                                 
public interface com.spirimark.corejava.annotation.MyAnnotation2 extends java.lang.annotation.Annotation {}

虽然Java中的所有注解都是基于Annotation接口,但即使接口本身支持多继承,注解的定义仍无法使用继承关键字来实现。定义注解的正确方式是使用特殊的关键字@interface声明注解类型。

同时需要注意的是,通过@interface声明的注解类型不支持继承其他注解或接口。任何尝试继承注解类型的操作都会导致编译错误。

public @interface MyAnnotation1 {
}

@interface MyAnnotation2 extends MyAnnotation1 {
}

@interface MyAnnotation3 extends Annotation {
}

自定义注解使用

使用方式 1

自定义注解的玩法有很多,最常见的莫过于

  • 声明注解
  • 通过反射读取

但是上面这种一般现在在开发中不怎么常用,最常用的就是,我们通过 切面去在注解的前后进行加载

创建注解

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BussinessLog {
 
    
    BusinessTypeEnum value();
 
    
    boolean isSaveRequestData() default true;
 
    
    boolean isSaveResponseData() default true;
}

设置枚举

public enum BusinessTypeEnum {
    
    OTHER,
 
    
    INSERT,
 
    
    UPDATE,
 
    
    DELETE,
 
    
    GRANT,
 
    
    EXPORT,
 
    
    IMPORT,
}

创建切面操作

@Slf4j
@Aspect
@Component
public class LoGConfig {
 
    @Autowired
    private IUxmLogService uxmLogService;
 
   
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "JSONResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }
 
   
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        handleLog(joinPoint, controllerLog, e, null);
    }
 
    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            String title = methodSignature.getMethod().getAnnotation(apiOperation.class).value();
            // 获取当前的用户
            String userName = CurrentUser.getCurrentUserName();
 
            // *========数据库日志=========*//
            UxmLog uxmLog = new UxmLog();
            uxmLog.setStatus(BaseConstant.YES);
            // 请求的地址
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            assert requestAttributes != null;
            httpservletRequest request = requestAttributes.getRequest();
            String ip = getIpAddr(request);
            // 设置标题
            uxmLog.setTitle(title);
            uxmLog.setOperIp(ip);
            uxmLog.setOperUrl(request.getRequestURI());
            uxmLog.setOperName(userName);
 
            if (e != null) {
                uxmLog.setStatus(BaseConstant.NO);
                uxmLog.setErrORMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            uxmLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            uxmLog.setRequestMethod(request.getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, uxmLog, jsonResult, request);
            // 保存数据库
            uxmLog.setOperTime(new Date());
            uxmLogService.save(uxmLog);
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }
 
    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
 
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
 
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, UxmLog uxmLog, Object jsonResult, HttpServletRequest request) throws Exception {
        // 设置action动作
        uxmLog.setBusinessType(log.value().ordinal());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息,传入到数据库中。
            setRequestValue(joinPoint, uxmLog, request);
        }
        // 是否需要保存response,参数和值
        if (log.isSaveResponseData()) {
            uxmLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }
 
    private void setRequestValue(JoinPoint joinPoint, UxmLog uxmLog, HttpServletRequest request) throws Exception {
        String requestMethod = uxmLog.getRequestMethod();
        if (RequestMethod.PUT.name().equals(requestMethod) || RequestMethod.POST.name().equals(requestMethod)) {
            String params = argsArrayToString(joinPoint.getArgs());
            uxmLog.setOperParam(StringUtils.substring(params, 0, 2000));
        } else {
            Map<?, ?> paramsMap = (Map<?, ?>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            uxmLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
        }
    }
 
    private String argsArrayToString(Object[] paramsArray) {
        StringBuilder params = new StringBuilder();
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
                    try {
                        Object jsonObj = JSON.toJSON(o);
                        params.append(jsonObj.toString()).append(" ");
                    } catch (Exception e) {
                        log.error(e.getMessage());
                    }
                }
            }
        }
        return params.toString().trim();
    }
 
    @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;
    }
}

这样的话,我们就可以 在 项目当中 去在标注注解的前后去进行输出 日志

使用方式 2

我们可能还会在每次请求的时候去输出日志,所以 我们也可以去定义一个 请求的 注解

@HttpLog 自动记录Http日志

在很多时候我们要把一些接口的Http请求信息记录到日志里面。通常原始的做法是利用日志框架如log4j,slf4j等,在方法里面打日志log.info(“xxxx”)。但是这样的工作无疑是单调而又重复的,我们可以采用自定义注解+切面的来简化这一工作。通常的日志记录都在Controller里面进行的比较多,我们可以实现这样的效果:
我们自定义@HttpLog注解,作用域在类上,凡是打上了这个注解的Controller类里面的所有方法都会自动记录Http日志。实现方式也很简单,主要写好切面表达式:

日志切面

下面代码的意思,就是当标注了注解,我们通过 @Pointcut 定义了切入点, 当标注了注解,我们会在标注注解的 前后进行输出 ,当然也包含了 spring 官方 自带的注解 例如 RestController

// 切面表达式,描述所有所有需要记录log的类,所有有@HttpLog 并且有 @Controller 或 @RestController 类都会被代理
    @Pointcut("@within(com.example.spiritmark.annotation.HttpLog) && (@within(org.springframework.WEB.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller))")
    public void httpLog() {
    }

    @Before("httpLog()")
    public void preHandler(JoinPoint joinPoint) {
        startTime.set(System.currentTimeMillis());
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
        log.info("Current Url: {}", httpServletRequest.getRequestURI());
        log.info("Current Http Method: {}", httpServletRequest.getMethod());
        log.info("Current IP: {}", httpServletRequest.getRemoteAddr());
        Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
        log.info("=======http headers=======");
        while (headerNames.hasMoreElements()) {
            String nextName = headerNames.nextElement();
            log.info(nextName.toUpperCase() + ": {}", httpServletRequest.getHeader(nextName));
        }
        log.info("======= header end =======");
        log.info("Current Class Method: {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("Parms: {}", null != httpServletRequest.getQueryString() ? JSON.toJSONString(httpServletRequest.getQueryString().split("&")) : "EMPTY");

    }

    @AfterReturning(returning = "response", pointcut = "httpLog()")
    public void afterReturn(Object response) {
        log.info("Response: {}", JSON.toJSONString(response));
        log.info("Spend Time: [ {}", System.currentTimeMillis() - startTime.get() + " ms ]");

    }

@TimeStamp 自动注入时间戳

如果我们想通过自定义注解,在我们每次保存数据的时候,自动的帮我们将标注注解的方法内的时间戳字段转换成 正常日期,我们就需要

我们的很多数据需要记录时间戳,最常见的就是记录created_at和updated_at,通常我们可以通常实体类中的setCreatedAt()方法来写入当前时间,然后通过ORM来插入到数据库里,但是这样的方法比较重复枯燥,给每个需要加上时间戳的类都要写入时间戳很麻烦而且不小心会漏掉。

另一个思路是在数据库里面设置默认值,插入的时候由数据库自动生成当前时间戳,但是理想很丰满,现实很骨感,在Mysql如果时间戳类型是datetime里即使你设置了默认值为当前时间也不会在时间戳为空时插入数据时自动生成,而是会在已有时间戳记录的情况下更新时间戳为当前时间,这并不是我们所需要的,比如我们不希望created_at每次更改记录时都被刷新,另外的方法是将时间戳类型改为timestamp,这样第一个类型为timestamp的字段会在值为空时自动生成,但是多个的话,后面的均不会自动生成。再有一种思路是,直接在sql里面用now()函数生成,比如created_at = now()。

但是这样必须要写sql,如果使用的不是主打sql流的orm不会太方便,比如hibernate之类的,并且也会加大sql语句的复杂度,同时sql的可移植性也会降低,比如sqlServer中就不支持now()函数。为了简化这个问题,我们可以自定义@TimeStamp注解,打上该注解的方法的入参里面的所有对象或者指定对象里面要是有setCreatedAt、setUpdatedAt这样的方法,便会自动注入时间戳,而无需手动注入,同时还可以指定只注入created_at或updated_at。实现主要代码如下:

@Aspect
@Component
public class TimeStampAspect {

    @Pointcut("@annotation(com.example.spiritmark.annotation.TimeStamp)")
    public void timeStampPointcut() {}

    @Before("timeStampPointcut() && @annotation(timeStamp)")
    public void setTimestamp(JoinPoint joinPoint, TimeStamp timeStamp) {
        Long currentTime = System.currentTimeMillis();
        Class<?> type = timeStamp.type();
        Object[] args = joinPoint.getArgs();

        for (Object arg : args) {
            if (type.isInstance(arg)) {
                setTimestampForArg(arg, timeStamp);
            }
        }
    }

    private void setTimestampForArg(Object arg, TimeStamp timeStamp) {
        Date currentDate = new Date(System.currentTimeMillis());
        TimeStampRank rank = timeStamp.rank();
        Method[] methods = arg.getClass().getMethods();

        for (Method method : methods) {
            String methodName = method.getName();
            if (isSetter(methodName) && isRelevantSetter(methodName, rank)) {
                try {
                    method.invoke(arg, currentDate);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private boolean isSetter(String methodName) {
        return methodName.startsWith("set") && methodName.length() > 3;
    }

    private boolean isRelevantSetter(String methodName, TimeStampRank rank) {
        if (rank.equals(TimeStampRank.FULL)) {
            return methodName.endsWith("At");
        }
        if (rank.equals(TimeStampRank.UPDATE)) {
            return methodName.startsWith("setUpdated");
        }
        if (rank.equals(TimeStampRank.CREATE)) {
            return methodName.startsWith("setCreated");
        }
        return false;
    }
}

1.使用@Aspect和@Component注解分别标注切面和切面类,更符合aop的实现方式。

2.将pointCut()和before()方法分别改名为timeStampPointcut()和setTimestamp(),更能表达它们的作用。

3.通过Class.isInstance(Object obj)方法,将原先的流操作改为了一个简单的for循环,使代码更加简洁。

4.将原先的setCurrentTime()方法改名为setTimestampForArg(),更能表达它的作用。

5.新增了两个私有方法isSetter()和isRelevantSetter(),将原先在setTimestampForArg()中的逻辑分离出来,提高了代码的可读性和可维护性

到此这篇关于详解Java中自定义注解的使用的文章就介绍到这了,更多相关Java自定义注解内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章