MiraiForum

    • Register
    • Login
    • Search
    • Popular
    • Recent
    • Unsolved
    • Tags
    • Groups
    • 友情链接
    1. Home
    2. wssy001
    • Profile
    • Following 0
    • Followers 0
    • Topics 5
    • Posts 86
    • Best 9
    • Controversial 0
    • Groups 1

    wssy001

    @wssy001

    ⭐2021⭐

    14
    Reputation
    64
    Profile views
    86
    Posts
    0
    Followers
    0
    Following
    Joined Last Online

    wssy001 Unfollow Follow
    ⭐2021⭐

    Best posts made by wssy001

    • Spring Cloud Alibaba + Mirai Core の使用蕉♂流

      写在开头

            写这篇教程的目的是为了给那些有毕设需求而准备的,实际开放中应该没多少人会把mirai拉进微服务这个圈子吧?
      本篇中,Mirai Core充当着用户终端的角色,把用户输入传出、系统信息传送至用户;对分布式环境做了初步适配;结合Nacos进行动态更新配置,并利用Sentinel进行端口的动态流控;使用RocketMQ;
             项目默认使用WebFlux、但Servlet我也做了支持,branch中带有-servlet则是完全采用Servlet。
            各层必要方法的逻辑我会详细说明。


      项目地址

            传送门

            PS:实际业务逻辑请以项目为准,文档有时候来不及更新!


      版本预览

            如果最新版代码中有对上版示例代码的进行大量变更时,将在最新版中附上完整的示例代码,否则只会针对需要更新的业务方法进行详细说明。
            若无必要,将不会删除各版间的留言!

            第一版:传送门
            PS:代码我并没有实机测试(特指集群环境),若有BUG,望不吝赐教!

            第二版:传送门
            PS: 正在更新,目前已更新了Mysql版,MongoDB正在调试中。项目中的一些代码我事后回想起来是有坑的,正在结合源码进行填坑…… 更新完毕,暂时解决了消息ID自定义,不过RocketMQ Spring Boot Starter异步回调确实不完美,实际使用还需注意。


      目录

      1. 主要组件版本控制
      2. 其他相关组件版本控制
      3. 主POM
      4. Bot
        4.1 多Bot的坑
        4.2 多Bot的坑(集群)
        4.3 其他业务对多Bot的适配
      5. Nacos
        5.1 整合Nacos
        5.2 接入配置中心
        5.3 整合Nacos
        5.4 设置配置文件
        5.5 监听Nacos配置刷新事件
      6. Sentinel
        6.1 整合Sentinel
        6.2 配置Sentinel客户端
        6.3 自定义流控返回
        6.4 其他类对Sentinel的适配
        6.5 配置持久化
      7. 配置参考
        7.1 Redis配置参考
        7.2 Redisson配置参考

      主要组件版本控制:

      Adopt OpenJDK Spring Boot Spring Cloud Spring Cloud Alibaba Mirai Core
      11.0.10 2.4.2 2020.0.1 2021.1 2.6.8

      其他相关组件版本控制

      Nacos Sentinel Hutool
      1.4.1 1.8.0 5.7.15

      主POM:

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
      
          <parent>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-parent</artifactId>
              <version>2.4.2</version>
              <relativePath/>
          </parent>
      
          <groupId>cyou.wssy001.cloud</groupId>
          <artifactId>bot</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <name>bot</name>
          <packaging>pom</packaging>
      
          <properties>
              <java.version>11</java.version>
              <spring.cloud.version>2020.0.1</spring.cloud.version>
              <spring.cloud.alibaba.version>2021.1</spring.cloud.alibaba.version>
              <lombok.version>1.18.20</lombok.version>
              <redisson.version>3.16.4</redisson.version>
              <hutool.version>5.7.15</hutool.version>
              <fastjson.version>1.2.78</fastjson.version>
              <commons.pool2.version>2.11.1</commons.pool2.version>
              <mirai.core.version>2.6.8</mirai.core.version>
          </properties>
      
          <dependencyManagement>
              <dependencies>
                  <!--        Spring Cloud Alibaba-->
                  <dependency>
                      <groupId>com.alibaba.cloud</groupId>
                      <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                      <version>${spring.cloud.alibaba.version}</version>
                      <type>pom</type>
                      <scope>import</scope>
                  </dependency>
      
                  <!--            Spring Cloud-->
                  <dependency>
                      <groupId>org.springframework.cloud</groupId>
                      <artifactId>spring-cloud-dependencies</artifactId>
                      <version>${spring.cloud.version}</version>
                      <type>pom</type>
                      <scope>import</scope>
                  </dependency>
      
                  <!--            Lombok-->
                  <dependency>
                      <groupId>org.projectlombok</groupId>
                      <artifactId>lombok</artifactId>
                      <version>${lombok.version}</version>
                  </dependency>
      
                  <!--            Redisson-->
                  <dependency>
                      <groupId>org.redisson</groupId>
                      <artifactId>redisson</artifactId>
                      <version>${redisson.version}</version>
                  </dependency>
      
                  <!--            Hutool-->
                  <dependency>
                      <groupId>cn.hutool</groupId>
                      <artifactId>hutool-core</artifactId>
                      <version>${hutool.version}</version>
                  </dependency>
      
                  <!--            Hutool-http-->
                  <dependency>
                      <groupId>cn.hutool</groupId>
                      <artifactId>hutool-http</artifactId>
                      <version>${hutool.version}</version>
                  </dependency>
      
                  <!--            FastJSON-->
                  <dependency>
                      <groupId>com.alibaba</groupId>
                      <artifactId>fastjson</artifactId>
                      <version>${fastjson.version}</version>
                  </dependency>
      
                  <!--            Commons Pool2-->
                  <dependency>
                      <groupId>org.apache.commons</groupId>
                      <artifactId>commons-pool2</artifactId>
                      <version>${commons.pool2.version}</version>
                  </dependency>
      
                  <!--            Mirai-->
                  <dependency>
                      <groupId>net.mamoe</groupId>
                      <artifactId>mirai-core-jvm</artifactId>
                      <version>${mirai.core.version}</version>
                  </dependency>
      
              </dependencies>
          </dependencyManagement>
      
      </project>
      

      Bot

            引入Mirai依赖,由于Spring Boot 2.4.2自带的kotlin依赖版本是

      <kotlin.version>1.4.21</kotlin.version>
      <kotlin-coroutines.version>1.4.2</kotlin-coroutines.version>
      

            所以可以直接引入mirai-core-jvm,无需再指定kotlin相关依赖的版本

      <dependency>
          <groupId>net.mamoe</groupId>
          <artifactId>mirai-core-jvm</artifactId>
      </dependency>
      

            定义一个一个存储Bot账户的类

      @Data
      public class BotAccount implements Serializable {
          private static final long serialVersionUID = 1L;
      
          private Long account;
          private String password;
      }
      

            然后有一个用户机器人管理的Service,Bot登录后可以不放入IOC,使用Bot.getInstanceOrNull(botQQNumber)获得指定的Bot实例。

      @Slf4j
      @Service
      @RequiredArgsConstructor
      public class RobotService {
          private final DynamicProperty dynamicProperty;
      
          private final BotOnlineHandler botOnlineHandler;
          private final BotOfflineHandler botOfflineHandler;
          private final GroupMessageHandler groupMessageHandler;
          private final MemberJoinHandler memberJoinHandler;
          private final MemberLeaveHandler memberLeaveHandler;
      
          @PostConstruct
          private void init() {
              login(stringToBotAccountList(dynamicProperty.getAccounts()));
          }
      
          public void refreshBot() {
              List<BotAccount> botAccounts = stringToBotAccountList(dynamicProperty.getAccounts())
                      .stream()
                      .filter(v -> Bot.getInstanceOrNull(v.getAccount()) == null)
                      .collect(Collectors.toList());
      
              login(botAccounts);
          }
      
          private void login(List<BotAccount> botAccounts) {
      
              BotConfiguration botConfiguration = new BotConfiguration();
              botConfiguration.fileBasedDeviceInfo("device.json");
      //        botConfiguration.noNetworkLog();
      //        botConfiguration.noBotLog();
      
              for (BotAccount botAccountInfo : botAccounts) {
      
                  Bot bot = BotFactory.INSTANCE.newBot(botAccountInfo.getAccount(), botAccountInfo.getPassword(), botConfiguration);
                  try {
                      bot.getEventChannel().subscribeAlways(BotOnlineEvent.class, botOnlineHandler::handle);
                      bot.getEventChannel().subscribeAlways(BotOfflineEvent.class, botOfflineHandler::handle);
                      bot.getEventChannel().subscribeAlways(GroupMessageEvent.class, groupMessageHandler::handle);
                      bot.getEventChannel().subscribeAlways(MemberJoinEvent.class, memberJoinHandler::handle);
                      bot.getEventChannel().subscribeAlways(MemberLeaveEvent.class, memberLeaveHandler::handle);
                      bot.login();
                      log.info("******RobotService QQ:{} 登陆成功", botAccountInfo.getAccount());
      
      //                去掉break即可实现多Bot在线
                      break;
                  } catch (LoginFailedException e) {
                      bot.close();
                      log.error("******Bot LoginFailedException:{} ,QQ:{}", e.getMessage(), botAccountInfo.getAccount());
                  }
              }
      
          }
      
          private List<BotAccount> stringToBotAccountList(String s) {
              return JSON.parseArray(s, BotAccount.class);
          }
      }
      

      多Bot的坑

            当你的项目同时启用多个Bot时,可能会遇到同时监听一个群消息的问题,这时我们需要加锁,在监听群消息上强行“单线程”。由于只部署一个实例,所以用JVM级别的锁就行。

      逻辑

            使用synchronized可以确保同一时间只有一个机器人能够调用该方法,同时检查消息ID是否存在于Redis来避免重复消费(这块我使用Redis的有序集合,通过设置scope为过期时间来实现过期,同时利用有序集合对存储已存在数据会执行更新操作且返回0的性质完成对消息IDs的存储和是否全部存在的判断),同时做了优化,只有存在多个机器人同时监听的群才会被强制“单线程”

      @Component
      @RequiredArgsConstructor
      public class GroupMessageHandler2 {
          private final RepetitiveGroupService repetitiveGroupService;
          @Resource
          private ReactiveZSetOperations<Long, Integer> reactiveZSetOperations;
      
          public void handle(@NotNull GroupMessageEvent event) {
              long groupId = event.getGroup().getId();
              long botId = event.getBot().getId();
              RepetitiveGroup repetitiveGroup = repetitiveGroupService.get(groupId)
                      .share()
                      .block();
      
              if (hasBeenConsumed(event, groupId)) return;
      
              if (containMultiBots(repetitiveGroup, botId)) {
                  synchronized (this) {
                      consume();
                  }
              } else {
                  consume();
              }
          }
      
          //    检查事件是否以及被消费过
          private boolean hasBeenConsumed(@NotNull GroupMessageEvent event, long groupId) {
      
              long second = new Date().getTime();
              Range<Double> range = Range.closed(0.0, second + 0.0);
              reactiveZSetOperations.removeRangeByScore(groupId, range);
      
              int[] ids = event.getSource().getIds();
              List<DefaultTypedTuple<Integer>> list = new ArrayList<>();
              for (int id : ids) {
                  list.add(new DefaultTypedTuple<>(id, second + 10.0));
              }
      
              Long unExist = reactiveZSetOperations.addAll(groupId, list)
                      .share()
                      .block();
      
              return unExist == null || unExist == 0;
          }
      
          //    检查这个群是否有多个机器人监听群消息
          private boolean containMultiBots(RepetitiveGroup repetitiveGroup, Long botId) {
              return repetitiveGroup.getId() == null || !repetitiveGroup.getBotIds().contains(botId);
          }
      
          //    消费事件
          private void consume() {
      
          }
      }
      

      多Bot的坑(集群)

            集群环境下,JVM级别的锁失效,考虑到接入了Redis,所以引用Redisson来实现分布式锁。

      <dependency>
          <groupId>org.redisson</groupId>
          <artifactId>redisson</artifactId>
      </dependency>
      

            代码逻辑大致和非集群的类似,redisson拿到的锁是有超时时间的,时间到了自动释放。当事件A同时被Bot A、Bot B监听时,若Bot A拿到了锁但是消费事件时突然宕机,无法正常释放锁。若锁的超时时间大于事件Ids的有效时间,那Bot B可以正常消费事件。反之Bot B会被系统判定为事件重复消费。

      @Component
      @RequiredArgsConstructor
      public class GroupMessageHandler {
          private final RedissonClient redissonClient;
          private final RepetitiveGroupService repetitiveGroupService;
          @Resource
          private ReactiveZSetOperations<Long, Integer> reactiveZSetOperations;
      
          public void handle(@NotNull GroupMessageEvent event) {
              long groupId = event.getGroup().getId();
              long botId = event.getBot().getId();
              RepetitiveGroup repetitiveGroup = repetitiveGroupService.get(groupId)
                      .share()
                      .block();
      
              RLock lock = null;
              if (hasBeenConsumed(event, groupId)) return;
      
              if (containMultiBots(repetitiveGroup, botId)) {
                  lock = redissonClient.getLock(IdUtil.fastSimpleUUID());
                  lock.lock(20, TimeUnit.SECONDS);
              }
      
              consume();
      
              if (lock != null) lock.unlock();
          }
      
          //    检查事件是否以及被消费过
          private boolean hasBeenConsumed(@NotNull GroupMessageEvent event, long groupId) {
      
              long second = new Date().getTime();
              Range<Double> range = Range.closed(0.0, second + 0.0);
              reactiveZSetOperations.removeRangeByScore(groupId, range);
      
              int[] ids = event.getSource().getIds();
              List<DefaultTypedTuple<Integer>> list = new ArrayList<>();
              for (int id : ids) {
                  list.add(new DefaultTypedTuple<>(id, second + 10.0));
              }
      
              Long unExist = reactiveZSetOperations.addAll(groupId, list)
                      .share()
                      .block();
      
              return unExist == null || unExist == 0;
          }
      
          //    检查这个群是否有多个机器人监听群消息
          private boolean containMultiBots(RepetitiveGroup repetitiveGroup, Long botId) {
              return repetitiveGroup.getId() != null && repetitiveGroup.getBotIds().contains(botId);
          }
      
          //    消费事件
          private void consume() {
      
          }
      }
      

      其他业务对多Bot的适配

            RepetitiveGroup,存储加入了多个机器人的群

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class RepetitiveGroup implements Serializable {
          private static final long serialVersionUID = 1L;
      
          @Id
          private Long id;
          private List<Long> botIds;
      }
      

            RepetitiveGroupService实现CRUD

      @Service
      public class RepetitiveGroupService {
          @Resource
          private ReactiveRedisOperations<String, RepetitiveGroup> reactiveRedisOperations;
          public static final String HASH_KEY = RepetitiveGroup.class.getSimpleName();
      
          public Mono<RepetitiveGroup> get(Long groupId) {
              return reactiveRedisOperations.opsForHash()
                      .get(HASH_KEY, groupId)
                      .map(v -> (RepetitiveGroup) v)
                      .switchIfEmpty(Mono.defer(() -> Mono.just(new RepetitiveGroup())));
          }
      
          public Flux<RepetitiveGroup> getAll() {
              return reactiveRedisOperations.opsForHash()
                      .values(HASH_KEY)
                      .map(v -> (RepetitiveGroup) v);
          }
      
          public Mono<Boolean> upset(RepetitiveGroup repetitiveGroup) {
              return reactiveRedisOperations.opsForHash()
                      .put(HASH_KEY, repetitiveGroup.getId(), repetitiveGroup);
          }
      
          public Mono<Boolean> delete(RepetitiveGroup repetitiveGroup) {
              return reactiveRedisOperations.opsForHash()
                      .remove(HASH_KEY, repetitiveGroup.getId())
                      .map(v -> v.equals(1L));
          }
      }
      

            BotOnlineHandler:

      @Slf4j
      @Component
      @RequiredArgsConstructor
      public class BotOnlineHandler {
          private final SentinelService sentinelService;
          private final RepetitiveGroupService repetitiveGroupService;
          @Resource
          private ReactiveSetOperations<String, Long> reactiveSetOperations;
      
          public void handle(@NotNull BotOnlineEvent event) {
              Bot bot = event.getBot();
              long id = bot.getId();
      
              log.info("******BotOnlineHandler:QQ:{}", id);
      
              List<Bot> instances = Bot.getInstances();
      
              Set<Long> groupIds = bot.getGroups()
                      .parallelStream()
                      .map(Group::getId)
                      .collect(Collectors.toSet());
      
              groupIds.parallelStream()
                      .forEach(v -> reactiveSetOperations.add(id + "", v));
      
              instances.removeIf(v -> v.getId() == id);
              instances.parallelStream()
                      .forEach(v -> sinter(id, v.getId()));
      
          }
      
          private void sinter(Long self, Long target) {
              ArrayList<Long> list = new ArrayList<>();
              list.add(self);
              list.add(target);
      
              reactiveSetOperations.intersect(self + "", target + "")
                      .subscribe(v -> insert(v, list));
          }
      
          private void insert(Long groupId, List<Long> botIds) {
              RepetitiveGroup repetitiveGroup = repetitiveGroupService.get(groupId)
                      .share()
                      .block();
      
              if (repetitiveGroup.getId() == null)
                  repetitiveGroup.setBotIds(new ArrayList<>());
      
              Set<Long> set = new HashSet<>(botIds);
              set.addAll(repetitiveGroup.getBotIds());
              repetitiveGroup.setBotIds(new ArrayList<>(set));
              repetitiveGroupService.upset(repetitiveGroup);
          }
      }
      

            BotOfflineHandler:

      @Slf4j
      @Component
      @RequiredArgsConstructor
      public class BotOfflineHandler {
          private final RepetitiveGroupService repetitiveGroupService;
      
          public void handle(@NotNull BotOfflineEvent event) {
              Bot bot = event.getBot();
              long id = bot.getId();
              String reason = "未知";
      
              if (event instanceof BotOfflineEvent.Active)
                  reason = "主动下线";
      
              if (event instanceof BotOfflineEvent.Force)
                  reason = "被挤下线";
      
              if (event instanceof BotOfflineEvent.Dropped)
                  reason = "被服务器断开或因网络问题而掉线";
      
              if (event instanceof BotOfflineEvent.RequireReconnect)
                  reason = "服务器主动要求更换另一个服务器";
      
              log.info("******BotOfflineHandler:QQ:{},Reason:{}", id, reason);
      
              repetitiveGroupService.getAll()
                      .parallel()
                      .runOn(Schedulers.parallel())
                      .sequential()
                      .filter(v -> v.getBotIds().contains(id))
                      .map(v -> updateBotIdList(id, v))
                      .subscribe(this::updateOrDeleteRepetitiveGroup);
      
              bot.close();
          }
      
          @NotNull
          private RepetitiveGroup updateBotIdList(long id, RepetitiveGroup repetitiveGroup) {
              List<Long> botIds = repetitiveGroup.getBotIds();
              botIds.remove(id);
              repetitiveGroup.setBotIds(botIds);
              return repetitiveGroup;
          }
      
          private void updateOrDeleteRepetitiveGroup(RepetitiveGroup repetitiveGroup) {
              if (repetitiveGroup.getBotIds().size() < 2) {
                  repetitiveGroupService.delete(repetitiveGroup);
              } else {
                  repetitiveGroupService.upset(repetitiveGroup);
              }
          }
      }
      

            MemberJoinHandler:

      @Component
      @RequiredArgsConstructor
      public class MemberJoinHandler {
          private final RedissonClient redissonClient;
          private final RepetitiveGroupService repetitiveGroupService;
      
          public void handle(@NotNull MemberJoinEvent event) {
              long memberId = event.getMember().getId();
              Group group = event.getGroup();
              if (event.getBot().getId() == memberId) return;
      
              RLock lock = redissonClient.getLock(IdUtil.fastSimpleUUID());
              lock.lock(30, TimeUnit.SECONDS);
      
              List<Long> idList = Bot.getInstances()
                      .parallelStream()
                      .map(Bot::getId)
                      .collect(Collectors.toList());
      
              if (!idList.contains(memberId)) return;
      
              RepetitiveGroup repetitiveGroup = repetitiveGroupService.get(group.getId())
                      .share()
                      .block();
      
              List<Long> botIds = idList.parallelStream()
                      .filter(group::contains)
                      .collect(Collectors.toList());
      
              if (repetitiveGroup.getId() != null && botIds.size() == repetitiveGroup.getBotIds().size()) return;
      
              repetitiveGroup.setId(group.getId());
              repetitiveGroupService.upset(repetitiveGroup);
      
              lock.unlock();
          }
      }
      

            MemberLeaveHandler:

      @Component
      @RequiredArgsConstructor
      public class MemberLeaveHandler {
          private final RedissonClient redissonClient;
          private final RepetitiveGroupService repetitiveGroupService;
      
          public void handle(@NotNull MemberLeaveEvent event) {
              long memberId = event.getMember().getId();
              Group group = event.getGroup();
              if (event.getBot().getId() == memberId) return;
      
              RLock lock = redissonClient.getLock(IdUtil.fastSimpleUUID());
              lock.lock(30, TimeUnit.SECONDS);
      
              List<Long> idList = Bot.getInstances()
                      .parallelStream()
                      .map(Bot::getId)
                      .collect(Collectors.toList());
      
              if (!idList.contains(memberId)) return;
      
              RepetitiveGroup repetitiveGroup = repetitiveGroupService.get(group.getId())
                      .share()
                      .block();
      
              if (repetitiveGroup.getId() == null) return;
      
              List<Long> botIds = idList.parallelStream()
                      .filter(group::contains)
                      .collect(Collectors.toList());
      
              if (botIds.size() > 1) {
                  repetitiveGroup.setId(group.getId());
                  repetitiveGroup.setBotIds(botIds);
                  repetitiveGroupService.upset(repetitiveGroup);
              }
      
              if (botIds.size() == 1)
                  repetitiveGroupService.delete(repetitiveGroup);
      
              lock.unlock();
          }
      }
      

            至此,Bot相关配置到此结束,下面介绍Nacos↓↓↓


      Nacos

      整合Nacos

            这里只做Nacos Client的相关配置,Nacos Server端的请参阅Nacos官方文档
      相关依赖:(注:因为要配合OpenFeign,所以将ribbon替换成了Loadbalancer)

      <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
      </dependency>
      <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
          <exclusions>
              <exclusion>
                  <groupId>org.springframework.cloud</groupId>
                  <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
      

      接入配置中心

            本项目中使用Nacos的主要目的还是其动态下发配置的便利。
      先设置bootstrap.yml,高版本的Spring Boot不支持bootstrap.yml,可以引入以下依赖:

      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-bootstrap</artifactId>
      </dependency>
      

            bootstrap.yml配置如下:

      spring:
        application:
          name: qqrobot
      
        cloud:
          nacos:
            server-addr: localhost:8848
            config:
      #       如果在Nacos Web端没设置命名空间的可以删除
              namespace: 3e4a06ab-3139-46e8-bc82-888154383de0
      #       可以指定配置文件名,也可以不配置,用默认规则
              name: qqrobot-dev.yml
          
        main:
      #    允许Bean覆盖
          allow-bean-definition-overriding: true
      

            使用@EnableDiscoveryClient就可以让模块注册进Nacos了

      设置配置文件

            Nacos Web端设置配置文件的相关操作我这边直接跳过,说说后端这边,
      可以用@Value或@ConfigurationProperties(prefix = "xxx")取值,使用@RefreshScope注解即可开启自动刷新

      @Data
      @Component
      @RefreshScope
      public class DynamicProperty {
      
          @Value("${accounts}")
          private String accounts;
      
      }
      

            这边说几个坑。有几个List写法我试了几次,后端自动装配没成功(从Nacos获取配置)
            list<Bean>

      test:
        list:
          - a:1
            b:1
      
      test:
        list:
          - {a:1,b:1}
      

            List<Map<String,String>>

      test:
        list:
          - a:1
            b:1
      

            所以我建议JSON

      监听Nacos配置刷新事件

            当Nacos刷新本地项目的配置后会被RefreshEventListener(org.springframework.cloud.endpoint.event.RefreshEventListener)监听,通过参考onApplicationEvent()方法我们可以实现一个监听器
            其中robotService是Bot的控制类,示例中我将机器人账号信息存入Nacos,当相关信息刷新后调用refreshBot()更新相关机器人的状态(上线、下线)

      @Configuration
      @RequiredArgsConstructor
      public class RefreshEventListenerConfig {
          private final RobotService robotService;
          private AtomicBoolean ready = new AtomicBoolean(false);
      
          @EventListener
          public void listen(ApplicationEvent event) {
              if (event instanceof ApplicationReadyEvent)
                  this.ready.compareAndSet(false, true);
      
              if (event instanceof RefreshEvent && this.ready.get())
                  robotService.refreshBot();
          }
      }
      

            至此,Nacos相关的适配到此结束


      Sentinel

            接入Sentinel的目的是进行动态流控,当机器人全部离线时,如果有请求打进来,立刻执行兜底方法,并存储请求参数做延迟处理。当机器人至少有一个在线时则开放端口,同时对已存储的请求进行消费。这块功能未来会持续优化。

      整合Sentinel

            我们要引入sentinel组件,sentinel对web框架的适配(这里我选择了webflux)以及配置持久化方式

      <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
      </dependency>
      <dependency>
          <groupId>com.alibaba.csp</groupId>
          <artifactId>sentinel-spring-webflux-adapter</artifactId>
      </dependency>
      <dependency>
          <groupId>com.alibaba.csp</groupId>
          <artifactId>sentinel-datasource-nacos</artifactId>
      </dependency>
      

      配置Sentinel客户端

            在bootstrap.yml中配置Sentinel Dashboard的地址

      spring:
        cloud:
          sentinel:
            transport:
              dashboard: localhost:38081
              port: 38719
      

            这里只做Sentinel客户端的适配,服务端配置请参考Sentinel官方文档

      @Configuration
      @RequiredArgsConstructor
      public class SentinelConfig {
          private final List<ViewResolver> viewResolvers;
          private final ServerCodecConfigurer serverCodecConfigurer;
      
          @Bean
          @Order(-1)
          public SentinelBlockExceptionHandler sentinelBlockExceptionHandler() {
              return new SentinelBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
          }
      
          @Bean
          @Order(-1)
          public SentinelWebFluxFilter sentinelWebFluxFilter() {
              return new SentinelWebFluxFilter();
          }
      }
      

      自定义流控返回

            先写一个请求参数存储类UnhandledHttpRequest

      @Data
      @AllArgsConstructor
      public class UnhandledHttpRequest implements Serializable {
          private static final long serialVersionUID = 1L;
      
          @Id
          private String id;
          private String body;
          private String method;
      
          public UnhandledHttpRequest() {
              id = IdUtil.fastSimpleUUID();
          }
      
          public UnhandledHttpRequest(String body) {
              id = IdUtil.fastSimpleUUID();
              this.body = body;
          }
      }
      

            相关的服务类UnhandledHttpRequestService

      @Service
      public class UnhandledHttpRequestService {
          @Resource
          private ReactiveRedisOperations<String, UnhandledHttpRequest> reactiveRedisOperations;
          public static final String HASH_KEY = RepetitiveGroup.class.getSimpleName();
      
          public Mono<UnhandledHttpRequest> get(String id) {
              return reactiveRedisOperations.opsForHash()
                      .get(HASH_KEY, id)
                      .map(v -> (UnhandledHttpRequest) v)
                      .switchIfEmpty(Mono.defer(() -> Mono.just(new UnhandledHttpRequest())));
          }
      
          public Flux<UnhandledHttpRequest> getAll() {
              return reactiveRedisOperations.opsForHash()
                      .values(HASH_KEY)
                      .map(v -> (UnhandledHttpRequest) v);
          }
      
          public Mono<Boolean> upset(UnhandledHttpRequest unhandledHttpRequest) {
              return reactiveRedisOperations.opsForHash()
                      .put(HASH_KEY, unhandledHttpRequest.getId(), unhandledHttpRequest);
          }
      
          public Mono<Boolean> delete(UnhandledHttpRequest unhandledHttpRequest) {
              return reactiveRedisOperations.opsForHash()
                      .remove(HASH_KEY, unhandledHttpRequest.getId())
                      .map(v -> v.equals(1L));
          }
      }
      

            最后定义一个返回内容处理类,WebFlux这边获取request的body没有Servlet容易,我试了几种方法都失败了,示例代码仅展示获取在URL上的请求参数。

      @Component
      @RequiredArgsConstructor
      public class RobotBlockExceptionHandler implements BlockRequestHandler {
          private final UnhandledHttpRequestService unhandledHttpRequestService;
      
          @SneakyThrows
          @Override
          public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
              URI uri = exchange.getRequest().getURI();
              JSONObject jsonObject = new JSONObject();
              jsonObject.putAll(decodeBody(uri.getRawQuery()));
      
              UnhandledHttpRequest unhandledHttpRequest = new UnhandledHttpRequest(jsonObject.toJSONString());
              if (uri.getPath().contains("/send/msg")) {
                  unhandledHttpRequest.setMethod("/send/msg");
              } else {
                  unhandledHttpRequest.setMethod("/collect/msg");
              }
      
              unhandledHttpRequestService.upset(unhandledHttpRequest);
              jsonObject.clear();
              jsonObject.put("msg", "机器人服务正忙,请稍后重试");
              jsonObject.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
      
              return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                      .contentType(MediaType.APPLICATION_JSON)
                      .bodyValue(jsonObject.toJSONString());
          }
      
          private Map<String, Object> decodeBody(String body) {
              if (StrUtil.isBlank(body)) return new HashMap<>();
      
              if (body.contains("&") && body.contains("="))
                  return Arrays.stream(body.split("&"))
                          .map(s -> s.split("="))
                          .collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
      
              return JSON.parseObject(body)
                      .getInnerMap();
          }
      }
      

      其他类对Sentinel的适配

      逻辑

            我这边仅仅简单实现了,当Bot全部离线时,相关端口全部熔断,当有Bot在线时恢复。这个做法不可实际应用,更好点的做法则是依据request进行的限流。
            BotOfflineHandler

      private final SentinelService sentinelService;
      
      if (Bot.getInstances().isEmpty()) {
          FlowRule rule = new FlowRule("/send/msg");
          rule.setCount(0);
          rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
          rule.setLimitApp("default");
      
          sentinelService.saveOrUpdateFlowRule(rule);
          rule.setResource("/collect/msg");
          sentinelService.saveOrUpdateFlowRule(rule);
      }
      

            BotOnlineHandler

      private final SentinelService sentinelService;
      
      if (Bot.getInstances().isEmpty()) {
          FlowRule rule = new FlowRule("/send/msg");
          rule.setCount(10);
          rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
          rule.setLimitApp("default");
      
          sentinelService.saveOrUpdateFlowRule(rule);
          rule.setResource("/collect/msg");
          sentinelService.saveOrUpdateFlowRule(rule);
      }
      

      配置持久化

            修改bootstrap.yml,这里仅做参考,具体参数详见官网

      spring:
        cloud:
          sentinel:
            transport:
              dashboard: localhost:38081
              port: 38719
            datasource:
              flow-rule:
                nacos:
                  server-addr: localhost:8848
                  username: nacos
                  password: nacos
                  namespace: 3e4a06ab-3139-46e8-bc82-888154383de0
                  data-id: qqrobot-sentinel-flow-dev.json
                  rule-type: flow
      

            至此,Sentinel相关的适配到此结束


      配置参考

      Redis配置参考

      @Configuration
      public class RedisConfig extends CachingConfigurerSupport {
      
          @Bean
          public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory) {
              return new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext.string());
          }
      
          @Bean
          public ReactiveRedisConnection connection(ReactiveRedisConnectionFactory connectionFactory) {
              return connectionFactory.getReactiveConnection();
          }
      
          @Bean
          ReactiveRedisOperations<String, Object> reactiveRedisOperations(ReactiveRedisConnectionFactory factory) {
              return new ReactiveRedisTemplate<>(factory, getObjectRedisSerializationContext());
          }
      
          @Bean
          ReactiveSetOperations<String, Object> reactiveSetOperations(ReactiveRedisConnectionFactory factory) {
              return new ReactiveRedisTemplate<>(factory, getObjectRedisSerializationContext()).opsForSet();
          }
      
          @Bean
          ReactiveZSetOperations<String, Object> reactiveZSetOperations(ReactiveRedisConnectionFactory factory) {
              return new ReactiveRedisTemplate<>(factory, getObjectRedisSerializationContext()).opsForZSet();
          }
      
          private static RedisSerializationContext<String, Object> getObjectRedisSerializationContext() {
              return RedisSerializationContext
                      .<String, Object>newSerializationContext(new GenericFastJsonRedisSerializer())
                      .key(new FastJsonRedisSerializer<>(String.class))
                      .value(new FastJsonRedisSerializer<>(Object.class))
                      .hashKey(new FastJsonRedisSerializer<>(String.class))
                      .hashValue(new FastJsonRedisSerializer<>(Object.class))
                      .build();
          }
      }
      

      Redisson配置参考

      @Configuration
      public class RedissonConfig {
      
          @Bean
          public RedissonClient redissonClient() {
              Config config = new Config();
              config.setLockWatchdogTimeout(10000L);
              SingleServerConfig singleServerConfig = config.useSingleServer();
              singleServerConfig.setPassword("password");
              singleServerConfig.setAddress("redis://localhost:6379");
              singleServerConfig.setDatabase(1);
              return Redisson.create(config);
          }
      }
      
      posted in 使用交流
      wssy001
      wssy001
    • Java + Springboot 2.X+ mirai最新RELEASE の采坑记录

      环境:
      SpringBoot 2.2.11.RELEASE
      Mirai-core-jvm 2.6.0(时刻试水最新release版)
      Open JDK 11

      Spring Cloud版
      开发第一天:
      新建了Spring Boot项目,鉴于前几次mirai的开发,我轻车熟路地加上了这个

      <dependency>
              <groupId>net.mamoe</groupId>
              <artifactId>mirai-core-all</artifactId>
              <version>2.6.0</version>
      </dependency>
      

      当然成功翻车。
      所以说 拥有一个良好的看wiki-->使用 Maven习惯是很重要的!

      下载了最新的依赖后,鉴于是springboot项目,还需要做的一件事是查看mirai依赖的kotlin版本,可以直接去wiki找,也可以自己点开依赖挖

      别忘了在主POM加上Mirai所依赖的kotlin版本

      <properties>
          <kotlin.version>1.4.32</kotlin.version>
      </properties>
      
      posted in 精华主题
      wssy001
      wssy001
    • RE: Java + Springboot 2.X+ mirai最新RELEASE の采坑记录

      开发第三天 (其实是对第二天的代码补充):

      先说说将bot加载至IOC,这个其实很简单,考虑到个别springboot新手,故贴一下代码 (这么好的水贴例子怎么能放过呢?)

      @Bean
      public Bot bot() {
          return BotFactory.INSTANCE.newBot(yourQQNumber, "password", new BotConfiguration() {
              {
                  fileBasedDeviceInfo("deviceInfo.json");
                  setProtocol(MiraiProtocol.ANDROID_PHONE);
              }
          });
      }
      

      如果需要设置事件监听,可以这样:

      @Bean
      public Bot bot() {
          Bot bot = BotFactory.INSTANCE.newBot(yourQQNumber, "password", new BotConfiguration() {
              {
                  fileBasedDeviceInfo("deviceInfo.json");
                  setProtocol(MiraiProtocol.ANDROID_PHONE);
              }
          });
          bot.getEventChannel().registerListenerHost(groupMessageHandler);       
          return bot;
      }
      

      这里我调用了一个handler来处理群消息,详见:wiki-->使用 @EventHandler 注解标注的方法监听事件。

      (P.S. Spring已经不推荐使用@Autowired注入,IDEA也明确给了一个warning,推荐使用构造器的方式注入,配合Lombok神器,构造器注入比前者更简单)

      然后再写了个QQRobotService,完成机器人登录。

      @PostConstruct
      public void init() {
          bot.login();
      }
      
      posted in 精华主题
      wssy001
      wssy001
    • RE: Java + Springboot 2.X+ mirai最新RELEASE の采坑记录

      写在开头:mirai 2.6.2出了,下文mirai版本为:2.6.2,同样的,滑动登录模块也更新了,这里一同贴出
      maven:

      <dependency>
          <groupId>net.mamoe</groupId>
          <artifactId>mirai-core-jvm</artifactId>
          <version>2.6.2</version>
      </dependency>
      <dependency>
          <groupId>net.mamoe</groupId>
          <artifactId>mirai-login-solver-selenium</artifactId>
          <version>1.0-dev-17</version>
      </dependency>
      

      ——————————这是分隔符——————————

      完成了IOC,就要用AOP来完成鉴权了,大概需要几步

      • 1:写一个注解
      • 2:写一个bean
      • 3:写一个切面

      记得先添加Spring AOP的依赖,最好是用AspectJ进行切面,毕竟基于动态代理的Spring AOP局限性太大!!!!
      GAV坐标:

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      

      先写一个注解,我写的注解是可以加在类上和方法上的,用于鉴权。我在其他权限管理项目中可遇到过要求某用户具有A权限,但没有B权限的业务,故有了这俩。未来打算适配Spring Security

      @Target({ElementType.METHOD, ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      public @interface CheckPermission {
      
          String[] roles() default {};
      
          String[] nonRoles() default {};
      
          boolean isGroupOwnerOnly() default false;
      
          boolean isAdminOnly() default false;
      
          String description() default "QQRobot自定义权限校验注解";
      }
      

      然后就是切面

      @Aspect
      @Component
      @RequiredArgsConstructor
      @Slf4j
      public class CheckPermissionAspect {
      
          @Pointcut("@annotation(cyou.wssy001.qqrobot.annotation.CheckPermission)")
          public void annotatedMethods() {
          }
      
          @Pointcut("@within(cyou.wssy001.qqrobot.annotation.CheckPermission)")
          public void annotatedClasses() {
          }
          
      
          @Before("annotatedClasses() || annotatedMethods()")
          public Object checkPermission(JoinPoint point) {
              Object[] args = point.getArgs();
              if (args == null) throw new RuntimeException("无发送者!!!");
              //这个event中可以获取发送者的信息,以便下文鉴权
              GroupMessageEvent event = (GroupMessageEvent) args[1];
      
              MethodSignature signature = (MethodSignature) point.getSignature();
              CheckPermission methodAnnotation = signature.getMethod().getAnnotation(CheckPermission.class);
              Class<?> aClass = point.getSignature().getDeclaringType();
              CheckPermission classAnnotation = aClass.getAnnotation(CheckPermission.class);
              Permission permission = getCheckPermission(methodAnnotation, classAnnotation);
      
              //这里的checkA()方法和checkB()方法是具体的判断逻辑
              if (checkA() && checkB()) {
                  return point.getArgs();
              } else {
                  throw new RuntimeException("无权访问!");
              }
          }
      
          private Permission getCheckPermission(CheckPermission methodAnnotation, CheckPermission classAnnotation) {
              Permission permission = new Permission();
              CheckPermission check = methodAnnotation != null ? methodAnnotation : classAnnotation;
              if (check != null) {
                  permission.getRoles().addAll(Arrays.asList(check.roles()));
                  permission.getNonRoles().addAll(Arrays.asList(check.nonRoles()));
                  permission.setAdminOnly(check.isAdminOnly());
                  permission.setGroupOwnerOnly(check.isGroupOwnerOnly());
              }
              return permission;
          }
      }
      

      我写了个Bean用于存储注解包含的信息

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class Permission {
          //存放所需要的角色
          Set<String> roles;
      
          //存放所不需要的角色
          Set<String> nonRoles;
      
          //是否仅限于群主
          boolean isGroupOwnerOnly;
      
          //是否仅限于管理(这里考虑的是 管理+群主,毕竟群主具有最高权限)
          boolean isAdminOnly;
      }
      

      使用的方法也很简单,举个栗子:

      @Component
      @Slf4j
      @RequiredArgsConstructor
      public class GroupMessageHandler {
          private final AdminMapService adminMapService;
      
          @CheckPermission(isAdminOnly = true)
          public void onMessage(@NotNull GroupMessageEvent event) throws Exception {
              Member sender = event.getSender();
              Group group = event.getSource().getGroup();
      
              StringBuilder reply = new StringBuilder();
                                              ………………这是省略号………………
      

      这样一来 所有调用onMessage(@NotNull GroupMessageEvent event)的方法都会先判断调用者是不是管理员 or 群主。

      最后别忘启用切面

      @EnableAspectJAutoProxy(exposeProxy = true)
      

      第三天内容补完了,后续就要进入实际业务开发了,我会优先展示群文件的相关操作,主要手头上刚好有项工作需要和群文件打交道

      posted in 精华主题
      wssy001
      wssy001
    • RE: 新人求助,怎么在linux上作为服务运行

      建议使用docker,写个Dockerfile就行了

      posted in 使用交流
      wssy001
      wssy001
    • RE: Java + Springboot 2.X+ mirai最新RELEASE の采坑记录

      边写代码 边采坑 获得了解决方案就更新

      posted in 精华主题
      wssy001
      wssy001
    • 非Spring环境使用Mybatis Plus

      理论上Mirai Console也是可以使用的。IService那些由于适配了spring的事务,所以无spring环境用会有点困难。个人建议对于Iservice提供的快捷方法 如:saveOrUpdate 还是通过手写xml实现吧。

      传送门:
      Mybatis Plus Generator(3.5.1以下版本)
      Mybatis Plus Generator(3.5.1及以上版本 ) 目标楼层末尾
      pon.xml

      <dependencies>
      
              <dependency>
                  <groupId>ch.qos.logback</groupId>
                  <artifactId>logback-classic</artifactId>
                  <version>1.2.3</version>
              </dependency>
      
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-api</artifactId>
                  <version>1.7.31</version>
              </dependency>
      
              <dependency>
                  <groupId>com.baomidou</groupId>
                  <artifactId>mybatis-plus</artifactId>
                  <version>3.4.3</version>
              </dependency>
      
              <dependency>
                  <groupId>com.baomidou</groupId>
                  <artifactId>mybatis-plus-extension</artifactId>
                  <version>3.4.3</version>
              </dependency>
      
              <dependency>
                  <groupId>mysql</groupId>
                  <artifactId>mysql-connector-java</artifactId>
                  <version>8.0.23</version>
              </dependency>
      
              <dependency>
                  <groupId>com.zaxxer</groupId>
                  <artifactId>HikariCP</artifactId>
                  <version>4.0.3</version>
              </dependency>
      
              <dependency>
                  <groupId>com.baomidou</groupId>
                  <artifactId>mybatis-plus-generator</artifactId>
                  <version>3.3.0</version>
              </dependency>
              <dependency>
                  <groupId>cn.hutool</groupId>
                  <artifactId>hutool-core</artifactId>
                  <version>5.7.17</version>
              </dependency>
              <dependency>
                  <groupId>org.apache.velocity</groupId>
                  <artifactId>velocity-engine-core</artifactId>
                  <version>2.2</version>
              </dependency>
              <dependency>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
                  <version>1.18.16</version>
                  <scope>compile</scope>
              </dependency>
      </dependencies>
      

      新建一个SqlSessionConfig

      public class SqlSessionConfig {
          public static SqlSession session = init();
      
          private static SqlSession init() {
              SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
              MybatisConfiguration configuration = new MybatisConfiguration();
              initConfiguration(configuration);
              configuration.addInterceptor(initInterceptor());
      
              //扫描DAO/Mapper所在包
              configuration.addMappers("org.test.mybatis.entity.dao");
              //配置日志实现
              configuration.setLogImpl(Slf4jImpl.class);
              //构建mybatis-plus需要的globalconfig
              GlobalConfig globalConfig = new GlobalConfig();
      
              //此参数会自动生成实现baseMapper的基础方法映射
              globalConfig.setSqlInjector(new DefaultSqlInjector());
              //设置id生成器
              globalConfig.setIdentifierGenerator(new DefaultIdentifierGenerator());
              //设置超类mapper
              globalConfig.setSuperMapperClass(BaseMapper.class);
      
      //        这里可以配置逻辑删除
              globalConfig.setDbConfig(initDBConfig());
      
              //给configuration注入GlobalConfig里面的配置
              GlobalConfigUtils.setGlobalConfig(configuration, globalConfig);
              //设置数据源
              Environment environment = new Environment("1", new JdbcTransactionFactory(), initDataSource());
              configuration.setEnvironment(environment);
      
      //        设置xxxDAO.xml所在的目录
      //        registryMapperXml(configuration, "dao");
      
              //构建sqlSessionFactory
              SqlSessionFactory sqlSessionFactory = builder.build(configuration);
              return sqlSessionFactory.openSession(true);
          }
      
          private static void initConfiguration(MybatisConfiguration configuration) {
              configuration.setMapUnderscoreToCamelCase(true);
              configuration.setUseGeneratedKeys(true);
          }
      
          private static DataSource initDataSource() {
              HikariDataSource dataSource = new HikariDataSource();
              dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/back_up?useSSL=false&autoReconnect=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true");
              dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
              dataSource.setUsername("root");
              dataSource.setPassword("123456");
      //        数据源设置,请按照当前DB参数设置
              dataSource.setIdleTimeout(60000);
              dataSource.setAutoCommit(true);
              dataSource.setMaximumPoolSize(5);
              dataSource.setMinimumIdle(1);
              dataSource.setMaxLifetime(60000 * 10);
              dataSource.setConnectionTestQuery("SELECT 1");
              return dataSource;
          }
      
          private static Interceptor initInterceptor() {
              //创建mybatis-plus插件对象
              MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
              //构建分页插件
              PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
              paginationInnerInterceptor.setDbType(DbType.MYSQL);
              paginationInnerInterceptor.setOverflow(true);
              paginationInnerInterceptor.setMaxLimit(500L);
              interceptor.addInnerInterceptor(paginationInnerInterceptor);
              return interceptor;
          }
      
          private static GlobalConfig.DbConfig initDBConfig() {
              GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
      //        逻辑删除字段
              dbConfig.setLogicDeleteField("enable");
              dbConfig.setLogicDeleteValue("0");
              dbConfig.setLogicNotDeleteValue("1");
              return dbConfig;
          }
      
          private static void registryMapperXml(MybatisConfiguration configuration, String classPath) throws IOException {
              ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
              Enumeration<URL> mapper = contextClassLoader.getResources(classPath);
              while (mapper.hasMoreElements()) {
                  URL url = mapper.nextElement();
                  if (url.getProtocol().equals("file")) {
                      String path = url.getPath();
                      File file = new File(path);
                      File[] files = file.listFiles();
                      for (File f : files) {
                          FileInputStream in = new FileInputStream(f);
                          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(in, configuration, f.getPath(), configuration.getSqlFragments());
                          xmlMapperBuilder.parse();
                          in.close();
                      }
                  } else {
                      JarURLConnection urlConnection = (JarURLConnection) url.openConnection();
                      JarFile jarFile = urlConnection.getJarFile();
                      Enumeration<JarEntry> entries = jarFile.entries();
                      while (entries.hasMoreElements()) {
                          JarEntry jarEntry = entries.nextElement();
                          if (jarEntry.getName().endsWith(".xml")) {
                              InputStream in = jarFile.getInputStream(jarEntry);
                              XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(in, configuration, jarEntry.getName(), configuration.getSqlFragments());
                              xmlMapperBuilder.parse();
                              in.close();
                          }
                      }
                  }
              }
          }
      }
      

      用法:

      SqlSession session = SqlSessionConfig.session;
      TImageDao tImageDao = session.getMapper(TImageDao.class);
      List<TImage> tImages = tImageDao.selectList(null);
      log.info("******{}", tImages);
      
      posted in 使用交流
      wssy001
      wssy001
    • RE: Java + Springboot 2.X+ mirai最新RELEASE の采坑记录

      更新至 2.7-M2 版本
      GAV:

      <dependency>
          <groupId>net.mamoe</groupId>
          <artifactId>mirai-core-jvm</artifactId>
          <version>2.7-M2</version>
      </dependency>
      

      之前有dalao说 2.7会解决 mirai-core-all 不过我目前还没看到解决方案,于是继续使用mirai-core-jvm
      这次更新,kotlin的版本需要进行大浮动升级,这里直接给出解决方案:

      <properties>
          <kotlin.version>1.5.10</kotlin.version>
          <kotlin-coroutines.version>1.5.0</kotlin-coroutines.version>
      </properties>
      

      不多说,继续试用新功能

      posted in 精华主题
      wssy001
      wssy001
    • RE: 非Spring环境使用Mybatis Plus

      旧版Mybatis Plus Generator

      // 需要构建一个 代码自动生成器 对象
              AutoGenerator mpg = new AutoGenerator();
      // 配置策略
      // 1、全局配置
              GlobalConfig gc = new GlobalConfig();
              String projectPath = System.getProperty("user.dir");
              gc.setOutputDir(projectPath + "/src/main/java");
              gc.setAuthor("Author");
              gc.setOpen(false);
              // 是否覆盖
              gc.setFileOverride(false);
              // 去Service的I前缀
              gc.setServiceName("%sService");
              gc.setMapperName("%sDao");
              gc.setIdType(IdType.ASSIGN_ID);
              gc.setDateType(DateType.ONLY_DATE);
      //        Swagger 2 API Doc相关注解,Knife4j亦可使用
      //        gc.setSwagger2(true);
              mpg.setGlobalConfig(gc);
      //2、设置数据源
              DataSourceConfig dsc = new DataSourceConfig();
              dsc.setUrl("jdbc:mysql://localhost:3306/back_up?useSSL=false");
              // 驱动包
              dsc.setDriverName("com.mysql.cj.jdbc.Driver");
              dsc.setUsername("root");
              dsc.setPassword("123456");
              // 数据库类型
              dsc.setDbType(DbType.MYSQL);
              mpg.setDataSource(dsc);
      //3、包的配置
              PackageConfig pc = new PackageConfig();
              pc.setModuleName("test");
              pc.setParent("org.test");
              pc.setEntity("entity");
              pc.setMapper("dao");
              pc.setService("service");
              pc.setController("controller");
              mpg.setPackageInfo(pc);
      //4、策略配置
              StrategyConfig strategy = new StrategyConfig();
              // 设置要映射的表名
              strategy.setInclude(
                      "t_image",
                      "t_plain_text"
              );
              strategy.setNaming(NamingStrategy.underline_to_camel);
              strategy.setColumnNaming(NamingStrategy.underline_to_camel);
              // 自动lombok;
              strategy.setEntityLombokModel(true);
              // 逻辑删除
              strategy.setLogicDeleteFieldName("enable");
              // 自动填充配置
              TableFill gmtCreate = new TableFill("create_time", FieldFill.INSERT);
              TableFill gmtModified = new TableFill("update_time",
                      FieldFill.INSERT_UPDATE);
              ArrayList<TableFill> tableFills = new ArrayList<>();
              tableFills.add(gmtCreate);
              tableFills.add(gmtModified);
              strategy.setTableFillList(tableFills);
              // 乐观锁
      //        strategy.setVersionFieldName("version");
              // Restful 风格
              strategy.setRestControllerStyle(true);
              strategy.setControllerMappingHyphenStyle(true);
              mpg.setStrategy(strategy);
              mpg.execute();
      
      posted in 使用交流
      wssy001
      wssy001

    Latest posts made by wssy001

    • RE: 关于Mirai在频道的运行

      暂不支持QQ频道 你需要关注其他框架

      posted in 使用交流
      wssy001
      wssy001
    • RE: 如何以较低的内存占用运行mirai

      @I-love-study 我去年的猜想正确 ,要想mirai运行时降低内存占用就得干掉JVM(Hotspot),方式有两种:
      1:可以换VM编译
      2:移植mirai协议至其他语言
      我采用的是方式1,成功地用GraalVM编译mirai-core项目(用的是OpenJDK 19,Ubuntu 20.04 Docker镜像,mirai-core 2.13.3),机器人登录成功后内存仅占用60MB
      同一套代码打成JAR包方式运行成功后则需要160MB的内存
      但方式2可以做到把内存压到30MB以内,比如:mirai-go

      posted in 使用交流
      wssy001
      wssy001
    • RE: 使用mirai-core不被ide识别

      个人猜测是你的IDE自带的kotlin插件版本过低,请尝试使用最新的IDEA

      posted in BUG反馈
      wssy001
      wssy001
    • RE: 怎么使用mirai-core 登录

      @Aye10032
      我是mirai-core 2.13.2用户
      新版的话 登录问题看论坛的一个帖子,大致步骤是:
      mirai选Android Pad协议
      手机QQ保持登录
      然后mirai登录,触发手机验证码

      posted in 开发交流
      wssy001
      wssy001
    • RE: 上传群文件回执问题

      @1130600015
      我用的是mirai-core-jvm 2.13.2

      import net.mamoe.mirai.event.events.GroupMessageEvent;
      
      bot.getEventChannel()
                          .subscribeAlways(GroupMessageEvent.class, groupMessageHandler::handle);
      

      88bf28d3-c48c-4e68-a40e-44f71258c24d-image.png
      可以点击此处在线查看Mirai提供的事件列表

      posted in 开发交流
      wssy001
      wssy001
    • RE: 上传群文件回执问题

      群文件上传成功会触发GroupMessageEvent,你只需要判断该event中是否包含FileMessage即可。
      个人建议,在你处理GroupMessageEvent代码中打个断点,然后上传一个群文件看看GroupMessageEvent中有哪些信息是你需要的。
      顺带说一下,群文件删除是会触发MessageRecallEvent.GroupRecall事件,但从中无法获取被删除文件的信息,可能需要其他方法或途径实现,还是等待大佬答疑解惑吧。

      posted in 开发交流
      wssy001
      wssy001
    • RE: 如何将内存占用映射到QQ昵称上

      获取机器人实时占用信息,这个调用JDK自带的方法(也可以使用非常棒的第三方库Oshi),对数据进行一些格式化即可,网上还是有不少使用案例,Ctrl + CV即可。
      貌似Mirai还没支持Bot的昵称修改,建议自定义一个关键词、指令,让bot将信息发送至目标QQ或QQ群。也可以使用定时任务让Bot主动推送系统资源占用情况。

      posted in 使用交流
      wssy001
      wssy001
    • Image.isUploaded()失效了?

      项目有个需求,当上传的图片过大时先返回一个“图片正在上传中……”的提示。

      查看文档,发现了这个Image.isUploaded()方法
      照着注释试了几次,发现效果不符预期,返回结果总是false。

      环境:

      OpenJDK 17
      Mac OS Monterey 12.6.1
      mirai-core-jvm 2.13.0-RC2
      SpringBoot Maven 项目
      

      测试操作
      我先是如下操作

      File file = new File(photoPath);
      try (ExternalResource resource = ExternalResource.create(file)) {
          String suffix = FileNameUtil.getSuffix(file);
      
      //  我能确保该方法返回的imageId与Bot.uploadImage()返回的一致
          String imageId = generateImageId(resource.getMd5(), suffix);
          Image image = Image.fromId(imageId);
      
      //  false,即使我能确保QQ服务器中存在该图片
          return Image.isUploaded(image, bot);
      } catch (Exception e) {
          return false;
      }
      

      查看源码,又通过Image.Builder构造Image对象

      File file = new File(photoPath);
      try (
          ExternalResource resource = ExternalResource.create(file)
      ) {
          BufferedImage bufferedImage = ImageIO.read(file);
          String suffix = FileNameUtil.getSuffix(file);
          String imageId = generateImageId(resource.getMd5(), suffix);
          Image.Builder builder = Image.Builder.newBuilder(imageId);
          builder.setType();
          builder.setSize();
          builder.setWidth();
          builder.setHeight();
          
      //  我能确保上述参数与Bot.uploadImage()返回的一致
          Image image = builder.build();
      //  仍然是false,图片的确在服务器上
          return Image.isUploaded(image, bot);
      } catch (Exception e) {
          return null;
      }
      

      不确定是不是BUG。X (

      暂时的替代方法如下:
      通过Image.queryUrl()获取图片的URL,调用http GET请求获取目标图片,也可通过状态码来进行简易判断。

      posted in 开发交流
      wssy001
      wssy001
    • RE: Bot定时主动发送一条消息

      @ConstantineQAQ 无非就是一个定时任务的编写 这里说一个原生方案, 用ScheduledExecutorService啊,JDK自带的,网上有使用方法

      posted in 技术交流板块
      wssy001
      wssy001
    • RE: 想问问如何在 mirai-core 中使用 Myabtis-plus

      关于spring环境的建议,个人看法是:
      认清mirai机器人在项目中的角色。如果你的项目简单、业务围绕机器人展开,不太建议使用spring;但如果mirai在你的项目中仅充当一个用户交互方式(例:消息通知、消息传递等),建议使用spring环境。当然,如果你熟悉spring或springboot,那为啥不用spring或springboot反而去用mirai的依赖环境?

      mybatis-plus使用了spring的AOP、事务等内容,导致它与非spring(或非springboot)项目不能完美兼容。当你使用mybatis-plus作为ORM时,你的项目注定是spring(或springboot)的!

      posted in 开发交流
      wssy001
      wssy001