美文网首页Annotation
Java注解详解,自定义注解,利用反射解析注解

Java注解详解,自定义注解,利用反射解析注解

作者: jackcooper | 来源:发表于2018-05-02 17:09 被阅读108次

概要

这篇文章将会带领你了解Java注解,注解的使用,注解的解析,利用反射解析运行时注解,相信有一定Java基础的小伙伴一定会接触大量的注解,Spring , Hibernate , MyBatis等著名的框架也有很多关于注解方面的应用,对于注解的使用小伙伴们应该一点都不陌生,那么如何自定义注解呢?学会自定义注解有什么好处呢?
下面就随笔者进入注解的世界

注解的作用

很多小伙伴在学习注解之前,都不知道学习注解到底可以用来干什么,可以给自身带来什么好处,那么在这里,笔者描述学习注解的几点好处

  • 用过Hibernate的小伙伴,应该使用过Hibernate的配置文件来描述ORM(数据库关系映射),Hibernate实现此功能除了写配置文件之外,当然还包含注解,那么第一个好处就是,注解可以替代配置文件完成对某些功能的描述,减少程序配置
  • 在没有配置文件的情况下,我们去观察代码,并不需要同时打开两个文件来观察这个字段到底对应数据库的哪个列,减少了程序繁琐性,使得代码更加清晰易懂
  • 目前市面上流行的框架基本上都包含了注解配置,那么针对于开源项目,我们在阅读项目代码时,不懂注解如何实现,真的是举步难坚,所以,学习注解也可以加强我们对开源项目源码的解读
  • 最重要的一点,会使用注解和会自定义注解完全是两码事,我的意思是,让别人可以高看你一眼(zhuang bi)

了解注解

注解是Java1.5,JDK5.0引用的技术,与类,接口,枚举处于同一层次 。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释 。

在Java中,自带了三种注解,这三种注解存在于java.lang包中,首先我们讲一讲这些注解

  • Override——它的作用是对覆盖超类中方法的方法进行标记,如果被标记的类并没有实际覆盖超类,则编译器会发出错误警告。
    很常见的一个注解,了解JavaOOP的小伙伴这个注解应该较为常用,告诉编译器,我这个方法是重写了父类方法,当然如果你的方法并没有实际重写父类方法时,那么编译器就会显示警告信息
  • Deprecated——它的作用是对不应该再使用的方法添加注解,当编程人员使用这些方法时,将会在编译时显示提示信息
    当一个方法名或者类名上面此注解之后,编译器会认为这个方法属于过期方法,明显的区别在于类名或者方法名上会画一道删除线,标识过期方法不影响方法的继续使用
  • SuppressWarnings——这个仅仅是告诉编译器忽略特定的警告信息,例如在泛型中使用原生数据类型
    例如我们在使用一些以Deprecated注解的方法时,编译器会提出黄线警告,那么只要在使用的地方加上@SuppressWarnings(“deprecation”)就可以使编译器忽略这个警告
    此注释常用的参数值有 : deprecation(忽略使用过时类或者方法),unchecked(忽略执行了未检查装换时警告) , fallthrough(忽略switch直接指向到下一个case块没有break警告),path(忽略类路径,源文件路径中有不存在路径时警告),serial(忽略可序列化类中没有serialVersionUID时的警告),finally(任何finally不能正常执行时的警告),all(以上所有)

自定义注解须知

首先,自定义注解我们必须了解四个元注解,什么是元注解?元注解指作用于注解之上的元数据或者元信息,简单通俗的讲,元注解就是注解的注解 .

  • Documented——指明拥有这个注解的元素可以被javadoc此类的工具文档化。这种类型应该用于注解那些影响客户使用带注释的元素声明的类型。如果一种声明使用Documented进行注解,这种类型的注解被作为被标注的程序成员的公共API 。
  • Inherited——指明该注解类型被自动继承。如果用户在当前类中查询这个元注解类型并且当前类的声明中不包含这个元注解类型,那么也将自动查询当前类的父类是否存在Inherited元注解,这个动作将被重复执行知道这个标注类型被找到,或者是查询到顶层的父类。
  • Retention——指明在什么级别显示此注解
  • Target——指明该类型的注解可以注解的程序元素的范围

Documented与Inherited是典型的标识性注解,也就是说在注解内部并没有成员变量,没有成员变量的注解称为标识注解
Target主要的参数类型包括以下几种

  • ElementType.TYPE 用于类,接口,枚举但不能是注解
  • ElementType.FIELD 作用于字段,包含枚举值
  • ElementType.METHOD 作用于方法,不包含构造方法
  • ElementType.PARAMETER 作用于方法的参数
  • ElementType.CONSTRUCTOR 作用于构造方法
  • ElementType.LOCAL_VERIABLE 作用于本地变量或者catch语句
  • ElementType.ANNOTATION_TYPE 作用于注解
  • ElementType.PACKAGE 作用于包

Retention主要的参数类型包括以下几种

  • RetentionPolicy.SOURCE 注解存在于源代码中,编译时会被抛弃
  • RetentionPolicy.CLASS 注解会被编译到class文件中,但是JVM会忽略
  • RetentionPolicy.RUNTIME JVM会读取注解,同时会保存到class文件中

自定义注解

首先,我们来看一段自定义注解实现的代码

@Documented
@Inherited
//该注解可以作用于方法,类与接口
@Target({ElementType.METHOD,ElementType.TYPE})
//JVM会读取注解,所以利用反射可以获得注解
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    //定义成员变量
    //成员变量可以通过default指定默认值
    //如果成员变量不指定默认值的情况下
    //我们在引用接口时则必须给没有默认值的成员变量赋值
    String name() ; 
    int age() default 18 ;
}

@interface 用于定义注解接口,接口中只能定义成员变量,且定义的成员变量必须以()结尾,可以使用default关键字为成员变量指定默认值,如果不为成员变量指定默认值的情况,则必须在引用注解时,对没有默认值的成员变量进行赋值操作

注解的使用规则:

//@注解名(变量1=变量1值,变量2=变量2值,...)
//如果注解中拥有数组类型,假设是String类型,那么赋值方式可以如下
//@注解名(String数组名称={"tset1","test2","test3"})

@TestAnnotation(name="Taro")

//因为我们注解中的age()是拥有默认值的,所以这边可以不为age()赋值
//如果我们的注解中只有一个成员变量,且成员变量的名称为value()
//那么可以使用如下赋值方式
//@注解名(属性值)
//如果我们的注解中没有成员变量,那么此时的注解被称为标识注解

注解中可以定义的数据类型是受到限制的,除了基本类型之外,String,Enums,Annotation,Class还有这些类型的数组
如何使用我们刚刚定义的注解呢?刚刚的注解我们声明了是针对方法和类或者接口生效,那么我们来看看使用方法

@TestAnnotation(name="I'm class annotation")
public class Test {

    @TestAnnotation(name="I'm method annotation")
    public static void showAnnotation(){

    }

}

怎么样,是不是很easy呢?

解析注解

主要使用Java的反射原理实现对注解的解析,不太懂反射的小伙伴通过笔者的注释看起来也不会很难

PS :下一篇博文是对Java反射的详解

public static void main(String[] args) {
    //解析注解
    //获得我们需要解析注解的类
    Class<Test> clz = Test.class;

    //解析Class
    //由于我们的注解是可以给类使用的,所以首先判断类上面有没有我们的注解
    //判断类上面是否有注解
    boolean clzHasAnnotation = clz.isAnnotationPresent(TestAnnotation.class);
    if(clzHasAnnotation){
        //类存在我们定义的注解
        //获得注解
        TestAnnotation clzAnnotation = clz.getAnnotation(TestAnnotation.class);
        //输出注解在类上的属性
        System.out.println("name="+clzAnnotation.name()+"\tage="+clzAnnotation.age());
    }

    //解析Method
    //两种解析方法上的注解方式
    //获得类中所有方法
    Method[] methods = clz.getMethods();
    //第一种解析方法
    for(Method m : methods){
        //获得方法中是否含有我们的注解
        boolean methodHasAnnotation = m.isAnnotationPresent(TestAnnotation.class);
        if(methodHasAnnotation){
            //注解存在
            //获得注解
            TestAnnotation methodAnnotation = m.getAnnotation(TestAnnotation.class);

            System.out.println("name="+methodAnnotation.name()+"\tage="+methodAnnotation.age());
        }
    }
    //第二种解析方式
    for(Method m : methods){
        //获得方法上所有注解
        Annotation[] annotations = m.getAnnotations();
        //循环注解
        for(Annotation a : annotations){
            //如果是我们自定义的注解
            if(a instanceof TestAnnotation){
                //输出属性,需要强制装换类型
                System.out.println("name="+((TestAnnotation)a).name()+"\tage="+((TestAnnotation)a).age());
            }
        }
    }

}

最后得出结果,因为我们使用了两种解析Method注解的方式,所以最终会得到两个Method上面的字符串

结果

例子:利用切面打印web日志


import java.lang.annotation.*;

/**
 * @description 环绕通知,增强controller接口响应
 * @author jack-cooper
 * @create 2018-05-31 11:22
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ApiResult {
}

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @desc 环绕通知,增强controller接口响应
 * <pre>
 *          使用@Before在切入点开始处切入内容
 *
 *          使用@After在切入点结尾处切入内容
 *
 *          使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
 *
 *          使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
 *
 *          使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑
 * </pre>
 *
 * <pre>
 *     https://blog.csdn.net/rainbow702/article/details/52185827
 * </pre>
 *
 * @author jack-cooper
 * @create 2018-05-31 11:28
 */
@Aspect
@Component
public class ApiResultAspect {

    private static final Logger logger = LoggerFactory.getLogger(ApiResultAspect.class);

    /**
     * 切入点
     */
    @Pointcut("@annotation(ApiResult)")
    public void apiResultPointCut(){}


    /**
     * 环绕通知,增强controller接口响应
     * @param joinPoint
     * @return
     */
    @Around(value = "apiResultPointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 记录下请求内容
        logger.info("====> 请求 URL : {} , HTTP_METHOD :{} , IP : {} , CLASS_METHOD : {} , ARGS : {}" ,
                request.getRequestURL().toString(),
                request.getMethod(),
                request.getRemoteAddr(),
                joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(),
                JSON.toJSONString(joinPoint.getArgs())
        );
        //执行目标方法
        final Object proceed = joinPoint.proceed();
        //所需时间
        long proceedTime =  System.currentTimeMillis() - startTime ;
        JSONObject result = new JSONObject();
        result.put("data", proceed);
        result.put("proceedTime", proceedTime);
        result.put("code", "200");
        result.put("message", "成功");
        logger.info("====> 响应结果:{}",result.toJSONString());
        return result;
    }

}



资料:
https://blog.csdn.net/rainbow702/article/details/52185827

相关文章

网友评论

    本文标题:Java注解详解,自定义注解,利用反射解析注解

    本文链接:https://www.haomeiwen.com/subject/fkvcrftx.html