{T}

创建项目

用户模块技术点

全局唯一用户名称 唯一索引+特定异常处理

全局用户信息上下文的搭建 拦截器+ 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 Boot2.1.9.RELEASE3.5.x(jakarta)
JDK1.821
唯一索引唯一索引 + 异常处理不变;可配合分布式锁(Redis/ZK)避免并发插入
数据库MySQL 8.08.0/8.4 LTS

用户模块设计(唯一索引兜底 + 业务异常处理)思路不变;升级时注意 jakarta 包名迁移。