「这是我参与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{
//接口
}
于是乎,就按照这个思路来了。其中关键点在于:
@MapperScan
配置了需要扫描的包路径,即其注解中的basePackage
元素- 需要通过
@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 上进行注解,但是不用回到解码器当中进行配置,也算是轻松了不少。