Java注解与反射这一篇就够了(近万字长文,建议收藏)

java.jpeg

传送锚点

  • 博客
  • github

前言

本章节主属于Java进阶部分,如果你刚接触Java或者完全没有编程基础,不建议阅读这部分内容,可以返回上一章节 基础部分 内容的讲解,本篇内容主要包括以下几个部分:

  • 什么是注解
  • 反射可以用来干嘛
  • 面向切面编程(包括Spring提供的aop的使用)

注解

何为注解

比较抽象的理解就是,用来标记程序的,这些标记在类加载,编译,运行时被读取,最后做出响应的处理。注解的本身其实也是一种配置,在传统开发中,我们会通过各种配置文件比如xml,yml这些文件来配置我们的程序,当大量配置时,无疑代码可读性会很差。通过注解,我们就可以很方便的给程序打上标签,很清楚的表名它是干嘛用。看一个注解的例子:

@RestController
@RequestMapping("/api")
public class ConsumeController {

    @RequestMapping("/hello/user")
    @ResponseBody
    public User hello() {
        return userService.hello();
    }
}

如果你学过 SpringBoot, 那么肯定不会陌生,该部分注解是spring框架提供的,这是一个简单的Api接口,我们可以看到类和方法都被打上了 ```@`` 这样的符号,这就是注解的标记,上边的代码这里不做过多解释,后边会有专门的文章讲这一部分的内容,这里只需要知道使用注解时,它大概是啥样子的。

手写一个注解

我们先看一个完整的注解定义:

@Target(ElementType.TYPE) // 可作用类接口枚举
@Retention(RetentionPolicy.RUNTIME) // 运行时
@Inherited // 可继承
@Documented
public @interface Log1 {
    String info() default "";
}
  • @Target(ElementType.TYPE) 表示作用的类型, TYPE这里表示可作用在类、接口、枚举身上
  • @Retention(RetentionPolicy.RUNTIME) 表示作用在运行时
  • @Inherited 表明可继承,Spring源码中有出现过,还可以跟 @AliasFor(value="xx") 结合去使用,后边也会举例
  • @Documented 表示是否生成Java doc,一般我们都会把它加上
  • @interface 语法声明,没啥好说的
  • String info() default ""; 这里要说明一下,这个不是Log1注解的方法,它是它的一个属性,你可以理解为字段,String表明类型, default是默认值。

使用注解

我们定义好了以后,怎么去使用它呢?很简单, 只需要在具体的类上加上我们定义的注解即可:

@Log1(info = "info")
public class AnnoTest {}

或许你还有疑问❓,我加上了好像没啥效果啊, 不急,这就是我要讲的下一个环节 反射

反射

何为反射

反射 从字面意思来看,意思是说不是从正面而是从反面...

搞错了,我们从一个例子来体会一下(java内置反射):

public static void reflect() {
    // 内置反射
    Class annoClass = null;
    try {
        // 获取对象
        annoClass = Class.forName("com.java.lang.base.annotation.AnnoTest");

        // 判断是否存在注解log1
        boolean isAnnoLog1 = annoClass.isAnnotationPresent(Log1.class);
        Log.info("anno log1 exist: " + isAnnoLog1); // true

        if (isAnnoLog1) {
            // 获取注解
            Log1 annotation = annoClass.getAnnotation(Log1.class);
            Log.info("info ----" + annotation.info()); 
        }
    } catch(Exception e) {
        Log.info(e.getMessage());
    }
}

我们来解读一下上边的代码, 主要做了以下几个事情

  • 首先定义了一个 annoClass ,主要作用是赋值目标类,当然也可以不定义,但是为了安全。
  • 避免异常,使用了try,catch捕捉一下
  • Class.forName("com.java.lang.base.annotation.AnnoTest") , 这里通过包名的形式获取目标类,这样我们就可以拿到目标类的信息
  • annoClass.isAnnotationPresent 判断该类是否存在 Log1的注解,返回类型是布尔。
  • annoClass.getAnnotation(Log1.class) 表示获取作用在类上的注解,这样就可以使用 annotation.info() 获取内容信息,还记得 @Log1(info = "info") 这个标记吗?这样就拿到了当时作用在类上的注解原信息

或许你还有疑问❓ 这好像也没啥用啊,获取到信息, 然后呢?

有用啊 ,谁说没用的,如果你有这样一个场景,需要在类执行的时候,你要记录一些日志,难不成你要给每个类都写一个Log方法手动调用吗?或许你会说我写个公共方法?写个公共方法,但是怎么拿到具类的信息呢?这些都是问题,当然非要那样,也是可以,注解 不是更帅一点吗。 好了,我再教你一招。

反射的应用

为了说的明白一点,对Log1的注解做一些改变:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Log1 {
    String info() default "";
}

@Target(ElementType.TYPE) 改成了 @Target(ElementType.METHOD), 通过字面意思看,只的是作用到了方法上,然后改一下我们的目标类:

public class AnnoTest {

    @Log1(info="info")
    public String hello(String msg) {
        System.out.println("动态设置 --- " +  msg);
        return "返回值---->" + msg;
    }
}

改变了作用对象,这次在方法上, 我们再改一下反射的代码:

 public static void reflect() {
    // 内置反射
    Class annoClass = null;
    try {
        // 获取对象
        annoClass = Class.forName("com.java.lang.base.annotation.AnnoTest");
        // 获取方法
        Method[] methods = annoClass.getDeclaredMethods();
        // 方法遍历:
        for (Method declaredMethod : methods) {
            // 判断方法是否存在注解
            boolean isAnnoLog = declaredMethod.isAnnotationPresent(Log1.class);
            if(isAnnoLog) {
                annotation = declaredMethod.getAnnotation(Log1.class);
                // 获取构造器
                Constructor constructor = annoClass.getConstructor();
                // 实例化
                Object obj = constructor.newInstance();
                // 执行方法 - 将注解的数据执行进去
                Log.info(declaredMethod.invoke(obj, annotation.info()));
            }
        }
    } catch(Exception e) {
        Log.info(e.getMessage());
    }
}

上述代码变化大的地方主要在获取方法那一块,讲一下具体的方法:

  • Method[] methods = annoClass.getDeclaredMethods(); 这个是获取目标类上定义的方法

  • 紧接着遍历了方法, boolean isAnnoLog = declaredMethod.isAnnotationPresent(Log1.class); 判断了方法上是否有 注解 标记。

  • 然后获取了注解, Constructor constructor = annoClass.getConstructor();这个意思是获取类的构造器,用于下边的实例化。

  • Object obj = constructor.newInstance(); 实例化类。

  • declaredMethod.invoke(obj, annotation.info()) 意思是执行具体的方法,并传参。

通过这个例子, 你可以发现,明明我啥也没干,以前我们都是先实例化它然后再执行,现在换成加了个注解,这个类方法就执行了。好了,到这里,我觉得你对反射和注解都有了一定的理解了。

现在总结一下什么是反射:

::: tip
每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
类加载相当于Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("xxx")。这种方式来控制类的加载,该方法会返回一个 Class 对象。反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
:::

Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

  • Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
  • Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
  • Constructor :可以用 Constructor 的 newInstance() 创建新的对象;

大家也可以试着在控制台输出一下,这里就不一一演示了。

注解的补充

为啥要在这一章节补充呢?因为它需要借助反射帮助大家更好的理解。主要补充以下几个知识点:

  • @注解的继承
  • @注解的显示传递与隐式传递

这两个知识点,我们通过一个例子,搞定它:

@Target(ElementType.TYPE) // 可作用类接口枚举
@Retention(RetentionPolicy.RUNTIME) // 运行时
@Inherited // 可继承
@Documented
// 继承Log2注解
@Log2
public @interface Log1 {

    // 显示传递 这里可以接收err的属性值
    // 显示专递时需要加上  @AliasFor 去标记, 并且default要一致
    @AliasFor(value="err")
    String info() default "";

    @AliasFor(value="info")
    String err() default "";

    // 隐式传递 - 属性名以Log2的属性名
    @AliasFor(annotation = Log2.class, attribute = "log2")
    String err1() default "";

    @AliasFor(annotation = Log2.class, attribute = "log2")
    String err2() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Log2 {
    String log2() default "";
}
@Log1(err1 = "隐式传递 err1")
public class AnnoTest {

}
 public static void reflect() {
    // 内置反射
    Class annoClass = null;
    try {
        // 获取对象
        annoClass = Class.forName("com.java.lang.base.annotation.AnnoTest");

        // 判断是否存在注解log1
        boolean isAnnoLog1 = annoClass.isAnnotationPresent(Log1.class);
        Log.info("anno log1 exist: " + isAnnoLog1); // true

        Log1 annotation = annoClass.getAnnotation(Log1.class);
        //    Log.info("info ----" + annotation.info()); // info ----显式传递 info

        // aop 版本获取注解
        annotation = AnnotationUtils.getAnnotation(annoClass, Log1.class);
        Log.info("err1 ----" + annotation.err1()); // 隐式传递 err1
        Log.info("err2 ----" + annotation.err2()); // 式传递 err2 - 发现err2 并没有传入属性值 - @Log1(err1 = "隐式传递 err1")


        
    } catch(Exception e) {
        Log.info(e.getMessage());
    }
    }

说人话

贴了这几段代码,懵逼很理解。现在就来解读一下,注解继承没啥好说的,可以直接作用在注解身上,跟类一样可以获取它。主要说说它的属性传递。

先说@注解显式传递:

@AliasFor(value="err")
String info() default "";

@AliasFor(value="info")
String err() default "";

这个@AliasFor作用就是传递属性,value="err" 表示显示的传递,err就是代表String err() default "";这一个属性。作用是什么呢?作用在于如果我给info字段传了值,err没有,那么它就会默认的传递到 String err() default "";这个属性身上,如果有点抽象,可以控制台输出一下,你就会明白了。

@注解隐式传递:

// 隐式传递 - 属性名以Log2的属性名
@AliasFor(annotation = Log2.class, attribute = "log2")
String err1() default "";

@AliasFor(annotation = Log2.class, attribute = "log2")
String err2() default "";

我们可以直观的看到跟上边的注解不一样, annotation = Log2.class, attribute = "log2"这一块不一样,啥意思呢?说的是继承了Log2的属性名log2,你会发现跟刚刚的不一样,刚刚的value两个都不一样啊。这个就是隐式传递的好处。当我们下次作用到类上的时候,可以省去写属性名,可以直接这样@Log("hh")。如果你学过spring你会发现很多注解你在用的时候都没加属性名,不如@Value("xxxx")。除此之外,跟上边的一样,可以互相传递属性值。

AOP

其实这一节是对上一节的补充与总结,AOP 是Aspect Oriented Program的首字母缩写, 简称 面向切面 编程。我们之前介绍过 面向对象 编程, 面向对象的特征:继承、多态和封装, 好家伙又来了一新名词。下面我通过最直白的话告诉你啥是 aop

::: tip
另外补充一下词汇 OOP 就是所说的面向对象
:::

何为AOP

举一个造汽车的例子,在 OOP 思想中,造一个小汽车是这么造的。汽车 -> 外壳 -> 底盘 -> 轮子,假设有这几部分,如果哪天老板有了新想法,我要改一下车子,那是不是要拆下来,每个都要重新造?牵一发动全身。我们写程序也一样,哪天产品说改个需求,拿前端来讲,改个按钮不至于改整个页面的样式吧。

说完 OOP ,我们在看 APO 是怎么造车的, 轮子 -> 底盘 -> 外壳 -> 汽车, 发现如果想改某个地方,我只需要切入到具体的点(切点),做具体的事就好了,这时候你就可以对老板讲,你说改哪吧?今晚就给你搞定,紧接着老板给你涨了工资。

其实这个例子呢,还引申了另外两个概念, 一个是 依赖倒置, 另一个是 控制反转(Inversion of Control)简称就是IOC, 好家伙又学了新词汇。如果学过 Spring 一定不会陌生,没错它就是 Spring 框架的核心部分,也是面试中常问的问题。这里呢,不做过多的介绍,Spring源码没看懂,没关系,先把概念理清楚了。其中特别喜欢知乎的一个回答,如果想理解啥是IOC的同学,可以看看这位大佬的回答 Spring IoC有什么好处呢?,刚刚也是借鉴了他的例子,我觉得讲的不错,分享给大家。

场景重现

回到正题,刚刚我们举了造汽车的例子,粗略的讲了一下这个概念,回到我们之前讲的打日志的场景。之前我们讨论过,使用公共打印日志的方法也可以,现在我要提问一个问题,如果我要改这个方法,你能保证依赖的类不受影响吗?显然不管影不影响,你也不敢立马确定吧,好了你犹豫了。

换成 AOP 的方式,你只需要切入到指定类和类的方法,做一些处理就好了,这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面,是不是so easy。

现在很多流行的框架都有用到AOP的思想,比如 spring-aop, 一些拦截器的框架,几乎都有它的影子。说了这么多,不是告诉大家 OOP 没有 AOP帅,以后全用 AOP来写代码,帅不能当饭吃,其实 AOPOOP的一种补充,弥补了OOP中一些不足的地方,目的都是为了让我们的程序更加的健壮,顺便插一嘴,我们要杜绝花里胡哨的编码方式,把坑尽量留的小一些。

Spring中的aop是如何使用的

spring-aop 帮我们封装了很多的功能,我们通过一个案例,来看一下如何使用它,如果你还没学过 Spring 可以了解下,相关的注释我已经加在了代码里。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Log3 {
    String hello() default "";
}
@Aspect
@Component
public class SpringAopMain {

    // 切点
    @Pointcut("@annotation(com.java.lang.base.annotation.Log3)")
    public void aspect() {}

    //配置前置通知,使用在方法aspect()上注册的切入点
    //同时接受JoinPoint切入点对象,可以没有该参数
    @Before("aspect()")
    public void Before() {
        Log.info("before ....");
    }

    // 后置通知
    @After("aspect()")
    public void After(JoinPoint point) {
        Log.info("after ...." + point.toString());
    }

    // 最终通知 - 设定返回值为String对象
    @AfterReturning(pointcut = "aspect()", returning = "res")
    public void AfterReturning(String res) {
        Log.info("----AfterReturning方法开始执行:---"+res);
    }

    //异常通知
    @AfterThrowing(pointcut="aspect()",throwing="e")
    public void AfterThrowing(Throwable e) {
        Log.info("-----AfterThrowing方法开始执行:"+e);
    }

    //@Around注解可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
    // 这里的返回值等于方法的返回值
    @Around("aspect()")
    public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.info("方法开始执行");
        Signature signature = joinPoint.getSignature();
        // 获取注解绑定的方法
        Method method = ((MethodSignature)signature).getMethod();
        Log3 annotation = AnnotationUtils.getAnnotation(method, Log3.class);

        Log.info("aop log3 --->" + annotation.hello());

        // 获取入参
        Object[] inputArgs = joinPoint.getArgs();
        // 获取参数名
        String[] argNames = ((MethodSignature) signature).getParameterNames();
        Map paramMap = new HashMap<>();
        for(int i = 0; i < argNames.length; i++) {
            paramMap.put(argNames[i], inputArgs[I]);
        }

        Log.info("入参" + paramMap.toString());

        // 修改入参
        inputArgs[0] = "spring aop args";
        // 执行方法
        Object result = joinPoint.proceed(inputArgs);

        Log.info( "反射结果----->"+ result);

        // 返回结果
        return result;
    }
}

可以看出,代码中并没有指向具体的类,而是以注解Log3为切点做了一些列处理,当这个注解作用到目标类上,就会自动做处理,学会之后给自己的请求做一个日志上报功能试试吧~

到这里,本章已完结,下期给大家讲一下 枚举和泛型,给个关注呗~

blog地址 获取更多内容~

你可能感兴趣的:(Java注解与反射这一篇就够了(近万字长文,建议收藏))