java反射与注解详解,共同实现动态代理模式

  • Post author:
  • Post category:java




java反射与注解详解,共同实现动态代理模式

个人主页:https://blog.csdn.net/hello_list

id:

学习日记


不知不觉一年过去了,整整一年,这一年写了60多篇博客,其实具体22年四月份才开始有认真写,之前就随便发了几篇,博主内容持续输出,看到这里就点个关注吧,点关注不迷路

在这里插入图片描述

今天我们来学习下反射和注解

思考:反射是什么?别的语言有没有反射,为什么会有反射,反射的作用有哪些?

注解又是什么?注解的作用是什么?反射与注解是什么关系,怎么样产生关系相互使用?

带着思考,我们开始学习java中的反射和注解

回到问题,什么是反射?

首先说明,反射是java特有的,jReflection(反射) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查。被private封装的资源只能类内部访问,外部是不行的,但反射能直接操作类私有属性。反射可以在运行时获取一个类的所有信息,(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法。所以先要获取到每一个字节码文件对应的Class类型的对象。

反射就是把java类中的各种成分映射成一个个的Java对象。

例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把一个个组成部分映射成一个个对象。(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)

加载的时候:Class对象的由来是将 .class 文件读入内存,并为之创建一个Class对象。


不要想的太难,其实很简单,看完这一篇就理解了

与反射机制相关的包就都在java.lang.reflect.*;这个里面了



反射获取class的三种方式

我们想要获取一个类,就要获取它的.class文件,在获取类中的信息,那下面就是三种获取方式

方式 备注
Class.forName(“完整类名带包名”) 静态方法
对象.getClass()
任何类型.class

比方说我这里有一个Student类

package com.xuexi.springboottest.pojo;

public class Student {
    private int sid;
    private String sname;
    private int sage;
    private String ssex;

    public int getSid() {
        return sid;
    }

    public void setSid(int sid) {
        this.sid = sid;
    }

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    public int getSage() {
        return sage;
    }

    public void setSage(int sage) {
        this.sage = sage;
    }

    public String getSsex() {
        return ssex;
    }

    public void setSsex(String ssex) {
        this.ssex = ssex;
    }

    public Student(int sid, String sname, int sage, String ssex) {
        this.sid = sid;
        this.sname = sname;
        this.sage = sage;
        this.ssex = ssex;
    }

    @Override
    public String toString() {
        return "Student{" +
                "sid=" + sid +
                ", sname='" + sname + '\'' +
                ", sage=" + sage +
                ", ssex='" + ssex + '\'' +
                '}';
    }
}

被jvm编译之后成为class文件,我想要再获取这个类,怎么获取呢,三种方式

public class ReflectTest {
    public static void main(String[] args) throws ClassNotFoundException {
        // 方式一:通过包路径
        Class<?> aClass = Class.forName("com.xuexi.springboottest.pojo.Student");
        // 方式二:直接通过Student.class获取
        Class<Student> studentClass = Student.class;
        // 方式三:通过对象获取
        Student student = new Student();
        Class<? extends Student> aClass1 = student.getClass();

    }
}

不管通过哪种方式,我们总能够可以获取到类的字节码文件,也就是类,那我们有了这个类,我们可以做什么呢?我们既然能够拿到类的字节码文件,那这个类中的所有都是我们可以获取并且操作的了,干什么不可以呢?

我们看下都有哪些操作方法

我们可以看jdk的帮助文档,里面的所有方法属性信息都有

在这里插入图片描述

但是很多,我们这里挑几个主要的看看

//获取包名、类名
clazz.getPackage().getName()//包名
clazz.getSimpleName()//类名
clazz.getName()//完整类名
 
//获取成员变量定义信息
getFields()//获取所有公开的成员变量,包括继承变量
getDeclaredFields()//获取本类定义的成员变量,包括私有,但不包括继承的变量
getField(变量名)
getDeclaredField(变量名)
 
//获取构造方法定义信息
getConstructor(参数类型列表)//获取公开的构造方法
getConstructors()//获取所有的公开的构造方法
getDeclaredConstructors()//获取所有的构造方法,包括私有
getDeclaredConstructor(int.class,String.class)
 
//获取方法定义信息
getMethods()//获取所有可见的方法,包括继承的方法
getMethod(方法名,参数类型列表)
getDeclaredMethods()//获取本类定义的的方法,包括私有,不包括继承的方法
getDeclaredMethod(方法名,int.class,String.class)
 
//反射新建实例
clazz.newInstance();//执行无参构造创建对象
clazz.newInstance(222,"韦小宝");//执行有参构造创建对象
clazz.getConstructor(int.class,String.class)//获取构造方法
 
//反射调用成员变量
clazz.getDeclaredField(变量名);//获取变量
clazz.setAccessible(true);//使私有成员允许访问
f.set(实例,);//为指定实例的变量赋值,静态变量,第一参数给null
f.get(实例);//访问指定实例变量的值,静态变量,第一参数给null
 
//反射调用成员方法
Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
m.setAccessible(true);//使私有方法允许被调用
m.invoke(实例,参数数据);//让指定实例来执行该方法

测试

public class ReflectTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        // 方式一:通过包路径
        Class<?> aClass = Class.forName("com.xuexi.springboottest.pojo.Student");
        // 方式二:直接通过Student.class获取
        Class<Student> studentClass = Student.class;
        // 方式三:通过对象获取
        Student student = new Student();
        Class<? extends Student> aClass1 = student.getClass();

        // 获取包名
        System.out.println(aClass.getPackage().getName());
        System.out.println(aClass.getSimpleName());
        System.out.println(aClass.getName());

        // 获取成员变量信息
        Field[] field = aClass.getFields();
        for (Field field1 : field) {
            System.out.println(field1.getName());
        }

        // 获取构造方法
        Constructor<?>[] constructors = aClass.getConstructors();
        System.out.println(constructors);

        // 获取方法
        Method[] methods = aClass.getMethods();
        for (Method method : methods) {
            System.out.println(method.getName());
        }

        // 我们可以创建一个student对象
        Student o = (Student) aClass.newInstance();
        System.out.println(student);



    }
}

结果

在这里插入图片描述

大家到这里可以尽情的去点方法,看看如何使用,不过一句话,只要能够拿到这个class类,我什么都可以获取到,包括调用它的方法,改造方法等等等等,都可以做到,当然这也是反射的缺点,因为有了反射,对于底层代码逻辑,就没有了安全。

反射就是这样,没什么可多说的了,我们再来学习一个东西,就是注解。



注解

什么又是注解呢?注解本省没什么作用,就是用来标注的一种元数据,可以把注解理解成一种配置文件,这么说可能有人要反驳了,我们用到的注解都是有功能的,加上什么注解就可以做什么什么,确实是这样,但注解本身就是一种标记类型的元数据,那具体怎么产生意义性的实质行为呢,那配合着反射使用就再好不过了

其实我们平常有会用到很多注解,比如

  • @Override – 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated – 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings – 指示编译器去忽略注解中声明的警告。

作用在其他注解的注解(或者说 元注解)是:

  • @Retention – 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented – 标记这些注解是否包含在用户文档中。
  • @Target – 标记这个注解应该是哪种 Java 成员。
  • @Inherited – 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)

从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs – Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface – Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable – Java 8 开始支持,标识某注解可以在同一个声明上使用多次

那我们先来了解如何自己去定义一个注解

比如我这里就定义了一个注解,就这样就可以定义一个注解,我们来了解下这个注解的组成

@Documented
@Inherited
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value() default "xuexi";
}

首先是上面几个元注解,JDK5.0提供了四种元注解:Retention, Target, Documented, Inherited

1、@Retention:用于指定修饰的注解的生命周期,@Rentention包含一个RetentionPolicy枚举类型的成员变量,使用@Rentention时必须为该value成员变量指定值:

SOURCE:只在源文件中有效,编译器直接丢弃这种策略的注释,在.class文件中不会保留注解信息。反编译查看字节码文件:发现字节码文件中没有MyAnnotation这个注解

CLASS: 在class文件中有效,保留在.class文件中,但是当运行Java程序时,不会继续加载了,不会保留在内存中,JVM不会保留注解。

如果注解没有加Retention元注解,那么相当于默认的注解就是这种状态。

RUNTIME:在运行时有效(即运行时保留),当运行 Java程序时,JVM会保留注释,加载在内存中了,那么程序可以通过反射获取该注释。

2、@Target:用于修饰注解的注解,用于指定被修饰的注解能用于修饰哪些程序元素,即修饰位置。@Target也包含一个名为value的成员变量。

3、@Documented:用于指定被该元注解修饰的注解类将被javadoc工具提取成文档。默认情况下,javadoc是 不包括注解的,但是加上了这个注解生成的文档中就会带着注解了

4、@Inherited: 被它修饰的Annotation将具有继承性。如果某个类使用了被 @Inherited修饰的Annotation,则其子类将自动具有该注解。

其实两个没用,我们这要来了解这两个

@Retention(RetentionPolicy.CLASS)  // 这个就是我们自定义的注解的生命周期,就像上面说的一样,一般我设置成RetentionPolicy.CLASS就可以了
生命周期类型	描述
RetentionPolicy.SOURCE	编译时被丢弃,不包含在类文件中
RetentionPolicy.CLASS	JVM加载时被丢弃,包含在类文件中,默认值
RetentionPolicy.RUNTIME	由JVM 加载,包含在类文件中,在运行时可以被获取到

@Target(ElementType.METHOD)  // 表示注解可以使用在什么上面,我这里就是ElementType.METHOD方法上,或者还可以这些:
Target类型	描述
ElementType.TYPE	应用于类、接口(包括注解类型)、枚举
ElementType.FIELD	应用于属性(包括枚举中的常量)
ElementType.METHOD	应用于方法
ElementType.PARAMETER	应用于方法的形参
ElementType.CONSTRUCTOR	应用于构造函数
ElementType.LOCAL_VARIABLE	应用于局部变量
ElementType.ANNOTATION_TYPE	应用于注解类型
ElementType.PACKAGE	应用于包
ElementType.TYPE_PARAMETER	应用于类型变量
ElementType.TYPE_USE	应用于任何使用类型的语句中(例如声明语句、泛型和强制转换语句中的类型)

就这样,就可以了,你已经学会注解了,注解就是一种标记,用来表示一个信息,数据,本身没有上面作用,那我们通过一个小例子就可以明白了,注解+反射的使用



注解+反射的使用

我们来做一个小例子,来学习注解+反射的使用,简单的模拟一下我们平时使用的注解都是怎么工作的

首先我们定义一个注解

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value() default "这里已经帮你写好了学习方法";
}

光有一个注解没什么用,注解用来的是标记,标记上的数据有用啊,我们再配合反射的使用,

原理:就是注解标记在类上一个信息,我们在类运行的使用,去扫描有没有,并且注解上的信息是什么,再通过注解上的信息用反射去动态实现需要帮这个类做些什么

这也是代理模式的应用,大家可以去了解一下什么是代理,代理的实现方式有很多种,我们先用一种简单是实现下

我们再创建一个对象,都没有去具体实现这个方法去做什么,但是我都用自己定义的注解,标记了这个方法应该做什么;

public class User {

    @MyAnnotation("打游戏")
    public void play(){

    }

    @MyAnnotation("吃饭")
    public void eat(){

    }

    @MyAnnotation
    public void learning(){

    }

}

怎么样让我们的注解发挥作用呢,怎么样用到反射呢,来一个代理

public class ProxyUser {

    public Object proXY(User user,String methodName,@Nullable Object... param) throws InvocationTargetException, IllegalAccessException {
        // 通过反射拿到类信息
        Class<? extends User> aClass = user.getClass();

        // 看看类中的方法有没有被我们自定义注解标记的,并拿到注解信息
        Method[] methods = aClass.getMethods();

        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                if (method.isAnnotationPresent(MyAnnotation.class)) {
                    MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
                    String value = annotation.value();
                    // 在这里我们就想当与可以把原本的方法覆盖了,
                    // 也可以拿到放的参数,以及参数类型,包括返回值,这里可以通过param接收参数
                    System.out.println("这里是代理对象帮你实现的方法,帮你实现了"+value+"方法");
                    return null;
                }
                // 如果方法本身就没有被我们的注解标记,我们就直接执行它自己的方法
                return method.invoke(aClass, param);
            }
        }
        // 包括没有进for循环啊,这些都可以抛出异常什么的。进行下处理
        return null;
    }

}

测试

public class Test01 {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {

        // 创建一个user对象
        User user = new User();
        // 我们可以先执行下本身的方法
        user.play(); // 当然就是什么都没有

        // 通过代理帮我们实现方法
        new ProxyUser().proXY(user,"play");
        new ProxyUser().proXY(user,"eat");
        new ProxyUser().proXY(user,"learning");
    }
}

结果

在这里插入图片描述

这样是不是就实现了,这就是代理的好处,有人可能认为,这样多写多少代码,这么多,但是这只是开始,往后只要是用到我这个注解的都可以通过这种方式,去实现一件事情;这就是代理,我不想做的就交给你,你帮我代理,做完就行

这只是一种方式,通过自己去实现的代理,我说过代理有n种方式,这里再给大家演示一种,通过spring中的aop,来实现对一个方法执行的扫描,aop本质上横切就是代理

是不是感觉这种方式比上面那个更好理解,其实都是一样的只是实现的步骤不一样吧,用spring有一个缺点,就是你这么做,你这个类必须是从spring容器中取出来的,为什么刚开始没有用aop,aop这个是spring的aop,我们要想用就必须用到spring,总所周知spring給java带来了春天,但是也有一个这样的说法,java现在已经严重被spring绑架了,所以说我们思考之前不要先想着具体架构实现,框架上的实现,在框架上实现,就通过最基本的代码来理解这个思想,总之一句话,要想自己轻松就要找到代理。

@Component
@Aspect
public class UserAspect {

    @Pointcut("execution(* com.xuexi.springboottest.pojo..*.*(..))")
    private void myPointcut() {
    }

    /**
     * 环绕通知
     */
    @Around("myPointcut()")
    public void advice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("around begin...");
        // 本身aop实现就是通过反射,这里的代理实现了更多的增强
        // 有before 环绕通知,方法执行后After
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        if (method.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
            String value = annotation.value();
            // 在这里我们就想当与可以把原本的方法覆盖了,
            // 也可以拿到放的参数,以及参数类型,包括返回值,这里可以通过param接收参数
            System.out.println("这里是代理对象帮你实现的方法,帮你实现了"+value+"方法");
        }

        System.out.println("around after....");
    }

    @Before("myPointcut()")
    public void record(JoinPoint joinPoint) {
        System.out.println("Before");
    }

    @After("myPointcut()")
    public void after() {
        System.out.println("After");
    }
}

结果:

在这里插入图片描述

使用aop常用方法

@Before("customerJoinPointerExpression()")
public void beforeMethod(JoinPoint joinPoint){
	joinPoint.getSignature().getName(); // 获取目标方法名
	joinPoint.getSignature().getDeclaringType().getSimpleName(); // 获取目标方法所属类的简单类名
	joinPoint.getSignature().getDeclaringTypeName(); // 获取目标方法所属类的类名
	joinPoint.getSignature().getModifiers(); // 获取目标方法声明类型(public、private、protected)
	Object[] args = joinPoint.getArgs(); // 获取传入目标方法的参数,返回一个数组
	joinPoint.getTarget(); // 获取被代理的对象
	joinPoint.getThis(); // 获取代理对象自己
}
 
// 获取目标方法上的注解
private <T extends Annotation> T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class<T> clazz) {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod();
    return method.getAnnotation(clazz);
}

当然了,还有一种实现方式,基于java的原生jdk的动态代理,这里就留给大家实现以下了,可以参考我上面的代理,实现一样,这是一种思想,大家一定要理解。



JDk动态代理

本来想留下让大家自己去试着实现下,看到这里你可以不看,自己试着去实现下,然后回来看看是不是你的思路更好,如果更好可以把你的答案留在评论区,我们再来理解下,我觉得这几个理解特别好,摆出来就懂了。

  • 什么是动态代理

    1、使用 jdk 的反射机制,创建对象的能力, 创建的是代理类的对象。 而不用你创建类文件。不用写 java 文件。

    2、动态:在程序执行时,调用 jdk 提供的方法才能创建代理类的对象。

    3、jdk 动态代理,必须有接口,目标类必须实现接口,没有接口时,需要使用 cglib 动态代理。
  • 动态代理能做什么

    1、可以在不改变原来目标方法功能的前提下,可以在代理中增强自己的功能代码。

    2、背景示例。

    比如:你所在的项目中,有一个功能是其他人(公司的其它部门,其它小组的人)写好的,你可以使用(但是只有 Class 文件):Demo.class。
Demo dm = new Demo();
dm.print();

当这个功能缺少时, 不能完全满足我项目的需要。我需要在 dm.print() 执行后,需要自己在增加代码。

这时就会用代理实现 dm.print() 调用时, 增加自己代码, 而不用去改原来的 Demo 文件。

(在 mybatis,spring 中也有应用)

2、就是当你写了一个接口,里面有方法,然后写了实现类实现方法,完成方法逻辑,然后有一天,想对这个方法进行修改,但是不想改源码,所以有两种方式可以实现。第一种是静态代理,创建一个类继承实现类,然后对方法进行修改,这样太局限,因为只能针对特定的类增强方法,有100个实现类就要创建100个子类去实现。第二种方式是动态代理,可以动态地生成代理类,这是可以接受的。

实现动态代理,我们需要学会什么



InvocationHandler 接口(调用处理器)

就一个方法 invoke()。

invoke():表示代理对象要执行的功能代码。你的代理类要完成的功能就写在 invoke() 方法中。

  • 代理类完成的功能:

    1、调用目标方法,执行目标方法的功能

    2、功能增强,在目标方法调用时,增加功能。
  • 方法原型:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

参数:

Object proxy:jdk创建的代理对象,无需赋值。
Method method:目标类中的方法,jdk提供method对象的
Object[] args:目标类中方法的参数, jdk提供的。

Proxy类:核心的对象,创建代理对象。之前创建对象都是 new 类的构造方法()

现在我们是使用Proxy类的方法,代替new的使用。

方法: 静态方法 newProxyInstance()

作用是: 创建代理对象, 等同于静态代理中的TaoBao taoBao = new TaoBao();

参数:

1、ClassLoader loader 类加载器,负责向内存中加载对象的。 使用反射获取对象的ClassLoader类a,

a.getCalss().getClassLoader(),目标对象的类加载器。

2、Class[] interfaces:接口,目标对象实现的接口,也是反射获取的。 3、InvocationHandler h:我们自己写的,代理类要完成的功能。 返回值:就是代理对象 public static Object newProxyInstance(ClassLoader loader, Class[] interfaces,

InvocationHandler h)

还是之前的功能,但是我们需要添加一个东西,其实之前我们就应该遵守面向接口原则,但是之前的user类,确实没有遵守,不过刚好这里也做了强调了,使用JDK动态代理,被代理的对象必须要有接口

添加接口

public interface UserInter {


    public void play();


    public void eat();


    public void learning();

}

然后我们写jdk的动态代理类,看到没spring中的aop简单实现

// 必须实现实现InvocationHandler进行方法增强
public class JDKProXYUser implements InvocationHandler {

    // 被代理对象
    private Object target;

    // 通过构造方法,传入被代理对象
    public JDKProXYUser(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // Object proxy, Method method, Object[] args
        // 被代理对象,被代理对象方法,被代理对象执行方法的参数
//        System.out.println(proxy);
//        System.out.println(method);
//        System.out.println(args);
        /**
         *  方法中的proxy,method都是拿的接口类中的
         *  比如这样:都是接口中的方法
         *  method.isAnnotationPresent(MyAnnotation.class)
         *                 || method.getDeclaringClass().isAnnotationPresent(MyAnnotation.class)
         */

        Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
        System.out.println("aop====方法前置增强====");

        // 看看类中的方法有没有被我们自定义注解标记的,并拿到注解信息

        if (target.getClass().isAnnotationPresent(MyAnnotation.class)
                || targetMethod.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation annotation = targetMethod.getAnnotation(MyAnnotation.class);
            String value = annotation.value();
            // 在这里我们就想当与可以把原本的方法覆盖了,
            // 也可以拿到放的参数,以及参数类型,包括返回值,这里可以通过param接收参数
            System.out.println("这里是代理对象帮你实现的方法,帮你实现了" + value + "方法");
        }

        System.out.println("aop====方法后置增强====");

        return null;
    }

}

测试

public class Test02 {
    public static void main(String[] args) {
        // 面向接口创建一个user类
        User user = new User();
        // 创建代理类进行代理
        JDKProXYUser jdkProXYUser = new JDKProXYUser(user);

        // 这里需要这个帮我们实现代理,并且返回一个全新的代理对象,三个参数
        UserInter newUser = (UserInter) Proxy.newProxyInstance(
                // 反射获取的被代理对向的类加载器
                user.getClass().getClassLoader(),
                // 目标对象实现的接口,所以这里为什么要实现接口
                user.getClass().getInterfaces(),
                // 实现代理的对象
                jdkProXYUser
        );

        newUser.eat();

    }
}

结果:

在这里插入图片描述

到这里就完事了,看下你的思路是什么样子的,最后这里再提一下,jdk的静态代理是什么,继承重写父类方法,这是静态代理。



小结


那都看到这里了,还不三连,点赞收藏关注吗,持续输出更好内容,bye~



版权声明:本文为hello_list原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。