First commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
// 只在 Controller(HandlerMethod)里才做注入
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user