Mirai使用注解编程
本文最后更新于 788 天前,其中的信息可能已经有所发展或是发生改变。

功能设想

把Mirai整合进springboot之后写代码就已经比较简单了,但是我感觉每次需要对某个指令(消息)做出处理时都需要在监听里面加上判断语句,这样太不优雅了,于是就想仿照 @RequestMapping 的效果——在实现方法之前添加注解,在注解内编写调用条件,自动判断条件是否满足并自动执行相应的实现方法;这样就避免了需要在监听内写很多判断语句的情况,开发效率也就更高了。

步骤

创建自定义注解

首先创建个枚举,用来标识实现方法是针对哪个类型的(比如说是针对群聊消息的,或者是私聊消息的)

public enum ReceiveType {
    None, User, Group
}

这里枚举只是起个标识的作用,里面也不会放什么参数,所以就只需要定义几个常量就行。

然后创建个注解(Annotation),我这里就叫做Receive,并在里面添加两个参数;如果你不熟悉注解的用法的话可以去看一下菜鸟教程

@Target(ElementType.METHOD) // 代表注解是用来修饰方法的
@Retention(RetentionPolicy.RUNTIME) // 代表注释会被编译,并且能通过反射获取
public @interface Receive {
    ReceiveType type() default ReceiveType.None; // 针对消息的类型
    String msg() default ""; // 要匹配的消息内容
}

编写反射工具类

获取带有注解的类有两种方法,一种是用SpringBoot自带的方法getBeansWithAnnotation()获取,另一种是用第三方的库Reflections扫描。

getBeansWithAnnotation()

创建工具类ReceiveReflectUtil

@Slf4j // 这是lombok里面的注解
public class ReceiveReflectUtil {
    private static List<Object[]> receiveMethods;

    public static void scanReceiveMethods() {
        log.info("Start to scan Receive");
        receiveMethods = new ArrayList<>();
        Map<String, Object> controllers = SpringContextUtil.getApplicationContext().getBeansWithAnnotation(Controller.class);
        // 获取带有Controller注解的Bean
        // SpringContextUtil的代码可以看我的上一篇文章
        for (Map.Entry<String, Object> entry : controllers.entrySet()) {
            Object value = entry.getValue();
            Class<?> aClass = AopUtils.getTargetClass(value);
            Method[] methods = aClass.getDeclaredMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(Receive.class)) {
                    log.info("Scanned " + method.getName() + "()");
                    receiveMethods.add(new Object[]{method, aClass});
                }
            }
        }
    }

    public static List<Object[]> getReceiveMethods() throws LocalException {
        if (receiveMethods == null) {
            throw new LocalException("反射调用列表为null");
        }
        return receiveMethods;
    }
}

Reflections

不推荐使用这个方法,因为在我自己的测试中发现打包成jar之后会有莫名奇妙的bug,扫描不到有注解的类。

要使用org.reflections.Reflections这个包来扫描所有添加了自定义注释的方法,我们需要先在pom.xml中导入包(最新的版本号请到github上查看)

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.10.2</version>
</dependency>

然后创建工具类ReceiveReflectUtil

@Slf4j // 这是lombok里面的注解
public class ReceiveReflectUtil {
    private static List<Object[]> receiveMethods;

    public static void scanReceiveMethods() {
        log.info("Start to scan Receive");
        receiveMethods = new ArrayList<>();
        Reflections reflections = new Reflections("xxx.xxxx.controller"); // 这是要扫描的路径(包名)
        Set<Class<?>> classSet = reflections.get(SubTypes.of(TypesAnnotated.with(Controller.class)).asClass()); // 这里通过Controller这个注解来获取类
        for (Class<?> clazz : classSet) {
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(Receive.class)) { // 然后在类里面查找声明了Receive注解的方法
                    log.info("Scanned " + method.getName() + "()");
                    receiveMethods.add(new Object[]{method, clazz});
                }
            }
        }
    }

    public static List<Object[]> getReceiveMethods() throws RuntimeException {
        if (receiveMethods == null) {
            throw new RuntimeException("反射调用列表为null");
        }
        return receiveMethods;
    }
}

反射调用方法

修改Mirai的监听(我这里用的是我上一篇博客内讲的第二种方法添加的监听)

@NotNull
@EventHandler
public ListeningStatus onMessage(@NotNull MessageEvent event) throws Exception {
    log.info("onMessage");
    for (Object[] objects : ReceiveReflectUtil.getReceiveMethods()) {
        Method method = (Method) objects[0];
        Receive annotation = method.getAnnotation(Receive.class);
        if (event.getMessage().contentToString().equals(annotation.msg())) { // 判断注解上填写的消息内容是否与接受到的消息一致
            if (event.getSubject() instanceof Group && annotation.type() == ReceiveType.Group) { // 判断接受的消息类型是否与注解上的类型一致
                invokeMethod((Class<?>) objects[1], method, event);
            } else if (event.getSubject() instanceof User && annotation.type() == ReceiveType.User) {
                invokeMethod((Class<?>) objects[1], method, event);
            }
        }

    }
    return ListeningStatus.LISTENING; // 继续监听事件 ListeningStatus.STOPPED 停止监听
}

private void invokeMethod(Class<?> clazz, Method method, MessageEvent event) throws Exception { // 判断将要调用的方法所需要的参数,并传入
    log.info("Invoke " + method.getName() + "()");
    Parameter[] parameters = method.getParameters(); // 这是方法的参数的集合
    final int length = parameters.length;
    if (length > 0) {
        Object[] args = new Object[length];
        for (int index = 0; index < length; index++) {
            Parameter parameter = parameters[index];
            Class<?> aClazz = (Class<?>) parameter.getParameterizedType();
            if (MessageEvent.class.isAssignableFrom(aClazz)) { // 注意这行代码
                args[index] = event;
            } else {
                args[index] = null;
            }
        }
        method.invoke(SpringContextUtil.getBean(clazz), args); // 再注意这行代码
    } else {
        method.invoke(SpringContextUtil.getBean(clazz));
    }
}
首先解释一下MessageEvent.class.isAssignableFrom(aClazz)这行代码(踩坑注意)

parameter代表的就是方法的参数,而parameter.getParameterizedType()返回这个参数的类型。注意,是参数的“类型 Type”而不是“类 Class”,两者的区别是 Type是Class的父接口,Class是Type的子类 。所以,如果我们直接使用parameter.getParameterizedType() instanceof MessageEvent来进行判断参数是否为MessageEvent是不会返回true的。

同时,如果我们使用parameter.getParameterizedType().getClass().isInstance(MessageEvent.class)来判断的话,Type.getClass返回的直接是java.lang.Class,而MessageEvent.class也是java.lang.Class,所以这样永远都是true。

想要实现我们希望的效果,我们需要先把Type转成Class,然后再用class.isAssignableFrom()进行判断。

再解释一下method.invoke(SpringContextUtil.getBean(clazz), args)这行代码

只是看method.invoke()的话就是很简单的调用method(),但是重点在SpringContextUtil.getBean(clazz),关于这个方法的实现代码可以看我上一篇文章里面的“2.2.2 通过ApplicationContextAware获取”。简单来说作用就是获取bean(这里是获取class)。而我这里没有直接传入clazz.newInstance()是因为如果传入的类不是Spring中已经创建好的Bean,而是被自行创建出来的,那么这个方法里面的@Value等自动注入注解是没有效果的,所以这里传入的是Spring已经创建的类。

启用扫描并测试反射

在Application的main()中加上(建议加在启动Mirai的代码之前)

ReceiveReflectUtil.scanReceiveMethods();

然后简单写个方法进行测试

@Slf4j
@Controller // 扫描类时会用到的标记
public class TestController {
    @Receive(type = ReceiveType.Group, msg = "/help") // 表示当在群内发送/help时触发
    public void showFunMenu(MessageEvent event) {
        event.getSubject().sendMessage("received Group"); // 回复消息
    }

    @Receive(type = ReceiveType.User, msg = "test") // 表示私聊机器人发送test时触发
    public void send() {
        log.info("test");
    }
}

最后启动程序,看看发送/help和test有没有反应

总结和感想

扫描声明了自定义注解的类时,我把这一操作放在了启动项目的时候就进行而不是每次调用监听都去扫描一遍,这样应该能提高一些性能。

国内的相关网站上关于反射的应用的相关资料有点少,上面这个功能的有些地方我查了好久的资料,特别是Reflections包那里,甚至网上有些示例代码都是错的,害我检查了好久才发现问题。但是最终结果还是不错的,之后就能愉快地写机器人代码咯。

参考资料

评论

  1. 衍方
    已编辑
    3 年前
    2022-4-11 18:21:04

    好好好

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇