Java反射机制-反射基础+对java.lang.Class类的理解+反射应用之动态代理+SpringAOP初体验

1. 反射概述

1.1 反射

  • Reflection〈反射) 是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

  • 加载完类之后,在堆内存的方法区中就产生了一个class类型的对象〈一个类只有一个class对象) ,这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为: 反射。

1.2 反射的作用


  1. 在运行时判断任意一个对象所属的类

  2. 在运行时构造任意一个类的对象

  3. 在运行时判断任意一个类所具有的成员变量和方法

  4. 在运行时获取泛型信息

  5. 在运行时调用任意一个对象的成员变量和方法

  6. 在运行时处理注解

  7. 生成动态代理

1.3 反射相关的主要API


  1. java.lang.Class:代表一个类

  2. java.lang.reflect.Method:代表类的方法

  3. java.lang.reflect.Field:代表类的成员变量

  4. java.lang.reflect.Constructor:代表类的构造器

  5. 。。。

1.4 反射初体验


将反射的一些基本的使用 与 不使用反射的情况对比

    //反射之前,对Person类的操作
    public void test1(){
        //1.创建对象
        Person p1 = new Person("luca",132);
        //2.访问属性
        System.out.println(p1.getName());
        //3.调用方法
        p1.show();
    }

    //使用反射
    public static void test2() throws Exception{
        Class c1 = Person.class;
        //1.通过反射,创建Person类的对象
        Constructor cons = c1.getConstructor(String.class,int.class);
        Object obj = cons.newInstance("luca",123); // 这里的Object 其实就是Person类
        Person p1 = (Person) obj;

        //2.通过反射,调用对象指定的属性,方法
        Field age = c1.getDeclaredField("age");
        age.set(p1,12);//将age属性设置为12
        System.out.println(p1.toString());
        //3.通过反射,调用对象的方法
        Method show = c1.getDeclaredMethod("show");
        show.invoke(p1);
        //4. 通过反射 调用Person类的私有结构
        //调用私有的构造器
        Constructor cons1 = c1.getDeclaredConstructor(String.class);
        cons1.setAccessible(true);
        Person p2 = (Person) cons1.newInstance("lucy");
        System.out.println(p2);
        //调用私有属性
        Field name = c1.getDeclaredField("name");
        name.setAccessible(true);
        name.set(p1,"lulu");
        System.out.println(p1);

        //调用私有方法
        Method showsex = c1.getDeclaredMethod("showsex",String.class);
        showsex.setAccessible(true);
        String len = (String) showsex.invoke(p1,"boy");
        System.out.println(len);

    }

1.5 关于反射的疑问


Q:可以通过反射创建对象,也可以通过new的方式,开发中到底使用哪一个?

A:大多数情况下都是使用new的方式;在某些情况下,比如在编译的时候还不能确定下来要创建哪一个对象,只有在运行是才能确定,这时就要使用反射,这也体现了反射的动态性。

Q:反射机制与面向对象的封装性是不是矛盾的?

A:不矛盾,对于封装性我们 设计一个类的时候 属性,方法,构造器等等 该私有的时候私有(private) 该 公共的时候公共(public)封装性给我们的启示是:当我们看到一个类写了一些私有的方法,一些公共的方法时 就告诉我们私有的方法就不要用了就用公共的方法就可以了 因为私有的方法可能类内部用了 这里体现了封装性。比如单例模式 你要想造对象 就不要用私有的构造器了 我已经把对象造好了直接用就行。但反射告诉我们在技术层面上,是可以调用私有的结构,只不过说不建议这样。

2. java.lang.Class类的理解

2.1 类的加载过程


程序经过编译后,会生成一个或多个字节码文件(.class),通过java命令对某个字节码文件进行解释运行,相当于将某个或某几个字节码文件加载到内存中。这个过程就称为类的加载。加载到内存中的类,就叫做运行时类,这个运行时类就作为Class的一个实例。所以类也是对象,是Class类的实例。这也体现的在java中万物皆对象。

换句话来说,一个Class的实例就对应着一个运行时类。

2.2 获取运行时类(获取Class的实例)

2.2.1 获取运行时类


加载到内存中的运行时类,会在内存中缓存一段时间,我们可以通过不同的方法来获取此运行时类。在这个运行时类的声明周期内,不会在加载第二个这个运行时类。

  1. 方式一:调用运行时类的.class属性 这一种就写死了,如果Person不存在,在编译的时候就会报错,灵活性不高,体现动态性不是能好

  2. 方式二:通过运行时类的对象,调用getClass() 使用的频率少,因为反射就是要创建运行时对象,第二种方法都已经有了对象了,所以与反射的作用不相匹配,所以用的比较少

  3. 方式三:调用Class的静态方法:forName(String classPath) 这种方式使用的最多,因为它体现动态性最好

  4. 方式四:使用类的加载器:ClassLoader(用的比较少,了解即可)

//方式一:调用运行时类的.class属性 这一种就写死了,如果Person不存在,在编译的时候就会报错,灵活性不高,体现动态性不是能好
        Class<Person> clazz1 = Person.class;
        System.out.println(clazz1);
        //方式二:通过运行时类的对象,调用getClass() 使用的频率少,因为反射就是要创建运行时对象,第二种方法都已经有了对象了,所以与反射的作用不相匹配,所以用的比较少
        Person p1 = new Person();
        Class<? extends Person> clazz2 = p1.getClass();
        System.out.println(clazz2);
        //方式三:调用Class的静态方法:forName(String classPath) 这种方式使用的最多,因为它体现动态性最好
        Class<?> clazz3 = null;
        try {
            clazz3 = Class.forName("com.luca.Person");//参数是类的全路径名
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(clazz3);

        System.out.println(clazz1 == clazz2);//返回的都是true,说明这三中方法都是获取的同一个东西
        System.out.println(clazz1 == clazz3);

        //方式四:使用类的加载器:ClassLoader(用的比较少,了解即可)
        ClassLoader classloader = ReflectionTest.class.getClassLoader();
        Class<?> clazz4 = null;
        try {
            clazz4 = classloader.loadClass("com.luca.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(clazz4);
        System.out.println(clazz1 == clazz4);

    }

2.2.2 哪些类型可以是Class的对象


Class的对象不光是运行时类,它还能是以下的结构

(1) class: 外部类,成员(成员内部类,毅态内部类),局部内部类,匿名内部类

(2) interface: 接口

(3) []: 数组

(4) enum: 枚举

(5) annotation: 注解@interface

(6) primitive type: 基本数据类型

(7) void

2.2.3 使用类加载器读取配置文件


public class ReflectionTest {
    //使用类加载器读取配置文件
    public static void loadProperties() throws IOException {
        Properties prop = new Properties();
        ClassLoader classLoader = ReflectionTest.class.getClassLoader();
        //配置文件默认至在当前module下的scr中
        InputStream resourceStream = classLoader.getResourceAsStream("test.properties");
        prop.load(resourceStream);
        System.out.println(prop.getProperty("name"));
        System.out.println(prop.getProperty("password"));

    }

}

2.3 创建运行时类的对象

2.5.1 步骤


  1. 创建运行时类:使用上述的四种方法,建议使用第三种

  2. 通过运行时类的构造器:使用Class.getConstructor(),这里面可填参数,参数会对应到相应的构造器。

  3. 通过构造器获取对象:使用Constructor.newInstance(),将具体的参数传进去,当然,在框架中,我么一般是调用空参的构造器

注意:在java9之前 2,3 步骤都被一个newInstance()代替,但现在已被弃用。

2.3.2 示例


这个示例演示了上面的步骤:

这里Constructor 会提示要使用泛型,根据反射的动态性,我们开发的时候根本不知道这个类是什么类,所以我们这里直接写一个’?’’;Class也是一样。

Class<?> c = null;
try {
    c = Class.forName("com.luca.Person");//参数是类的全路径名
    Constructor<?> cons =  c.getConstructor();//提示使用泛型
    Object obj1 = cons.newInstance(); // 这里的Object 其实就是Person类
    Person p1 = (Person) obj1;
} catch (ClassNotFoundException e) {
    e.printStackTrace();
    }

2.3.1 被弃用的newInstance()


  1. 使用这个方法会创建运行时类的对象,在底层它会调用空参构造器,返回这个对象。 创建对象只能通过构造器。

  2. 我们看看它的异常IllegalAccessException,非法进入异常,也就是访问这个空参构造器权限不够就会抛异常。

  3. InstantiationException 实例化异常,也就是说如果这个类没有空参构造器就会报异常

  4. 这个方法因为异常的原因而被弃用,因为它绕过了编译时异常检查,在编译时不会报错,但是在运行时,就可能出现异常,就算这个异常被我们异常处理过,现在用 Constructor.newInstance 来代替。也就是我们在反射初体验中的方法。

  5. This method propagates any exception thrown by the nullary constructor, including a checked exception. Use of this method effectively bypasses the compile-time exception checking that would otherwise be performed by the compiler. The Constructor.newInstance method avoids this problem by wrapping any exception thrown by the constructor in a (checked) InvocationTargetException.

    请注意,此方法传播由nullary构造函数引发的任何异常,包括已检查的异常。这种方法的使用有效地绕过了编译时异常检查,否则该检查将由编译器执行。该Constructor.newInstance方法通过将构造函数抛出的任何异常包装在(选中)中来避免此问题InvocationTargetException。

例子:使用被弃用的Class.newInstance()

    //通过反射创建对应的运行时类的对象
    public static void test1() throws IllegalAccessException, InstantiationException {
        //1.使用上面四种方法的一种获取一个运行时类
        Class<Person> c1 = Person.class;

        //2.
        Person person = c1.newInstance();
        System.out.println(person);

        //不使用泛型,就需要强转,再次体会一下泛型的作用
        Class c2 = Person.class;
        Object obj = c2.newInstance();
        Person person2 = (Person) obj;

    }

2.3.3 启示:为什么要提供空参构造器


根据上面代码得出的启示:在javabean中我们最好是要提供一个public的空参构造器,

  1. 原因1:在框架中,我们经常会这样通过反射来创建对象,都是调的空参构造器,属性都是自己在后面获取出来,然后加上去的

  2. 原因2:如果一个类没有提供构造器,或者有构造器,没有显示的写this或super的话,那就会执行super(),也就是调用父类的空参构造器

2.3.4 体会反射的动态性


反射的动态性就体现在,在程序运行的时候,能够根据需求来创建不同的对象。

下面代码演示了,在运行时,根据这个随机数来创建不同的对象。

/*
    * 创建一个指定类的对象
    * classPath:指定类的全类名
    * */
    public static Object getInstance(String classPath) throws Exception{
        Class<?> aClass = Class.forName(classPath);
        return aClass.getConstructor().newInstance();
    }

    //动态的生成一个对象
    public static void test2(){
        int i = new Random().nextInt(3); //随机生成一个0-3的数组(3不包括)
        String classPath = "";
        switch (i){
            case 0:
                classPath = "java.util.Date";

                break;
            case 1:
                classPath = "com.luca.Person";
                break;
            case 2:
                classPath = "java.lang.Object";
                break;
        }
        try {
            Object obj = getInstance(classPath);
            System.out.println(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        test2();
    }

2.4 获取运行时类的对象的结构

现在知道了,所有的类都会被加载内存中,而且作为一个Class的实例,有一个Class的引用(在上面的代码中 就是 aClass)指向这个类,所以我们就可以获取这个类的全部信息,包括但不限于:注解,异常,构造器,属性,方法,这个类实现的接口,它的父类。

现在我们获取到了运行时类,之后又创建了运行时类的对象,现在来获取运行时类的对象的结构。

2.4.1 获取属性


    //获取属性
    public static void getField() throws Exception {
        Class<?> aClass = Class.forName("com.luca.Person");
        //getFields:获取当前运行时类,及其所有父类中 声明为public的所有属性。
        Field[] fields = aClass.getFields();
        for (Field f:fields){
            System.out.println(f);
            //1。获取单个属性的权限修饰符
            int modifiers = f.getModifiers();//权限用数字来表示
            System.out.println(Modifier.toString(modifiers));
            //2。数据类型
            Class<?> type = f.getType();
            System.out.println(type);//打印的是全类名,可以通过getName()只获取类名
            //3。变量名
            String name = f.getName();
            System.out.println(name);
        }
        //getDeclaredFields:获取当前运行时类自己的所有属性(不包括父类中的)
        Field[] fields1 = aClass.getDeclaredFields();
        for (Field f:fields1){
            System.out.println(f);
        }
        //获取指定属性,但属性要是public,默认的也不行
        Field age = aClass.getField("age");
        //获取指定属性,可以使任意的
        Field name = aClass.getDeclaredField("name");

    }

2.4.2 获取方法


    public static void getMethod() throws Exception{
        Class<?> aClass = Class.forName("com.luca.Person");
        //getMethods:获取当前运行时类,及其父类中声明为public的所有方法
        //getDeclaredMethods:获取当前运行时类所有的方法(不包含父类中的)
        Method[] methods = aClass.getMethods();
        for (Method method:methods){
            System.out.println("--------------------------------------------------------------------");
            System.out.println(method);
            //1。获取方法声明的注解(主要就是这个),我们一般都是获取到注解的value
            Annotation[] annotations = method.getAnnotations();
            System.out.println("annotations: "+Arrays.toString(annotations));
//            for (Annotation annotation : annotations){
//                System.out.println("annotation: "+annotation);
//            }
            //2. 获取权限徐修饰符
            System.out.println(Modifier.toString(method.getModifiers()));
            //3. 返回值 (很少用)
            System.out.println("返回值:"+method.getReturnType().getName());
            //4.方法名
            System.out.println(method.getName());
            //5. 形参列表(用的也不多)
            Parameter[] parameters = method.getParameters();
            System.out.println(Arrays.toString(parameters));
            Class<?>[] parameterTypes = method.getParameterTypes();
            System.out.println(Arrays.toString(parameterTypes));
            //6. 抛出的异常
            Class<?>[] exceptionTypes = method.getExceptionTypes();
            System.out.println("异常:" + Arrays.toString(exceptionTypes));
        }
    }

2.4.3 获取当前运行时类带泛型的父类的泛型


这个在数据库DAO中会用到

    public static void getSuperClassGeneric() throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.luca.Person");
        //获取父类
        Class<?> superclass = aClass.getSuperclass();
        //获取代泛型的父类,Type是一个接口

        Type genericSuperclass = aClass.getGenericSuperclass();
        System.out.println(genericSuperclass);//将输出带泛型的父类
        ParameterizedType paramtype= (ParameterizedType) genericSuperclass;
        //获取代泛型的父类的泛型
        Type[] actualTypeArguments = paramtype.getActualTypeArguments();
        System.out.println(Arrays.toString(actualTypeArguments));
    }

还有获取构造器,获取运行时类所在的包,它的接口,等等等,都是差不多的,这里就不再赘述了。

2.5 调用运行时类的对象的结构


怎么我们也获取到了运行时类的对象的结构,现在我们就来看看怎么调用它们。重点在于:调用方法,属性,构造器。调用方法最重要。

2.5.1 获取并修改运行时类的对象的属性


//获取并修改属性
    public static void useField() throws Exception {
        Class<?> aClass = Class.forName("com.luca.Person");
        Person p = (Person) aClass.getConstructor().newInstance();

        //Field age = aClass.getField("age"); 这个只能获取public,但是public的属性又少,开发中不常用,用Declared
        Field age = aClass.getDeclaredField("age");
        //虽然可以拿到的不是public的属性,但是改不了,我们要设置可以进入为true

        age.setAccessible(true);

        //set() 参数1:设置哪一个对象的属性;参数2:将这个属性设置成多少
        age.set(p,123);
        //获取p对象的age属性
        int pAge = (int) age.get(p);
        System.out.println(pAge);

    }

2.5.2 使用运行时类的对象的方法


    //使用运行时类的对象的方法
    public static void useMethod()throws Exception{
        /* ****************88****调用非静态方法******************** */
        Class<?> aClass = Class.forName("com.luca.Person");
        //既然调用的是非静态方法,当然要创建对象
        Person p = (Person) aClass.getConstructor().newInstance();
        /*
        * 使用getDeclaredMethod,参数1:方法的名称,参数2:方法的参数;因为方法有多态嘛。
        * 同样我们不使用getMethod,理由和属性是一样的。
        * */
        Method show = aClass.getDeclaredMethod("show", String.class);
        //与属性一样,虽然可以拿到的不是public的,但没有权限,要把setAccessible改成true
        show.setAccessible(true);
        /*
         *invoke(): 参数1:参数的调用者;参数2:传入的实参
         */
        Object returnvalue = show.invoke(p, "你好");
        System.out.println(returnvalue);
        /* *************8******调用静态方法********************* */
        //其实差不多,就是不用
        Method showme = aClass.getDeclaredMethod("showme");
        showme.setAccessible(true);
        Object i = showme.invoke(aClass);
        //Object i = showme.invoke(Person.class); 也可以,aClass就是Person.class
    }

2.5.3 使用运行时类的对象的构造器


这个肯定是用来创建对象的。

//使用运行时类的对象的构造器
    public static void useConstructor() throws Exception {
        Class<?> aClass = Class.forName("com.luca.Person");
        //getDeclaredConstructor: 参数:形参列表,传入形参的类型,看一下你要调哪一个构造器
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class);
        declaredConstructor.setAccessible(true);
        //创建对象
        Object luca = declaredConstructor.newInstance("luca");
    }

3.反射的应用:动态代理


这里不涉及到框架相关的知识,这里我们把代理就简单的当成反射的应用,来体会反射的动态性。更深的知识在之后框架在学习。[https://www.cnblogs.com/cenyu/p/6289209.html : Java的三种代理模式]

  • 代理设计模式的原理: 使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原折对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

  • 之前为大家讲解过代理机制的操作,属于静态代理,特征是代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。同时,每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。最好可以通过一个代理类完成全部的代理功能。而且如果接口中有privite的方法,就没有用了,因为接口中private的方法,代理类的代理类就无法调被代理类的方法;这时必须使用动态代理。

  • 动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时 ,根据需要动态创建目标类的代理对象。

  • 动态代理使用场合:

    • 调试

    • 远程方法调用

  • 动态代理相比于静态代理的优点: 抽象角色中〈接口) 声明的所有方法都被转移到调用处理器一个集中的方法处理,这样,我们可以更加灵活和统一的处理众多的方法。

package com.luca;

import javax.naming.spi.ObjectFactoryBuilder;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface Human{
    String getName();
    void eat(String food);
}

class SuperMan implements Human{
    public SuperMan() {
        System.out.println("正在调用空参构造器");
    }

    @Override
    public void eat(String food) {
        System.out.println("我喜欢吃" + food);
    }

    @Override
    public String getName() {
        return "i'm name is superman";
    }
}

/*
*   要实现动态代理,需要解决的两个问题
*   1。如何根据加载到内存中的被代理类来动态地创建一个代理类及其对象
*   2。怎么保证:当我们调用代理类的方法时,该方法会去动态地调用被代理类的同名方法
 */

//用来创建代理类的工厂
class ProxyFactory{
    //调用此静态方法,传入被代理对象,返回一个代理类对象(解决问题1)
    public static Object getProxyInstance(Object obj) {
        //创建一个被代理类对象
        MyInvocationHandler handler = new MyInvocationHandler();
        handler.bind(obj);
        //java.lang.reflect.Proxy 这个Proxy是这个包下的。
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),handler);
    }
}

//这个MyInvocationHandler 就解决的问题2
class MyInvocationHandler implements InvocationHandler{

    private Object obj; //为了不写死,声明为Object,它将是被代理类对象的容器

    public void bind(Object obj){
        this.obj = obj;
    }

    //当我们通过代理类的对象调用某方法时,就会自动调用这个invoke方法
    //所以我们要将被代理类要执行的方法声明在invoke中
    //method:就是被代理类的方法,现在被传过来了
    //args: 是method所需的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("增强功能1");
        //invoke方法就是之前在调用运行时类对象的方法里学的那个
        Object returnvalue =  method.invoke(obj,args);
        System.out.println("增强功能2");

        return returnvalue;//原封不动的返回被代理类方法返回的返回值
    }
}

//也可以这样写 版本2
class ProxyFactory2{

    //维护一个目标对象
    private Object target;
    public ProxyFactory2(Object target){
        this.target=target;
    }

    //给目标对象生成代理对象
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("开始事务2");
                        //执行目标对象方法
                        Object returnValue = method.invoke(target, args);
                        System.out.println("提交事务2");
                        return returnValue;
                    }
                }
        );
    }
}
public class DynamicProxyTest {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        System.out.println(superMan.getClass().getClassLoader());
        //返回的是代理类的对象,由于它是动态生成的,我们不知道它的类型,所以我们就把它强转为共有接口的类型
        Human proxyInstance = (Human)ProxyFactory.getProxyInstance(superMan);
        System.out.println(proxyInstance.getName());
        proxyInstance.eat("麻辣烫");
    }

}

4. Spring—AOP初体验


AOP即面切面编程。看下面一种情况:

不同的代码段中有相同的代码,会有点冗余,所以我们希望把它提取出来,提取成一个方法,在各个方法中调用该方法就实现代码的复用;但这里又出现了一个问题:这三个代码段与这个方法耦合了,我们最理想的状态就是三个代码段既可以执行方法A,但又不以硬编码的方式调用方法A,即不把它写死,

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这时我们就可以用动态代理来实现这个。

invoke里的method就是不确定的,动态的,所以我们就可以把方法A“放到”method这个位置,所以我们就可以用动态代理来实现。

class MyInvocationHandler implements InvocationHandler{

    private Object obj; //为了不写死,声明为Object,它将是被代理类对象的容器

    public void bind(Object obj){
        this.obj = obj;
    }

    //当我们通过代理类的对象调用某方法时,就会自动调用这个invoke方法
    //所以我们要将被代理类要执行的方法声明在invoke中
    //method:就是被代理类的方法,现在被传过来了
    //args: 是method所需的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("这里放通用方法一");
        //invoke方法就是之前在调用运行时类对象的方法里学的那个
        Object returnvalue =  method.invoke(obj,args);
        System.out.println("这里放通用方法二");

        return returnvalue;//原封不动的返回被代理类方法返回的返回值
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值