[Change] Issue #12 调整框架启动机器人应用的方式, 增加一个用于机器人应用内部共享数据的类;

[Fix] BotEventHandler, ImageCacheStore 增加ShutdownHook用于关闭线程池, 解决线程池阻塞关闭过程的问题;
[Change] BotEventHandler 调整'match(String)'方法;
[Change] BotAdminCommandProcess 调整'savePushList()'方法对文件创建失败的行为;
This commit is contained in:
LamGC 2020-06-04 11:19:18 +08:00
parent 69da2b02ac
commit 8b8ef7e744
11 changed files with 163 additions and 105 deletions

View File

@ -12,6 +12,7 @@ import net.lamgc.cgj.bot.framework.mirai.MiraiMain;
import net.lamgc.cgj.pixiv.PixivDownload;
import net.lamgc.cgj.pixiv.PixivSearchBuilder;
import net.lamgc.cgj.pixiv.PixivURL;
import net.lamgc.cgj.util.PropertiesUtils;
import net.lamgc.plps.PixivLoginProxyServer;
import net.lamgc.utils.base.ArgumentsProperties;
import net.lamgc.utils.base.runner.Argument;
@ -54,11 +55,13 @@ public class Main {
log.trace("ContentGrabbingJi 正在启动...");
log.debug("Args: {}, LogsPath: {}", Arrays.toString(args), System.getProperty("cgj.logsPath"));
log.debug("运行目录: {}", System.getProperty("user.dir"));
ApplicationBoot.initialApplication(args);
log.debug("botDataDir: {}", System.getProperty("cgj.botDataDir"));
ArgumentsProperties argsProp = new ArgumentsProperties(args);
if(!getSettingToSysProp(argsProp, "proxy", null)) {
getEnvSettingToSysProp("CGJ_PROXY", "proxy", null);
if(!PropertiesUtils.getSettingToSysProp(argsProp, "proxy", null)) {
PropertiesUtils.getEnvSettingToSysProp("CGJ_PROXY", "proxy", null);
}
String proxyAddress = System.getProperty("cgj.proxy");
@ -70,20 +73,7 @@ public class Main {
proxy = null;
}
if(!storeDir.exists() && !storeDir.mkdirs()) {
log.error("创建文件夹失败!");
}
if(!getSettingToSysProp(argsProp, "botDataDir", "./") &&
!getEnvSettingToSysProp("CGJ_BOT_DATA_DIR", "botDataDir", "./")) {
log.warn("未设置botDataDir, 当前运行目录将作为酷Q机器人所在目录.");
}
if(!getSettingToSysProp(argsProp, "redisAddress", "127.0.0.1") &&
!getEnvSettingToSysProp("CGJ_REDIS_URI", "redisAddress", "127.0.0.1")) {
log.warn("未设置RedisAddress, 将使用默认值连接Redis服务器(127.0.0.1:6379)");
}
File cookieStoreFile = new File(System.getProperty("cgj.botDataDir"), "cookies.store");
File cookieStoreFile = new File(BotGlobal.getGlobal().getDataStoreDir(), "cookies.store");
if(!cookieStoreFile.exists()) {
log.warn("未找到cookies.store文件, 是否启动PixivLoginProxyServer? (yes/no)");
try(Scanner scanner = new Scanner(System.in)) {
@ -104,37 +94,7 @@ public class Main {
log.debug("传入参数: {}", Arrays.toString(args));
ArgumentsRunner.run(Main.class, args);
}
/**
* 从ArgumentsProperties获取设置项到System Properties
* @param prop ArgumentsProperties对象
* @param key 设置项key
* @param defaultValue 默认值
* @return 如果成功从ArgumentsProperties获得设置项, 返回true, 如未找到(使用了defaultValue或null), 返回false;
*/
private static boolean getSettingToSysProp(ArgumentsProperties prop, String key, String defaultValue) {
if(prop.containsKey(key)) {
log.info("{}: {}", key, prop.getValue(key));
System.setProperty("cgj." + key, prop.getValue(key));
return true;
} else {
if(defaultValue != null) {
System.setProperty("cgj." + key, defaultValue);
}
return false;
}
}
private static boolean getEnvSettingToSysProp(String envKey, String sysPropKey, String defaultValue) {
String env = System.getenv(envKey);
if(env != null) {
System.setProperty("cgj." + sysPropKey, env);
return true;
} else if(defaultValue != null) {
System.setProperty("cgj." + sysPropKey, defaultValue);
}
return false;
System.exit(0);
}
@Command
@ -146,10 +106,7 @@ public class Main {
@Command
public static void pluginMode(@Argument(name = "args", force = false) String argsStr) {
if(!System.getProperty("cgj.botDataDir").endsWith("\\") && !System.getProperty("cgj.botDataDir").endsWith("/")) {
System.setProperty("cgj.botDataDir", System.getProperty("cgj.botDataDir") + "/");
}
log.info("酷Q机器人根目录: {}", System.getProperty("cgj.botDataDir"));
log.info("酷Q机器人根目录: {}", BotGlobal.getGlobal().getDataStoreDir().getPath());
CQConfig.init();
Pattern pattern = Pattern.compile("/\\s*(\".+?\"|[^:\\s])+((\\s*:\\s*(\".+?\"|[^\\s])+)|)|(\".+?\"|[^\"\\s])+");
Matcher matcher = pattern.matcher(Strings.nullToEmpty(argsStr));
@ -165,7 +122,7 @@ public class Main {
@Command
public static void collectionDownload() throws IOException {
PixivDownload pixivDownload = new PixivDownload(Objects.requireNonNull(cookieStore), proxy);
File outputFile = new File(storeDir, "collection.zip");
File outputFile = new File(getStoreDir(), "collection.zip");
if(!outputFile.exists() && !outputFile.createNewFile()) {
log.error("文件创建失败: " + outputFile.getAbsolutePath());
}
@ -192,10 +149,10 @@ public class Main {
PixivDownload pixivDownload = new PixivDownload(Objects.requireNonNull(cookieStore), proxy);
String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
int id = 1;
File outputFile = new File(storeDir, "recommends-" + date + "-" + id + ".zip");
File outputFile = new File(getStoreDir(), "recommends-" + date + "-" + id + ".zip");
while(outputFile.exists()) {
id++;
outputFile = new File(storeDir, "recommends-" + date + "-" + id + ".zip");
outputFile = new File(getStoreDir(), "recommends-" + date + "-" + id + ".zip");
}
if(!outputFile.createNewFile()) {
@ -259,10 +216,10 @@ public class Main {
}
int id = 1;
File outputFile = new File(storeDir, "ranking" + rankingMode.modeParam + "-" + date + "-" + id + ".zip");
File outputFile = new File(getStoreDir(), "ranking" + rankingMode.modeParam + "-" + date + "-" + id + ".zip");
while(outputFile.exists()) {
id++;
outputFile = new File(storeDir, "ranking" + rankingMode.modeParam + "-" + date + "-" + id + ".zip");
outputFile = new File(getStoreDir(), "ranking" + rankingMode.modeParam + "-" + date + "-" + id + ".zip");
}
if(!outputFile.createNewFile()) {
@ -401,7 +358,7 @@ public class Main {
private static void saveCookieStoreToFile() throws IOException {
log.info("正在保存CookieStore...");
File outputFile = new File(System.getProperty("cgj.botDataDir"), "cookies.store");
File outputFile = new File(BotGlobal.getGlobal().getDataStoreDir(), "cookies.store");
if(!outputFile.exists() && !outputFile.createNewFile()){
log.error("保存CookieStore失败.");
return;
@ -450,4 +407,11 @@ public class Main {
}
}
private static File getStoreDir() {
if(!storeDir.exists() && !storeDir.mkdirs()) {
log.error("创建文件夹失败!");
}
return storeDir;
}
}

View File

@ -5,6 +5,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import net.lamgc.cgj.bot.boot.BotGlobal;
import net.lamgc.cgj.bot.message.MessageSenderBuilder;
import net.lamgc.cgj.bot.message.MessageSource;
import net.lamgc.cgj.pixiv.PixivDownload;
@ -22,7 +23,7 @@ public class BotAdminCommandProcess {
private final static Logger log = LoggerFactory.getLogger(BotAdminCommandProcess.class.getName());
private final static File pushListFile = new File(System.getProperty("cgj.botDataDir"), "pushList.json");
private final static File pushListFile = new File(BotGlobal.getGlobal().getDataStoreDir(), "pushList.json");
private final static Hashtable<Long, JsonObject> pushInfoMap = new Hashtable<>();
@ -182,8 +183,8 @@ public class BotAdminCommandProcess {
@Command
public static String savePushList() {
try {
if(!pushListFile.exists()) {
pushListFile.createNewFile();
if(!pushListFile.exists() && !pushListFile.createNewFile()) {
throw new IOException("文件夹创建失败!(Path: " + pushListFile.getPath() + ")");
}
} catch (IOException e) {
log.error("PushList.json文件创建失败", e);

View File

@ -5,8 +5,8 @@ import com.google.common.base.Throwables;
import com.google.gson.*;
import io.netty.handler.codec.http.HttpHeaderNames;
import net.lamgc.cgj.Main;
import net.lamgc.cgj.bot.boot.BotGlobal;
import net.lamgc.cgj.bot.cache.*;
import net.lamgc.cgj.bot.event.BotEventHandler;
import net.lamgc.cgj.bot.event.BufferMessageEvent;
import net.lamgc.cgj.bot.sort.PreLoadDataComparator;
import net.lamgc.cgj.pixiv.PixivDownload;
@ -41,7 +41,7 @@ public class BotCommandProcess {
private final static Logger log = LoggerFactory.getLogger(BotCommandProcess.class.getName());
private final static File imageStoreDir = new File(System.getProperty("cgj.botDataDir"), "data/image/cgj/");
private final static File imageStoreDir = new File(BotGlobal.getGlobal().getDataStoreDir(), "data/image/cgj/");
private final static Gson gson = new GsonBuilder()
.serializeNulls()
.create();
@ -54,38 +54,38 @@ public class BotCommandProcess {
* 作品信息缓存 - 不过期
*/
private final static CacheStore<JsonElement> illustInfoCache =
new JsonRedisCacheStore(BotEventHandler.redisServer, "illustInfo", gson);
new JsonRedisCacheStore(BotGlobal.getGlobal().getRedisServer(), "illustInfo", gson);
/**
* 作品信息预加载数据 - 有效期 2 小时, 本地缓存有效期1 ± 0.25
*/
private final static CacheStore<JsonElement> illustPreLoadDataCache =
CacheStoreUtils.hashLocalHotDataStore(
new JsonRedisCacheStore(BotEventHandler.redisServer, "illustPreLoadData", gson),
new JsonRedisCacheStore(BotGlobal.getGlobal().getRedisServer(), "illustPreLoadData", gson),
3600000, 900000);
/**
* 搜索内容缓存, 有效期 2 小时
*/
private final static CacheStore<JsonElement> searchBodyCache =
new JsonRedisCacheStore(BotEventHandler.redisServer, "searchBody", gson);
new JsonRedisCacheStore(BotGlobal.getGlobal().getRedisServer(), "searchBody", gson);
/**
* 排行榜缓存, 不过期
*/
private final static CacheStore<List<JsonObject>> rankingCache =
new JsonObjectRedisListCacheStore(BotEventHandler.redisServer, "ranking", gson);
new JsonObjectRedisListCacheStore(BotGlobal.getGlobal().getRedisServer(), "ranking", gson);
/**
* 作品页面下载链接缓存 - 不过期
*/
private final static CacheStore<List<String>> pagesCache =
new StringListRedisCacheStore(BotEventHandler.redisServer, "imagePages");
new StringListRedisCacheStore(BotGlobal.getGlobal().getRedisServer(), "imagePages");
/**
* 作品报告存储 - 不过期
*/
public final static CacheStore<JsonElement> reportStore =
new JsonRedisCacheStore(BotEventHandler.redisServer, "report", gson);
new JsonRedisCacheStore(BotGlobal.getGlobal().getRedisServer(), "report", gson);
private final static RankingUpdateTimer updateTimer = new RankingUpdateTimer();
@ -462,8 +462,6 @@ public class BotCommandProcess {
PixivURL.getPixivRefererLink(illustId)
);
//pageCount
String imageMsg = getImageById(fromGroup, illustId, PixivDownload.PageQuality.REGULAR, 1);
if (isNoSafe(illustId, SettingProperties.getProperties(fromGroup), true)) {
log.warn("作品Id {} 为R-18作品, 跳过.", illustId);
@ -663,7 +661,7 @@ public class BotCommandProcess {
illustPreLoadDataCache.clear();
pagesCache.clear();
searchBodyCache.clear();
File imageStoreDir = new File(System.getProperty("cgj.botDataDir") + "data/image/cgj/");
File imageStoreDir = new File(BotGlobal.getGlobal().getDataStoreDir(), "data/image/cgj/");
File[] listFiles = imageStoreDir.listFiles();
if (listFiles == null) {
log.debug("图片缓存目录为空或内部文件获取失败!");

View File

@ -1,6 +1,7 @@
package net.lamgc.cgj.bot;
import com.google.common.base.Throwables;
import net.lamgc.cgj.bot.boot.BotGlobal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -157,7 +158,7 @@ public final class SettingProperties {
* @return 返回目录File对象.
*/
private static File getPropertiesDir() {
File propDir = new File(System.getProperty("cgj.botDataDir"), "/setting/");
File propDir = new File(BotGlobal.getGlobal().getDataStoreDir(), "/setting/");
if(!propDir.exists() && !propDir.mkdirs()) {
log.warn("Setting文件夹创建失败!");
}

View File

@ -0,0 +1,43 @@
package net.lamgc.cgj.bot.boot;
import net.lamgc.cgj.bot.event.BotEventHandler;
import net.lamgc.cgj.util.PropertiesUtils;
import net.lamgc.utils.base.ArgumentsProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ApplicationBoot {
private final static Logger log = LoggerFactory.getLogger(ApplicationBoot.class);
private ApplicationBoot() {}
/**
* 初始化应用.
* <p>该方法不会初始化机器人, 仅初始化应用所需的配置信息.</p>
*/
public static void initialApplication(String[] args) {
ArgumentsProperties argsProp = new ArgumentsProperties(args);
if(!PropertiesUtils.getSettingToSysProp(argsProp, "botDataDir", "./") &&
!PropertiesUtils.getEnvSettingToSysProp("CGJ_BOT_DATA_DIR", "botDataDir", "./")) {
log.warn("未设置botDataDir, 当前运行目录将作为酷Q机器人所在目录.");
}
if(!PropertiesUtils.getSettingToSysProp(argsProp, "redisAddress", "127.0.0.1") &&
!PropertiesUtils.getEnvSettingToSysProp("CGJ_REDIS_URI", "redisAddress", "127.0.0.1")) {
log.warn("未设置RedisAddress, 将使用默认值连接Redis服务器(127.0.0.1:6379)");
}
// 初始化 BotGlobal
//noinspection ResultOfMethodCallIgnored 这里仅仅是加载BotGlobal而已
BotGlobal.getGlobal();
}
/**
* 初始化机器人.
* <p>本方法由框架调用.</p>
*/
public static void initialBot() {
BotEventHandler.initial();
}
}

View File

@ -0,0 +1,56 @@
package net.lamgc.cgj.bot.boot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.JedisPool;
import java.io.File;
import java.net.URI;
public final class BotGlobal {
private final static BotGlobal instance = new BotGlobal();
public static BotGlobal getGlobal() {
if(instance == null) {
throw new IllegalStateException("");
}
return instance;
}
private final static Logger log = LoggerFactory.getLogger(BotGlobal.class);
private final URI redisUri;
/**
* 所有缓存共用的JedisPool
*/
private final JedisPool redisServer;
private final File dataStoreDir;
private BotGlobal() {
this.redisUri = URI.create("redis://" + System.getProperty("cgj.redisAddress"));
this.redisServer = new JedisPool(
getRedisUri().getHost(),
getRedisUri().getPort() == -1 ? 6379 : getRedisUri().getPort());
String dataStoreDirPath = System.getProperty("cgj.botDataDir");
this.dataStoreDir = new File((!dataStoreDirPath.endsWith("/") || !dataStoreDirPath.endsWith("\\")) ?
dataStoreDirPath + System.getProperty("file.separator") : dataStoreDirPath);
}
public URI getRedisUri() {
return redisUri;
}
public File getDataStoreDir() {
if(!dataStoreDir.exists() && !dataStoreDir.mkdirs()) {
log.error("DataStoreDir 创建失败, 数据存储可能失效!");
}
return dataStoreDir;
}
public JedisPool getRedisServer() {
return redisServer;
}
}

View File

@ -33,6 +33,12 @@ public final class ImageCacheStore {
private final static ImageCacheHandler handler = new ImageCacheHandler();
static {
Thread shutdownThread = new Thread(imageCacheExecutor::shutdownNow);
shutdownThread.setName("Thread-ImageCacheShutdown");
Runtime.getRuntime().addShutdownHook(shutdownThread);
}
private ImageCacheStore() {}
/**

View File

@ -20,10 +20,8 @@ import net.lamgc.utils.event.EventObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.JedisPool;
import java.lang.reflect.Method;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
@ -44,12 +42,6 @@ public class BotEventHandler implements EventHandler {
private final static Map<Long, AtomicBoolean> muteStateMap = new Hashtable<>();
/**
* 所有缓存共用的JedisPool
*/
private final static URI redisServerUri = URI.create("redis://" + System.getProperty("cgj.redisAddress"));
public final static JedisPool redisServer = new JedisPool(redisServerUri.getHost(), redisServerUri.getPort() == -1 ? 6379 : redisServerUri.getPort());
/**
* 消息事件执行器
*/
@ -66,10 +58,10 @@ public class BotEventHandler implements EventHandler {
));
private static boolean initialled = false;
static {
initial();
}
/**
* 初始化BotEventHandler
*/
public synchronized static void initial() {
if(initialled) {
Logger logger = LoggerFactory.getLogger("BotEventHandler@<init>");
@ -92,25 +84,19 @@ public class BotEventHandler implements EventHandler {
});
try {
executor.addHandler(new BotEventHandler());
Thread shutdownThread = new Thread(() -> executor.shutdown(true));
shutdownThread.setName("Thread-EventHandlerShutdown");
Runtime.getRuntime().addShutdownHook(shutdownThread);
} catch (IllegalAccessException e) {
LoggerFactory.getLogger("BotEventHandler@Static").error("添加Handler时发生异常", e);
}
initialled = true;
}
private final static AtomicBoolean preLoaded = new AtomicBoolean();
/**
* 预加载
*/
public synchronized static void preLoad() {
if(preLoaded.get()) {
return;
}
try {
BotAdminCommandProcess.loadPushList();
} finally {
preLoaded.set(true);
} catch(Throwable e) {
log.error("加载推送列表失败", e);
}
initialled = true;
}
private BotEventHandler() {
@ -159,7 +145,7 @@ public class BotEventHandler implements EventHandler {
public void processMessage(MessageEvent event) {
String msg = event.getMessage();
log.debug(event.toString());
if(!match(msg)) {
if(mismatch(msg)) {
return;
} else if(isMute(event.getFromGroup())) {
log.debug("机器人已被禁言, 忽略请求.");
@ -253,8 +239,8 @@ public class BotEventHandler implements EventHandler {
* @param message 要检查的消息
* @return 如果为true则提交
*/
public static boolean match(String message) {
return message.startsWith(COMMAND_PREFIX) || message.startsWith(ADMIN_COMMAND_PREFIX);
public static boolean mismatch(String message) {
return !message.startsWith(COMMAND_PREFIX) && !message.startsWith(ADMIN_COMMAND_PREFIX);
}
private static boolean isMute(long groupId) {

View File

@ -1,5 +1,6 @@
package net.lamgc.cgj.bot.framework.coolq;
import net.lamgc.cgj.bot.boot.ApplicationBoot;
import net.lamgc.cgj.bot.event.BotEventHandler;
import net.lamgc.cgj.bot.framework.coolq.message.SpringCQMessageEvent;
import net.lamgc.utils.event.EventHandler;
@ -18,7 +19,7 @@ public class CQPluginMain extends CQPlugin implements EventHandler {
public CQPluginMain() {
// TODO(LamGC, 2020.04.21): SpringCQ无法适配MessageSenderBuilder
// MessageSenderBuilder.setCurrentMessageSenderFactory(new SpringCQMessageSenderFactory());
BotEventHandler.preLoad();
ApplicationBoot.initialBot();
LoggerFactory.getLogger(CQPluginMain.class.getName())
.info("BotEventHandler.COMMAND_PREFIX = {}", BotEventHandler.COMMAND_PREFIX);
}
@ -45,7 +46,7 @@ public class CQPluginMain extends CQPlugin implements EventHandler {
* @return 是否拦截消息
*/
private static int processMessage(CoolQ cq, CQMessageEvent event) {
if(!BotEventHandler.match(event.getMessage())) {
if(BotEventHandler.mismatch(event.getMessage())) {
return MESSAGE_IGNORE;
}
BotEventHandler.executeMessageEvent(new SpringCQMessageEvent(cq, event));

View File

@ -1,5 +1,7 @@
package net.lamgc.cgj.bot.framework.mirai;
import net.lamgc.cgj.bot.boot.ApplicationBoot;
import net.lamgc.cgj.bot.boot.BotGlobal;
import net.lamgc.cgj.bot.event.BotEventHandler;
import net.lamgc.cgj.bot.framework.mirai.message.MiraiMessageEvent;
import net.lamgc.cgj.bot.message.MessageSenderBuilder;
@ -35,7 +37,7 @@ public class MiraiMain implements Closeable {
return;
}
File botPropFile = new File(System.getProperty("cgj.botDataDir"), "./bot.properties");
File botPropFile = new File(BotGlobal.getGlobal().getDataStoreDir(), "./bot.properties");
try (Reader reader = new BufferedReader(new FileReader(botPropFile))) {
botProperties.load(reader);
} catch (IOException e) {
@ -55,7 +57,7 @@ public class MiraiMain implements Closeable {
event -> BotEventHandler.setMuteState(event.getGroup().getId(), false));
bot.login();
MessageSenderBuilder.setCurrentMessageSenderFactory(new MiraiMessageSenderFactory(bot));
BotEventHandler.preLoad();
ApplicationBoot.initialBot();
bot.join();
}

View File

@ -2,11 +2,11 @@ package net.lamgc.cgj.bot.framework.mirai.message;
import com.google.common.base.Strings;
import net.lamgc.cgj.bot.BotCode;
import net.lamgc.cgj.bot.boot.BotGlobal;
import net.lamgc.cgj.bot.cache.CacheStore;
import net.lamgc.cgj.bot.cache.HotDataCacheStore;
import net.lamgc.cgj.bot.cache.LocalHashCacheStore;
import net.lamgc.cgj.bot.cache.StringRedisCacheStore;
import net.lamgc.cgj.bot.event.BotEventHandler;
import net.lamgc.cgj.bot.message.MessageSender;
import net.lamgc.cgj.bot.message.MessageSource;
import net.mamoe.mirai.Bot;
@ -28,7 +28,7 @@ public class MiraiMessageSender implements MessageSender {
private final MessageSource source;
private final static Logger log = LoggerFactory.getLogger(MiraiMessageSender.class.getName());
private final static CacheStore<String> imageIdCache = new HotDataCacheStore<>(
new StringRedisCacheStore(BotEventHandler.redisServer, "mirai.imageId"),
new StringRedisCacheStore(BotGlobal.getGlobal().getRedisServer(), "mirai.imageId"),
new LocalHashCacheStore<>(),
5400000, 1800000, true);