前言
今天复习一下反射,说是复习,基本上已经忘干净了,只知道用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 方法:
1. public void sayHello(String name){ 2. System.out.println("hello "+name); 3. }
通过 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 反射机制来实现下面这个接口的实现类:
1. public interface UserService { 2. String addUser(String user,int age); 3. }
6.1、实现思路
- 通过反射生成 UserServiceImpl.java
- 将该 .java 文件编译为 UserServiceImpl.class
- 通过类加载器将该 .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<? extends JavaFileObject> 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); }
运行结果:
success
7、注解
7.1、什么是注解?
- Annotation是从JDK1.5开始引入的技术
- Annotation的作用:
- 不是程序本身,可以对程序做出解释。(和注释差不多)
- 可以被其他程序读取
- Annotation的格式:
- 注解是以“@参数名”在代码中存在的,还可以添加一些参数值,比如元注解:@SuppressWarnings(value="unchecked").
- Annotation在哪里使用?
- 可以附加在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 才能帮我们进行一个回滚。
注解的使用因为涉及到一些框架的东西,后面用到再来更新。