JDK1.5 新特性【反射】

前言

        今天复习一下反射,说是复习,基本上已经忘干净了,只知道用Spring、Mybatis、JavaFX 的时候加个注解,具体原理就不知道了。所以必须再深入学习一下。

1、设计一个框架?

设计一个框架需要什么技术?

反射机制、自定义注解、设计模式、AOP技术、Netty、Spring 架构、SpringBoot 自定义插件、多线程或 JUC。

2、反射技术

2.1、概念

  • Java中的反射是指程序在运行时动态地获取类的信息(比如方法信息、注解信息、方法参数、类的属性等)以及操作类的成员变量、方法和构造方法的能力。
  • 通过反射,可以在运行时检查类的属性和方法,获取类的构造函数并实例化对象,调用类的成员变量和方法,甚至可以在运行时动态地生成新的类,这使得Java程序具有更大的灵活性和动态性。
  • 但是,反射机制也会导致一些性能上的问题,因为反射调用的速度通常比直接调用要慢得多。

反射机制的核心是在运行时动态地获取类的信息,并通过这些信息来调用类的成员变量和方法,这种能力使得Java程序可以在运行时动态地加载和执行代码,从而实现更加灵活和动态的功能。 

2.2、Java中反射的主要API

  • Class类:用于表示Java类的信息,包括类的名称、父类、接口、构造函数、成员变量和方法等。
  • Constructor类:用于表示Java类的构造函数信息。
  • Method类:用于表示Java类的方法信息。
  • Field类:代表类的成员变量

2.3、应用场景

  • JDBC 加载驱动 Class.forName("com.mysql.cj.jdbc.Driver")
  • Spring 容器框架 IOC 实例化对象
  • 自定义注解生效(反射+AOP)
  • 第三方核心的框架
  • 在 Spring 中每个 Bean 的对象都是通过反射技术来初始化的。
  • 以及在之前 Hadoop 的学习中,当我们自定义的对象需要序列化的时候,我们说必须要有无参构造方法,因为反序列化需要反射调用无参构造函数。

3、使用反射初始化对象

我们以往的学习经历告诉我们,初始化对象的方式无非就是直接 new:

User user = new User();

下面给出两种通过反射初始化对象的方式: 

3.1、通过反射实现无参构造

原理:Class 对象的 newInstance 方法会默认执行我们的无参构造方法来初始化对象(所以在定义一个 JavaBean的时候一定要记得写无参构造的方法)。

    // 通过反射调用无参构造来初始化对象
    Class aClass = Class.forName("kt.pojo.User");
        // newInstance() 默认执行到我们的无参构造方法来初始化对象
        User user = (User) aClass.newInstance();
        System.out.println(user);

3.2、通过反射实现有参构造

3.2.1、一个标准的 JavaBean

package kt.pojo;

import java.util.Objects;

public class User {
    public String name;
    public Integer age;

    public User(){}

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age.equals(user.age) && Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

注意:这里的 age 属性类型也可以用 int ,区别就是 int 的默认值为 0,Integer 的默认值为 null。

输出结果:

User{name='null', age=null}

3.2.2、有参构造实现 

通过我们 Class 对象来获取我们的构造器对象 Constructor,然后调用 Constructor 对象的 newInstacne 方法来实现有参构造。

注意:如果 age 属性类型是 int,那么 getConstructor 的第二个参数应该是 int.class 。 

    // 通过反射调用有参构造来初始化对象
    Class aClass = Class.forName("kt.pojo.User");
    // 通过反射调用有参构造来初始化对象
        Constructor constructor = aClass.getConstructor(String.class, Integer.class);
        User tom = (User) constructor.newInstance("tom", 10);
        System.out.println(tom);

输出结果:

User{name='tom', age=10}

4、使用反射给对象的属性赋值

4.1、通过对象给属性赋值

我们通过反射获取到对象,然后调用对象的方法来赋值(必须保证对象的 setter 方法是 public 的):

    User user = (User) aClass.newInstance();
        user.setName("tom");
        user.setAge(10);
        System.out.println(user);

运行结果:

User{name='tom', age=10}

4.2、通过反射给属性赋值

通过反射获取到 Class 对象,再调用 Class 对象的 getDeclareField 方法获得指定属性的属性对象,最后通过属性的 set 方法来实现赋值:

User user = (User) aClass.newInstance();
        //        aClass.getField(); //这个方法会把我们 User 类的父类(这里是Object)中的属性页获取到
        Field name = aClass.getDeclaredField("name");
        name.setAccessible(true);   // 如果属性是私有的(private) 就需要添加访问权限
        Field age = aClass.getDeclaredField("age");
        age.setAccessible(true);    // 如果属性是私有的(private) 就需要添加访问权限
        name.set(user,"tom");
        age.set(user,10);
        System.out.println(user);        

注意:

  • JavaBean 中的属性如果是私有的需要调用属性对象(Field)的 setAccessible 方法传进一个 true 来使得属性可访问。
  • 如果需要使用该对象父类的属性的话,可以使用 getField 方法。

5、通过反射调用方法

我们在上面的 User 类中添加一个 sayHello 方法:

public void sayHello(String name){
        System.out.println("hello "+name);
    }

通过 Class 对象的 getDeclaredMethod 方法获取到指定的方法对象 Method,最后调用 Method 对象的 invoke 方法(需要传入执行该方法的对象和需要的参数):

    Class aClass = Class.forName("kt.pojo.User");
    User user = (User)aClass.newInstance();
    Method toString = aClass.getDeclaredMethod("sayHello", String.class);
    toString.invoke(user,"Tom");

运行结果:

hello Tom

6、使用反射实现拼接代码

我们需要通过 java 反射机制来实现下面这个接口的实现类:

public interface UserService {
    String addUser(String user,int age);
}

6.1、实现思路

  1. 通过反射生成 UserServiceImpl.java
  2. 将该 .java 文件编译为 UserServiceImpl.class
  3. 通过类加载器将该 .class 文件加载到内存当中去

6.2、生成源代码并保存到本地

public static String createSourceCode(Class classInfo){
        //1. 通过反射生成 UserServiceImpl.java
        StringBuilder builder = new StringBuilder();
        builder.append("package kt.reflect;");
        builder.append("public class ").append(classInfo.getSimpleName()).append("Impl implements ").append(classInfo.getSimpleName()).append(" {");
        Method[] methods = classInfo.getMethods();
        for (Method method : methods) {
            builder.append("public ").append(method.getReturnType().getSimpleName()).append(" ").append(method.getName()).append(" (String user, int age) { return \"success\"; }");
        }

        builder.append("}");
        return builder.toString();
    }
/**
     * 保存代码到本地磁盘
     * @param code 源代码
     * @param name name.java
     */
    public static void saveToLocal(String code,String name) throws IOException {
        String fileName = "D:/code/"+name+".java";
        File f = new File(fileName);
        FileWriter fw = new FileWriter(f);
        fw.write(code);
        fw.flush();
        fw.close();
    }

运行测试:

package kt.reflect;public class UserServiceImpl implements UserService {public String addUser (String user, int age) { return "success"; }}

6.3、编译为 class 文件

在我们生成的 java 文件的同级目录下生成 class 文件。

/**
     * 编译为 class 文件
     * @param fileName 完整路径(比如 D:/code//UserServiceImpl.java)
     * @throws IOException
     */
    public static void compile(String fileName) throws IOException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        Iterable units = fileMgr.getJavaFileObjects(fileName);
        JavaCompiler.CompilationTask t = compiler.getTask(null,fileMgr,null,null,null,units);
        t.call();
        fileMgr.close();
    }

6.4、加载到内存

到这里我们都已经有编译好的字节码文件了,直接放到 JVM 内存不就万事大吉了嘛。

我们先编写一个工具类:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * @author 刘xx
 * @version 1.0
 * @date 2023-11-20 18:52
 */
public class JavaClassLoader extends ClassLoader{

    private File classPathFile;


    public JavaClassLoader(){
//     String classPath = JavaClassLoader.class.getResource("").getPath();
     String classPath = "D:\\code";
     this.classPathFile=new File(classPath);
    }

    // 根据类名来查找(比如 UserServiceImpl)
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        String className = JavaClassLoader.class.getPackage().getName()+"."+name;
        if (classPathFile!=null){
            File classFile = new File(classPathFile,name.replaceAll("\\.","/")+".class");
            if (classFile.exists()){
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try{
                    in = new FileInputStream(classFile);
                    out = new ByteArrayOutputStream();
                    byte[] buff = new byte[1024];
                    int len;
                    while ((len=in.read(buff))!=-1){
                        out.write(buff,0,len);
                    }
                    return defineClass(className,out.toByteArray(),0,out.size());
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    if (in!=null){
                        try {
                            in.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (out!=null){
                        try {
                            out.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        return null;
    }
}
public static Class readToMemory() throws ClassNotFoundException {
        JavaClassLoader javaClassLoader = new JavaClassLoader();
        return javaClassLoader.findClass("UserServiceImpl");
    }

6.5、最终实现

我们需要把我们的 接口类 传进去:

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException, IOException {

    String s = createSourceCode(UserService.class);
        System.out.println(s);
        // 将源代码保存到本地磁盘
        saveToLocal(s,"UserServiceImpl");
        compile("D:/code/UserServiceImpl.java");
        Class aClass = readToMemory();
        UserService service = (UserService) aClass.newInstance();
        String res = service.addUser("lyh", 20);
        System.out.println(res);
}

运行结果:

JDK1.5 新特性【反射】_第1张图片

success

7、注解

7.1、什么是注解?

  1. Annotation是从JDK1.5开始引入的技术
  2. Annotation的作用:
    1. 不是程序本身,可以对程序做出解释。(和注释差不多)
    2. 可以被其他程序读取
  3. Annotation的格式:
    1. 注解是以“@参数名”在代码中存在的,还可以添加一些参数值,比如元注解:@SuppressWarnings(value="unchecked").
  4. Annotation在哪里使用?
    1. 可以附加在package、class、method、field等上面,相当于给他们添加了额外的辅助信息,我们可以同反射机制来实现对这些元数据的访问

7.2、自定义注解

  • 使用 @interface 自定义注解时,自动继承了java.lang.annotation.Annotation 接口。
  • 分析
    • @interface 用来声明一个注解,格式:public @interface 注解名 { 方法名()... }
    • 其中每一个方法实际上是一个配置参数
    • 方法名称就是参数名称
    • 返回值类型就是参数类型
    • 可以通过default来声明参数的默认值
    • 如果只有一个参数成员,一般参数名用value
    • 注解必须要有值,我们定义注解时,经常使用空字符串、0作为默认值

7.3、元注解

  • 元注解的作用就是负责注解其他注解,Java定义了4个标准的 meta-annotation类型,它们被用来提供对其他annotation类型作说明
  • 这些类型和它们所支持的类在 java.lang.annotation中可以找到(@Target、#Documented、@Inherrited、@Retention)
    • @Target:用于描述注解的使用范围(即注解可以标注在什么地方,类上面或者方法、属性等)
    • @Retention:表示在什么级别保存该注释信息,用于描述注解的声明周期
      • SOURCE:源码阶段-就是写代码的时候] 
      • CLASS :表示注解将在编译时被保留,并且会被包含在类文件中,但在运行时不可用。但这意味着,当程序运行时,无法访问该注解。
      • RUNTINUE :程序运行时,可以访问该注解(通过反射)。
    • @Documented:说明该注解将被包含在javadoc中。(javadoc是Java语言中自带的一种工具,用于生成API文档。)
    • @Inherited:说明子类可以继承父类的注解

7.4、案例

7.4.1、定义注解

我们在 User 的 sayHello 上定义一个注解:getMapping,并给改注解添加两个参数 name 和 age。

注意:

  • 要想注解对象能够通过反射被获取到,就要添加元注解(@Retention)
  • 想要该注解只能被标注在方法上,就要添加元注解(@Target)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) //运行时可被反射机制获取
@Target({ElementType.METHOD,ElementType.TYPE})   // 设置该注解只可以被标记在 方法上
public @interface GetMapping {
    String name();
    int age();

}

 7.4.2、使用注解

    @GetMapping(name = "lyh",age = 1)
    public void sayHello(String name){
        System.out.println("hello "+name);
    }

7.4.3、反射获得注解

获得注解对象需要使用 getAnnoation 方法。

注意:

  • 调用该注解(调用 getAnnotation 方法)的对象必须被该注解标注才能正确被获取到(比如这里的 sayHello 方法已经被该注解标注了;
  • 但如果这里是 aClass.getAnnotation(GetMapping.class) 那么我们的 User 类也必须被该注解标注)。
Class aClass = Class.forName("ke.pojo.User");
Method sayHello = aClass.getDeclaredMethod("sayHello", String.class);
        // 调用该注解(调用 getAnnotation)的对象必须被该注解标注 才能正确被获取到
        GetMapping getMapping = sayHello.getAnnotation(GetMapping.class);
        System.out.println(getMapping);

8、使用 Spring 事务的注意事项

我们看一段使用了 Spring 事务的代码:

@Transactional
public String insertUser(User user){
    try{
    
        int res = userMapper.insertUser(user);
        int i = 1/0;  // 会报错
        return "success";
    }catch(Exception e){
        e.printStackTrace();
        return "false";
    }
}

代码中,我们使用 @Transactional注解帮助我们自动提交事务和回滚事务。

但是事实上这段代码中的插入语句仍然会被执行,也就是出现了 事务失效的情况,这是因为我们的 try-catch 语句,默认情况下,当事务方法中抛出未检查异常(继承自 RuntimeException 的异常)时,Spring才会标记事务为回滚。在这个例子中,1/0 会抛出 ArithmeticException,这是一个已检查异常。由于我们已经捕获了这个异常,并且没有让它继续抛出,Spring认为事务应该正常提交,而不是回滚。

所以要解决这个问题,就需要我们手动来进行回滚:

@Transactional
public String insertUser(User user){
    try{
    
        int res = userMapper.insertUser(user);
        int i = 1/0;  // 会报错
        return "success";
    }catch(Exception e){
        e.printStackTrace();        
        // 手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return "false";
    }
}

这样,我们的异常才会返回给 AOP ,这样 AOP 才能帮我们进行一个回滚。

注解的使用因为涉及到一些框架的东西,后面用到再来更新。

你可能感兴趣的:(JavaSE,java,开发语言)