反射的应用与注解式开发
七夕怎么过?全看编译过不过! 最近一直在做并发式Java开发和学习计算机视觉,又忘记照顾博客了。这次直接上一套开发实例作为学习笔记记录一下~ 文中的IDEA使用了new-ui预览插件,需要在jetbrains官网进行申请,此外从IDEA 2022.2版本开始无需EAP版本就可使用new-ui预览了
基本概念回顾
详细的请看另一篇笔记:Java基础知识:注解与反射
什么是反射
Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。而这也是Java被视为动态(或准动态,为啥要说是准动态,因为一般而言的动态语言定义是程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。从这个观点看,Perl,Python,Ruby是动态语言,C ,Java,C#不是动态语言。)语言的一个关键性质。
Java程序的运行依托于JVM虚拟机对class字节码文件的汇编解析,所有Java的进程都是运行在JVM之上,基于此运行方式任何Java程序都可以通过反射得到JVM虚拟机内的地址从而得到一个Java实体类。有了Java类后就可以毫无限制地修改JVM内存,甚至可以获取私有类的私有方法。这也使得Java的游戏外挂大肆兴起。
什么是注解
常见的注解包括 @Override @interface @Deprecated @SuppressWarnings @SafeVarargs @FunctionalInterface
注解能够让JVM认识、知道该 类 方法 变量
是做什么用途的。
从JDK5开始,Java增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
注解式开发
众所周知,使用注解式开发最明显的就属Spring框架了。所熟知 @Resource @Autowried @Bean
都是通过反射加注解的一些列操作来实现一些类似 BeanFactory注入 Resource资源注入
等操作。以及经典的AOP面向切面编程思路,都是注解式开发的缩影。
注解式开发允许我们为一个待处理的类、方法、变量进行预处理。以 Minecraft Server 插件开发为举例对象给出一个案例:
在 JavaPlugin 的 onEnable() 方法中我们需要注入 Listener(监听器类) 与 Command(指令类) 对于已经写好的这些类都需要通过
Bukkit.getPluginCommand()
进行注入 一旦我们需要 modules 化开发遇到很多 Command 与 Listener 类时就无形之中增加代码量、降低了代码的可读性 同时我们如果构造了模块的轮子允许其他开发者注入这些类时就会出现类难以注入到母Jar包内的情况进一步增加代码量
为了解决上述操作,我们使用注解式开发来降低代码量并提高可读性:
定义一个
@Handler
注解我们只允许他被挂载到类上 且被该注解修饰的类会在onEnable()
时自动注入到Bukkit..getPluginManager().registerEvents()
内 为了进一步规范化后期子插件的开发,统一将@Handler
限制在xxx.xxx.xxx.modules.xxx.handler
包内
我们对该注解的功能有了一定的期待和设计思路,现在我们将其细化:
- 为了能取到所有被
@Handler
注解修饰的类,我们需要通过一定的 URI 协议来遍历每个包下的类,现在我们构造这个获取指定包下class的工具类:
package ink.swordkernel.utils;
import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author DioxideCN
*/
public class reflectUtil {
/**
* 在jar包内反射所有类
* @param pack String 包名/包的路径
* @return Set<Class<?>> 返回所有类
*/
public static Set<Class<?>> getClasses(String pack) {
Set<Class<?>> classes = new LinkedHashSet<>();
boolean recursive = true;
String packageDirName = pack.replace('.', '/');
Enumeration<URL> dirs;
//处理当前线程资源路径
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
findClassesInPackageByFile(pack, filePath, recursive, classes);
} else if ("jar".equals(protocol)) {
JarFile jar;
try {
jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
findClassesInPackageByJar(pack, entries, packageDirName, recursive, classes);
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
private static void findClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, Set<Class<?>> classes) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
File[] dirfiles = dir.listFiles(file -> (recursive && file.isDirectory()) || (file.getName().endsWith(".class")));
for (File file : dirfiles) {
System.out.println("文件绝对路径:" file.getAbsolutePath());
if (file.isDirectory()) {
findClassesInPackageByFile(packageName "." file.getName(), file.getAbsolutePath(), recursive, classes);
} else {
String className = file.getName().substring(0, file.getName().length() - 6);
try {
classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName '.' className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
private static void findClassesInPackageByJar(String packageName, Enumeration<JarEntry> entries, String packageDirName, final boolean recursive, Set<Class<?>> classes) {
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.charAt(0) == '/') {
name = name.substring(1);
}
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
if (idx != -1) {
packageName = name.substring(0, idx).replace('/', '.');
}
if ((idx != -1) || recursive) {
if (name.endsWith(".class") && !entry.isDirectory()) {
String className = name.substring(packageName.length() 1, name.length() - 6);
try {
classes.add(Class.forName(packageName '.' className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
}
}
这是一个非常简单的工具类,传入包名我们就可以得到包下的所有class类的Set<Class<?>>集合了,我们用Junit来实现一下这个工具类并看下结果:
代码语言:javascript复制public static void main(String[] args) throws IOException, ClassNotFoundException {
try {
Set<Class<?>> classes = reflectUtil.getClasses("ink.swordkernel.modules");
classes.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
从结果中我们已经可以看到这个工具类已经帮我们把 ink.swordkernel.modules
包下的所有类都获取了出来
- 但是我们需要的是
handler
包下的类,所以我们使用上一期提到的Lambda表达式中Stream流来做一次筛选处理,再看一下最后的结果:
//筛选出 .handler 包内的类并存入List<>集合中
List<Class<?>> listenerClasses = classes.stream().filter(clazz -> clazz.getName().contains(".handler")).toList();
结果很明显,我们得到了我们想要的 handler
包下的所有的类
- 为了规范 @Handler 的模型,我们对筛选出来的handler包下类再做一次反射的筛选,通过
getAnnotation()
方法我们可以快速获取到类上的注解,再来看一下筛选出被 @Handler 修饰的类的结果:
listenerClasses.forEach(clazz -> {
if (clazz.getAnnotation(Handler.class) != null) {
System.out.println(clazz.getName());
}
});
根据《阿里Java开发规范》,即使是单语句if也必须加上大括号。此外,一个方法内代码量不得超过80行。
不难看出,这里对获取注解只做了一次 !null 判断,还是很简单的处理模式
- 最后将得到的符合条件的类全部注入
Bukkit
的监听器池中即可完成一套注解式开发:
public static void main(String[] args) {
try {
Set<Class<?>> classes = reflectUtil.getClasses("ink.swordkernel.modules");
List<Class<?>> listenerClasses = classes.stream().filter(clazz -> clazz.getName().contains(".handler")).toList();
listenerClasses.forEach(clazz -> {
if (clazz.getAnnotation(Handler.class) != null) {
try {
//这里将所有监听器注入到Bukkit中
Bukkit.getPluginManager().registerEvents((Listener) clazz.getDeclaredConstructor().newInstance(), SwordKernel.getPlugin(SwordKernel.class));
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
小结
得益于注解式开发的优点,我们很容易做出一系列预处理框架。以我的好兄弟正在做的 MenuAPI 为例:
借助注解的简便性将原本的4700多行代码通过构造的轮子,在前台类中缩短至了3行。
注解式开发无疑是Java开发的一大核心框架思想,在注解式开发带来便利的同时,更需要考虑反射带来JVM内存开销。通过合理的JVM参数调优以及适当的协程与线程池的构建能达到更优异的效果。