02-后端万用模板

1. 模块概况

NlRzUFk0eTRzODNuMEczTm9HMzJMR0J6OGU3SlJBYzBHL1NLdjlZPQ==

2. 重点模块

1. 全局项目配置

Mysql、Redis 配置文件

spring:
  # Mysql 配置
  # todo 需替换配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://listao.cn:20003/generator
    username: root
    password: ooxx$123
  # Redis 配置
  # todo 需替换配置,然后取消注释
  redis:
    database: 1
    host: localhost
    port: 6379
    timeout: 5000
    password: 123456

Redis 启用

/**
 * 主类(项目启动入口)
 */
// todo 如需开启 Redis,须移除 exclude 中的内容
@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
@MapperScan("com.listao.boot_init.mapper")
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }

}




 










2. 全局请求、鉴权拦截器

1. AuthInterceptor

权限校验机制,判断用户的 role 是否为管理员、用户、ban(封号)三种情况

VzAxc3RodnpWdXBGc2ZoejRBRmJnV0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  • @AuthCheck 自定义注解,然后写上使用该方法需要的权限
  • @Around 环绕通知,进行权限校验
@PostMapping("/add")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest, HttpServletRequest request) {
    if (userAddRequest == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    User user = new User();
    BeanUtils.copyProperties(userAddRequest, user);
    // 默认密码 12345678
    String defaultPassword = "12345678";
    String encryptPassword = DigestUtils.md5DigestAsHex((SALT + defaultPassword).getBytes());
    user.setUserPassword(encryptPassword);
    boolean result = userService.save(user);
    ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
    return ResultUtils.success(user.getId());
}

 














2. LogInterceptor

请求日志拦截器,用于输出请求日志

  • @Around 是环绕通知,用了切入点表达式,要拦截哪个包或哪些包下面的哪个方法或全部方法
  • 切入点表达式就是对 com.yupi.springbootinit.controller 中所有方法进行拦截。控制层执行方法就会打印日志进行输出
/**
 * 请求响应日志 AOP
 **/
@Aspect
@Component
@Slf4j
public class LogInterceptor {

    /**
     * 执行拦截
     */
    @Around("execution(* com.listao.boot_init.controller.*.*(..))")
    public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {
        // 计时
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 获取请求路径
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
        // 生成请求唯一 id
        String requestId = UUID.randomUUID().toString();
        String url = httpServletRequest.getRequestURI();
        // 获取请求参数
        Object[] args = point.getArgs();
        String reqParam = "[" + StringUtils.join(args, ", ") + "]";
        // 输出请求日志
        log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
                httpServletRequest.getRemoteHost(), reqParam);
        // 执行原方法
        Object result = point.proceed();
        // 输出响应日志
        stopWatch.stop();
        long totalTimeMillis = stopWatch.getTotalTimeMillis();
        log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
        return result;
    }
}











 

























3. 通用响应类

1. BaseResponse

/**
 * 通用返回类
 */
@Data
public class BaseResponse<T> implements Serializable {

    // 响应状态码
    private int code;

    // 响应数据
    private T data;

    // 成功、失败的额外信息
    private String message;
}

2. ResultUtils

主要用于简化 BaseResponse 的操作,将成功、失败的一些通用情况进行静态方法的封装

3. ErrorCode

枚举类将常规的响应状态码和响应信息进行封装

  • 无权限访问(40300)
  • 服务器内部异常(50000)
  • 接口调用失败(50003)
/**
 * 自定义错误码
 */
public enum ErrorCode {

    SUCCESS(0, "ok"),
    PARAMS_ERROR(40000, "请求参数错误"),
    NOT_LOGIN_ERROR(40100, "未登录"),
    NO_AUTH_ERROR(40101, "无权限"),
    NOT_FOUND_ERROR(40400, "请求数据不存在"),
    FORBIDDEN_ERROR(40300, "禁止访问"),
    SYSTEM_ERROR(50000, "系统内部异常"),
    OPERATION_ERROR(50001, "操作失败");

    /**
     * 状态码
     */
    private final int code;

    /**
     * 信息
     */
    private final String message;

}

4. 配置类

1. JsonConfig

自定义序列化和反序列JSON数据,Spring Boot 默认使用 JackSon 进行序列化和反序列化

/**
 * Spring MVC Json 配置
 */
@JsonComponent
public class JsonConfig {

    /**
     * 添加 Long 转 json 精度丢失的配置
     */
    @Bean
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        SimpleModule module = new SimpleModule();
        module.addSerializer(Long.class, ToStringSerializer.instance);
        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(module);
        return objectMapper;
    }
}
  • 精度丢失场景:id 在数据库是 BigInteger 类型,雪花算法生成 id 大于 17 位,因此在序列化的时候会产生精度丢失
  • @JsonComponent 作用
    • @Bean 覆盖组件后,重写逻辑代码,将包装类 Long 和基础数据类型 long 转化成字符串序列化成字符串防止在序列化的时候丢失精度。

2. Knife4jConfig

  • @Profile:指定环境配置,和配置文件的 spring.profiles.active 相互关联。因此只在开发环境、测试环境中生效
  • basePackage:指定要扫描 Controller 层,如果控制层的包名有所不同,需要进行修改,Knife4j才能生效
ZVR6K21KSVRuQnFzM2NxRUgyRDRwbUJ6OGU3SlJBYzBHL1NLdjlZPQ==

3. MyBatisPlusConfig

  • @MapperScan:指定扫描的 dao 层路径
  • @Bean 进行组件的注入,添加了分页插件
/**
 * MyBatis Plus 配置
 */
@Configuration
@MapperScan("com.yupi.web.mapper")
public class MyBatisPlusConfig {

    /**
     * 拦截器配置
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

4. CorsConfig

解决全局跨域配置问题,可以指定请求方法、是否允许发送 Cookie、放行哪些特定域名或 ip、允许哪些请求头

/**
 * 全局跨域配置
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 覆盖所有请求
        registry.addMapping("/**")
                // 允许发送 Cookie
                .allowCredentials(true)
                // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}

5. CosClientConfig

/**
 * 腾讯云对象存储客户端
 */
@Configuration
@ConfigurationProperties(prefix = "cos.client")
@Data
public class CosClientConfig {

    private String accessKey;

    private String secretKey;

    /**
     * 区域
     */
    private String region;

    /**
     * 桶名
     */
    private String bucket;

    @Bean
    public COSClient cosClient() {
        // 初始化用户身份信息(secretId, secretKey)
        COSCredentials cred = new BasicCOSCredentials(accessKey, secretKey);
        // 设置bucket的区域,COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
        ClientConfig clientConfig = new ClientConfig(new Region(region));
        // 生成cos客户端
        return new COSClient(cred, clientConfig);
    }
}
cos:
  client:
    accessKey: xxx
    secretKey: xxx
    region: xxx
    bucket: xxx

6. WxOpenConfig

在微信开放平台获取 appIdappSecret 等配置后,在 applicaiton.yml 中替换即可

/**
 * 微信开放平台配置
 */
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "wx.open")
@Data
public class WxOpenConfig {

    private String appId;

    private String appSecret;

    private WxMpService wxMpService;

    /**
     * 单例模式(不用 @Bean 是为了防止和公众号的 service 冲突)
     */
    public WxMpService getWxMpService() {
        if (wxMpService != null) {
            return wxMpService;
        }
        synchronized (this) {
            if (wxMpService != null) {
                return wxMpService;
            }
            WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
            config.setAppId(appId);
            config.setSecret(appSecret);
            WxMpService service = new WxMpServiceImpl();
            service.setWxMpConfigStorage(config);
            wxMpService = service;
            return wxMpService;
        }
    }
}
# 微信相关
wx:
  # 微信公众平台
  # todo 需替换配置
  mp:
    token: xxx
    aesKey: xxx
    appId: xxx
    secret: xxx
    config-storage:
      http-client-type: HttpClient
      key-prefix: wx
      redis:
        host: 127.0.0.1
        port: 6379
      type: Memory
  # 微信开放平台
  # todo 需替换配置
  open:
    appId: xxx
    appSecret: xxx

5. 全局异常处理

1. BusinessException

结合 ErrorCode 使用

/**
 * 自定义异常类
 */
public class BusinessException extends RuntimeException {

    /**
     * 错误码
     */
    private final int code;

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
    }

    // 常用方法,指定错误码、错误信息
    public BusinessException(ErrorCode errorCode, String message) {
        super(message);
        this.code = errorCode.getCode();
    }

    public int getCode() {
        return code;
    }
}

2. GlobalExceptionHandler

  • @RestControllerAdvice = @ControllerAdvie + @ResponseBody:捕获整个应用程序抛出的异常
  • @ExceptionHandler:指定什么异常需要被捕获
/**
 * 全局异常处理器
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public BaseResponse<?> businessExceptionHandler(BusinessException e) {
        log.error("BusinessException", e);
        return ResultUtils.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(RuntimeException.class)
    public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
        log.error("RuntimeException", e);
        return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
    }
}

3. ThrowUtils

一般用于请求参数的校验,如果请求参数为空,直接抛出业务异常,然后指明 ErroCode 和 message

/**
 * 抛异常工具类
 */
public class ThrowUtils {

    /**
     * 条件成立则抛异常
     */
    public static void throwIf(boolean condition, RuntimeException runtimeException) {
        if (condition) {
            throw runtimeException;
        }
    }

    /**
     * 条件成立则抛异常
     */
    public static void throwIf(boolean condition, ErrorCode errorCode) {
        throwIf(condition, new BusinessException(errorCode));
    }

    /**
     * 条件成立则抛异常
     */
    public static void throwIf(boolean condition, ErrorCode errorCode, String message) {
        throwIf(condition, new BusinessException(errorCode, message));
    }
}

6. 数据库和 ES 同步

IncSyncPostToEs:

  • @Component:如果取消注解,就将这个定时任务加入到 Spring 容器中,Spring Boot 启动类启动后将会开启这个定时任务
  • @Scheduled:Spring Boot 的定时任务控制注解

应用场景:

  1. 统计 Top10 的接口调用次数(API接口),在数据库量大后,每个用户去发送请求获取 Top10 的接口调用次数会造成数据库请求压力过大,此时可以写个定时任务,可以定时 24h,每天将 Top10 的接口调用次数同步到 Redis,以接口名称和键以接口调用次数为值保存即可。实时性要求不太强的功能,可以采用定时任务
  2. 某个 API 接口不用用户传参,而且大多数时间回复的调用结果都是相通的,那么就可以采取定时任务,将这些不用用户传参,并且调用结果都是相同的接口定时同步到Redis存储,相对于数据库的IO读取,Redis存储会大大提升这些接口的QPS,可以自己在简历上进行量化处理

7. 工具类

1. NetUtils

主要用于获取客户端的 IP 地址

/**
 * 网络工具类
 */
public class NetUtils {

    /**
     * 获取客户端 IP 地址
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
            if (ip.equals("127.0.0.1")) {
                // 根据网卡取本机配置的 IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (inet != null) {
                    ip = inet.getHostAddress();
                }
            }
        }
        // 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ip != null && ip.length() > 15) {
            if (ip.indexOf(",") > 0) {
                ip = ip.substring(0, ip.indexOf(","));
            }
        }
        if (ip == null) {
            return "127.0.0.1";
        }
        return ip;
    }

}

2. SpringContextUtils

通过名称、类型、名称、类型,获取 Spring 容器中 bean

/**
 * Spring 上下文获取工具
 */
@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    /**
     * 通过名称获取 Bean
     */
    public static Object getBean(String beanName) {
        return applicationContext.getBean(beanName);
    }

    /**
     * 通过 class 获取 Bean
     */
    public static <T> T getBean(Class<T> beanClass) {
        return applicationContext.getBean(beanClass);
    }

    /**
     * 通过名称和类型获取 Bean
     */
    public static <T> T getBean(String beanName, Class<T> beanClass) {
        return applicationContext.getBean(beanName, beanClass);
    }
}

3. SqlUtils

检查 SQL 注入问题

/**
 * SQL 工具
 */
public class SqlUtils {

    /**
     * 校验排序字段是否合法(防止 SQL 注入)
     */
    public static boolean validSortField(String sortField) {
        if (StringUtils.isBlank(sortField)) {
            return false;
        }
        return !StringUtils.containsAny(sortField, "=", "(", ")", " ");
    }
}