This commit is contained in:
2025-10-27 23:30:05 +08:00
parent 4bf21639c1
commit 6327874166
197 changed files with 5866 additions and 1580 deletions

View File

@@ -0,0 +1,150 @@
package link.at17.mid.tushare.component;
import java.net.Proxy;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.function.Consumer;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import link.at17.mid.tushare.system.config.SystemConfig;
import link.at17.mid.tushare.system.util.SpringContextHolder;
import okhttp3.OkHttpClient;
/**
* OkHttpClient 提供器
* @see link.at17.mid.tushare.system.config.SystemConfig
* @see okhttp3.internal.http.BridgeInterceptor
*/
public class OkHttpClientProvider {
private static volatile SystemConfig systemConfig;
private static SystemConfig getSystemConfig() {
if (systemConfig == null) {
synchronized (OkHttpClientProvider.class) {
if (systemConfig == null) {
systemConfig = SpringContextHolder.getBean(SystemConfig.class);
}
}
}
return systemConfig;
}
/**
* 根据 SystemConfig 获取一个 OkHttpClient 实例
* @return
*/
public static OkHttpClient getInstance() {
return getInstance(null);
}
/**
* 根据 SystemConfig 获取一个 OkHttpClient 实例
* @param builderConsumer 可根据该 consumer 自定义 builder 其他参数,注意 proxy、https 校验等最终仍会根据 systemConfig 情况覆盖
* @return
*/
public static OkHttpClient getInstance(Consumer<OkHttpClient.Builder> builderConsumer) {
SystemConfig systemConfig = getSystemConfig();
return getInstance(
systemConfig.getProxy(),
systemConfig.getIgnoreHttpsVerification(),
builderConsumer);
}
/**
* 根据指定代理和是否忽略 https 证书获取一个 OkHttpClient 实例
* @param proxy 指定代理
* @param ignoreHttpsVerification 是否忽略 https 证书
* @return
*/
public static OkHttpClient getInstance(Proxy proxy, boolean ignoreHttpsVerification) {
return getInstance(proxy, ignoreHttpsVerification, null);
}
/**
* 根据指定代理、是否忽略 https 证书和额外 builder 设置获取一个 OkHttpClient 实例
* @param proxy 指定代理
* @param ignoreHttpsVerification 是否忽略 https 证书
* @param builderConsumer 可根据该 consumer 自定义 builder 其他参数,注意 proxy、https 校验等最终仍会根据其他参数覆盖
* @return
*/
public static OkHttpClient getInstance(
Proxy proxy,
boolean ignoreHttpsVerification,
Consumer<OkHttpClient.Builder> builderConsumer) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (builderConsumer != null) {
builderConsumer.accept(builder);
}
builder.proxy(proxy);
if (ignoreHttpsVerification) {
builder
.sslSocketFactory(getSSLSocketFactory(), getX509TrustManager())
.hostnameVerifier(getHostnameVerifier());
}
return builder.build();
}
private static HostnameVerifier getHostnameVerifier() {
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
};
return hostnameVerifier;
}
private static SSLSocketFactory getSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, getTrustManager(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static X509TrustManager getX509TrustManager() {
X509TrustManager trustManager = null;
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
trustManager = (X509TrustManager) trustManagers[0];
} catch (Exception e) {
e.printStackTrace();
}
return trustManager;
}
private static TrustManager[] getTrustManager() {
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
};
return trustAllCerts;
}
}

View File

@@ -3,7 +3,6 @@ package link.at17.mid.tushare.component;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -13,15 +12,6 @@ import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import link.at17.mid.tushare.annotation.StaticAttribute;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
@@ -34,17 +24,6 @@ public class PlatformInterceptor implements HandlerInterceptor {
@Autowired
Reflections reflections;
/**
* 静态注入的类缓存
*/
Map<String, Class<?>> staticAttributeClassCache;
/**
* 静态注入的枚举字段缓存
*/
Map<String, Map<String, Enum<?>>> options = null;
Map<String, String> optionNameMap = new HashMap<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
@@ -75,85 +54,6 @@ public class PlatformInterceptor implements HandlerInterceptor {
}
request.setAttribute("request", request); // 把 request 本身放到 attribute 里去,供前端部分位置用
// 查找添加了 @StaticAttribute 注解的类,将其中的枚举注入前端供使用
if (staticAttributeClassCache == null) {
log.debug("init static attribute classes.");
staticAttributeClassCache = new HashMap<>();
Set<Class<?>> staticClasses = reflections.getTypesAnnotatedWith(StaticAttribute.class);
for (Class<?> staticClass : staticClasses) {
StaticAttribute sa = staticClass.getAnnotation(StaticAttribute.class);
String name = staticClass.getSimpleName();
if (StringUtils.isNotBlank(sa.value())) {
name = sa.value();
}
if (staticAttributeClassCache.containsKey(name)) {
// 缓存中已经存在了这个名字,直接忽略
log.warn("StaticAttribute annotation name {} for class {} has been taken, ignore.",
name, staticClass.getName());
}
staticAttributeClassCache.put(name, staticClass);
log.debug("{} injected as name {}", staticClass, name);
}
}
else if (staticAttributeClassCache.isEmpty()) {
// 跑完一次后,缓存仍然为空,可能不正常,提示一下
log.warn("StaticAttributes' staticAttributeClassCache is empty, it's unusual, if you didn't exclude it manually, please check.");
}
Iterator<Map.Entry<String, Class<?>>> iterator = staticAttributeClassCache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Class<?>> entry = iterator.next();
request.setAttribute(entry.getKey(), entry.getValue());
}
if (options == null) {
options = new HashMap<>();
Set<Field> enumOptionFields = reflections.getFieldsAnnotatedWith(StaticAttribute.class);
for (Field f : enumOptionFields) {
if (!f.isAnnotationPresent(StaticAttribute.class)) continue;
if (!Enum.class.isAssignableFrom(f.getType())) continue;
// 拿到注解和名前缀
StaticAttribute anno = f.getAnnotation(StaticAttribute.class);
String prefix = anno.value().isBlank() ?
f.getName() + "Enum" :
anno.value();
prefix = prefix.toUpperCase().charAt(0) + prefix.substring(1);
if (optionNameMap.containsKey(prefix)) {
log.warn("EnumOption name {}:{} has already been taken by {}, please check",
prefix, f.getType().getName(), optionNameMap.get(prefix));
continue;
}
optionNameMap.put(prefix, f.getType().getName());
// enum 值列表
@SuppressWarnings("unchecked")
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) f.getType();
Enum<?>[] constants = enumType.getEnumConstants();
// 构造 Map<name, constant>
Map<String, Enum<?>> optionsMap = new LinkedHashMap<>();
for (Enum<?> c : constants) {
optionsMap.put(c.name(), c);
}
// 放到 Model 里
options.put(prefix, optionsMap);
}
}
for (Entry<String, Map<String, Enum<?>>> entry : options.entrySet()) {
request.setAttribute(entry.getKey(), entry.getValue());
log.debug("Inject enums {}: {} to request {}",
entry.getKey(), optionNameMap.get(entry.getKey()),
request.getRequestURI());
}
}
}

View File

@@ -0,0 +1,155 @@
package link.at17.mid.tushare.component;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import link.at17.mid.tushare.annotation.StaticAttribute;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* 注入拦截器
*/
@Component
@Slf4j
public class StaticAttributeInterceptor implements HandlerInterceptor {
@Autowired
Reflections reflections;
/**
* 静态注入的类缓存
*/
Map<String, Class<?>> staticAttributeClassCache;
/**
* 静态注入的枚举字段缓存
*/
Map<String, Map<String, Enum<?>>> options = null;
Map<String, String> optionNameMap = new HashMap<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
injectNecessary(request, handler);
return true;
}
/**
* 为前端模板注入变量
* @param request
*/
public void injectNecessary(HttpServletRequest request, Object handler) {
// 只在 ControllerHandlerMethod里才做注入
if (!(handler instanceof HandlerMethod)) {
// 静态资源、图片、css、js 都会被 ResourceHttpRequestHandler 处理,
// 这里一律跳过
return;
}
// 排除 @ResponseBody/json 接口
HandlerMethod hm = (HandlerMethod) handler;
if (hm.hasMethodAnnotation(ResponseBody.class)
|| hm.getBeanType().isAnnotationPresent(RestController.class)) {
return;
}
// 查找添加了 @StaticAttribute 注解的类,将其中的枚举注入前端供使用
if (staticAttributeClassCache == null) {
log.debug("init static attribute classes.");
staticAttributeClassCache = new HashMap<>();
Set<Class<?>> staticClasses = reflections.getTypesAnnotatedWith(StaticAttribute.class);
for (Class<?> staticClass : staticClasses) {
StaticAttribute sa = staticClass.getAnnotation(StaticAttribute.class);
String name = staticClass.getSimpleName();
if (StringUtils.isNotBlank(sa.value())) {
name = sa.value();
}
if (staticAttributeClassCache.containsKey(name)) {
// 缓存中已经存在了这个名字,直接忽略
log.warn("StaticAttribute 注解的类 {} 指定的名称 {} 已经存在, 旧值会被覆盖",
staticClass.getName(),
name);
}
staticAttributeClassCache.put(name, staticClass);
log.debug("{} 类以 {} 为名称注入", staticClass, name);
}
}
else if (staticAttributeClassCache.isEmpty()) {
// 跑完一次后,缓存仍然为空,可能不正常,提示一下
log.warn("初始化后StaticAttribute 缓存为空,这仅在无任何类被 @StaticAttribute 注解的情况下出现。如果这与您的期望不符,请检查");
}
Iterator<Map.Entry<String, Class<?>>> iterator = staticAttributeClassCache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Class<?>> entry = iterator.next();
request.setAttribute(entry.getKey(), entry.getValue());
}
if (options == null) {
options = new HashMap<>();
Set<Field> enumOptionFields = reflections.getFieldsAnnotatedWith(StaticAttribute.class);
for (Field f : enumOptionFields) {
if (!f.isAnnotationPresent(StaticAttribute.class)) continue;
if (!Enum.class.isAssignableFrom(f.getType())) continue;
// 拿到注解和名前缀
StaticAttribute anno = f.getAnnotation(StaticAttribute.class);
String prefix = anno.value().isBlank() ?
f.getName() + "Enum" :
anno.value();
prefix = prefix.toUpperCase().charAt(0) + prefix.substring(1);
if (optionNameMap.containsKey(prefix)) {
log.warn("EnumOption name {}:{} has already been taken by {}, please check",
prefix, f.getType().getName(), optionNameMap.get(prefix));
continue;
}
optionNameMap.put(prefix, f.getType().getName());
// enum 值列表
@SuppressWarnings("unchecked")
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) f.getType();
Enum<?>[] constants = enumType.getEnumConstants();
// 构造 Map<name, constant>
Map<String, Enum<?>> optionsMap = new LinkedHashMap<>();
for (Enum<?> c : constants) {
optionsMap.put(c.name(), c);
}
// 放到 Model 里
options.put(prefix, optionsMap);
}
}
for (Entry<String, Map<String, Enum<?>>> entry : options.entrySet()) {
request.setAttribute(entry.getKey(), entry.getValue());
log.debug("Inject enums {}: {} to request {}",
entry.getKey(), optionNameMap.get(entry.getKey()),
request.getRequestURI());
}
}
}

View File

@@ -0,0 +1,107 @@
package link.at17.mid.tushare.component;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import link.at17.mid.tushare.annotation.StaticAttribute;
import link.at17.mid.tushare.data.models.UpdateMethodInfo;
import link.at17.mid.tushare.service.UpdateMethodService;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* 更新注入拦截器
*/
@Component
@Slf4j
public class UpdateMethodInterceptor implements HandlerInterceptor {
@Autowired
Reflections reflections;
Map<String, Class<?>> optionArgCache;
@Autowired
UpdateMethodService updateDataService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
injectNecessary(request, handler);
return true;
}
/**
* 为前端模板注入变量
* @param request
*/
public void injectNecessary(HttpServletRequest request, Object handler) {
// 只在 ControllerHandlerMethod里才做注入
if (!(handler instanceof HandlerMethod)) {
// 静态资源、图片、css、js 都会被 ResourceHttpRequestHandler 处理,
// 这里一律跳过
return;
}
// 排除 @ResponseBody/json 接口
HandlerMethod hm = (HandlerMethod) handler;
if (hm.hasMethodAnnotation(ResponseBody.class)
|| hm.getBeanType().isAnnotationPresent(RestController.class)) {
return;
}
if (optionArgCache == null) {
optionArgCache = new HashMap<>();
List<UpdateMethodInfo> potentialUpdateMethods = updateDataService.getPotentialUpdateMethodInfos();
for (UpdateMethodInfo info : potentialUpdateMethods) {
for (UpdateMethodInfo.UpdateParamInfo paramInfo : info.getParams()) {
Class<?> typeClass = paramInfo.getTypeClass();
if (typeClass == null) continue;
if (typeClass.isEnum()) {
StaticAttribute sa = typeClass.getAnnotation(StaticAttribute.class);
String simpleNmae = typeClass.getSimpleName();
if (sa != null) {
log.info("UpdateMethod {} 的参数 {}, 类型 {} 已被 @StaticAttribute 注解,不再重复注入",
info.getMethodName(), paramInfo.getName(), simpleNmae);
continue;
}
if (optionArgCache.containsKey(simpleNmae)) {
log.warn("UpdateMethod {} 参数 {}, 类型 {} 指定的名称 {} 已经存在, 旧值会被覆盖",
typeClass,
simpleNmae);
}
optionArgCache.put(simpleNmae, typeClass);
log.debug("{} 类以 {} 为名称注入", typeClass, simpleNmae);
}
}
}
}
else if (optionArgCache.isEmpty()) {
// 跑完一次后,缓存仍然为空,可能不正常,提示一下
log.warn("初始化后Option(s)Arg(s)' 缓存为空,这仅在无任何方法被 UpdateMethod 注解,或 UpdateMethod 不存在枚举类参数的情况下出现。如果这与您的期望不符,请检查");
}
Iterator<Map.Entry<String, Class<?>>> iterator = optionArgCache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Class<?>> entry = iterator.next();
request.setAttribute(entry.getKey(), entry.getValue());
}
}
}

View File

@@ -0,0 +1,96 @@
package link.at17.mid.tushare.component;
import java.util.Collection;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import link.at17.mid.tushare.data.models.UpdatePlan;
import link.at17.mid.tushare.service.UpdatePlanService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 更新计划同步切片器<p>
* 主要用于拦截通过 updatePlanService 下的所有 save/update/remove 方法
* <p>注意:
* <ol>
* <li>仅限第一个参数是 {@code UpdatePlan}, {@code Collection<UpdatePlan>} 或 {@code Wrapper<UpdatePlan>} 的方法
* <li>由于切片是对 UpdatePlanService 切片, 但切片器内部又调用了 UpdatePlanService,
* 如果调用的方法恰好是被切片的方法, 则会进入调用递归导致栈溢出, 需要额外注意.
*/
@Aspect
@Component
@Slf4j
@Validated
@RequiredArgsConstructor
public class UpdatePlanSyncAspect {
private final UpdatePlanService updatePlanService;
// 拦截所有 UpdatePlanService 的 save/update/remove 方法
@Pointcut("(" +
"execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.save*(..)) || " +
"execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.update*(..)) || " +
"execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.remove*(..)) || " +
"execution(* com.baomidou.mybatisplus.extension.service.IService.save*(..)) || " +
"execution(* com.baomidou.mybatisplus.extension.service.IService.update*(..)) || " +
"execution(* com.baomidou.mybatisplus.extension.service.IService.remove*(..))" +
") && " +
"target(link.at17.mid.tushare.service.UpdatePlanService)")
public void taskOps() {}
@AfterReturning(pointcut = "taskOps()", returning = "result")
public void afterTaskOp(JoinPoint jp, Object result) {
if (!(result instanceof Boolean bool) || !bool) {
// 未成功执行,不进行切片
return;
}
Object arg = jp.getArgs()[0];
if (arg instanceof UpdatePlan task) {
handle(task, jp.getSignature().getName());
}
else if (arg instanceof Collection<?> coll) {
coll.stream()
.filter(UpdatePlan.class::isInstance)
.map(UpdatePlan.class::cast)
.forEach(t -> handle(t, jp.getSignature().getName()));
}
else if (arg instanceof Wrapper<?> wrapper) {
@SuppressWarnings("unchecked")
List<UpdatePlan> plans = updatePlanService.list((Wrapper<UpdatePlan>)wrapper);
plans.stream()
.filter(UpdatePlan.class::isInstance)
.map(UpdatePlan.class::cast)
.forEach(t -> handle(t, jp.getSignature().getName()));
}
}
private void handle(UpdatePlan task, String methodName) {
// 但凡存在的都应该 reschedule
try {
if (methodName.startsWith("remove")) {
updatePlanService.deleteTask(task.getId());
log.info("已同步删除 Quartz 任务: [{}]{}", task.getId(), task.getName());
}
else {
updatePlanService.rescheduleTask(task);
log.info("已同步更新 Quartz 任务: [{}]{}", task.getId(), task.getName());
}
}
catch (Exception e) {
log.error("同步 Quartz 调度失败: [{}]{}", task.getId(), task.getName(), e);
}
}
}