前段时间研究Bukkit NMS包时遇到了每个版本包名不同导致的插件严格受到依赖版本限制的问题,最终尝试了使用反射机制变相调用NMS包的内容,虽然最后由于方法名混淆导致依旧没有成功,但是关于Java反射机制又有了新的认识。
什么是反射
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
这是比较正规的说法,用我自己的理解来说就是:
在Java中万物皆为对象,也就是说包括类本身、类的方法、属性、构造器也都可以看做是对象,而实际上Java的reflect包中也确实提供了对应的类来表示这些对象,这样我们就可以利用反射机制来实现在不导入任何包的情况下调用任意类方法和属性,甚至获取这个类的对象
不使用反射的代码
public void AITaskTest(Player sender){
Location loc = sender.getEyeLocation();
WorldServer world = ((CraftWorld) sender.getWorld()).getHandle();
EntitySkeleton skeleton = new EntitySkeleton(world);
skeleton.setCustomName(ChatColor.RED + "a_skeleton");
skeleton.setLocation(loc.getX(), loc.getY(), loc.getZ(), 0f, 0f);
skeleton.goalSelector = new PathfinderGoalSelector(world != null && world.methodProfiler != null ? world.methodProfiler : null);
skeleton.goalSelector.a(1, new PathfinderGoalMeleeAttack(skeleton, 1, false));
skeleton.targetSelector.a(2, new PathfinderGoalNearestAttackableTarget<>(skeleton, EntityCreeper.class, true));
skeleton.goalSelector.a(3, new PathfinderGoalMoveTowardsTarget(skeleton, 10D, 10f));
world.addEntity(skeleton);
sender.sendMessage(ChatColor.RED + "Command run finished");
}
如上述代码,WorldServer、EntitySkeleton、PathfinderGoalSelector等类都是属于NMS的类,其包名命名规则为:net.minecraft.server.版本号.*;
也就是说Bukkit版本不同类的包名就不同,插件在不同版本的服务器上完全没有兼容性可言。
为此需要对代码进行亿点小小的改动,使用反射机制来实现以上代码
由此最终代码被改为如下内容:
private Class<?> worldServerClass;
private Class<?> worldClass;
private Class<?> craftWorldClass;
private Class<?> entitySkeletonClass;
private Class<?> pathfinderGoalSelector;
private Class<?> pathfinderGoal;
// private Class<?> gameProfilerFiller;
private Class<?> methodProfiler;
private Class<?> pathfinderGoalMeleeAttack;
private Class<?> entityCreature;
private Class<?> entityCreeper;
// private Class<?> entityInsentient;
private Class<?> pathfinderGoalNearestAttackableTarget;
public DreamLandsCommandExecutor(){
try {
worldServerClass = BaseUtil.getNmsClass("WorldServer");
worldClass = BaseUtil.getNmsClass("World");
craftWorldClass = BaseUtil.getCraftBukkitClass("CraftWorld");
entitySkeletonClass = BaseUtil.getNmsClass("EntitySkeleton");
pathfinderGoalSelector = BaseUtil.getNmsClass("PathfinderGoalSelector");
pathfinderGoal = BaseUtil.getNmsClass("PathfinderGoal");
// gameProfilerFiller = BaseUtil.getNmsClass("GameProfilerFiller");
pathfinderGoalMeleeAttack = BaseUtil.getNmsClass("PathfinderGoalMeleeAttack");
entityCreature = BaseUtil.getNmsClass("EntityCreature");
entityCreeper = BaseUtil.getNmsClass("EntityCreeper");
// entityInsentient = BaseUtil.getNmsClass("EntityInsentient");
pathfinderGoalNearestAttackableTarget = BaseUtil.getNmsClass("PathfinderGoalNearestAttackableTarget");
methodProfiler = BaseUtil.getNmsClass("MethodProfiler");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private void AITaskTest(Player sender) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchFieldException {
Location loc = sender.getEyeLocation();
// 获取CraftWorld下的getHandle方法
Method getHandle = craftWorldClass.getMethod("getHandle");
// 将World对象强制类型转换为CraftWorld,并执行getHandle方法,获得WorldServer对象
Object world = getHandle.invoke(craftWorldClass.cast(sender.getWorld()));
// 调用EntitySkeleton的构造器并新建一个对象
Object skeleton = entitySkeletonClass.getConstructor(worldClass).newInstance(world);
// 调用setCustomName方法为骷髅命名
// 这里在spigot中运行会抛出NoSuchMethodException异常
// 但是实际上我找到的方法名就是这个,如果有懂的老哥还请赐教
// entitySkeletonClass.getMethod("setCustomName", String.class).invoke(skeleton, ChatColor.RED + "a_skeleton");
// 调用setLocation方法设置骷髅的位置,即玩家目光所视之处
// entitySkeletonClass.getMethod("setLocation", double.class, double.class, double.class).invoke(skeleton, loc.getX(), loc.getY(), loc.getZ(), 0f, 0f);
// 重新设置骷髅的goalSelector属性
Object methodProfiler = world == null ? null : worldServerClass.getField("methodProfiler").get(world);
Field goalSelector = entitySkeletonClass.getField("goalSelector");
goalSelector.set(skeleton, pathfinderGoalSelector.getConstructor(this.methodProfiler).newInstance(methodProfiler));
// 获取PathfinderGoalMeleeAttack的构造器
Constructor<?> constructor = pathfinderGoalMeleeAttack.getConstructor(entityCreature, double.class, boolean.class);
// 调用PathfinderGoalSelector的a方法并传入PathfinderGoalMeleeAttack的构造器产生的实例
// 此时invoke的对象应该的skeleton对象的goalSelector属性,所以使用Field goalSelector的get方法获取EntitySkeletonClass类下的goalSelector属性
Object selector = constructor.newInstance(skeleton, 1, false);
pathfinderGoalSelector.getMethod("a", int.class, pathfinderGoal).invoke(goalSelector.get(skeleton), 1, selector);
// 获取EntitySkeletonClass对象的targetSelector属性
Field targetSelector = entitySkeletonClass.getField("targetSelector");
// 使用PathfinderGoalNearestAttackableTarget的构造器新建一个PathfinderGoalNearestAttackableTarget对象
Object target = pathfinderGoalNearestAttackableTarget.getConstructor(entityCreature, Class.class, boolean.class).newInstance(skeleton, entityCreeper, true);
// 与上面道理相同,invoke的对象应该是skeleton的targetSelector属性
pathfinderGoalSelector.getMethod("a", int.class, pathfinderGoal).invoke(targetSelector.get(skeleton), 2, target);
// worldServerClass.getMethod("addEntity", entityInsentient).invoke(world, skeleton);
sender.sendMessage(ChatColor.RED + "command run finished");
}
BaseUtil的代码如下:
public static Class<?> getNmsClass(String name) throws ClassNotFoundException {
return Class.forName("net.minecraft.server." + version + "." + name);
}
public static Class<?> getCraftBukkitClass(String name) throws ClassNotFoundException {
return Class.forName("org.bukkit.craftbukkit." + version + "." + name);
}
不难发现,使用反射机制的话,我们完全不需要导入任何第三方类,所有对象都可以使用Object来表示,同时又能调用该对象所有已知且已存在的方法,但是同时也需要处理一堆异常,代码也远比原先复杂,那么付出这么大的代价,我们能做什么?
从这个例子上我们就可以得出,使用反射机制可以减少程序的外部依赖,尽管在Bukkit插件上,已经有Protocollib这类的发包前置库,但是不排除其他需要需要在不同包名下调用同一个类的场景。
再看java.lang.reflect包的UML图
Constructor、Method、Field、Class最终都是AnnotatedElement的实现类,与注解的使用范围一致,也就是说可以利用反射机制来获取相应对象的注解,以此来实现自定义注解的功能,通过自定义注解来实现各种功能。
最后值得一提的是,使用反射生成对象的效率远不如在程序运行时生成对象的效率,使用反射获取类同样会降低程序的运行效率,因此除了需要慎重使用外,在使用反射机制时应尽将反射相关的代码放在程序启动时,尽可能的将需要使用的类对象在程序初始化的时候就全部赋予。