创建项目
用户模块技术点
全局唯一用户名称 唯一索引+特定异常处理
全局用户信息上下文的搭建 拦截器+ JWT全局 token + ThreadLocal
文件模块技术点
优化文件上传下载 sendfile 零拷贝、文件软连接
文件分片上传、断点续传、秒传 文件唯一1D+缓存文件分片
文件快速移动、复制 文件软连接
文件系统对接第三方存储 策略模式抽象
项目架构设计
项目创建
定义常量类 Constants
java
package com.xiaoye.core.constants;
import org.apache.commons.lang3.StringUtils;
/**
* @author zhangzhengyang
* @ClassName com.xiaoye.core.constants
*/
public interface Constants {
/**
* 公用的字符串分隔符
*/
String COMMON_SEPARATOR = "__,__";
/**
* 空字符串
*/
String EMPTY_STR = StringUtils.EMPTY;
/**
* 点 常量
*/
String POINT_STR = ".";
/**
* 斜杠字符串
*/
String SLASH_STR = "/";
/**
* Long 常量 0
*/
Long ZERO_LONG = 0L;
/**
* Integer 常量 0
*/
Integer ZERO_INT = 0;
/**
* Integer 常量 1
*/
Integer ONE_INT = 1;
/**
* Integer 常量 2
*/
Integer TWO_INT = 2;
/**
* Integer 常量 -1
*/
Integer MINUS_ONE_INT = -1;
/**
* TRUE 字符串
*/
String TRUE_STR = "true";
/**
* FALSE 字符串
*/
String FALSE_STR = "false";
/**
* 组件扫描基础路径
*/
String BASE_COMPONENT_SCAN_PATH = "com.xiaoye";
/**
* 问号常量
*/
String QUESTION_MARK_STR = "?";
/**
* 等号常量
*/
String EQUALS_MARK_STR = "=";
/**
* 逻辑与常量
*/
String AND_MARK_STR = "&";
/**
* 左中括号常量
*/
String LEFT_BRACKET_STR = "[";
/**
* 右中括号常量
*/
String RIGHT_BRACKET_STR = "]";
/**
* 公用加密字符串
*/
String COMMON_ENCRYPT_STR = "****";
}定义 response 枚举类
java
package com.xiaoye.core.response;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author zhangzhengyang
* @ClassName com.xiaoye.core.response
* @description 项目公用返回状态码
*/
@AllArgsConstructor
@Getter
public enum ResponseCode {
/**
* 成功
*/
SUCCESS(0, "SUCCESS"),
/**
* 错误
*/
ERROR(1, "ERROR"),
/**
* token过期
*/
TOKEN_EXPIRE(2, "TOKEN_EXPIRE"),
/**
* 参数错误
*/
ERROR_PARAM(3, "ERROR_PARAM"),
/**
* 无权限访问
*/
ACCESS_DENIED(4, "ACCESS_DENIED"),
/**
* 分享的文件丢失
*/
SHARE_FILE_MISS(5, "分享的文件丢失"),
/**
* 分享已经被取消
*/
SHARE_CANCELLED(6, "分享已经被取消"),
/**
* 分享已过期
*/
SHARE_EXPIRE(7, "分享已过期"),
/**
* 需要登录
*/
NEED_LOGIN(10, "NEED_LOGIN");
private final Integer code;
private final String desc;
}定义返回类
java
package com.xiaoye.core.response;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import java.io.Serializable;
import java.util.Objects;
/**
* @author zhangzhengyang
* @ClassName com.xiaoye.core.response
* @createTime 2024/1/4
* @description 公用返回对象
*/
// 保证 json 序列化的时候,如果属性为 null,key 也就一起消失
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
public class R<T> implements Serializable {
private final Integer code;
private String message;
private T data;
private R(Integer code) {
this.code = code;
}
private R(Integer code, String message) {
this.code = code;
this.message = message;
}
private R(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
@JsonIgnore
@JSONField(serialize = false)
public boolean isSuccess() {
return Objects.equals(this.code, ResponseCode.SUCCESS.getCode());
}
/**
* 公用的静态方法
*
* @param <T>
* @return
*/
public static <T> R<T> success() {
return new R<T>(ResponseCode.SUCCESS.getCode());
}
public static <T> R<T> success(String message) {
return new R<T>(ResponseCode.SUCCESS.getCode(), message);
}
/**
* public static <T> R<T> success(T data) {
* return new R<T>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getDesc(), data);
* }
*/
public static <T> R<T> data(T data) {
return new R<T>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getDesc(), data);
}
public static <T> R<T> fail() {
return new R<T>(ResponseCode.ERROR.getCode());
}
public static <T> R<T> fail(String message) {
return new R<T>(ResponseCode.ERROR.getCode(), message);
}
/**
* 自动义状态码
*
* @param <T>
* @return
*/
public static <T> R<T> fail(Integer code, String message) {
return new R<T>(code, message);
}
public static <T> R<T> fail(ResponseCode responseCode) {
return new R<T>(responseCode.getCode(), responseCode.getDesc());
}
}定义全局异常类 BusinessException
java
package com.xiaoye.core.exception;
import com.xiaoye.core.response.ResponseCode;
import lombok.Data;
/**
* @author zhangzhengyang
* @ClassName com.xiaoye.core.exception
* @createTime 2024/1/4
* @description 自定义全局业务异常类
*/
@Data
public class BusinessException extends RuntimeException {
private Integer code;
private String message;
public BusinessException(ResponseCode responseCode) {
this.code = responseCode.getCode();
this.message = responseCode.getDesc();
}
public BusinessException(Integer code, String message) {
this.code = code;
this.message = message;
}
public BusinessException(String message) {
this.code = ResponseCode.ERROR_PARAM.getCode();
this.message = message;
}
public BusinessException() {
this.code = ResponseCode.ERROR_PARAM.getCode();
this.message = ResponseCode.ERROR_PARAM.getDesc();
}
}工具类
雪花算法 id 生成器
java
package com.xiaoye.core.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.ArrayUtil;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.xiaoye.core.constants.Constants;
import com.xiaoye.core.exception.BusinessException;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 雪花算法 id 生成器
*/
public class IdUtil {
/**
* 工作id 也就是机器id
*/
private static long workerId;
/**
* 数据中心id
*/
private static long dataCenterId;
/**
* 序列号
*/
private static long sequence;
/**
* 初始时间戳
*/
private static long startTimestamp = 1288834974657L;
/**
* 工作id长度为5位
*/
private static long workerIdBits = 5L;
/**
* 数据中心id长度为5位
*/
private static long dataCenterIdBits = 5L;
/**
* 工作id最大值
*/
private static long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 数据中心id最大值
*/
private static long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
/**
* 序列号长度
*/
private static long sequenceBits = 12L;
/**
* 序列号最大值
*/
private static long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作id需要左移的位数,12位
*/
private static long workerIdShift = sequenceBits;
/**
* 数据id需要左移位数 12+5=17位
*/
private static long dataCenterIdShift = sequenceBits + workerIdBits;
/**
* 时间戳需要左移位数 12+5+5=22位
*/
private static long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
/**
* 上次时间戳,初始值为负数
*/
private static long lastTimestamp = -1L;
static {
workerId = getMachineNum() & maxWorkerId;
dataCenterId = getMachineNum() & maxDataCenterId;
sequence = 0L;
}
/**
* 获取机器编号
*
* @return
*/
private static long getMachineNum() {
long machinePiece;
StringBuilder sb = new StringBuilder();
Enumeration<NetworkInterface> e = null;
try {
e = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e1) {
e1.printStackTrace();
}
while (e.hasMoreElements()) {
NetworkInterface ni = e.nextElement();
sb.append(ni.toString());
}
machinePiece = sb.toString().hashCode();
return machinePiece;
}
/**
* 获取时间戳,并与上次时间戳比较
*
* @param lastTimestamp
* @return
*/
private static long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 获取系统时间戳
*
* @return
*/
private static long timeGen() {
return System.currentTimeMillis();
}
/**
* 生成 ID
*
* @return
*/
public synchronized static Long get() {
long timestamp = timeGen();
// 获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
// 获取当前时间戳如果等于上次时间戳
// 说明:还处在同一毫秒内,则在序列号加1;否则序列号赋值为0,从0开始。
// 0 - 4095
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//将上次时间戳值刷新
lastTimestamp = timestamp;
/**
*
* 返回结果:
* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
* (workerId << workerIdShift) 表示将工作id左移相应位数
* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
return ((timestamp - startTimestamp) << timestampLeftShift) |
(dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
/**
* 加密ID
*
* @return
*/
public static String encrypt(Long id) {
if (Objects.nonNull(id)) {
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
byteBuffer.putLong(0, id);
byte[] content = byteBuffer.array();
byte[] encrypt = AES128Util.aesEncrypt(content);
return Base64.encode(encrypt);
}
return StringUtils.EMPTY;
}
/**
* 解密ID
*
* @param decryptId
* @return
*/
public static Long decrypt(String decryptId) {
if (StringUtils.isNotBlank(decryptId)) {
byte[] encrypt = Base64.decode(decryptId);
byte[] content = AES128Util.aesDecode(encrypt);
if (ArrayUtil.isNotEmpty(content)) {
ByteBuffer byteBuffer = ByteBuffer.wrap(content);
return byteBuffer.getLong();
}
throw new BusinessException("AES128Util.aesDecode fail");
}
throw new BusinessException("the decryptId can not be empty");
}
/**
* 解密多个加密ID拼接的字符串
*
* @param decryptIdStr
* @return
*/
public static List<Long> decryptIdList(String decryptIdStr) {
if (StringUtils.isBlank(decryptIdStr)) {
return Lists.newArrayList();
}
List<String> decryptIdList = Splitter.on(Constants.COMMON_SEPARATOR).splitToList(decryptIdStr);
if (CollectionUtils.isEmpty(decryptIdList)) {
return Lists.newArrayList();
}
List<Long> result = decryptIdList.stream().map(IdUtil::decrypt).collect(Collectors.toList());
return result;
}
}Jwt 工具类
java
package com.xiaoye.core.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import java.util.Date;
/**
* Jwt工具类
*/
public class JwtUtil {
public static final Long TWO_LONG = 2L;
/**
* 秘钥
*/
private final static String JWT_PRIVATE_KEY = "0CB16040A41140E48F2F93A7BE222C46";
/**
* 刷新时间
*/
private final static String RENEWAL_TIME = "RENEWAL_TIME";
/**
* 生成token
*
* @param subject
* @param claimKey
* @param claimValue
* @param expire
* @return
*/
public static String generateToken(String subject, String claimKey, Object claimValue, Long expire) {
String token = Jwts.builder()
.setSubject(subject)
.claim(claimKey, claimValue)
.claim(RENEWAL_TIME, new Date(System.currentTimeMillis() + expire / TWO_LONG))
.setExpiration(new Date(System.currentTimeMillis() + expire))
.signWith(SignatureAlgorithm.HS256, JWT_PRIVATE_KEY)
.compact();
return token;
}
/**
* 解析token
*
* @param token
* @return
*/
public static Object analyzeToken(String token, String claimKey) {
if (StringUtils.isBlank(token)) {
return null;
}
try {
Claims claims = Jwts.parser()
.setSigningKey(JWT_PRIVATE_KEY)
.parseClaimsJws(token)
.getBody();
return claims.get(claimKey);
} catch (Exception e) {
return null;
}
}
}MessageDigestUtil
MessageDigest 加密工具类,单向加密的工具类
java
package com.xiaoye.core.utils;
import cn.hutool.core.codec.Base64;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
* MessageDigest 加密工具类,单向加密的工具类
*/
public class MessageDigestUtil {
private static final String MD5_STR = "MD5";
private static final String SHA1_STR = "SHA1";
private static final String SHA256_STR = "SHA-256";
public static final String ZERO_STR = "0";
/**
* 采用指定的单向散列模式加密
*
* @param originBytes
* @param mode
* @return
* @throws Exception
*/
public static byte[] encrypt(byte[] originBytes, String mode) throws Exception {
if (ArrayUtils.isEmpty(originBytes) || StringUtils.isBlank(mode)) {
return ArrayUtils.EMPTY_BYTE_ARRAY;
}
MessageDigest md = MessageDigest.getInstance(mode);
byte[] digestBytes = md.digest(originBytes);
return digestBytes;
}
/**
* 单向散列指定模式的字符串
*
* @param originContent
* @param mode
* @return
* @throws Exception
*/
public static byte[] encrypt(String originContent, String mode) throws Exception {
if (StringUtils.isBlank(originContent)) {
return ArrayUtils.EMPTY_BYTE_ARRAY;
}
return encrypt(originContent.getBytes(StandardCharsets.UTF_8), mode);
}
/**
* 采用指定模式散列字符串
*
* @param originContent
* @param mode
* @return
*/
public static String encryptString(String originContent, String mode) {
String result = StringUtils.EMPTY;
try {
byte[] encryptBytes = encrypt(originContent, mode);
if (ArrayUtils.isEmpty(encryptBytes)) {
return result;
}
result = Base64.encode(encryptBytes);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 获取md5加密串
*
* @param originContent
* @return
*/
public static String md5(String originContent) {
return encryptString(originContent, MD5_STR);
}
/**
* 获取sha1加密串
*
* @param originContent
* @return
*/
public static String sha1(String originContent) {
return encryptString(originContent, SHA1_STR);
}
/**
* 获取sha256加密串
*
* @param originContent
* @return
*/
public static String sha256(String originContent) {
return encryptString(originContent, SHA256_STR);
}
}PasswordUtil
java
package com.xiaoye.core.utils;
/**
* 密码工具类
*/
public class PasswordUtil {
/**
* 随机生成盐值
*
* @return
*/
public static String getSalt() {
return MessageDigestUtil.md5(UUIDUtil.getUUID());
}
/**
* 密码加密
*
* @param salt
* @param inputPassword
* @return
*/
public static String encryptPassword(String salt, String inputPassword) {
return MessageDigestUtil.sha256(MessageDigestUtil.sha1(inputPassword) + salt);
}
}UUIDUtil
java
package com.xiaoye.core.utils;
import java.util.UUID;
/**
* UUID 工具类
*/
public class UUIDUtil {
public static final String EMPTY_STR = "";
public static final String HYPHEN_STR = "-";
/**
* 获取 UUID 字符串
*
* @return
*/
public static String getUUID() {
return UUID.randomUUID().toString().replace(HYPHEN_STR, EMPTY_STR).toUpperCase();
}
}统一的参数校验器
java
package com.xiaoye.web.validator;
import com.xiaoye.core.constants.Constants;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.HibernateValidator;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* 统一的参数校验器
*/
@SpringBootConfiguration
@Slf4j
public class WebValidatorConfig {
/**
* hibernate.validator.fail_fast 为固定值
*/
private static final String FAIL_FAST_KEY = "hibernate.validator.fail_fast";
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
postProcessor.setValidator(PanValidator());
log.info("The hibernate validator is loaded successfully!");
return postProcessor;
}
/**
* 构造项目的方法参数校验器
*
* @return
*/
private Validator PanValidator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.addProperty(FAIL_FAST_KEY, Constants.TRUE_STR)
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}全局过滤器
跨域全局过滤器
java
package com.xiaoye.web.cors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.core.annotation.Order;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 支持跨域全局过滤器
*/
@WebFilter(filterName = "corsFilter")
@Order(1)
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
addCorsResponseHeader(response);
filterChain.doFilter(servletRequest, servletResponse);
}
/**
* 添加跨域的响应头
*
* @param response
*/
private void addCorsResponseHeader(HttpServletResponse response) {
response.setHeader(CorsConfigEnum.CORS_ORIGIN.getKey(), CorsConfigEnum.CORS_ORIGIN.getValue());
response.setHeader(CorsConfigEnum.CORS_CREDENTIALS.getKey(), CorsConfigEnum.CORS_CREDENTIALS.getValue());
response.setHeader(CorsConfigEnum.CORS_METHODS.getKey(), CorsConfigEnum.CORS_METHODS.getValue());
response.setHeader(CorsConfigEnum.CORS_MAX_AGE.getKey(), CorsConfigEnum.CORS_MAX_AGE.getValue());
response.setHeader(CorsConfigEnum.CORS_HEADERS.getKey(), CorsConfigEnum.CORS_HEADERS.getValue());
}
/**
* 跨域设置枚举类
*/
@AllArgsConstructor
@Getter
public enum CorsConfigEnum {
/**
* 允许所有远程访问
*/
CORS_ORIGIN("Access-Control-Allow-Origin", "*"),
/**
* 允许认证
*/
CORS_CREDENTIALS("Access-Control-Allow-Credentials", "true"),
/**
* 允许远程调用的请求类型
*/
CORS_METHODS("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT"),
/**
* 指定本次预检请求的有效期,单位是秒
*/
CORS_MAX_AGE("Access-Control-Max-Age", "3600"),
/**
* 允许所有请求头
*/
CORS_HEADERS("Access-Control-Allow-Headers", "*");
private final String key;
private final String value;
}
}HTTP 调用日志过滤器
http 调用日志实体
java
package com.xiaoye.web.log;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.Date;
import java.util.Map;
/**
* http 调用日志实体
*/
@NoArgsConstructor
@Getter
@Setter
@Accessors(chain = true)
@Slf4j
public class HttpLogEntity {
private String requestUri;
private String method;
private String remoteAddr;
private String ip;
private Map<String, String> requestHeaders;
private String requestParams;
private Integer status;
private Map<String, String> responseHeaders;
private String responseData;
/**
* 接口耗时
*/
private String resolveTime;
/**
* 打印日志
*/
public void print() {
log.info("====================HTTP CALL START====================");
log.info("callTime: {}", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT.format(new Date()));
log.info("requestUri: {}", getRequestUri());
log.info("method: {}", getMethod());
log.info("remoteAddr: {}", getRemoteAddr());
log.info("ip: {}", getIp());
log.info("requestHeaders: {}", getRequestHeaders());
log.info("requestParams: {}", getRequestParams());
log.info("status: {}", getStatus());
log.info("responseHeaders: {}", getResponseHeaders());
log.info("responseData: {}", getResponseData());
log.info("resolveTime: {}", getResolveTime());
log.info("====================HTTP CALL FINISH====================");
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("====================HTTP CALL START====================");
stringBuilder.append("callTime: ");
stringBuilder.append(DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT.format(new Date()));
stringBuilder.append(System.lineSeparator());
stringBuilder.append("requestUri: ");
stringBuilder.append(getRequestUri());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("method: ");
stringBuilder.append(getMethod());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("remoteAddr: ");
stringBuilder.append(getRemoteAddr());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("ip: ");
stringBuilder.append(getIp());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("requestHeaders: ");
stringBuilder.append(getRequestHeaders());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("requestParams: ");
stringBuilder.append(getRequestParams());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("status: ");
stringBuilder.append(getStatus());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("responseHeaders: ");
stringBuilder.append(getResponseHeaders());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("responseData: ");
stringBuilder.append(getResponseData());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("resolveTime: ");
stringBuilder.append(getResolveTime());
stringBuilder.append(System.lineSeparator());
stringBuilder.append("====================HTTP CALL FINISH====================");
return stringBuilder.toString();
}
}HttpLogEntity 构造器
java
package com.xiaoye.web.log;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.Map;
import java.util.Objects;
/**
* HttpLogEntity 构造器
*/
public class HttpLogEntityBuilder {
/**
* 构建 HTTP 日志对象
*
* @param requestWrapper
* @param responseWrapper
* @param stopWatch
* @return
*/
public static HttpLogEntity build(ContentCachingRequestWrapper requestWrapper, ContentCachingResponseWrapper responseWrapper, StopWatch stopWatch) {
HttpLogEntity httpLogEntity = new HttpLogEntity();
httpLogEntity.setRequestUri(requestWrapper.getRequestURI())
.setMethod(requestWrapper.getMethod())
.setRemoteAddr(requestWrapper.getRemoteAddr())
.setIp(getIpAddress(requestWrapper))
.setRequestHeaders(getRequestHeaderMap(requestWrapper));
if (requestWrapper.getMethod().equals(RequestMethod.GET.name())) {
httpLogEntity.setRequestParams(JSON.toJSONString(requestWrapper.getParameterMap()));
} else {
httpLogEntity.setRequestParams(new String(requestWrapper.getContentAsByteArray()));
}
String responseContentType = responseWrapper.getContentType();
if (StringUtils.equals("application/json;charset=UTF-8", responseContentType)) {
httpLogEntity.setResponseData(new String(responseWrapper.getContentAsByteArray()));
} else {
httpLogEntity.setResponseData("Stream Body...");
}
httpLogEntity.setStatus(responseWrapper.getStatusCode())
.setResponseHeaders(getResponseHeaderMap(responseWrapper))
.setResolveTime(stopWatch.toString());
return httpLogEntity;
}
/**
* 获取IP地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
/**
* 获取请求头MAP
*
* @param request
* @return
*/
public static Map<String, String> getRequestHeaderMap(HttpServletRequest request) {
Map<String, String> result = Maps.newHashMap();
if (Objects.nonNull(request)) {
Enumeration<String> headerNames = request.getHeaderNames();
if (Objects.nonNull(request)) {
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
result.put(headerName, headerValue);
}
}
}
return result;
}
/**
* 获取响应头MAP
*
* @param response
* @return
*/
public static Map<String, String> getResponseHeaderMap(HttpServletResponse response) {
Map<String, String> result = Maps.newHashMap();
if (Objects.nonNull(response)) {
String contentType = response.getContentType();
result.put("contentType", contentType);
}
return result;
}
}HttpLogFilter
java
package com.xiaoye.web.log;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 打印 HTTP 调用日志过滤器,使用者可以按需将其注入到过滤器容器中使用
* 这里只提供基础的过滤实现
*/
@WebFilter(filterName = "httpLogFilter")
@Slf4j
@Order(Integer.MAX_VALUE)
public class HttpLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
StopWatch stopWatch = StopWatch.createStarted();
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
filterChain.doFilter(requestWrapper, responseWrapper);
HttpLogEntity httpLogEntity = HttpLogEntityBuilder.build(requestWrapper, responseWrapper, stopWatch);
httpLogEntity.print();
// 重要,不然流会被关闭,无法返回数据
responseWrapper.copyBodyToResponse();
}
}Date2StringSerializer
java
package com.xiaoye.web.serializer;
import cn.hutool.core.date.DateUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.Date;
import java.util.Objects;
/**
* Date 转 String 的 JSON 序列化器
* 用于返回实体 Date 类型字段的自动序列化
*/
public class Date2StringSerializer extends JsonSerializer<Date> {
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (Objects.isNull(date)) {
jsonGenerator.writeString(StringUtils.EMPTY);
} else {
jsonGenerator.writeString(DateUtil.formatDateTime(date));
}
}
}IdEncryptSerializer
java
package com.xiaoye.web.serializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.xiaoye.core.utils.IdUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.Objects;
/**
* Id 自动加密的 JSON 序列化器
* 用于返回实体 Long 类型 ID 字段的自动序列化
*/
public class IdEncryptSerializer extends JsonSerializer<Long> {
/**
* Method that can be called to ask implementation to serialize values of type this serializer handles.
*
* @param value Value to serialize; can <b>not</b> be null.
* @param gen Generator used to output resulting Json content
* @param serializers Provider that can be used to get serializers for
* serializing Objects value contains, if any.
*/
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (Objects.isNull(value)) {
gen.writeString(StringUtils.EMPTY);
} else {
gen.writeString(IdUtil.encrypt(value));
}
}
}server
java
package com.xiaoye.server;
import com.xiaoye.core.constants.Constants;
import com.xiaoye.core.response.R;
import io.swagger.annotations.Api;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
/**
* @author zhangzhengyang
* @ClassName com.xiaoye.server
*/
@SpringBootApplication(scanBasePackages = Constants.BASE_COMPONENT_SCAN_PATH)
@ServletComponentScan(basePackages = Constants.BASE_COMPONENT_SCAN_PATH)
@EnableTransactionManagement // 开启事务管理
@MapperScan(basePackages = Constants.BASE_COMPONENT_SCAN_PATH + ".server.modules.**.mapper")
@EnableAsync // 开启异步任务
//@RestController
//@Api("测试接口类")
//@Validated
public class ServerLauncher {
public static void main(String[] args) {
SpringApplication.run(ServerLauncher.class);
}
@GetMapping("/hello")
public R<String> hello(@NotBlank(message = "name 不能为空") String name) {
System.out.println(Thread.currentThread().getContextClassLoader());
return R.success("hello " + name + "!!");
}
}启动监听器
java
package com.xiaoye.server.common.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
/**
* 项目启动成功日志打印监听器
*/
@Component
@Slf4j
public class startedListener implements ApplicationListener<ApplicationReadyEvent> {
/**
* 项目启动成功将会在日志中输出对应的启动信息
*
* @param applicationReadyEvent
*/
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
String serverPort = applicationReadyEvent.getApplicationContext().getEnvironment().getProperty("server.port");
String serverUrl = String.format("http://%s:%s", "127.0.0.1", serverPort);
log.info(AnsiOutput.toString(AnsiColor.BRIGHT_BLUE, "pan server started at: ", serverUrl));
if (checkShowServerDoc(applicationReadyEvent.getApplicationContext())) {
log.info(AnsiOutput.toString(AnsiColor.BRIGHT_BLUE, "pan server's doc started at:", serverUrl + "/doc.html"));
}
log.info(AnsiOutput.toString(AnsiColor.BRIGHT_YELLOW, "pan server has started successfully!"));
}
/**
* 判断是否开启了接口文档
*
* @param applicationContext
* @return
*/
private boolean checkShowServerDoc(ConfigurableApplicationContext applicationContext) {
return applicationContext.getEnvironment().getProperty("swagger2.show", Boolean.class, true)
&& applicationContext.containsBean("swagger2Config");
}
}定义全局异常处理器
java
package com.xiaoye.web.exception;
import com.xiaoye.core.exception.BusinessException;
import com.xiaoye.core.exception.FrameworkException;
import com.xiaoye.core.response.R;
import com.xiaoye.core.response.ResponseCode;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class WebExceptionHandler {
@ExceptionHandler(value = BusinessException.class)
public R BusinessExceptionHandler(BusinessException e) {
return R.fail(e.getCode(), e.getMessage());
}
/**
* @Validated 注解的参数校验异常
* @param e
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 获取第一个校验失败的异常
ObjectError objectError = e.getBindingResult().getAllErrors().stream().findFirst().get();
return R.fail(ResponseCode.ERROR_PARAM.getCode(), objectError.getDefaultMessage());
}
/**
* @Valid 注解的参数校验异常
* @param e
* @return
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public R constraintDeclarationExceptionHandler(ConstraintViolationException e) {
ConstraintViolation<?> constraintViolation = e.getConstraintViolations().stream().findFirst().get();
return R.fail(ResponseCode.ERROR_PARAM.getCode(), constraintViolation.getMessage());
}
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public R missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) {
return R.fail(ResponseCode.ERROR_PARAM);
}
@ExceptionHandler(value = IllegalStateException.class)
public R illegalStateExceptionHandler(IllegalStateException e) {
return R.fail(ResponseCode.ERROR_PARAM);
}
@ExceptionHandler(value = BindException.class)
public R bindExceptionHandler(BindException e) {
FieldError fieldError = e.getBindingResult().getFieldErrors().stream().findFirst().get();
return R.fail(ResponseCode.ERROR_PARAM.getCode(), fieldError.getDefaultMessage());
}
// 框架异常
@ExceptionHandler(value = FrameworkException.class)
public R FrameworkExceptionHandler(FrameworkException e) {
return R.fail(ResponseCode.ERROR.getCode(), e.getMessage());
}
// 其他异常
@ExceptionHandler(value = RuntimeException.class)
public R runtimeExceptionHandler(RuntimeException e) {
return R.fail(ResponseCode.ERROR.getCode(), e.getMessage());
}
}distribution 打包模块
code
mvn clean install配置 IDEA 热部署
xml
<!-- 热部署插件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>数据库预热脚本
对于 InnoDB 数据库,可通过以下脚本生成预热 SQL 语句,再执行这些语句来预热:
sql
SELECT DISTINCT
CONCAT('SELECT ',ndxcollist,' FROM ',db,'.',tb,
' ORDER BY ',ndxcollist,';') SelectQueryToLoadCache
FROM
(
SELECT
engine,table_schema db,table_name tb,
index_name,GROUP_CONCAT(column_name ORDER BY seq_in_index) ndxcollist
FROM
(
SELECT
B.engine,A.table_schema,A.table_name,
A.index_name,A.column_name,A.seq_in_index
FROM
information_schema.statistics A INNER JOIN
(
SELECT engine,table_schema,table_name
FROM information_schema.tables WHERE
engine='InnoDB'
) B USING (table_schema,table_name)
WHERE B.table_schema NOT IN ('information_schema','mysql')
ORDER BY table_schema,table_name,index_name,seq_in_index
) A
GROUP BY table_schema,table_name,index_name
) AA
ORDER BY db,tb;将上述脚本保存为 MakeSelectQueriesToLoad.sql,然后执行:
bash
mysql -uroot -AN < /root/MakeSelectQueriesToLoad.sql > /root/SelectQueriesToLoad.sql每次重启数据库或整库备份前需要预热时,执行:
bash
mysql -uroot < /root/SelectQueriesToLoad.sql > /dev/null 2>&1版本差异(项目创建 → 当前)
| 特性 | 旧版 | 当前 |
|---|---|---|
| Spring Boot | 2.1.9.RELEASE | 3.5.x(jakarta) |
| JDK | 1.8 | 21 |
| 唯一索引 | 唯一索引 + 异常处理 | 不变;可配合分布式锁(Redis/ZK)避免并发插入 |
| 数据库 | MySQL 8.0 | 8.0/8.4 LTS |
用户模块设计(唯一索引兜底 + 业务异常处理)思路不变;升级时注意 jakarta 包名迁移。