02-后端万用模板
1. 模块概况
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(封号)三种情况
@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才能生效
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
在微信开放平台获取 appId
、appSecret
等配置后,在 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 的定时任务控制注解
应用场景:
- 统计 Top10 的接口调用次数(API接口),在数据库量大后,每个用户去发送请求获取 Top10 的接口调用次数会造成数据库请求压力过大,此时可以写个定时任务,可以定时 24h,每天将 Top10 的接口调用次数同步到 Redis,以接口名称和键以接口调用次数为值保存即可。实时性要求不太强的功能,可以采用定时任务
- 某个 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, "=", "(", ")", " ");
}
}