模板大致介绍
这是一个基于 Java SpringBoot 的项目初始模板,整合了常用框架和主流业务的示例代码,来减少平常开发项目时所做的重复工作,并可以使用 Swagger + Knife4j 接口文档来调试接口功能。
所有需要修改的地方最好都标记 todo
,便于后续开发找到修改的位置~
主流框架 & 特性
- Spring Boot 2.7.x (包含调试工具和项目处理器)
- Spring MVC
- MyBatis + MyBatis Plus 数据访问
- Spring AOP 切面编程
本篇功能模块
- 自定义权限注解 + 全局校验
- 全局请求响应拦截器(记录日志)
- 通用响应类、自定义错误码
- 配置类(框架配置、全局跨域处理配置)
- 全局异常处理器
- 单元测试
本篇功能模块详细介绍
自定义注解(Annotation)
(package annotation)
@Target(ElementType.METHOD) // 表示这个注解只能应用于方法上
@Retention(RetentionPolicy.RUNTIME) // 表示这个注解的元数据信息在运行时可用
public @interface AuthCheck {
String mustRole() default "";
...
}
实现原理:
这个注解使用了 Java 1.8 引入的注解处理器(Annotation Processor)功能。
是通过 Java 编译器(Javac)在编译时处理注解,将注解信息添加到类或方法上。在运行时,可以通过反射(Reflection)获取注解的元数据信息。
用途:
用于检查方法调用者是否具有某个角色,通常用于身份验证和权限控制。通过在方法上添加这个注解,可以确保只有具有指定角色的用户才能调用该方法。
定义注解通常涉及的步骤:
- 定义注解:使用
@interface
关键字来声明一个新的注解类型。
@interface
和传统接口定义的语法非常相似,但实际上它定义的是一个注解,而不是一个接口。 - 设置注解参数:在注解中定义方法,这些方法实际上是注解的参数。如
String mustRole() default "";
- 指定注解的元注解:使用元注解(例如
@Retention
和@Target
)来指定注解的保留策略和适用目标。
AOP 模块
权限校验
当一个方法被调用且该方法上有@AuthCheck
注解时,Spring AOP会触发doInterceptor
方法。
@Aspect
@Component
public class AuthInterceptor {
@Resource
private UserService userService;
/**
* 执行拦截
*
* @param joinPoint
* @param authCheck
* @return
*/
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
// 获取调用当前方法所需要的调用者角色
String mustRole = authCheck.mustRole();
UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
// 使用RequestContextHolder获取当前请求的属性,然后从中获取HttpServletRequest对象。
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 当前登录用户
User loginUser = userService.getLoginUser(request);
// 调用当前方法不需要权限,放行
if (mustRoleEnum == null) {
return joinPoint.proceed();
}
// 必须有该权限才通过,
UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());
...权限校验逻辑
// 通过权限校验,放行
return joinPoint.proceed();
}
}
@Aspect
:标记该类是一个切面,定义了切面逻辑。
@Resource
:注入一个服务对象,这里是UserService,用于获取当前登录用户的信息。
@Around("@annotation(authCheck)")
:定义一个环绕通知,当目标方法上有@AuthCheck
注解时,会执行这个切面方法。
ProceedingJoinPoint
:提供对被拦截方法的访问。
AuthCheck authCheck
:传入目标方法上的@AuthCheck注解实例。
RequestContextHolder.currentRequestAttributes()
:这是Spring框架提供的一个方法,用于获取当前请求的属性,包含了当前请求的一些基本信息,如请求的URL、请求方法、请求头等。
请求日志
实现了一个日志拦截器,可以记录请求的详细信息和方法执行的时间。这对于调试和监控应用的性能非常有用。
@Aspect
@Component
@Slf4j
public class LogInterceptor {
/**
* 执行拦截
*/
@Around("execution(* ...controller.*.*(..))")
public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {
// 计时
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 获取请求路径
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
String url = httpServletRequest.getRequestURI();
// 生成请求唯一 id
String requestId = UUID.randomUUID().toString();
// 获取请求参数
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;
}
}
@Slf4j
:SLF4J(Simple Logging Facade for Java)库,使用 Lombok 的注解,自动生成一个 Logger 实例,用于日志记录。
logo.info
:在Java中,{}是字符串格式化的一种方式,称为占位符。当你在字符串中使用{}时,Java编译器会自动将其替换为对应格式的值。所以当运行这段代码时,request start,id: {}, path: {}, ip: {}, params: {}
中的{}会被替换为requestId
、url
、httpServletRequest.getRemoteHost()
和reqParam
的值。
通用类模块
通用响应类
每个请求的响应值增加 code、data、message 三个字段来丰富请求的执行状况给前端:
public BaseResponse(int code, T data) {
this(code, data, "");
}
public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage());
}
code:为了强化状态码与业务的相关性并更友好地提示错误信息,因此除了复用 HTTP 的状态码之外还自定义了 ErrorCode
类将更详细的错误状态码与错误信息 message
绑定。
message:一些请求成功或失败的额外提示信息。
请求结果封装类
对请求执行成功和各类失败情况响应信息的静态方法的封装,主要是为了简化调用。例如:
public class ResultUtils {
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "success/200/...");
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}
...
}
配置类模块
配置类在Spring 项目中的作用:
- 集中配置管理:配置类将配置和初始化逻辑集中在一个地方,便于管理和维护。配置参数可以从外部配置文件读取,易于修改和扩展。
- 依赖注入:通过使用@Bean注解,配置类定义的 Bean 可以被 Spring 容器管理,并在需要的地方通过依赖注入(如
@Autowired
)获取,促进了松耦合设计。 - 多环境:通过
@ConfigurationProperties
,可以轻松地根据不同的环境(开发、测试、生产)使用不同的配置文件,实现环境的快速切换。
包含的类
MyBatisPlusConfig - - 配置一个拦截器和分页插件等,除此之外还有乐观锁等插件。MyBatisPlus
JsonConfig - - 在将 Java 对象转换为 JSON 字符串时,通过将 Long 类型的对象将转换为字符串,从而避免精度丢失的问题。
CorsConfig - - 解决全局跨域问题。
…
后续还需要添加一些业务的配置类,例如我们 AIAppMarket项目 中的 AI 配置类:
@Configuration
@ConfigurationProperties(prefix = "ai")
@Data
public class AiConfig {
/**
* api key,需要从开放平台获取
*/
private String apiKey;
@Bean
public ClientV4 getClientV4(){
return new ClientV4.Builder(apiKey).build();
}
}
全局异常处理
全局异常处理器能够捕获应用程序中抛出的异常,并对这些异常进行统一的处理, 从而避免因未处理的异常导致系统崩溃或无法正常工作,能够提高系统的稳定性。
1)错误信息统一处理:全局异常处理器可以将不同种类的异常转化为统一的错误信息格式,提供一致的错误响应给客户端,增强了用户体验。
2)错误日志记录:可以在全局异常处理器中记录异常信息,包括异常类型、发生时间、请求参数等,有助于排查问题和分析系统健康状况。
3)异常信息隐藏:通过全局异常处理器,可以隐藏敏感信息,以防止敏感信息泄露到客户端。
实现方法:BusinessException、GlobalExceptionHandler和ThrowUtils
- 定义一个全局异常处理类
GlobalExceptionHandler
,并且使用@RestControllerAdvice
(或者@ControllerAdvice
)注解该类,表示这是一个全局异常处理器类。
然后针对每种异常类定义一个处理异常的方法,并且使用@ExceptionHandler(异常类.class)
注解标注这些方法,可以在这些方法中进行日志记录等具体的异常处理操作,并返回一个响应对象,便于前端识别并给用户友好的错误提示。例如:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
log.error("BusinessException", e);
return ResultUtils.error(e.getCode(), e.getMessage());
}
...
}
- 针对定义的异常处理方法编写封装类
- 编写异常抛出类,针对各类特定的条件抛出异常
工具类
NetUtils
获取客户端的 IP 地址,并处理一些特殊情况,例如 unknown 或 127.0.0.1 。
SpringContextUtils
提供用于通过名称、类型、名称和类型获取 Spring 上下文的容器的方法。
SqlUtils
校验排序字段是否合法(防止 SQL 注入)
多环境 yml
为什么需要?鱼皮多环境介绍
# 公共配置文件
# @author xht
#
spring:
application:
name: ....
# 默认 dev 环境
profiles:
active: dev
# 支持 swagger3
mvc:
pathmatch:
matching-strategy:
# session 配置
session:
# todo 取消注释开启分布式 session(须先配置 Redis)
# store-type: redis
# 30 天过期
timeout: 2592000
# 数据库配置
# todo 需替换配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/....
username: ....
password: ....
# Redis 配置
# todo 需替换配置,然后取消注释
redis:
database: 1
host: localhost
port: 6379
timeout: 5000
# password:
# 文件上传
....
# 分库分表配置
....
server:
....
mybatis-plus:
....
# 对象存储
# todo 需替换配置
cos:
client:
accessKey: xxx
secretKey: xxx
region: xxx
bucket: xxx
# 接口文档配置
knife4j:
....
其他配置....