Initial project

This commit is contained in:
Timi
2025-07-08 14:34:32 +08:00
parent 271e2ae673
commit c27146aa91
56 changed files with 3050 additions and 80 deletions

View 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>
* &#64;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");
}
}

View 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 {
}

View File

@ -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();
}
}

View 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>
*
* &#64;Entity
* &#64;NoArgsConstructor // 需要个空的构造方法让 MyBatis 正常实例化
* public class Entity {
*
* &#64;Transient
* private transient static Service service;
*
* // 通过构造方法注入
* &#64;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 "";
}

View 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;
/**
* 忽略全局返回处理器注解
*
* @author 夜雨
* @version 2023-07-16 10:58
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreGlobalReturn {
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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 {
}

View File

@ -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");
}
}

View File

@ -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 {
}

View File

@ -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);
}

View File

@ -0,0 +1,2 @@
/** 注解和注解实现 */
package com.imyeyu.spring.annotation;

View File

@ -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;
}

View 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();
}

View 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 {
}

View 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();
}

View File

@ -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 {
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -0,0 +1,2 @@
/** 配置对象 */
package com.imyeyu.spring.bean;

View 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&lt;?&gt; config = new GenericObjectPoolConfig&lt;&gt;();
* 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);
}
}

View File

@ -0,0 +1,2 @@
/** 配置 */
package com.imyeyu.spring.config;

View 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();
}
}

View 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);
}

View 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();
}

View 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 {
}

View 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;
}
}

View 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);
}

View 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;
}
}

View 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);
}

View File

@ -0,0 +1,2 @@
/** 基本实体 */
package com.imyeyu.spring.entity;

View 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);
}

View File

@ -0,0 +1,2 @@
/** 基本 MyBatis 映射 */
package com.imyeyu.spring.mapper;

View File

@ -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);
}
}

View 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> {
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View 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);
}

View 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);
}

View File

@ -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);
}

View File

@ -0,0 +1,2 @@
/** 基本服务 */
package com.imyeyu.spring.service;

View 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;
}
}

View 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);
}
}

View File

@ -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;
}
}

View 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;
}
});
}
}

View 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);
}
};
}
}

View 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&lt;列名,字段名&gt; */
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;
}
}
}
}

View File

@ -0,0 +1,2 @@
/** 其他工具 */
package com.imyeyu.spring.util;