概述
使用的是springboot2.x版本。
记录springboot项目中常见的配置和功能实现。
Mybatisplus
官网地址:https://baomidou.com/
使用mybatisplus
导入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
xml路径配置
mybatisplus默认的路径是:classpath*:/mapper/**/*.xml。
这个路径表示在classpath下寻找mapper目录及其所有子目录下的所有xml文件
根据自己的xml位置去选择是否配置,否则不用配置,使用默认路径即可。
classpath包括两个路径,第一个是src/main/resources,第二个是src/main/java。
classpath*会递归查询类路径的子目录,classpath则不会。
** 表示递归地匹配任意多个子目录。
所以mybatisplus默认会递归查询类路径下的mapper包中所有子目录的xml文件。
需要注意的是,一般情况下我们会把mapper包放在另一个文件夹中,这个时候默认的路径就会找不到xml,需要将路径改成
xml文件模板
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.StudentMapper">
</mapper>
mapper标签的namespace用来给xml指定关联的mapper接口
Invalid bound statement (not found)的坑
很多人都碰过这个问题。
第一种:打的文件包taget里没有xml
在pom文件的build标签下指定打包的资源
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
然后用maven的clean把原来的target包删掉,再用下面的install重新打包
在新的target中看到了xml
第二种:配置的xml路径有问题。
配置全局字段
@Configuration
public class MybatisPlusConfig implements MetaObjectHandler {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createdAt", new Timestamp(System.currentTimeMillis()), metaObject);
System.out.println(metaObject);
this.setFieldValByName("updateAt", new Timestamp(System.currentTimeMillis()), metaObject);
System.out.println(metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateAt", new Timestamp(System.currentTimeMillis()), metaObject);
System.out.println(metaObject);
}
}
主键生成策略
mybatisplus的主键生成方式有以下几种:
1 主键自增,但数据库里面也要设置成自增
2 …
可查看这篇blog:小书MybatisPlus第6篇-主键生成策略精讲
具体看下面源码
@Getter
public enum IdType {
/**
* 数据库ID自增
* <p>该类型请确保数据库设置了 ID自增 否则无效</p>
*/
AUTO(0),
/**
* 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
*/
NONE(1),
/**
* 用户输入ID
* <p>该类型可以通过自己注册自动填充插件进行填充</p>
*/
INPUT(2),
/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 分配ID (主键类型为number或string),
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
*
* @since 3.3.0
*/
ASSIGN_ID(3),
/**
* 分配UUID (主键类型为 string)
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
*/
ASSIGN_UUID(4);
private final int key;
IdType(int key) {
this.key = key;
}
}
字段的全局配置
mybatis-plus:
global-config:
db-config:
# 主键自增
id-type: auto
# 逻辑删除使用的字段
logic-delete-field: deleted
# 没删用0表示
logic-not-delete-value: 0
# 删了用1表示
logic-delete-value: 1
逻辑删除
局部:
使用Mybatisplus的BaseMapper的deleteById
全局:
mybatis-plus:
global-config:
db-config:
# 逻辑删除使用的字段
logic-delete-field: deleted
# 没删用0表示
logic-not-delete-value: 0
# 删了用1表示
logic-delete-value: 1
再在相应的字段使用@TableLogic
统一数据返回体
@Data
@NoArgsConstructor
public class ResponseResult<T> {
private Integer code;
private String message;
private T data;
private static <T> ResponseResult<T> buildResponse(Integer code, String message, T data){
ResponseResult<T> response = new ResponseResult<>();
response.setCode(code);
response.setMessage(message);
response.setData(data);
return response;
}
public static <T> ResponseResult<T> success(T data){
return buildResponse(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getMsg(), data);
}
public static <T> ResponseResult<T> success(Integer code, String message, T data){
return buildResponse(code, message, data);
}
public static <T> ResponseResult<T> error(){
return buildResponse(ResponseEnum.SERVICE_ERROR.getCode(), ResponseEnum.SERVICE_ERROR.getMsg(), null);
}
public static <T> ResponseResult<T> error(Integer code, String message, T data){
return buildResponse(code, message, data);
}
}
@Getter
@AllArgsConstructor
public enum ResponseEnum {
/**
* 响应的枚举类
*/
SUCCESS(200, "响应成功"),
SERVICE_ERROR(500, "服务端响应异常");
private final Integer code;
private final String msg;
}
统一异常处理
在这里可以通过@ExceptionHandler处理指定的异常
例子1:
@Slf4j
@RestControllerAdvice(basePackages = "com.zou.metabox")
public class ExceptionControllerAdvice {
/**
* 处理验证异常的方法
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseResult<Map<String,String>> handlerValidExecption(MethodArgumentNotValidException e){
Map<String,String> map = new HashMap<>();
e.getFieldErrors().forEach(fieldError-> map.put(fieldError.getField(),fieldError.getDefaultMessage()));
return ResponseResult.error(BusinessCodeEnum.VALID_EXCEPTION.getCode(), BusinessCodeEnum.VALID_EXCEPTION.getMsg(), map);
}
/**
* 系统其他的异常处理
*/
@ExceptionHandler(Throwable.class)
public ResponseResult<String> handlerExecption(Throwable throwable){
log.error("错误信息:",throwable);
return ResponseResult.error(BusinessCodeEnum.UNKNOW_EXCEPTION.getCode(), BusinessCodeEnum.UNKNOW_EXCEPTION.getMsg(), throwable.getMessage());
}
/**
* 指定异常去处理,这里处理了自己定义的异常
*/
@ExceptionHandler(value = MyException.class)
public ResponseResult<Map<String, String>> handlerMyException(MyException e){
return ResponseResult.error(BusinessCodeEnum.MYEXCEPTION.getCode(), BusinessCodeEnum.MYEXCEPTION.getMsg(), null);
}
}
例子2:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.ArrayList;
import java.util.List;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ValidationErrorResponse handleValidationException(MethodArgumentNotValidException ex) {
List<String> details = new ArrayList<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
details.add(error.getField() + ": " + error.getDefaultMessage());
}
return new ValidationErrorResponse("Validation failed", details);
}
}
public class ValidationErrorResponse {
private String message;
private List<String> details;
// constructors, getters, and setters
}
自定义异常
feign异常处理
@Configuration
@Slf4j
public class FeignExceptionConfiguration {
@Bean
public ErrorDecoder errorDecoder() {
return new UserErrorDecoder();
}
public static class UserErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
ObjectMapper mapper = new ObjectMapper();
String message = null;
try {
String responseBody = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
JsonNode rootNode = mapper.readTree(responseBody);
ErrorResponse errorResp = new ErrorResponse();
errorResp.setStatus(rootNode.get("status").asInt());
errorResp.setError(rootNode.get("error").asText());
errorResp.setPath(rootNode.get("path").asText());
// 获取 message 字段的值
String messageText = rootNode.get("message").asText();
String messageStr = messageText.substring(messageText.indexOf("{"), messageText.lastIndexOf("}") + 1);
ErrorMessage errorMessage = mapper.readValue(messageStr, ErrorMessage.class);
errorResp.setMessage(errorMessage);
message = errorResp.getMessage().getMessage();
} catch (Exception e) {
log.error("服务出现异常,但异常解析失败,详情:{}", e.getMessage(), e);
message = "服务异常";
}
return new RuntimeException(message);
}
}
@Data
public static class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private ErrorMessage message;
private String path;
}
@Data
public static class ErrorMessage {
private int errorCode;
private String systemId;
private String message;
}
}
参数校验
在Spring Boot中,可以使用JSR-303标准的注解来进行接口参数校验。以下是一些常用的注解:
@NotNull:用于验证参数不能为null。
@NotEmpty:用于验证字符串参数不能为空。
@NotBlank:用于验证字符串参数不能为空且长度必须大于0。
@Min:用于验证数字参数的最小值。
@Max:用于验证数字参数的最大值。
@Size:用于验证字符串、集合或数组参数的长度或大小。
@Pattern:用于验证字符串参数是否匹配指定的正则表达式。
要在接口方法中使用参数校验,可以在方法参数上添加相应的注解。
日志
设置日志级别
在application.yml中设置
logging:
level:
root: INFO
com.zou.metabox.mapper: DEBUG
AOP日志
使用aop做日志记录,记录输入的参数名及参数值,并且记录接口响应结果。
package com.zou.metabox.common.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;
/**
* @author BIGSHU0923
* @description com.zou.metabox 中Controller层的的日志切面
* @since 7/30/2023 5:32 PM
*/
@Aspect
@Component
@Slf4j
public class LoggingAspect {
/**
* com.zou.metabox.controller 包中公共方法的切入点
*/
@Pointcut("execution(public * com.zou.metabox.controller.*.*(..))")
public void loggingPointcut(){
// 暂不用处理
}
/**
* 此方法用于在日志中记录请求和返回信息。
*
* @param pjp ProceedingJoinPoint对象,用于执行目标方法
* @return 目标方法的返回结果
* @throws Throwable 抛出异常时,将它传递给调用方
*/
@Around("loggingPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 获取类名
String className = pjp.getTarget().getClass().getTypeName();
// 获取方法名
String methodName = pjp.getSignature().getName();
// 获取参数名
String[] parameterNames = ((MethodSignature) pjp.getSignature()).getParameterNames();
Object result = null;
// 获取参数值
Object[] args = pjp.getArgs();
// 获取请求
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 获取请求的url
String url = request.getRequestURL().toString();
// 请求参数,以参数名和值为键值对
Map<String, Object> paramMap = new HashMap<>();
IntStream.range(0, parameterNames.length).forEach(i -> paramMap.put(parameterNames[i], args[i]));
// header参数
Enumeration<String> headerNames = request.getHeaderNames();
Map<String, Object> headerMap = new HashMap<>();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
headerMap.put(headerName, headerValue);
}
// 打印请求参数,记录起始时间
long start = System.currentTimeMillis();
// log.info("请求| 请求接口:{} | 类名:{} | 方法:{} | header参数:{} | 参数:{} | 请求时间:{}", url, className, methodName, headerMap, paramMap, LocalDateTime.now());
log.info("请求| 请求接口:{} | 类名:{} | 方法:{} | 参数:{} | 请求时间:{}", url, className, methodName, paramMap, LocalDateTime.now());
try {
result = pjp.proceed();
} catch (Exception e) {
log.error("返回| 处理时间:{} 毫秒 | 返回结果 :{}", (System.currentTimeMillis() - start), "failed");
throw e;
}
// 获取执行完的时间 打印返回报文
log.info("返回| 处理时间:{} 毫秒 | 返回结果 :{}", (System.currentTimeMillis() - start), "success");
return result;
}
}
接口请求的结果
Springboot事务
单数据源
配置单数据源
spring:
datasource:
url: jdbc:mysql://localhost:3306/xinyan
port: 3306
username: root
password: root
使用事务
代码:
下面这代码会更新,但是传入的id>5时,会抛出个异常,如果没有加事务,则不会回滚。
public void testRollback(Long id) {
MyRecord myRecord = new MyRecord();
myRecord.setId(id);
myRecord.setRecord("66666");
myRecordMapper.updateRecord(myRecord);
if(id > 5){
int a = 10/0;
}
}
使用事务前,使用事务后。
@Transactional(rollbackFor = Exception.class)
public void testRollback(Long id) {
MyRecord myRecord = new MyRecord();
myRecord.setId(id);
myRecord.setRecord("66666");
myRecordMapper.updateRecord(myRecord);
if(id > 5){
int a = 10/0;
}
}
如果抛异常后,会回滚更改。
多数据源
引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
配置多数据源
spring:
datasource:
dynamic:
primary: master
strict: true
lazy: true
datasource:
master:
url: jdbc:mysql://localhost:3306/xinyan
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/metabox
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
使用事务
@DSTransactional
// @Transactional(rollbackFor = Exception.class)
@DS("slave")
public void testRollback2(Integer id){
Student student = new Student();
student.setId(id);
student.setAge(100);
studentMapper.updateStudent(student);
if(id == 3){
int a = 10/0;
}
}
多数据源,所以使用@DSTransactional才有用,使用@Transactional(rollbackFor = Exception.class)没用
Springboot拦截器
概念
拦截器是基于Java反射机制实现的一种AOP思想,它可以在请求到达Controller层之前和之后对请求进行拦截。Spring Boot中使用HandlerInterceptor接口定义拦截器,在实现类中重写预定义好的方法来实现自己的业务逻辑。
拦截器的使用步骤如下:
1 创建一个实现了 HandlerInterceptor 接口的类,并实现该接口所需要的三个方法:preHandle(), postHandle() 和 afterCompletion()。
2 配置拦截器。在 Web MVC 配置文件中配置拦截器的 Bean。
3 定义拦截规则和拦截顺序。
可以通过以下示例来了解如何编写和配置拦截器:
public class MyInterceptor implements HandlerInterceptor {
// 在请求处理之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("Interceptor: preHandle method is running");
return true;
}
// 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("Interceptor: postHandle method is running");
}
// 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行。主要用于进行资源清理工作
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("Interceptor: afterCompletion method is running");
}
}
然后在配置类中添加 InterceptorRegistration 来注册拦截器,并指定其需要拦截的路径。addPathPatterns() 方法用于设置拦截的路径,而 excludePathPatterns() 方法用于设置不拦截的路径。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(interceptor);
registration.addPathPatterns("/**"); //需要拦截的路径
registration.excludePathPatterns("/login"); //不需要拦截的路径
}
}
Springboot监听器
概念
过滤器是Servlet中的组件,它可以对HTTP请求进行过滤处理。在 Spring Boot 中使用Filter接口来实现过滤器,在实现类中重写 doFilter() 方法以实现自己的业务逻辑。
过滤器的使用步骤如下:
创建一个实现了 Filter 接口的类,并实现 doFilter() 方法。
在 Configuration 类中注册该 Filter。
下面是一个简单的过滤器示例:
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter: init method is running ");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter: doFilter method is running");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("Filter: destroy method is running");
}
}
然后在配置类中添加 FilterRegistrationBean 来注册过滤器。
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean myFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new MyFilter());
registration.addUrlPatterns("/*");
registration.setName("MyFilter");
registration.setOrder(1);
return registration;
}
}
Springboot控制器
控制器是Spring MVC中的组件,它可以接收用户的HTTP请求,并将请求交给相应的Service进行业务处理,最终返回客户端所需的结果。在 Spring Boot 中,我们通常使用 @Controller 或 @RestController 注解来标注我们的控制器类。
下面是一个简单的控制器示例:
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public List<User> getUsers() {
return userService.getUsers();
}
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
@PutMapping("/users/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
return userService.updateUser(id, user);
}
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}
在上述示例中,我们定义了一个 UserController 类,并使用 @RestController 注解标注它。我们使用 @RequestMapping 注解来指定该控制器的路由前缀,进而可以通过 /api/users、/api/users/{id} 等 URL 访问到对应的控制器方法。
Springboot swagger
导入依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
创建配置类
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.zou.metabox.controller"))
.paths(PathSelectors.any())
.build();
}
/**
* 构建api文档的详细信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 设置页面标题
.title("Spring Boot集成Swagger2接口总览")
// 设置接口描述
.description("metabox")
// 设置联系方式
.contact("123456")
// 设置版本
.version("1.0")
// 构建
.build();
}
}
启动服务,然后输入http://localhost:8001/metaBox/swagger-ui.html
后面的/swagger-ui.html是固定的,前面的http://localhost:8001/metaBox则根据自己项目配置的端口号和上下文路径去决定。我的项目配置的端口号是8001,上下文路径是/metaBox
cors跨域配置
@Configuration
public class CorsWebFilterConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("http://localhost:8080");
corsConfiguration.addAllowedOrigin("http://localhost:8082");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig()); // 4
return new CorsFilter(source);
}
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new UserIdArgumentResolver());
}
};
}
}