使用 Reflections 扫描被注解标记的类

1,840 阅读1分钟

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

这篇文章具体实现功能是:自定义一个注解,装配类会扫描所给包路径所有被注解类进行配置。

想直接看实现方式可以跳到目录里的 Google救我

起因

最近在捣鼓 Netty,通过 Netty 实现一个简单的 IM Demo,其中使用到了自定义的协议,格式如下:

其中指令类型指明了内容的解析类型。

例如

 public final class Command {
     //登录请求指令
     public static final byte LOGIN_REQUEST = 1;
     
     //...其余指令
 }

然后需要创建对应的协议内容对象以便进行解析

 @Data   //lombok
 public class LoginRequestPacket extends Packet {
 ​
     private String userId;
 ​
     private String username;
 ​
     private String password;
 ​
     @Override
     public Byte getCommand() {
         return LOGIN_REQUEST;
     }
 }

最后需要在解码器中将指令和对应 Pakcet 关联起来

 public PacketCodec() {
     packetMap = new HashMap<>();
     packetMap.put(Command.LOGIN_REQUEST, LoginRequestPacket.class);
     packetMap.put(Command.LOGIN_RESPONSE, LoginResponsePacket.class);
     packetMap.put(Command.MESSAGE_REQUEST, MessageRequestPacket.class);
     packetMap.put(Command.ENTER_GROUP, EnterGroupRequestPacket.class);
     packetMap.put(Command.RESPONSE, ResponsePacket.class);
     packetMap.put(Command.GROUP_MESSAGE, GroupMessageRequestPacket.class);
     packetMap.put(Command.LOGOUT_REQUEST, LogoutRequestPacket.class);
     packetMap.put(Command.LOGOUT_RESPONSE, LogoutResponsePacket.class);
     packetMap.put(Command.EXIT_GROUP, ExitGroupRequestPacket.class);
     // 就这一堆,我去好家伙,那是真的麻烦,前面弄着忘记配了,测试还会出问题导致通信一直没反应
 }

思路

为了能让自己不用每次添加命令都跑到解码器进行添加,我想能不能直接在创建对应 Packet 的时候他能自己添加到解析器呢?很容易就联想到 Mybatis 中的 @MapperScan自动扫描 Mapper 的功能,使用 Mybatis 创建接口时,需要在接口处添加@Mapper如下:

 @Mapper
 public interface TableMapper{
     //接口
 }

于是乎,就按照这个思路来了。其中关键点在于:

  1. @MapperScan配置了需要扫描的包路径,即其注解中的basePackage元素
  2. 需要通过@Mapper标记需要装配的类

因为自己的 IM 中主要是PacketCodec需要进行扫描装配,那么我就在其构造方法中加入扫描配置的逻辑就好

剩下的就是创建一个被标记类,并且传入 Command 的值与该 Packet 进行绑定即可

初遇难处

按照思路,其实最简单的是先去看 Mybatis 中包扫描的源码。先查看@MapperScan,其中有一行代码如下,MapperScannerRegistrar会提前加载。

 // @Import 是 Spring 框架中的注解,用于说明注解元素属性中的类需要提前与被注解类加载
 @Import({MapperScannerRegistrar.class})

顺着看 MapperScannerRegistrar,我去?!都是些操作与 Spring 相关的方法,涉及到 BeanDefinition 、BeanDefinitionRegistry...

卧槽,完了,我这 Netty 小 Demo 一开始哪想得到会用这些东西,别说 BeanDefinition 了,连个容器都没...到哪注册去....难不成给这个小 Demo 换个血型?

我再想想有没有其他思路好了...其实还有一个思路,便是用ClassLoader然后传入 Packet 所在的路径一个一个进行解析装配,不过想到麻烦的路径操作,以及后面的维护难度,还是不要堆屎山了。

Google救我

在寻找其他方式的时候,我发现了个好东西 —— Reflections。芜湖,直接起飞~简单方便,一用就会

Reflections 是一个反射框架,能够扫描 classpath,获取元数据信息

参考链接:

www.jianshu.com/p/49199793a…

blog.csdn.net/java_faep/a…

着手修改

创建 Packet 注解

 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.TYPE})
 public @interface Packet {
 ​
     byte value() default -1; // 接受传入的 Command 指令值,用于与被注解类绑定
 }

标记类

 @Data
 @codec.Packet(LOGIN_REQUEST)    //自定义的注解,LOGIN_REQUEST 是其指令值
 public class LoginRequestPacket extends Packet {
 ​
     //...
 }

修改 PacketCodec 构造方法,也就是需要进行扫描装配的类

 public PacketCodec() {
     packetMap = new HashMap<>();
     // 配置需要扫描的包路径,我的是 PacketCodec 类所在包的 protocol 包下
     Reflections reflections = new Reflections(this.getClass().getPackage().getName() + ".protocol");
     // getTypesAnnotatedWith(注解类),即可获得被对应注解类标记的类
     Set<Class<?>> packets = reflections.getTypesAnnotatedWith(codec.Packet.class);
     // 对被注解类的处理
     for (Class<?> pClazz : packets) {
         // 绑定指令值和被注解类,取代了之前的 packetMap.put(Command.LOGIN_REQUEST, LoginRequestPacket.class);
         packetMap.put(pClazz.getAnnotation(codec.Packet.class).value(), (Class<? extends Packet>) pClazz);
     }
 }

修改后程序能够正常运行,扫描标记类进行配置大功告成。虽然还是需要在 Packet 上进行注解,但是不用回到解码器当中进行配置,也算是轻松了不少。