Initial project
This commit is contained in:
35
src/main/java/com/imyeyu/spring/util/AbstractValidator.java
Normal file
35
src/main/java/com/imyeyu/spring/util/AbstractValidator.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package com.imyeyu.spring.util;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* 数据验证动态消息返回抽象类
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2023-05-07 00:08
|
||||
*/
|
||||
public abstract class AbstractValidator<A extends Annotation, T> implements ConstraintValidator<A, T> {
|
||||
|
||||
/**
|
||||
* 验证处理器,入参验证数据,返回错误消息语言映射,返回 null 时表示通过验证
|
||||
*
|
||||
* @param t 验证数据
|
||||
* @return 验证消息回调
|
||||
*/
|
||||
protected abstract String inspector(T t);
|
||||
|
||||
@Override
|
||||
public boolean isValid(T value, ConstraintValidatorContext context) {
|
||||
String msgKey = inspector(value);
|
||||
if (TimiJava.isNotEmpty(msgKey)) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate(msgKey).addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
103
src/main/java/com/imyeyu/spring/util/GlobalExceptionHandler.java
Normal file
103
src/main/java/com/imyeyu/spring/util/GlobalExceptionHandler.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.imyeyu.spring.util;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.validation.ValidationException;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.java.bean.timi.TimiResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.TypeMismatchException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.converter.HttpMessageConversionException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2023-05-06 16:28
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
private static final String DEV_LANG_CONFIG = "dev.lang";
|
||||
|
||||
@Value("${spring.profiles.active}")
|
||||
private String env;
|
||||
|
||||
/**
|
||||
* @param e
|
||||
* @return
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageConversionException.class)
|
||||
public TimiResponse<?> conversionException(HttpMessageConversionException e) {
|
||||
log.warn(e.getMessage());
|
||||
if (env.contains("dev") || log.isDebugEnabled()) {
|
||||
log.error("conversion error", e);
|
||||
}
|
||||
return new TimiResponse<>(TimiCode.ARG_BAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求异常
|
||||
*
|
||||
* @param e 异常
|
||||
* @return 异常返回
|
||||
*/
|
||||
@ExceptionHandler(ServletException.class)
|
||||
public TimiResponse<?> headerException(ServletException e) {
|
||||
log.warn(e.getMessage());
|
||||
if (env.contains("dev") || log.isDebugEnabled()) {
|
||||
log.error("header error", e);
|
||||
}
|
||||
return new TimiResponse<>(TimiCode.REQUEST_BAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口入参基本校验异常
|
||||
*
|
||||
* @param e 异常
|
||||
* @return 异常返回
|
||||
*/
|
||||
@ExceptionHandler({BindException.class, ValidationException.class, MethodArgumentNotValidException.class, TypeMismatchException.class})
|
||||
public TimiResponse<?> paramsException(Exception e) {
|
||||
if (e instanceof MethodArgumentNotValidException subE) {
|
||||
log.warn("request error", e);
|
||||
FieldError error = subE.getBindingResult().getFieldError();
|
||||
if (error != null) {
|
||||
return new TimiResponse<>(TimiCode.ARG_BAD, error.getDefaultMessage());
|
||||
}
|
||||
}
|
||||
if (env.startsWith("dev") || log.isDebugEnabled()) {
|
||||
log.error("request error", e);
|
||||
}
|
||||
return new TimiResponse<>(TimiCode.REQUEST_BAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局异常
|
||||
*
|
||||
* @param e 异常
|
||||
* @return 异常返回
|
||||
*/
|
||||
@ExceptionHandler(Throwable.class)
|
||||
public TimiResponse<?> error(Throwable e) {
|
||||
if (e instanceof TimiException timiE) {
|
||||
// TODO 400 以下即使是开发环境也不算异常
|
||||
if (env.startsWith("dev") || log.isDebugEnabled()) {
|
||||
log.error(timiE.getMessage(), e);
|
||||
}
|
||||
// 一般异常
|
||||
return timiE.toResponse();
|
||||
}
|
||||
// 致命异常
|
||||
log.error("fatal error", e);
|
||||
return new TimiResponse<>(TimiCode.ERROR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.imyeyu.spring.util;
|
||||
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiResponse;
|
||||
import com.imyeyu.spring.TimiSpring;
|
||||
import com.imyeyu.spring.annotation.AOPLogInterceptor;
|
||||
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 全局返回处理器,包装 TimiResponse
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2023-04-30 00:59
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalReturnHandler.class);
|
||||
|
||||
private CallbackArgReturn<String, String> multilingualHeader;
|
||||
|
||||
@Override
|
||||
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return Objects.requireNonNull(returnType.getMethod()).getAnnotation(IgnoreGlobalReturn.class) == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(
|
||||
Object body,
|
||||
@NonNull MethodParameter returnType,
|
||||
@NonNull MediaType selectedContentType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
@NonNull ServerHttpRequest request,
|
||||
@NonNull ServerHttpResponse response)
|
||||
{
|
||||
TimiResponse<?> result;
|
||||
if (body instanceof TimiResponse<?> timiResponse) {
|
||||
// 可能已被全局异常包装
|
||||
result = timiResponse;
|
||||
} else {
|
||||
result = new TimiResponse<>(TimiCode.SUCCESS, body);
|
||||
}
|
||||
if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) {
|
||||
result.setMsg(multilingualHeader.handler(result.getMsgKey()));
|
||||
} else if (TimiJava.isEmpty(result.getMsg())) {
|
||||
result.setMsg(TimiCode.fromCode(result.getCode()).toString());
|
||||
}
|
||||
if (30000 < result.getCode()) {
|
||||
log.warn("ID: {} Response -> Exception.{}.{}", TimiSpring.getSessionAttr(AOPLogInterceptor.REQUEST_ID), result.getCode(), result.getMsg());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public CallbackArgReturn<String, String> getMultilingualHeader() {
|
||||
return multilingualHeader;
|
||||
}
|
||||
|
||||
public void setMultilingualHeader(CallbackArgReturn<String, String> multilingualHeader) {
|
||||
this.multilingualHeader = multilingualHeader;
|
||||
}
|
||||
}
|
||||
295
src/main/java/com/imyeyu/spring/util/Redis.java
Normal file
295
src/main/java/com/imyeyu/spring/util/Redis.java
Normal file
@@ -0,0 +1,295 @@
|
||||
package com.imyeyu.spring.util;
|
||||
|
||||
import com.imyeyu.spring.config.AbstractRedisConfig;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* RedisTemplate 功能封装,简化 Redis 操作
|
||||
* <p>serializer 为该 RedisTemplate 的键的序列化操作,序列化解析器由 {@link AbstractRedisConfig} 提供
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-11-21 09:58
|
||||
*/
|
||||
public class Redis<K, V> {
|
||||
|
||||
private final RedisSerializer<K> serializer;
|
||||
private final RedisTemplate<K, V> redis;
|
||||
|
||||
public Redis(RedisTemplate<K, V> redis, RedisSerializer<K> serializer) {
|
||||
this.redis = redis;
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Redis 模板对象
|
||||
*
|
||||
* @return Redis 模板对象
|
||||
*/
|
||||
public RedisTemplate<?, ?> getRedis() {
|
||||
return redis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加锁
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
* @param timeoutMS
|
||||
* @return true 为加锁成功
|
||||
*/
|
||||
public boolean lock(K key, V value, long timeoutMS) {
|
||||
Boolean lock = redis.opsForValue().setIfAbsent(key, value, timeoutMS, TimeUnit.MILLISECONDS);
|
||||
return lock != null && lock;
|
||||
}
|
||||
|
||||
public void releaseLock(K key) {
|
||||
destroy(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置存活时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param ms 毫秒 TTL
|
||||
*/
|
||||
public void setExpire(K key, long ms) {
|
||||
redis.expire(key, Duration.ofMillis(ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该数据 TTL
|
||||
*
|
||||
* @param key 键
|
||||
* @return 毫秒 TTL
|
||||
*/
|
||||
public long getExpire(K key) {
|
||||
return Objects.requireNonNullElse(redis.getExpire(key, TimeUnit.MILLISECONDS), -1L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置数据并保持 TTL
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public void setAndKeepTTL(K key, V value) {
|
||||
Long expire = redis.getExpire(key, TimeUnit.MILLISECONDS);
|
||||
if (expire == null || expire <= 0) {
|
||||
// 判死
|
||||
destroy(key);
|
||||
} else {
|
||||
redis.opsForValue().set(key, value, Duration.ofMillis(expire));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置数据
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ms 毫秒 TTL
|
||||
*/
|
||||
public void set(K key, V value, long ms) {
|
||||
redis.opsForValue().set(key, value, Duration.ofMillis(ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public V get(K key) {
|
||||
return redis.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值,强转为 String
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public String getString(K key) {
|
||||
return get(key).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值,强转为 Boolean
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public Boolean is(K key) {
|
||||
return Boolean.parseBoolean(getString(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值,强转为 Boolean 并取反
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public Boolean not(K key) {
|
||||
return !is(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在
|
||||
*
|
||||
* @param key 键
|
||||
* @return true 为存在
|
||||
*/
|
||||
public boolean has(K key) {
|
||||
return get(key) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对列表添加值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public void add(K key, V value) {
|
||||
redis.opsForList().leftPush(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对列表批量添加值
|
||||
*
|
||||
* @param key 键
|
||||
* @param values 值
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final void addAll(K key, V... values) {
|
||||
redis.opsForList().leftPushAll(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取为列表
|
||||
*
|
||||
* @param key 键
|
||||
* @return 列表
|
||||
*/
|
||||
public List<V> getList(K key) {
|
||||
return redis.opsForList().range(key, 0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有数据列表
|
||||
*
|
||||
* @return 所有数据列表
|
||||
*/
|
||||
public Map<K, List<V>> getAllList() {
|
||||
Map<K, List<V>> r = new HashMap<>();
|
||||
List<K> ks = keys("*");
|
||||
for (int i = 0; i < ks.size(); i++) {
|
||||
r.put(ks.get(i), getList(ks.get(i)));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 值为列表时查找是否存在某值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return true 为存在
|
||||
*/
|
||||
public boolean contains(K key, V value) {
|
||||
return getList(key).contains(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有值
|
||||
*
|
||||
* @return 所有值
|
||||
* @throws TimiException 异常
|
||||
*/
|
||||
public List<V> values() {
|
||||
List<V> r = new ArrayList<>();
|
||||
List<K> keys = keys("*");
|
||||
for (K key : keys) {
|
||||
r.add(get(key));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有数据(包括键)
|
||||
*
|
||||
* @return 所有数据(包括键)
|
||||
*/
|
||||
public Map<K, V> map() {
|
||||
Map<K, V> r = new HashMap<>();
|
||||
List<K> ks = keys("*");
|
||||
for (int i = 0; i < ks.size(); i++) {
|
||||
r.put(ks.get(i), get(ks.get(i)));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取符合条件的 key
|
||||
*
|
||||
* @param pattern 表达式
|
||||
* @return keys
|
||||
*/
|
||||
public List<K> keys(String pattern) {
|
||||
List<K> keys = new ArrayList<>();
|
||||
scan(pattern, item -> {
|
||||
if (item != null) {
|
||||
keys.add(serializer.deserialize(item));
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁对象
|
||||
*
|
||||
* @param key 键
|
||||
* @return true 为成功
|
||||
*/
|
||||
public boolean destroy(K key) {
|
||||
if (TimiJava.isNotEmpty(key) && has(key)) {
|
||||
Boolean isSucceed = redis.delete(key);
|
||||
return isSucceed != null && isSucceed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 删库 */
|
||||
public void flushAll() {
|
||||
Objects.requireNonNull(redis.getConnectionFactory()).getConnection().serverCommands().flushAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* scan 实现
|
||||
*
|
||||
* @param pattern 表达式
|
||||
* @param consumer 对迭代到的 key 进行操作
|
||||
*/
|
||||
private void scan(String pattern, Consumer<byte[]> consumer) {
|
||||
redis.execute((RedisConnection connection) -> {
|
||||
try (Cursor<byte[]> cursor = connection.keyCommands().scan(ScanOptions.scanOptions().count(Long.MAX_VALUE).match(pattern).build())) {
|
||||
cursor.forEachRemaining(consumer);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
99
src/main/java/com/imyeyu/spring/util/RedisSerializers.java
Normal file
99
src/main/java/com/imyeyu/spring/util/RedisSerializers.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package com.imyeyu.spring.util;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @version 2023-07-17 16:20
|
||||
*/
|
||||
public class RedisSerializers {
|
||||
|
||||
/** 字符串序列化 */
|
||||
public static final StringRedisSerializer STRING = new StringRedisSerializer();
|
||||
|
||||
/** 长整型序列化 */
|
||||
public static final RedisSerializer<Integer> INTEGER = new RedisSerializer<>() {
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Integer value) throws SerializationException {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] result = new byte[Integer.BYTES];
|
||||
for (int i = Integer.BYTES - 1; 0 <= i; i--) {
|
||||
result[i] = (byte) (value & 0xFF);
|
||||
value >>= Byte.SIZE;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer deserialize(byte[] bytes) throws SerializationException {
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
int result = 0;
|
||||
for (int i = 0; i < Integer.BYTES; i++) {
|
||||
result <<= Byte.SIZE;
|
||||
result |= (bytes[i] & 0xFF);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/** 长整型序列化 */
|
||||
public static final RedisSerializer<Long> LONG = new RedisSerializer<>() {
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Long value) throws SerializationException {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] result = new byte[Long.BYTES];
|
||||
for (int i = Long.BYTES - 1; 0 <= i; i--) {
|
||||
result[i] = (byte) (value & 0xFF);
|
||||
value >>= Byte.SIZE;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long deserialize(byte[] bytes) throws SerializationException {
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
long result = 0;
|
||||
for (int i = 0; i < Long.BYTES; i++) {
|
||||
result <<= Byte.SIZE;
|
||||
result |= (bytes[i] & 0xFF);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/** Gson 序列化 */
|
||||
public static <T> RedisSerializer<T> gsonSerializer(Class<T> clazz) {
|
||||
return new RedisSerializer<>() {
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
@Override
|
||||
public byte[] serialize(T object) throws SerializationException {
|
||||
return GSON.toJson(object).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(byte[] bytes) throws SerializationException {
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), clazz);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
322
src/main/java/com/imyeyu/spring/util/SQLProvider.java
Normal file
322
src/main/java/com/imyeyu/spring/util/SQLProvider.java
Normal file
@@ -0,0 +1,322 @@
|
||||
package com.imyeyu.spring.util;
|
||||
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.spring.annotation.table.AutoUUID;
|
||||
import com.imyeyu.spring.annotation.table.Column;
|
||||
import com.imyeyu.spring.annotation.table.Id;
|
||||
import com.imyeyu.spring.annotation.table.Table;
|
||||
import com.imyeyu.spring.annotation.table.Transient;
|
||||
import com.imyeyu.spring.entity.Creatable;
|
||||
import com.imyeyu.spring.entity.Deletable;
|
||||
import com.imyeyu.spring.entity.Destroyable;
|
||||
import com.imyeyu.spring.entity.Updatable;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import com.imyeyu.utils.Text;
|
||||
import com.imyeyu.utils.Time;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.builder.annotation.ProviderContext;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 通用 Mapper SQL 代理器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-05 23:34
|
||||
*/
|
||||
public class SQLProvider {
|
||||
|
||||
/** 反射缓存 */
|
||||
private static final Map<Class<?>, EntityMeta> ENTITY_META_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 插入
|
||||
* <p><i>不实现 {@link Creatable} 也允许调用是合理的,某些数据属于关联数据,不参与主创建过程</i></p>
|
||||
*
|
||||
* @param entity 实体
|
||||
* @return SQL
|
||||
*/
|
||||
public String insert(ProviderContext context, Object entity) {
|
||||
EntityMeta meta = getEntityMeta(entity.getClass());
|
||||
String columns = meta.fieldColumnList.stream().map(fc -> "`%s`".formatted(fc.columnName)).collect(Collectors.joining(", "));
|
||||
String values = meta.fieldColumnList.stream().map(fc -> {
|
||||
try {
|
||||
if (fc.isAutoUUID && TimiJava.isEmpty(Ref.getFieldValue(entity, fc.field, String.class))) {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
if (fc.isAutoUpperUUID) {
|
||||
uuid = uuid.toUpperCase();
|
||||
}
|
||||
Ref.setFieldValue(entity, fc.field, uuid);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new TimiException(TimiCode.ERROR).msgKey("auto set field:%s value error".formatted(fc.fieldName));
|
||||
}
|
||||
if (entity instanceof Creatable creatableEntity && creatableEntity.getCreatedAt() == null) {
|
||||
creatableEntity.setCreatedAt(Time.now());
|
||||
}
|
||||
return "#{%s}".formatted(fc.fieldName);
|
||||
}).collect(Collectors.joining(", "));
|
||||
return "INSERT INTO `%s` (%s) VALUES (%s)".formatted(meta.table, columns, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 查询
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param id ID
|
||||
* @return SQL
|
||||
*/
|
||||
public String select(ProviderContext context, @Param("id") Object id) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT * FROM `%s` WHERE `%s` = #{%s}".formatted(meta.table, meta.idFieldColumn.columnName, id));
|
||||
if (meta.canDelete) {
|
||||
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
|
||||
}
|
||||
return sql.append(" LIMIT 1").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体非空字段使用等号查询
|
||||
*
|
||||
* @param entity 实体
|
||||
* @return SQL
|
||||
*/
|
||||
public String selectByExample(Object entity) {
|
||||
return selectAllByExample(entity) + BaseMapper.LIMIT_1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体非空字段使用等号查询
|
||||
*
|
||||
* @param entity 实体
|
||||
* @return SQL
|
||||
*/
|
||||
public String selectAllByExample(Object entity) {
|
||||
EntityMeta meta = getEntityMeta(entity.getClass());
|
||||
String conditionClause = meta.fieldColumnList.stream()
|
||||
.filter(fc -> {
|
||||
try {
|
||||
return Ref.getFieldValue(entity, fc.field, Object.class) != null;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.map(fc -> {
|
||||
return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName);
|
||||
})
|
||||
.collect(Collectors.joining(" AND "));
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT * FROM `%s` WHERE %s".formatted(meta.table, conditionClause));
|
||||
if (meta.canDelete) {
|
||||
if (TimiJava.isNotEmpty(conditionClause)) {
|
||||
sql.append(" AND ");
|
||||
}
|
||||
sql.append("(`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
|
||||
}
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 更新,需要实体实现 {@link Updatable}
|
||||
*
|
||||
* @param entity 实体
|
||||
* @return SQL
|
||||
*/
|
||||
public String update(Object entity) {
|
||||
EntityMeta meta = getEntityMeta(entity.getClass());
|
||||
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
|
||||
TimiException.required(meta.canUpdate, "not allow update for %s".formatted(meta.entityClass));
|
||||
|
||||
String setClause = meta.fieldColumnList.stream()
|
||||
.filter(fc -> !fc.isId)
|
||||
.map(fc -> {
|
||||
if (entity instanceof Updatable updatableEntity) {
|
||||
updatableEntity.setUpdatedAt(Time.now());
|
||||
}
|
||||
return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName);
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
return "UPDATE `%s` SET `%s` WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 软删除,需要实体实现 {@link Deletable}
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param id ID
|
||||
* @return SQL
|
||||
*/
|
||||
public String delete(ProviderContext context, @Param("id") Object id) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
|
||||
TimiException.requiredTrue(meta.canDelete, "not allow soft delete for %s".formatted(meta.entityClass));
|
||||
|
||||
return "UPDATE `%s` SET `deleted_at` = %s WHERE `%s` = #{id}".formatted(meta.table, Time.now(), meta.idFieldColumn.columnName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬删除,需要实体实现 {@link Destroyable}
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param id ID
|
||||
* @return SQL
|
||||
*/
|
||||
public String destroy(ProviderContext context, @Param("id") Object id) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
|
||||
TimiException.requiredTrue(meta.canDestroy, "not allow destroy for %s".formatted(meta.entityClass));
|
||||
|
||||
return "DELETE FROM `%s` WHERE `%s` = #{id}".formatted(meta.table, meta.idFieldColumn.columnName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代理器上下文获取 Mapper 实体类元数据
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @return 实体类元数据
|
||||
*/
|
||||
private EntityMeta getEntityMeta(ProviderContext context) {
|
||||
Type[] types = context.getMapperType().getGenericInterfaces();
|
||||
ParameterizedType type = (ParameterizedType) types[0];
|
||||
Class<?> entityClass = (Class<?>) type.getActualTypeArguments()[0];
|
||||
return getEntityMeta(entityClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体类元数据
|
||||
*
|
||||
* @param entityClass 实体类
|
||||
* @return 元数据
|
||||
*/
|
||||
private EntityMeta getEntityMeta(Class<?> entityClass) {
|
||||
return ENTITY_META_CACHE.computeIfAbsent(entityClass, EntityMeta::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体元数据
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-05 23:47
|
||||
*/
|
||||
private static class EntityMeta {
|
||||
|
||||
/** 实体类 */
|
||||
final Class<?> entityClass;
|
||||
|
||||
/** 表名 */
|
||||
final String table;
|
||||
|
||||
/** ID 字段 */
|
||||
final FieldColumn idFieldColumn;
|
||||
|
||||
/** 只读的列名字段名映射,Map<列名,字段名> */
|
||||
final List<FieldColumn> fieldColumnList;
|
||||
|
||||
/** true 为可更新 */
|
||||
final boolean canUpdate;
|
||||
|
||||
/** true 为可删除(软删除) */
|
||||
final boolean canDelete;
|
||||
|
||||
/** true 为可销毁(硬删除) */
|
||||
final boolean canDestroy;
|
||||
|
||||
public EntityMeta(Class<?> entityClass) {
|
||||
this.entityClass = entityClass;
|
||||
|
||||
// 表名
|
||||
while (entityClass.isAnnotationPresent(Transient.class)) {
|
||||
entityClass = entityClass.getSuperclass();
|
||||
}
|
||||
Table table = entityClass.getAnnotation(Table.class);
|
||||
if (table == null) {
|
||||
this.table = Text.camelCase2underscore(entityClass.getSimpleName());
|
||||
} else {
|
||||
this.table = table.value();
|
||||
TimiException.required(this.table, String.format("empty table annotation value for %s entity", entityClass.getName()));
|
||||
}
|
||||
List<Field> allFieldList = Ref.listAllFields(entityClass);
|
||||
|
||||
FieldColumn idFieldColumn = null;
|
||||
List<FieldColumn> fieldColumnList = new ArrayList<>();
|
||||
for (int i = 0; i < allFieldList.size(); i++) {
|
||||
Field field = allFieldList.get(i);
|
||||
if (field.isAnnotationPresent(Transient.class)) {
|
||||
continue;
|
||||
}
|
||||
FieldColumn fieldColumn = new FieldColumn(field);
|
||||
if (fieldColumn.isId) {
|
||||
TimiException.requiredNull(idFieldColumn, String.format("multi id field for %s entity", entityClass.getName()));
|
||||
idFieldColumn = fieldColumn;
|
||||
}
|
||||
fieldColumnList.add(fieldColumn);
|
||||
}
|
||||
this.idFieldColumn = idFieldColumn;
|
||||
this.fieldColumnList = List.of(fieldColumnList.toArray(new FieldColumn[0])); // 转为只读
|
||||
canUpdate = Updatable.class.isAssignableFrom(entityClass);
|
||||
canDelete = Deletable.class.isAssignableFrom(entityClass);
|
||||
canDestroy = Destroyable.class.isAssignableFrom(entityClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体字段属性
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-07 09:54
|
||||
*/
|
||||
private static class FieldColumn {
|
||||
|
||||
/** 字段 */
|
||||
final Field field;
|
||||
|
||||
/** 字段名 */
|
||||
final String fieldName;
|
||||
|
||||
/** 列名 */
|
||||
final String columnName;
|
||||
|
||||
/** true 为 ID */
|
||||
final boolean isId;
|
||||
|
||||
/** true 为自动生成 UUID */
|
||||
final boolean isAutoUUID;
|
||||
|
||||
final boolean isAutoUpperUUID;
|
||||
|
||||
public FieldColumn(Field field) {
|
||||
this.field = field;
|
||||
|
||||
fieldName = field.getName();
|
||||
Column column = field.getAnnotation(Column.class);
|
||||
if (column == null) {
|
||||
columnName = Text.camelCase2underscore(field.getName());
|
||||
} else {
|
||||
columnName = column.value();
|
||||
TimiException.required(columnName, "empty field:%s column annotation value for %s entity".formatted(field.getName(), field.getDeclaringClass()));
|
||||
}
|
||||
isId = field.isAnnotationPresent(Id.class);
|
||||
isAutoUUID = field.isAnnotationPresent(AutoUUID.class);
|
||||
if (isAutoUUID) {
|
||||
isAutoUpperUUID = field.getAnnotation(AutoUUID.class).upper();
|
||||
} else {
|
||||
isAutoUpperUUID = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/main/java/com/imyeyu/spring/util/package-info.java
Normal file
2
src/main/java/com/imyeyu/spring/util/package-info.java
Normal file
@@ -0,0 +1,2 @@
|
||||
/** 其他工具 */
|
||||
package com.imyeyu.spring.util;
|
||||
Reference in New Issue
Block a user