Initial project
This commit is contained in:
360
src/main/java/com/imyeyu/spring/TimiSpring.java
Normal file
360
src/main/java/com/imyeyu/spring/TimiSpring.java
Normal file
@ -0,0 +1,360 @@
|
||||
package com.imyeyu.spring;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.Language;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.java.bean.timi.TimiResponse;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* TimiSpring
|
||||
*
|
||||
* <p>如果使用本依赖相关组件,务必让 SpringBoot 扫描本依赖的包,在 SpringApplication 上加上注解
|
||||
* <pre>
|
||||
* @ComponentScan({"自己扫描的包", "com.imyeyu.spring"})
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-11-20 17:16
|
||||
*/
|
||||
public class TimiSpring {
|
||||
|
||||
/** 版本号 */
|
||||
public static final String VERSION = "0.0.1";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TimiSpring.class);
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
/**
|
||||
* 回调数据
|
||||
*
|
||||
* @param response 返回
|
||||
* @param resp 返回结果
|
||||
*/
|
||||
public static void render(HttpServletResponse response, TimiResponse<?> resp) {
|
||||
try {
|
||||
HttpSession session = getSession();
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
OutputStream out = response.getOutputStream();
|
||||
out.write(GSON.toJson(resp).getBytes(StandardCharsets.UTF_8));
|
||||
out.flush();
|
||||
out.close();
|
||||
} catch (Exception e) {
|
||||
log.error("TimiSpring.render Error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调错误
|
||||
*
|
||||
* @param response 返回
|
||||
* @param code 代码
|
||||
* @param msgKey 消息映射键
|
||||
*/
|
||||
public static void renderError(HttpServletResponse response, TimiCode code, String msgKey) {
|
||||
try {
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
OutputStream out = response.getOutputStream();
|
||||
out.write(GSON.toJson(code.toResponse().msg(msgKey)).getBytes(StandardCharsets.UTF_8));
|
||||
out.flush();
|
||||
out.close();
|
||||
} catch (Exception e) {
|
||||
log.error("TimiSpring.renderError error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Servlet 请求属性
|
||||
*
|
||||
* @return Servlet 请求属性
|
||||
* @throws TimiException 请求异常
|
||||
*/
|
||||
public static ServletRequestAttributes getServletRequestAttributes() {
|
||||
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes sra) {
|
||||
return sra;
|
||||
}
|
||||
throw new TimiException(TimiCode.ERROR_NPE_VARIABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HttpServlet 请求
|
||||
*
|
||||
* @return HttpServlet 请求
|
||||
*/
|
||||
public static HttpServletRequest getRequest() {
|
||||
return getServletRequestAttributes().getRequest();
|
||||
}
|
||||
|
||||
public static String getDomain() {
|
||||
return getRequest().getServerName();
|
||||
}
|
||||
|
||||
public static String getFullDomain() {
|
||||
HttpServletRequest req = getRequest();
|
||||
String port = req.getServerPort() == 80 || req.getServerPort() == 443 ? "" : ":" + req.getServerPort();
|
||||
return "%s://%s%s".formatted(req.getScheme(), getDomain(), port);
|
||||
}
|
||||
|
||||
public static String getURL() {
|
||||
return getRequest().getRequestURL().toString();
|
||||
}
|
||||
|
||||
public static String getURI() {
|
||||
return getRequest().getRequestURI();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HttpServlet 回调
|
||||
*
|
||||
* @return HttpServlet 回调
|
||||
*/
|
||||
public static HttpServletResponse getResponse() {
|
||||
return getServletRequestAttributes().getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Http 会话
|
||||
*
|
||||
* @return Http 会话
|
||||
*/
|
||||
public static HttpSession getSession() {
|
||||
return getRequest().getSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求地理区域
|
||||
*
|
||||
* @return 地区
|
||||
*/
|
||||
public static Locale getLocale() {
|
||||
return getRequest().getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头属性
|
||||
*
|
||||
* @param key 属性键
|
||||
* @return 属性值
|
||||
*/
|
||||
public static String getHeader(String key) {
|
||||
return getRequest().getHeader(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话数据
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public static Object getSessionAttr(String key) {
|
||||
return getSession().getAttribute(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话数据
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public static boolean hasSessionAttr(String key) {
|
||||
return getSessionAttr(key) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话数据(字符串)
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public static String getSessionAttrAsString(String key) {
|
||||
Object sessionAttr = getSessionAttr(key);
|
||||
if (sessionAttr == null) {
|
||||
return null;
|
||||
}
|
||||
return sessionAttr.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话数据
|
||||
*
|
||||
* @param key 键
|
||||
* @param clazz 值类型
|
||||
* @param <T> 值类型
|
||||
* @return 值
|
||||
*/
|
||||
public static <T> T getSessionAttr(String key, Class<T> clazz) {
|
||||
return clazz.cast(getSessionAttr(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话数据
|
||||
*
|
||||
* @param key 键
|
||||
* @param t 值
|
||||
* @param <T> 值类型
|
||||
*/
|
||||
public static <T> void setSessionAttr(String key, T t) {
|
||||
getSession().setAttribute(key, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除会话数据
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public static void removeSessionAttr(String key) {
|
||||
getSession().removeAttribute(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求数据
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public static Object getRequestAttr(String key) {
|
||||
return getRequest().getAttribute(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求数据
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public static boolean hasRequestAttr(String key) {
|
||||
return getRequestAttr(key) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求数据(字符串)
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public static String getRequestAttrAsString(String key) {
|
||||
Object reqAttr = getRequestAttr(key);
|
||||
if (reqAttr == null) {
|
||||
return null;
|
||||
}
|
||||
return reqAttr.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求数据
|
||||
*
|
||||
* @param key 键
|
||||
* @param clazz 值类型
|
||||
* @param <T> 值类型
|
||||
* @return 值
|
||||
*/
|
||||
public static <T> T getRequestAttr(String key, Class<T> clazz) {
|
||||
return clazz.cast(getRequestAttr(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求数据
|
||||
*
|
||||
* @param key 键
|
||||
* @param t 值
|
||||
* @param <T> 值类型
|
||||
*/
|
||||
public static <T> void setRequestAttr(String key, T t) {
|
||||
getRequest().setAttribute(key, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除请求数据
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public static void removeRequestAttr(String key) {
|
||||
getRequest().removeAttribute(key);
|
||||
}
|
||||
|
||||
public static void addCookie(Cookie cookie) {
|
||||
getResponse().addCookie(cookie);
|
||||
}
|
||||
|
||||
public static void addCookie(String key, String value) {
|
||||
addCookie(new Cookie(key, value));
|
||||
}
|
||||
|
||||
public static Cookie getCookie(String key) {
|
||||
Cookie[] cookies = getRequest().getCookies();
|
||||
if (cookies == null) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < cookies.length; i++) {
|
||||
if (cookies[i].getName().equals(key)) {
|
||||
return cookies[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头的令牌,键为 Token
|
||||
*
|
||||
* @return 令牌
|
||||
*/
|
||||
public static String getToken() {
|
||||
return getHeader("Token");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return 客户端地区语言
|
||||
*/
|
||||
public static Language getLanguage() {
|
||||
String name = TimiSpring.getHeader("Language");
|
||||
if (TimiJava.isEmpty(name)) {
|
||||
name = TimiSpring.getLocale().toString();
|
||||
}
|
||||
if (TimiJava.isEmpty(name)) { // use for not support
|
||||
return Language.zh_CN;
|
||||
}
|
||||
return Ref.toType(Language.class, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求 IP
|
||||
*
|
||||
* @return 请求 IP
|
||||
* @throws TimiException 服务异常
|
||||
*/
|
||||
public static String getRequestIP() {
|
||||
String ip = getHeader("x-forwarded-for");
|
||||
// nginx 转发
|
||||
if (TimiJava.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = getHeader("X-Forwarded-For");
|
||||
}
|
||||
if (TimiJava.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = getHeader("X-Real-IP");
|
||||
}
|
||||
// 默认获取
|
||||
if (TimiJava.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = getRequest().getRemoteAddr();
|
||||
}
|
||||
// 本地 IP
|
||||
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
|
||||
}
|
||||
|
||||
public static boolean isLocalIP() {
|
||||
return getRequestIP().startsWith("127");
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/imyeyu/spring/annotation/AOPLog.java
Normal file
17
src/main/java/com/imyeyu/spring/annotation/AOPLog.java
Normal file
@ -0,0 +1,17 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* AOP 切面日志注解,应用在 Controller 的接口上
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-07-21 23:34
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AOPLog {
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.spring.TimiSpring;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import com.imyeyu.spring.entity.IDEntity;
|
||||
import com.imyeyu.utils.Text;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* AOP 切面日志
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-08-17 16:26
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class AOPLogInterceptor {
|
||||
|
||||
public static final String REQUEST_ID = "TIMI_SPRING_REQUEST_ID";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AOPLogInterceptor.class);
|
||||
|
||||
/** 注入注解 */
|
||||
@Pointcut("@annotation(annotation.com.imyeyu.spring.AOPLog)")
|
||||
public void logPointCut() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行前
|
||||
*
|
||||
* @param joinPoint 切入点
|
||||
*/
|
||||
@Before("logPointCut()")
|
||||
public void doBefore(JoinPoint joinPoint) {
|
||||
String uuid = Text.tempUUID();
|
||||
TimiSpring.setSessionAttr(REQUEST_ID, uuid);
|
||||
log.info("ID: {} Request -> IP: {}, URI: {}", uuid, TimiSpring.getRequestIP(), TimiSpring.getRequest().getRequestURI());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行后
|
||||
*
|
||||
* @param response 返回内容
|
||||
* @throws Throwable 异常
|
||||
*/
|
||||
@AfterReturning(returning = "response", pointcut = "logPointCut()")
|
||||
public void doAfterReturning(Object response) throws Throwable {
|
||||
String msg = "ID: {} Response <- Return.";
|
||||
if (response instanceof IDEntity<?> entity) {
|
||||
// 返回实体
|
||||
msg += entity.getClass().getSimpleName() + "." + entity.getId();
|
||||
} else if (response instanceof PageResult<?> pageResult) {
|
||||
// 返回数组
|
||||
if (pageResult.getList().isEmpty()) {
|
||||
msg += "PageResult<?> Empty";
|
||||
} else {
|
||||
if (pageResult.getList().get(0) == null) {
|
||||
msg += "PageResult<?>." + pageResult.getList().size();
|
||||
} else {
|
||||
msg += "PageResult<" + pageResult.getList().get(0).getClass().getSimpleName() + ">[" + pageResult.getList().size() + "]";
|
||||
}
|
||||
}
|
||||
// 返回数据页
|
||||
} else if (response instanceof String string) {
|
||||
// 返回字符串
|
||||
if (string.length() < 64) {
|
||||
msg += string;
|
||||
} else {
|
||||
msg += string.substring(0, 64) + "..";
|
||||
}
|
||||
msg = msg.replaceAll("[\\r\\n]+", "");
|
||||
} else if (response instanceof Boolean bool) {
|
||||
// 返回布尔值
|
||||
msg += bool;
|
||||
} else if (response instanceof Number number) {
|
||||
// 返回数字
|
||||
msg += response.getClass().getSimpleName() + ".[" + number.doubleValue() + "]";
|
||||
} else {
|
||||
// 其他对象
|
||||
if (TimiJava.isNotEmpty(response)) {
|
||||
msg += response.getClass().getSimpleName();
|
||||
} else {
|
||||
msg += "NULL";
|
||||
}
|
||||
}
|
||||
log.info(msg, TimiSpring.getSessionAttr(REQUEST_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* 环绕
|
||||
*
|
||||
* @param pjp 切入点
|
||||
* @return 执行返回
|
||||
* @throws Throwable 异常
|
||||
*/
|
||||
@Around("logPointCut()")
|
||||
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
|
||||
return pjp.proceed();
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/imyeyu/spring/annotation/Entity.java
Normal file
46
src/main/java/com/imyeyu/spring/annotation/Entity.java
Normal file
@ -0,0 +1,46 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 实体注解,Component 别名,只是为了在实体类注入服务接口(如果实体需要注入服务,需要这个类注解)
|
||||
* <pre>
|
||||
*
|
||||
* @Entity
|
||||
* @NoArgsConstructor // 需要个空的构造方法让 MyBatis 正常实例化
|
||||
* public class Entity {
|
||||
*
|
||||
* @Transient
|
||||
* private transient static Service service;
|
||||
*
|
||||
* // 通过构造方法注入
|
||||
* @Autowired
|
||||
* public Entity(Service service) {
|
||||
* Entity.service = service;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-08-18 16:31
|
||||
*/
|
||||
@Component
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Entity {
|
||||
|
||||
/**
|
||||
* 设置控制反转 ID
|
||||
*
|
||||
* @return 控制反转 ID
|
||||
*/
|
||||
String value() default "";
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 忽略全局返回处理器注解
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2023-07-16 10:58
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface IgnoreGlobalReturn {
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 应用在 Controller 的接口上
|
||||
*
|
||||
* <p>{@link #lifeCycle()} 生命周期内(秒)限制访问 {@link #value()} 次
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-08-16 17:57
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Component
|
||||
public @interface RequestRateLimit {
|
||||
|
||||
/**
|
||||
* 生命周期内访问限制,默认 10 秒内 10 次
|
||||
*
|
||||
* @return 生命周期内限制访问次数
|
||||
*/
|
||||
int value() default 10;
|
||||
|
||||
/**
|
||||
* 生命周期
|
||||
*
|
||||
* @return 生命周期秒数
|
||||
*/
|
||||
int lifeCycle() default 10;
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* 抽象访问频率限制,具体子类实现
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-08-16 18:07
|
||||
*/
|
||||
public abstract class RequestRateLimitAbstractInterceptor implements HandlerInterceptor {
|
||||
|
||||
protected String buildId(HandlerMethod handlerMethod) {
|
||||
return handlerMethod.getMethod().getDeclaringClass().getSimpleName() + "." + handlerMethod.getMethod().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理前
|
||||
*
|
||||
* @param req 请求
|
||||
* @param resp 返回
|
||||
* @param handler 处理方法
|
||||
* @return true 为通过
|
||||
*/
|
||||
@Override
|
||||
public boolean preHandle(@NonNull HttpServletRequest req,
|
||||
@NonNull HttpServletResponse resp,
|
||||
@NonNull Object handler) {
|
||||
// 方法注解
|
||||
if (handler instanceof HandlerMethod handlerMethod) {
|
||||
RequestRateLimit requestRateLimit = handlerMethod.getMethodAnnotation(RequestRateLimit.class);
|
||||
if (requestRateLimit == null) {
|
||||
return true;
|
||||
}
|
||||
// 频率限制
|
||||
return beforeRun(req, resp, buildId(handlerMethod), requestRateLimit.lifeCycle(), requestRateLimit.value());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口访问前触发
|
||||
*
|
||||
* @param req 请求
|
||||
* @param resp 返回
|
||||
* @param id 方法
|
||||
* @param cycle 生命周期
|
||||
* @param limit 访问限制
|
||||
* @return true 为通过
|
||||
*/
|
||||
public abstract boolean beforeRun(HttpServletRequest req, HttpServletResponse resp, String id, int cycle, int limit);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 单字段 Json 数据体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2023-08-09 10:36
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RequestSingleParam {
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
@Component
|
||||
public class RequestSingleParamResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(RequestSingleParam.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(@Nonnull MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
if (request == null) {
|
||||
throw new TimiException(TimiCode.REQUEST_BAD, "request illegal");
|
||||
}
|
||||
JsonElement element = JsonParser.parseString(IO.toString(request.getInputStream()));
|
||||
if (!element.isJsonObject()) {
|
||||
throw new TimiException(TimiCode.ARG_BAD, "not json object");
|
||||
}
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
String parameterName = parameter.getParameterName();
|
||||
if (!object.has(parameterName)) {
|
||||
throw new TimiException(TimiCode.ARG_MISS, "not found " + parameterName + " param");
|
||||
}
|
||||
JsonElement el = object.get(parameterName);
|
||||
if (parameter.getParameterType().isAssignableFrom(Long.class)) {
|
||||
return el.getAsLong();
|
||||
}
|
||||
if (parameter.getParameterType().isAssignableFrom(Integer.class)) {
|
||||
return el.getAsInt();
|
||||
}
|
||||
if (parameter.getParameterType().isAssignableFrom(String.class)) {
|
||||
return el.getAsString();
|
||||
}
|
||||
throw new TimiException(TimiCode.ERROR, "not support parameter type");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 应用在 Controller 接口上
|
||||
*
|
||||
* <p>需要验证令牌,只验证该令牌是否有效,不验证数据和令牌的关系
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-08-16 17:58
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Component
|
||||
public @interface RequiredToken {
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.imyeyu.spring.annotation;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* 抽象验证令牌
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-08-16 18:07
|
||||
*/
|
||||
public abstract class RequiredTokenAbstractInterceptor<A extends Annotation> implements HandlerInterceptor {
|
||||
|
||||
private final Class<A> annotation;
|
||||
|
||||
public RequiredTokenAbstractInterceptor(Class<A> annotation) {
|
||||
this.annotation = annotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理前
|
||||
*
|
||||
* @param req 请求
|
||||
* @param resp 返回
|
||||
* @param handler 处理方法
|
||||
* @return true 为通过
|
||||
*/
|
||||
@Override
|
||||
public boolean preHandle(@NonNull HttpServletRequest req,
|
||||
@NonNull HttpServletResponse resp,
|
||||
@NonNull Object handler) {
|
||||
// 方法注解
|
||||
if (handler instanceof HandlerMethod handlerMethod) {
|
||||
A requiredTokenAnnotation = handlerMethod.getMethodAnnotation(annotation);
|
||||
if (requiredTokenAnnotation == null) {
|
||||
return true;
|
||||
}
|
||||
return beforeRun(req, resp);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问前(通过 Token 限制)
|
||||
*
|
||||
* @param req 请求
|
||||
* @param resp 返回
|
||||
* @return true 为通过
|
||||
*/
|
||||
protected abstract boolean beforeRun(HttpServletRequest req, HttpServletResponse resp);
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
/** 注解和注解实现 */
|
||||
package com.imyeyu.spring.annotation;
|
||||
@ -0,0 +1,26 @@
|
||||
package com.imyeyu.spring.annotation.table;
|
||||
|
||||
import com.imyeyu.spring.util.SQLProvider;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 自动生成 UUID,在 {@link SQLProvider} 代理器中的方法会对此注解字段自动生成 UUID
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-05 23:44
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface AutoUUID {
|
||||
|
||||
/**
|
||||
* 是否全大写 UUID
|
||||
*
|
||||
* @return true 为使用全大写 UUID
|
||||
*/
|
||||
boolean upper() default false;
|
||||
}
|
||||
21
src/main/java/com/imyeyu/spring/annotation/table/Column.java
Normal file
21
src/main/java/com/imyeyu/spring/annotation/table/Column.java
Normal file
@ -0,0 +1,21 @@
|
||||
package com.imyeyu.spring.annotation.table;
|
||||
|
||||
import com.imyeyu.spring.util.SQLProvider;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 指定列名,在 {@link SQLProvider} 代理器中的方法无法简单将字段转为数据库列名时,使用此注解指定
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-01-29 09:53
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface Column {
|
||||
|
||||
String value();
|
||||
}
|
||||
25
src/main/java/com/imyeyu/spring/annotation/table/Id.java
Normal file
25
src/main/java/com/imyeyu/spring/annotation/table/Id.java
Normal file
@ -0,0 +1,25 @@
|
||||
package com.imyeyu.spring.annotation.table;
|
||||
|
||||
import com.imyeyu.spring.util.SQLProvider;
|
||||
import org.apache.ibatis.builder.annotation.ProviderContext;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 标记为 ID 字段,在 {@link SQLProvider} 代理器中的方法使用此字段进行以下操作
|
||||
* <ul>
|
||||
* <li>{@link SQLProvider#select(ProviderContext, Object)}</li>
|
||||
* <li>{@link SQLProvider#delete(ProviderContext, Object)}</li>
|
||||
* <li>{@link SQLProvider#destroy(ProviderContext, Object)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-01-29 09:54
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface Id {
|
||||
}
|
||||
21
src/main/java/com/imyeyu/spring/annotation/table/Table.java
Normal file
21
src/main/java/com/imyeyu/spring/annotation/table/Table.java
Normal file
@ -0,0 +1,21 @@
|
||||
package com.imyeyu.spring.annotation.table;
|
||||
|
||||
import com.imyeyu.spring.util.SQLProvider;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 指定表名,在 {@link SQLProvider} 代理器中的方法无法简单将实体类名转为数据库表名时,使用此注解指定
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-01-29 09:53
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface Table {
|
||||
|
||||
String value();
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.imyeyu.spring.annotation.table;
|
||||
|
||||
import com.imyeyu.spring.util.SQLProvider;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* 忽略字段或表示非表实体,在 {@link SQLProvider} 代理器中的方法无需处理该字段时,使用此注解标记,不标记的字段均视为映射数据库列
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-06 17:46
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Transient {
|
||||
}
|
||||
51
src/main/java/com/imyeyu/spring/bean/CaptchaData.java
Normal file
51
src/main/java/com/imyeyu/spring/bean/CaptchaData.java
Normal file
@ -0,0 +1,51 @@
|
||||
package com.imyeyu.spring.bean;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 含验证码数据实体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-03-01 17:10
|
||||
*/
|
||||
public class CaptchaData<T> {
|
||||
|
||||
/** 来源 */
|
||||
@NotBlank(message = "timijava.code.request_bad")
|
||||
protected String from;
|
||||
|
||||
/** 验证码 */
|
||||
@NotBlank(message = "captcha.miss")
|
||||
protected String captcha;
|
||||
|
||||
/** 数据体 */
|
||||
@Valid
|
||||
@NotNull
|
||||
protected T data;
|
||||
|
||||
public String getFrom() {
|
||||
return from;
|
||||
}
|
||||
|
||||
public void setFrom(String from) {
|
||||
this.from = from;
|
||||
}
|
||||
|
||||
public String getCaptcha() {
|
||||
return captcha;
|
||||
}
|
||||
|
||||
public void setCaptcha(String captcha) {
|
||||
this.captcha = captcha;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
83
src/main/java/com/imyeyu/spring/bean/Page.java
Normal file
83
src/main/java/com/imyeyu/spring/bean/Page.java
Normal file
@ -0,0 +1,83 @@
|
||||
package com.imyeyu.spring.bean;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 抽象页面查询参数
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2023-06-02 14:47
|
||||
*/
|
||||
public class Page {
|
||||
|
||||
/** 下标 */
|
||||
@Min(value = 0, message = "page.min_index")
|
||||
protected int index = 0;
|
||||
|
||||
/** 数据量 */
|
||||
@Max(value = 64, message = "page.max_size")
|
||||
protected int size = 16;
|
||||
|
||||
/** 关键字 */
|
||||
protected String keyword;
|
||||
|
||||
protected LinkedHashMap<String, BaseMapper.OrderType> orderMap;
|
||||
|
||||
public Page() {
|
||||
}
|
||||
|
||||
public Page(int index, int size) {
|
||||
this.index = index;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return (long) index * size;
|
||||
}
|
||||
|
||||
public int getLimit() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public LinkedHashMap<String, BaseMapper.OrderType> getOrderMap() {
|
||||
return orderMap;
|
||||
}
|
||||
|
||||
public void setOrderMap(LinkedHashMap<String, BaseMapper.OrderType> orderMap) {
|
||||
this.orderMap = orderMap;
|
||||
}
|
||||
|
||||
public static <T, P extends Page, R extends PageResult<T>> R toResult(BaseMapper<T, ?> pageMapper, P page, R result) {
|
||||
result.setList(pageMapper.list(page.getOffset(), page.getLimit()));
|
||||
result.setTotal(pageMapper.count());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
59
src/main/java/com/imyeyu/spring/bean/PageResult.java
Normal file
59
src/main/java/com/imyeyu/spring/bean/PageResult.java
Normal file
@ -0,0 +1,59 @@
|
||||
package com.imyeyu.spring.bean;
|
||||
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.utils.Calc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 抽象页面查询结果
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2023-06-02 14:47
|
||||
*/
|
||||
public class PageResult<T> {
|
||||
|
||||
/** 总数据量 */
|
||||
protected long total;
|
||||
|
||||
/** 总页数 */
|
||||
protected int pages;
|
||||
|
||||
protected List<T> list;
|
||||
|
||||
/**
|
||||
* 获取总数据量
|
||||
*
|
||||
* @return 总数据量
|
||||
*/
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置总数据量
|
||||
*
|
||||
* @param total 总数据量
|
||||
*/
|
||||
public void setTotal(long total) {
|
||||
this.total = total;
|
||||
if (TimiJava.isNotEmpty(list)) {
|
||||
pages = Calc.ceil(1D * total / list.size());
|
||||
}
|
||||
}
|
||||
|
||||
public List<T> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setList(List<T> list) {
|
||||
this.list = list;
|
||||
if (TimiJava.isNotEmpty(list)) {
|
||||
pages = Calc.ceil(1D * total / list.size());
|
||||
}
|
||||
}
|
||||
|
||||
public int getPages() {
|
||||
return pages;
|
||||
}
|
||||
}
|
||||
157
src/main/java/com/imyeyu/spring/bean/RedisConfigParams.java
Normal file
157
src/main/java/com/imyeyu/spring/bean/RedisConfigParams.java
Normal file
@ -0,0 +1,157 @@
|
||||
package com.imyeyu.spring.bean;
|
||||
|
||||
/**
|
||||
* RedisConfig 配置参数
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-11-21 10:02
|
||||
*/
|
||||
public class RedisConfigParams {
|
||||
|
||||
/** 地址 */
|
||||
private String host;
|
||||
|
||||
/** 端口 */
|
||||
private int port;
|
||||
|
||||
/** 密码 */
|
||||
private String password;
|
||||
|
||||
/** 超时(毫秒) */
|
||||
private int timeout;
|
||||
|
||||
/** 最大活跃连接 */
|
||||
private int maxActive;
|
||||
|
||||
/** 最小空闲连接 */
|
||||
private int minIdle;
|
||||
|
||||
/** 最大空闲连接 */
|
||||
private int maxIdle;
|
||||
|
||||
/**
|
||||
* 获取地址
|
||||
*
|
||||
* @return 地址
|
||||
*/
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置地址
|
||||
*
|
||||
* @param host 地址
|
||||
*/
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取端口
|
||||
*
|
||||
* @return 端口
|
||||
*/
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置端口
|
||||
*
|
||||
* @param port 端口
|
||||
*/
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取密码
|
||||
*
|
||||
* @return 密码
|
||||
*/
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置密码
|
||||
*
|
||||
* @param password 密码
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取超时(毫秒)
|
||||
*
|
||||
* @return 超时(毫秒)
|
||||
*/
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置超时(毫秒)
|
||||
*
|
||||
* @param timeout 超时(毫秒)
|
||||
*/
|
||||
public void setTimeout(int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大活跃连接
|
||||
*
|
||||
* @return 最大活跃连接
|
||||
*/
|
||||
public int getMaxActive() {
|
||||
return maxActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大活跃连接
|
||||
*
|
||||
* @param maxActive 最大活跃连接
|
||||
*/
|
||||
public void setMaxActive(int maxActive) {
|
||||
this.maxActive = maxActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最小空闲连接
|
||||
*
|
||||
* @return 最小空闲连接
|
||||
*/
|
||||
public int getMinIdle() {
|
||||
return minIdle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最小空闲连接
|
||||
*
|
||||
* @param minIdle 最小空闲连接
|
||||
*/
|
||||
public void setMinIdle(int minIdle) {
|
||||
this.minIdle = minIdle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大空闲连接
|
||||
*
|
||||
* @return 最大空闲连接
|
||||
*/
|
||||
public int getMaxIdle() {
|
||||
return maxIdle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大空闲连接
|
||||
*
|
||||
* @param maxIdle 最大空闲连接
|
||||
*/
|
||||
public void setMaxIdle(int maxIdle) {
|
||||
this.maxIdle = maxIdle;
|
||||
}
|
||||
}
|
||||
2
src/main/java/com/imyeyu/spring/bean/package-info.java
Normal file
2
src/main/java/com/imyeyu/spring/bean/package-info.java
Normal file
@ -0,0 +1,2 @@
|
||||
/** 配置对象 */
|
||||
package com.imyeyu.spring.bean;
|
||||
123
src/main/java/com/imyeyu/spring/config/AbstractRedisConfig.java
Normal file
123
src/main/java/com/imyeyu/spring/config/AbstractRedisConfig.java
Normal file
@ -0,0 +1,123 @@
|
||||
package com.imyeyu.spring.config;
|
||||
|
||||
import com.imyeyu.spring.bean.RedisConfigParams;
|
||||
import com.imyeyu.spring.util.Redis;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
import org.springframework.cache.annotation.CachingConfigurer;
|
||||
import org.springframework.cache.interceptor.KeyGenerator;
|
||||
import org.springframework.data.redis.connection.RedisPassword;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 抽象 RedisConfig
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-11-21 10:00
|
||||
*/
|
||||
public abstract class AbstractRedisConfig implements CachingConfigurer {
|
||||
|
||||
/**
|
||||
* 构建 Redis 基本配置
|
||||
*
|
||||
* @return Redis 基本配置
|
||||
*/
|
||||
protected abstract RedisConfigParams configParams();
|
||||
|
||||
/**
|
||||
* 连接池配置
|
||||
* <p>参考:
|
||||
* <pre>
|
||||
* GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
|
||||
* config.setMaxTotal(config.getMaxActive());
|
||||
* config.setMinIdle(config.getMinIdle());
|
||||
* config.setMaxIdle(config.getMaxIdle());
|
||||
* config.setMaxWait(Duration.ofMillis(config.getMaxWait()));
|
||||
* </pre>
|
||||
*
|
||||
* @return GenericObjectPoolConfig
|
||||
*/
|
||||
public abstract GenericObjectPoolConfig<?> getPoolConfig();
|
||||
|
||||
/**
|
||||
* Redis key 生成策略
|
||||
* <p>参考:
|
||||
* <pre>
|
||||
* return (target, method, params) -> {
|
||||
* StringBuilder sb = new StringBuilder();
|
||||
* sb.append(target.getClass().getName());
|
||||
* sb.append(method.getName());
|
||||
* for (Object obj : params) {
|
||||
* sb.append(obj.toString());
|
||||
* }
|
||||
* return sb.toString();
|
||||
* };
|
||||
* </pre>
|
||||
*
|
||||
* @return KeyGenerator
|
||||
*/
|
||||
public abstract KeyGenerator keyGenerator();
|
||||
|
||||
/**
|
||||
* 构造自定义 RedisTemplate
|
||||
*
|
||||
* @param database 数据库
|
||||
* @param keySerializer 键序列化方式
|
||||
* @param <K> 键类型
|
||||
* @param <V> 值类型
|
||||
* @return RedisTemplate
|
||||
*/
|
||||
public <K, V> Redis<K, V> getRedis(int database, RedisSerializer<K> keySerializer) {
|
||||
return getRedis(database, keySerializer, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造自定义 RedisTemplate
|
||||
* <p>针对同一服务器不同数据库
|
||||
*
|
||||
* @param database 数据库
|
||||
* @param keySerializer 键序列化方式
|
||||
* @param valueSerializer 值序列化方式
|
||||
* @param <K> 键类型
|
||||
* @param <V> 值类型
|
||||
* @return RedisTemplate
|
||||
*/
|
||||
public <K, V> Redis<K, V> getRedis(int database, RedisSerializer<K> keySerializer, RedisSerializer<V> valueSerializer) {
|
||||
RedisConfigParams configParams = configParams();
|
||||
|
||||
// 构建 Redis 配置
|
||||
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
|
||||
|
||||
// 连接参数
|
||||
config.setHostName(configParams.getHost());
|
||||
config.setPort(configParams.getPort());
|
||||
config.setPassword(RedisPassword.of(configParams.getPassword()));
|
||||
config.setDatabase(database);
|
||||
|
||||
// 构造连接池
|
||||
LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(configParams.getTimeout())).poolConfig(getPoolConfig()).build();
|
||||
|
||||
// 重构连接工厂
|
||||
LettuceConnectionFactory factory = new LettuceConnectionFactory(config, clientConfig);
|
||||
|
||||
// 设置数据库
|
||||
factory.afterPropertiesSet();
|
||||
|
||||
RedisTemplate<K, V> rt = new RedisTemplate<>();
|
||||
rt.setConnectionFactory(factory);
|
||||
rt.setKeySerializer(keySerializer);
|
||||
rt.setHashKeySerializer(keySerializer);
|
||||
if (valueSerializer != null) {
|
||||
rt.setValueSerializer(valueSerializer);
|
||||
rt.setHashValueSerializer(valueSerializer);
|
||||
}
|
||||
rt.setConnectionFactory(factory);
|
||||
rt.afterPropertiesSet();
|
||||
return new Redis<>(rt, keySerializer);
|
||||
}
|
||||
}
|
||||
2
src/main/java/com/imyeyu/spring/config/package-info.java
Normal file
2
src/main/java/com/imyeyu/spring/config/package-info.java
Normal file
@ -0,0 +1,2 @@
|
||||
/** 配置 */
|
||||
package com.imyeyu.spring.config;
|
||||
82
src/main/java/com/imyeyu/spring/entity/BaseEntity.java
Normal file
82
src/main/java/com/imyeyu/spring/entity/BaseEntity.java
Normal file
@ -0,0 +1,82 @@
|
||||
package com.imyeyu.spring.entity;
|
||||
|
||||
import com.imyeyu.utils.Time;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 基本实体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-11-20 17:45
|
||||
*/
|
||||
public class BaseEntity implements Serializable, Creatable, Updatable, Deletable {
|
||||
|
||||
/** 创建时间 */
|
||||
protected Long createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
protected Long updatedAt;
|
||||
|
||||
/** 删除时间 */
|
||||
protected Long deletedAt;
|
||||
|
||||
/**
|
||||
* 获取创建时间
|
||||
*
|
||||
* @return 创建时间
|
||||
*/
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置创建时间
|
||||
*
|
||||
* @param createdAt 创建时间
|
||||
*/
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更新时间
|
||||
*
|
||||
* @return 更新时间
|
||||
*/
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置更新时间
|
||||
*
|
||||
* @param updatedAt 更新时间
|
||||
*/
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取删除时间
|
||||
*
|
||||
* @return 删除时间
|
||||
*/
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置删除时间
|
||||
*
|
||||
* @param deletedAt 删除时间
|
||||
*/
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null && deletedAt < Time.now();
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/imyeyu/spring/entity/Creatable.java
Normal file
24
src/main/java/com/imyeyu/spring/entity/Creatable.java
Normal file
@ -0,0 +1,24 @@
|
||||
package com.imyeyu.spring.entity;
|
||||
|
||||
/**
|
||||
* 可创建实体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-06 15:30
|
||||
*/
|
||||
public interface Creatable {
|
||||
|
||||
/**
|
||||
* 获取创建时间
|
||||
*
|
||||
* @return 创建时间
|
||||
*/
|
||||
Long getCreatedAt();
|
||||
|
||||
/**
|
||||
* 设置创建时间
|
||||
*
|
||||
* @param createdAt 创建时间
|
||||
*/
|
||||
void setCreatedAt(Long createdAt);
|
||||
}
|
||||
31
src/main/java/com/imyeyu/spring/entity/Deletable.java
Normal file
31
src/main/java/com/imyeyu/spring/entity/Deletable.java
Normal file
@ -0,0 +1,31 @@
|
||||
package com.imyeyu.spring.entity;
|
||||
|
||||
/**
|
||||
* 可软删除实体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-06 15:30
|
||||
*/
|
||||
public interface Deletable {
|
||||
|
||||
/**
|
||||
* 获取删除时间
|
||||
*
|
||||
* @return 删除时间
|
||||
*/
|
||||
Long getDeletedAt();
|
||||
|
||||
/**
|
||||
* 设置删除时间
|
||||
*
|
||||
* @param deletedAt 删除时间
|
||||
*/
|
||||
void setDeletedAt(Long deletedAt);
|
||||
|
||||
/**
|
||||
* 是否已删除
|
||||
*
|
||||
* @return true 为已删除
|
||||
*/
|
||||
boolean isDeleted();
|
||||
}
|
||||
12
src/main/java/com/imyeyu/spring/entity/Destroyable.java
Normal file
12
src/main/java/com/imyeyu/spring/entity/Destroyable.java
Normal file
@ -0,0 +1,12 @@
|
||||
package com.imyeyu.spring.entity;
|
||||
|
||||
import com.imyeyu.spring.util.SQLProvider;
|
||||
|
||||
/**
|
||||
* 可销毁实体,仅用于 {@link SQLProvider} 标记
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-06 18:28
|
||||
*/
|
||||
public interface Destroyable {
|
||||
}
|
||||
34
src/main/java/com/imyeyu/spring/entity/Entity.java
Normal file
34
src/main/java/com/imyeyu/spring/entity/Entity.java
Normal file
@ -0,0 +1,34 @@
|
||||
package com.imyeyu.spring.entity;
|
||||
|
||||
import com.imyeyu.spring.annotation.table.Id;
|
||||
|
||||
/**
|
||||
* 基本长整型 ID 实体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-07 12:51
|
||||
*/
|
||||
public class Entity extends BaseEntity implements IDEntity<Long> {
|
||||
|
||||
/** ID */
|
||||
@Id
|
||||
protected Long id;
|
||||
|
||||
/**
|
||||
* 获取 ID
|
||||
*
|
||||
* @return ID
|
||||
*/
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 ID
|
||||
*
|
||||
* @param id ID
|
||||
*/
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/imyeyu/spring/entity/IDEntity.java
Normal file
24
src/main/java/com/imyeyu/spring/entity/IDEntity.java
Normal file
@ -0,0 +1,24 @@
|
||||
package com.imyeyu.spring.entity;
|
||||
|
||||
/**
|
||||
* ID 实体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-07 17:10
|
||||
*/
|
||||
public interface IDEntity<T> {
|
||||
|
||||
/**
|
||||
* 获取 ID
|
||||
*
|
||||
* @return ID
|
||||
*/
|
||||
T getId();
|
||||
|
||||
/**
|
||||
* 设置 ID
|
||||
*
|
||||
* @param id ID
|
||||
*/
|
||||
void setId(T id);
|
||||
}
|
||||
26
src/main/java/com/imyeyu/spring/entity/UUIDEntity.java
Normal file
26
src/main/java/com/imyeyu/spring/entity/UUIDEntity.java
Normal file
@ -0,0 +1,26 @@
|
||||
package com.imyeyu.spring.entity;
|
||||
|
||||
import com.imyeyu.spring.annotation.table.AutoUUID;
|
||||
import com.imyeyu.spring.annotation.table.Id;
|
||||
|
||||
/**
|
||||
* 基本 UUID 实体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-07 12:07
|
||||
*/
|
||||
public class UUIDEntity extends BaseEntity implements IDEntity<String> {
|
||||
|
||||
/** ID */
|
||||
@Id
|
||||
@AutoUUID
|
||||
protected String id;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/imyeyu/spring/entity/Updatable.java
Normal file
24
src/main/java/com/imyeyu/spring/entity/Updatable.java
Normal file
@ -0,0 +1,24 @@
|
||||
package com.imyeyu.spring.entity;
|
||||
|
||||
/**
|
||||
* 可更新实体
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-06 15:30
|
||||
*/
|
||||
public interface Updatable {
|
||||
|
||||
/**
|
||||
* 获取更新时间
|
||||
*
|
||||
* @return 更新时间
|
||||
*/
|
||||
Long getUpdatedAt();
|
||||
|
||||
/**
|
||||
* 设置更新时间
|
||||
*
|
||||
* @param updatedAt 更新时间
|
||||
*/
|
||||
void setUpdatedAt(Long updatedAt);
|
||||
}
|
||||
2
src/main/java/com/imyeyu/spring/entity/package-info.java
Normal file
2
src/main/java/com/imyeyu/spring/entity/package-info.java
Normal file
@ -0,0 +1,2 @@
|
||||
/** 基本实体 */
|
||||
package com.imyeyu.spring.entity;
|
||||
104
src/main/java/com/imyeyu/spring/mapper/BaseMapper.java
Normal file
104
src/main/java/com/imyeyu/spring/mapper/BaseMapper.java
Normal file
@ -0,0 +1,104 @@
|
||||
package com.imyeyu.spring.mapper;
|
||||
|
||||
import com.imyeyu.spring.util.SQLProvider;
|
||||
import org.apache.ibatis.annotations.DeleteProvider;
|
||||
import org.apache.ibatis.annotations.InsertProvider;
|
||||
import org.apache.ibatis.annotations.Options;
|
||||
import org.apache.ibatis.annotations.SelectProvider;
|
||||
import org.apache.ibatis.annotations.UpdateProvider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基本 SQL 映射,子接口可以不实现
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-07-16 09:40
|
||||
*/
|
||||
public interface BaseMapper<T, P> {
|
||||
|
||||
/**
|
||||
* 排序方式
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2023-09-05 22:14
|
||||
*/
|
||||
enum OrderType {
|
||||
|
||||
ASC,
|
||||
|
||||
DESC
|
||||
}
|
||||
|
||||
static final String NOT_DELETE = " AND `deleted_at` IS NULL ";
|
||||
|
||||
static final String LIMIT_1 = " LIMIT 1";
|
||||
|
||||
static final String UNIX_TIME = " FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) ";
|
||||
|
||||
static final String PAGE = NOT_DELETE + " LIMIT #{offset}, #{limit}";
|
||||
|
||||
/**
|
||||
* 统计数据量
|
||||
*
|
||||
* @return 数据量
|
||||
*/
|
||||
long count();
|
||||
|
||||
/**
|
||||
* 获取部分数据
|
||||
*
|
||||
* @param offset 偏移
|
||||
* @param limit 数据量
|
||||
* @return 数据列表
|
||||
*/
|
||||
List<T> list(long offset, int limit);
|
||||
|
||||
/**
|
||||
* 创建数据。默认自增主键为 id,如需修改请重写此接口
|
||||
*
|
||||
* @param t 数据对象
|
||||
*/
|
||||
@InsertProvider(type = SQLProvider.class, method = "insert")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id")
|
||||
void insert(T t);
|
||||
|
||||
/**
|
||||
* 根据 ID 获取对象
|
||||
*
|
||||
* @param id 索引
|
||||
* @return 数据对象
|
||||
*/
|
||||
@SelectProvider(type = SQLProvider.class, method = "select")
|
||||
T select(P id);
|
||||
|
||||
@SelectProvider(type = SQLProvider.class, method = "selectByExample")
|
||||
T selectByExample(T t);
|
||||
|
||||
@SelectProvider(type = SQLProvider.class, method = "selectAllByExample")
|
||||
List<T> selectAllByExample(T t);
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
*
|
||||
* @param t 数据对象
|
||||
*/
|
||||
@UpdateProvider(type = SQLProvider.class, method = "update")
|
||||
void update(T t);
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
*
|
||||
* @param id 索引
|
||||
*/
|
||||
@UpdateProvider(type = SQLProvider.class, method = "delete")
|
||||
void delete(P id);
|
||||
|
||||
/**
|
||||
* 销毁(物理删除)
|
||||
*
|
||||
* @param id 索引
|
||||
*/
|
||||
@DeleteProvider(type = SQLProvider.class, method = "destroy")
|
||||
void destroy(P id);
|
||||
}
|
||||
2
src/main/java/com/imyeyu/spring/mapper/package-info.java
Normal file
2
src/main/java/com/imyeyu/spring/mapper/package-info.java
Normal file
@ -0,0 +1,2 @@
|
||||
/** 基本 MyBatis 映射 */
|
||||
package com.imyeyu.spring.mapper;
|
||||
@ -0,0 +1,62 @@
|
||||
package com.imyeyu.spring.service;
|
||||
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* 抽象实体服务类
|
||||
*
|
||||
* @param <T> 实体类型
|
||||
* @param <P> 实体主键类型
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:06
|
||||
*/
|
||||
public abstract class AbstractEntityService<T, P> implements BaseService<T, P> {
|
||||
|
||||
/** 基本 Mapper */
|
||||
protected BaseMapper<T, P> baseMapper;
|
||||
|
||||
/** @return Mapper 实例 */
|
||||
protected abstract BaseMapper<T, P> mapper();
|
||||
|
||||
/** 检查 mapper */
|
||||
private void checkMapper() {
|
||||
if (baseMapper == null) {
|
||||
baseMapper = mapper();
|
||||
}
|
||||
TimiException.required(baseMapper, "mapper() can not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<T> page(Page page) {
|
||||
checkMapper();
|
||||
return Page.toResult(baseMapper, page, new PageResult<>());
|
||||
}
|
||||
|
||||
public void create(T t) {
|
||||
checkMapper();
|
||||
baseMapper.insert(t);
|
||||
}
|
||||
|
||||
public T get(P id) {
|
||||
checkMapper();
|
||||
return baseMapper.select(id);
|
||||
}
|
||||
|
||||
public void update(T t) {
|
||||
checkMapper();
|
||||
baseMapper.update(t);
|
||||
}
|
||||
|
||||
public void delete(P p) {
|
||||
checkMapper();
|
||||
baseMapper.delete(p);
|
||||
}
|
||||
|
||||
public void destroy(P p) {
|
||||
checkMapper();
|
||||
baseMapper.destroy(p);
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/imyeyu/spring/service/BaseService.java
Normal file
18
src/main/java/com/imyeyu/spring/service/BaseService.java
Normal file
@ -0,0 +1,18 @@
|
||||
package com.imyeyu.spring.service;
|
||||
|
||||
/**
|
||||
* 基本实体服务接口
|
||||
* <p>实现类方法顺序:
|
||||
* <ol>
|
||||
* <li>基本接口 CRUD</li>
|
||||
* <li>自定义接口 CRUD</li>
|
||||
* <li>内部方法</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param <T> 交互实体
|
||||
* @param <P> 交互实体类型
|
||||
* @author 夜雨
|
||||
* @version 2021-02-23 21:32
|
||||
*/
|
||||
public interface BaseService<T, P> extends PageableService<T>, CreatableService<T>, GettableService<T, P>, UpdatableService<T>, DeletableService<P>, DestroyableService<P> {
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.imyeyu.spring.service;
|
||||
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
|
||||
/**
|
||||
* 可创建实体服务
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:29
|
||||
*/
|
||||
public interface CreatableService<T> {
|
||||
|
||||
/**
|
||||
* 创建数据
|
||||
*
|
||||
* @param t 数据对象
|
||||
* @throws TimiException 服务异常
|
||||
*/
|
||||
void create(T t);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.imyeyu.spring.service;
|
||||
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
|
||||
/**
|
||||
* 可软删除实体服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:30
|
||||
*/
|
||||
public interface DeletableService<P> {
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
*
|
||||
* @param p 数据对象
|
||||
* @throws TimiException 服务异常
|
||||
*/
|
||||
void delete(P p);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.imyeyu.spring.service;
|
||||
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
|
||||
/**
|
||||
* 可销毁(物理删除)实体服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:30
|
||||
*/
|
||||
public interface DestroyableService<P> {
|
||||
|
||||
/**
|
||||
* 销毁(物理删除)
|
||||
*
|
||||
* @param p 数据对象
|
||||
* @throws TimiException 服务异常
|
||||
*/
|
||||
void destroy(P p);
|
||||
}
|
||||
20
src/main/java/com/imyeyu/spring/service/GettableService.java
Normal file
20
src/main/java/com/imyeyu/spring/service/GettableService.java
Normal file
@ -0,0 +1,20 @@
|
||||
package com.imyeyu.spring.service;
|
||||
|
||||
/**
|
||||
* 可获取实体服务
|
||||
*
|
||||
* @param <T> 实体类型
|
||||
* @param <P> 主键类型
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:31
|
||||
*/
|
||||
public interface GettableService<T, P> {
|
||||
|
||||
/**
|
||||
* 获取实体
|
||||
*
|
||||
* @param p 主键
|
||||
* @return 实体
|
||||
*/
|
||||
T get(P p);
|
||||
}
|
||||
22
src/main/java/com/imyeyu/spring/service/PageableService.java
Normal file
22
src/main/java/com/imyeyu/spring/service/PageableService.java
Normal file
@ -0,0 +1,22 @@
|
||||
package com.imyeyu.spring.service;
|
||||
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
|
||||
/**
|
||||
* 可分页实体服务
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:30
|
||||
*/
|
||||
public interface PageableService<T> {
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param page 页面查询参数
|
||||
* @return 查询页面结果
|
||||
*/
|
||||
PageResult<T> page(Page page);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.imyeyu.spring.service;
|
||||
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
|
||||
/**
|
||||
* 可更新实体服务
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:30
|
||||
*/
|
||||
public interface UpdatableService<T> {
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
*
|
||||
* @param t 数据对象
|
||||
* @throws TimiException 服务异常
|
||||
*/
|
||||
void update(T t);
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
/** 基本服务 */
|
||||
package com.imyeyu.spring.service;
|
||||
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