First commit

This commit is contained in:
2025-10-14 15:12:24 +08:00
commit 4bf21639c1
370 changed files with 93952 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
package link.at17.mid.tushare.component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import link.at17.mid.tushare.annotation.BatchInsert;
import link.at17.mid.tushare.annotation.BatchList;
import lombok.RequiredArgsConstructor;
import lombok.Value;
/**
* 批量插入切片器
*/
@Aspect
@Component
@RequiredArgsConstructor
public class BatchInsertAspect {
private final SqlSessionFactory sqlSessionFactory;
private final ConcurrentMap<Method, Meta> cache = new ConcurrentHashMap<>();
@Around("@annotation(batchInsert)")
public Object around(ProceedingJoinPoint pjp, BatchInsert batchInsert) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Object[] args = pjp.getArgs();
// 定位批量参数索引
int listIdx = findBatchListIndex(method);
List<?> list = (List<?>) args[listIdx];
if (list == null || list.isEmpty()) {
return pjp.proceed(); // 无数据,原样调用一次
}
// 计算并缓存批大小
Meta meta = cache.computeIfAbsent(method, m -> computeMeta(m, args, listIdx, batchInsert.maxParams()));
int batchSize = Math.max(1, meta.batchSize);
// 分批执行
if (method.getReturnType() == Void.TYPE) {
for (int i = 0; i < list.size(); i += batchSize) {
Object[] cloned = args.clone();
cloned[listIdx] = list.subList(i, Math.min(i + batchSize, list.size()));
pjp.proceed(cloned);
}
return null;
}
if (method.getReturnType() == int.class || method.getReturnType() == Integer.class) {
int total = 0;
for (int i = 0; i < list.size(); i += batchSize) {
Object[] cloned = args.clone();
cloned[listIdx] = list.subList(i, Math.min(i + batchSize, list.size()));
total += (int) pjp.proceed(cloned);
}
return total;
}
throw new IllegalStateException("不支持的返回类型:" + method.getReturnType());
}
private int findBatchListIndex(Method method) {
Annotation[][] pa = method.getParameterAnnotations();
for (int i = 0; i < pa.length; i++) {
for (Annotation a : pa[i]) {
if (a.annotationType() == BatchList.class)
return i;
}
}
throw new IllegalArgumentException(method + " 缺少 @BatchList 标注的集合参数");
}
private Meta computeMeta(Method method, Object[] args, int listIdx, int maxParams) {
String statementId = method.getDeclaringClass().getName() + "." + method.getName();
try (SqlSession session = sqlSessionFactory.openSession()) {
MappedStatement ms = session.getConfiguration().getMappedStatement(statementId);
ParamNameResolver resolver = new ParamNameResolver(ms.getConfiguration(), method);
// 用真实参数计算 base/perRow一次放1条一次放2条两者相减即为单条参数数量
Object[] a1 = args.clone();
Object sample = ((List<?>) args[listIdx]).get(0);
a1[listIdx] = Collections.singletonList(sample);
Object p1 = resolver.getNamedParams(a1);
BoundSql bs1 = ms.getBoundSql(p1);
int s1 = bs1.getParameterMappings().size();
Object[] a2 = args.clone();
a2[listIdx] = Arrays.asList(sample, sample);
Object p2 = resolver.getNamedParams(a2);
BoundSql bs2 = ms.getBoundSql(p2);
int s2 = bs2.getParameterMappings().size();
int perRow = s2 - s1;
if (perRow <= 0)
throw new IllegalStateException("无法识别每行占位符数量");
int base = s1 - perRow;
int batch = Math.max(1, (maxParams - base) / perRow);
return new Meta(perRow, base, batch);
}
}
@Value
static class Meta {
int perRow;
int base;
int batchSize;
}
}

View File

@@ -0,0 +1,74 @@
package link.at17.mid.tushare.component;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import link.at17.mid.tushare.annotation.ConfigInfo;
import link.at17.mid.tushare.interfaces.IConfig;
import lombok.extern.slf4j.Slf4j;
/**
* 实现自动化注册 Config
*/
@Slf4j
@DependsOn("configService")
public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AssignableTypeFilter(IConfig.class));
scanner.addIncludeFilter(new AnnotationTypeFilter(ConfigInfo.class));
scanner.findCandidateComponents("link.at17.mid.tushare.system.config").forEach(beanDefinition -> {
String className = beanDefinition.getBeanClassName();
try {
// 确保其 field 规则与 configService 内 field 生成规则一致,即:
// 如果 @ConfigInfo 指定了 field 的,使用该 field + "Config"
// 作为 beanName否则使用首字母小写的 simpleClassName 作为
// beanName且 simpleClassName 无论如何必须以 Config 作为结尾。
Class<?> clazz = Class.forName(className);
String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase()
+ clazz.getSimpleName().substring(1);
if (!IConfig.class.isAssignableFrom(clazz)) {
log.warn("Config {} does not implement IConfig, ignore", beanName);
return;
}
if (!beanName.endsWith("Config")) {
log.warn("Config {}'s simple class name does not end with \"Config\", ignore", beanName);
return;
}
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
if (info == null) {
log.warn("Config {} does not have @ConfigInfo annotation, ignore", clazz.getName());
return;
}
if (StringUtils.isNotBlank(info.field())) {
beanName = info.field() + "Config";
}
BeanDefinitionBuilder factoryBean = BeanDefinitionBuilder
.genericBeanDefinition(ConfigServiceFactoryBean.class)
.addConstructorArgValue(clazz);
// 注意此处注册 factoryBean 不意味着 FactoryBean.getObject() 方法立即被执行,
// Spring 管理的 Bean 默认在其被使用时才创建,所以如果 getObject() 调用一些方
// 法,这些方法会在初次使用 Bean 时才被创建。如果这些方法对于启动过程很重要,
// 需要在对应 Config(Bean) 上加上 @Bean 和 @Lazy(false) 注解,确保一旦准备好
// 相应的 Bean 就会被创建。
registry.registerBeanDefinition(beanName, factoryBean.getBeanDefinition());
log.info("Add config {} to bean register", beanName);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load class: " + className, e);
}
});
}
}

View File

@@ -0,0 +1,72 @@
package link.at17.mid.tushare.component;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import link.at17.mid.tushare.config.ConstructionGuard;
import link.at17.mid.tushare.interfaces.IConfig;
import link.at17.mid.tushare.service.ConfigService;
import lombok.extern.slf4j.Slf4j;
/**
* 实现配置项自动载入
* @param <T>
*/
@Slf4j
public class ConfigServiceFactoryBean<T extends IConfig<T>> implements FactoryBean<T>, BeanNameAware {
private final Class<T> targetClass;
private String beanName;
@Autowired
private AutowireCapableBeanFactory beanFactory;
@Autowired
private ConfigService configService;
public ConfigServiceFactoryBean(Class<T> targetClass) {
this.targetClass = targetClass;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
@Override
public T getObject() throws Exception {
ConstructionGuard.enter(targetClass);
boolean success = true;
try {
T bean = configService.getConfig(targetClass);
beanFactory.autowireBean(bean);
beanFactory.initializeBean(bean, beanName);
configService.saveOrUpdate(bean);
return bean;
}
catch (Exception e) {
log.error("Fail to load config: " + targetClass.getName(), e);
success = false;
throw e;
}
finally {
ConstructionGuard.exit(targetClass);
if (success) {
log.debug("getObject() for {} success", targetClass.toString());
}
}
}
@Override
public Class<?> getObjectType() {
return targetClass;
}
@Override
public boolean isSingleton() {
return true;
}
}

View File

@@ -0,0 +1,159 @@
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 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 {
injectNecessary(request, handler);
request.getSession(true);
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;
}
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());
}
}
}