官网文档: https://docs.spring.io/spring-boot/docs/current/reference/html/
项目创建 可以选择在官网上选择好依赖并创建项目,之后下载下来使用 idea 打开即可。 官网地址:https://start.spring.io/
也可以创建普通的 Maven 项目,然后修改在 pom.xml
中使用 parent
标签引入 spring-boot-starter-parent
。
以下为创建一个 web 项目的依赖配置文件示例。时间:2023-04-25
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > site.chilisdy</groupId > <artifactId > demo-spring-boot</artifactId > <version > 1.0-SNAPSHOT</version > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.10</version > <relativePath /> </parent > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.32</version > </dependency > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 3.0.0</version > </dependency > </dependencies > </project >
在项目的根目录创建以下文件夹,如果不存在的话:
src/main/java
src/main/resources
创建主类:在上面的 java 文件夹右键填写 org.example.Main
,参考下方的代码,编写代码,然后右键即可运行 SpringBoot 项目。
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import site.chilisdy.Application;@SpringBootApplication public class Main { public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
HelloWorld 在 Main 类的同级创建 controller
包,当然你不叫这个名字也行,你可以随意定义。
在 controller
包内新建类 Hello
,并参考下方代码填写内容。
1 2 3 4 5 6 7 8 @RestController public class Hello { @GetMapping("/hello") public String hello () { return "hello" ; } }
现在回到 Main
类,右键 Run
,注意 idea 控制台刷新的消息,观察下 tomcat 的端口号,默认为 8080
1 2 3 ... Tomcat started on port(s): 8080 Started Application in 0.943 seconds (JVM running for 1.149)
打开浏览器访问:http://localhost:8080/hello
项目部署 在 pom.xml
配置文件中追加以下配置。然后在项目根目录运行命令:mvn clean package
,然后在项目根目录会出现一个 target
文件夹,里面有两个打包完成的文件:
后缀为:.jar
这个是包含所有项目依赖的包,可以使用:java -jar xxx.jar
直接运行
后缀为:.jar.original
这个是不包含项目依赖的包,无法运行,内部主要是用户写的类,用途是提供给其他项目依赖。
1 2 3 4 5 6 7 8 9 10 11 <project > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
基础概念 Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。 Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。
简而言之就是围绕 Spring 这一对象容器框架,官方帮你集成各种常见开发环境的第三方包。让你快速的进入开发业务,省去每个依赖包的单独配置工作,充分利用 Spring 对象管理和依赖注入。
和 Spring 容器打交道的常用注解有:@Component
、@Bean
、@Autowired
、@Qualifier
。 使用步骤也很简单:
把对象交给 Spring 容器
告诉 Spring 这里需要某个对象
使用对象
依赖注入的几种方式
直接在类的属性字段上使用 @Autowired
,让 Spring 注入。
在 setter 方法上使用 @Autowired
,让 Spring 注入。
在类上使用 @Component
标记,然后通过该类的构造函数给属性字段赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController public class GetContainerObj { @Autowired CustomBeanConfig customBeanConfig; private User user; @Autowired public void setUser (User user) { this .user = user; } public GetContainerObj (User user) { this .user = user; } }
不推荐直接注入方式。
依赖注入冲突 指当 Spring 容器内存在多个相同类型的对象时,直接使用 @Autowired
索要对象就会产生该错误,解决方式就是在使用 @Autowired
的基础上追加使用 @Qualifier("obj_name")
注解指定对象名称。
对象名称可以在使用 @Bean
时指定名称:@Bean("obj_name")
。
也可以在 @Bean
的基础上追加 @@Primary
注解,标记该 Bean 对象是其类型的默认对象。
常用注解
注解名称
作用
用处
@Component
标记类为组件类
类
@Controller
标记类为控制类,@Component 注解的别名
类
@Service
标记类为服务类,@Component 注解的别名
类
@Repository
标记类为仓库类,@Component 注解的别名
类
@Configuration
标记类为配置类,@Component 注解的别名
类
@ResponseBody
转换函数返回值为 JSON 类型
类、方法。在类上时对类内部所有的方法生效
@RestController
@Controller 和 @ResponseBody 的别名
类
@RequestMapping
在 @Controller 标记的类中的方法上使用,表示配置访问该方法的方式和网络路径。
方法
@PostMapping
等价于 @RequestMapping 的 method 参数写死为 RequestMethod.POST
方法
@PutMapping
等价于 @RequestMapping 的 method 参数写死为 RequestMethod.PUT
方法
@DeleteMapping
等价于 @RequestMapping 的 method 参数写死为 RequestMethod.DELETE
方法
@GetMapping
等价于 @RequestMapping 的 method 参数写死为 RequestMethod.GET
方法
@PatchMapping
等价于 @RequestMapping 的 method 参数写死为 RequestMethod.PATCH
方法
@Autowired
标记需要被 Spring 容器注入的属性或者方法
属性字段、方法
@Qualifier
必须和 @Autowired 一起使用,表示使用指定名称的对象,对象名由 @Bean 注解设置
属性字段、方法
@Bean
标记在方法上,把方法返回的对象纳入 Spring 容器中管理,可以指定对象的名称
方法
@JsonProperty
jackson 包提供的注解,当处理 Json 数据时,标记在类的属性上,解决属性字段名和 Json 的 key 名称不一致的问题。
属性字段
@ControllerAdvice
高级控制器,用于实现全局注册拦截器、错误处理等操作
类
@ExceptionHandler
标记在方法上,被标记的方法为本类中其他方法的错误处理函数。当在被 @ControllerAdvice 标记的类内部时,表示注册全局错误拦截器。
方法
@Mapper
MyBatis 提供的注解,被标记的类为数据库操作类
类
@MapperScan
在类上标记,通常和 @Configuration 注解一起使用,或者直接标记在项目启动的主类上,通过指定路径告知 MyBatis 去哪里扫描 Mapper 类,当项目中存在任意一个该注解,则 Spring 项目默认的 @MapperScan 配置将不生效。默认扫描整个项目
类
@SpringBootApplication
被标记的类为 SpringBoot 启动类。
类
@Primary
必须和 @Bean 一起使用,标记一个Bean 对象为主要对象(默认)可以解决依赖注入冲突问题
方法
参数接口配置 表单参数接口 使用 POST
方式传输 form 表单
形式的数据。
可以直接使用变量来对应前端 form 表单的 key 值。当不一致时可以使用 @RequestParam
注解设置形参对应的 form 表单 key。 也可以直接使用一个对象接受前端传入的参数,当 form 表单的 key 和对象的属性字段名不一致时,需要安装表单的 key 名称编写一个 setter
方法,比如:前端 form 表单 key 为 username,对象属性为 name,那么应该编写一个 public void setUsername()
方法给对象的类。
Spring 使用 getter/setter
方法来设置对象的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @PostMapping("/upload") public String upload (MultipartFile[] file, @RequestParam("username") String username, @RequestParam("password") String password) { } @PostMapping("/upload2") public String upload2 (UploadFileRequest request) { } class UploadFileRequest { private MultipartFile[] file; private String name; private String password; }
JSON 参数接口 使用 POST
方式传输 json
形式的数据。
使用 @RequestBody
注解接受前端传入的 JSON 数据。注解后面可以使用对应 JSON 数据类型的 数组
或者 Map
和 对象
。
⚠️注意: 如果使用了 @JsonProperty
注解来变更对象属性字段和 JSON 的 key 映射关系,那么被修饰的属性字段自己的 set 方法就会失效,即:你前端只能使用注解指定的 key 名称传递数据,使用属性字段自己的名字当作 key 会无法被赋值。 但是,如果你不使用 @JsonProperty
注解来变更对象属性字段和 JSON 的 key 映射关系。而是按照前端 JSON 传递过来的 key 名称编写 set 方法(参考上面表单数据接口,key 和属性不一致问题解决),那么两种 JSON key 值都可以正确的赋值给对象。比如下面 add3
的对象,如果没有 @JsonProperty
修饰 username 属性字段,并且你提供了一个 setName
方法,那么前端传递数据时,可以使用 name
或者 username
给你传递数据。传递的值都会赋值给对象的 username
字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @PostMapping("/add") public String getJSONData (@RequestBody Map<String, Object> body) { return "ok" ; } @PostMapping("/add2") public String getJSONData2 (@RequestBody Map<String, Object>[] body) { return "ok" ; } @PostMapping("/add3") public String getJSONData3 (@RequestBody JsonRequest jsonRequest) { return "ok" ; } class JsonRequest { @JsonProperty("name") private String username; @JsonProperty("pwd") private String password; private String email; }
URL-PATH 参数接口 在 URL 上传递数据。
1 2 3 4 5 6 @GetMapping("/get/{id}") public JsonResponse getUserByID (@PathVariable("id") int id) { String res = "user id: " + id; return JsonResponse.ok((Object) res); }
URL 参数接口 在 URL 上使用 ?
间隔,参数之间使用 &
符号相连的普通 GET
方式传递参数。
1 2 3 4 @GetMapping("/get") public String getUserByName (@RequestParam("name") String name) { return "user name: " + name; }
获取最原始的 Request 和 Response 1 2 3 4 @GetMapping("/test") public String test (HttpServletRequest request, HttpServletResponse response) { return "ok" ; }
使用注解 @RequestHeader
和 @CookieValue
分别获取请求头和 Cookie 中的数据。
1 2 3 4 5 6 @PostMapping("/add") public String addUser (@RequestBody Map<String, Object> body, @RequestHeader("token") String token, @CookieValue("login") String login) { return String.format("body: %s, token: %s, login: %s" , body, token, login); }
Cookie 操作 Cookie 对象所属的包:import javax.servlet.http.Cookie;
获取单个 Cookie 的值使用 @CookieValue
。
获取所有 Cookie 的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @GetMapping("/test1") public String test1 (@RequestParam("name") String name, @CookieValue("aaa") String value, HttpServletRequest request, HttpServletResponse response) { Cookie[] allCookies = request.getCookies(); for (Cookie cookie : allCookies) { System.out.println(cookie.getName() + ": " + cookie.getValue()); } Cookie newCookie = new Cookie ("client_user" , name); newCookie.setPath("/" ); newCookie.setMaxAge(60 * 60 * 24 * 7 ); response.addCookie(newCookie); return "test1" ; }
上传文件 修改最大上传文件大小配置:编辑 SpringBoot 的 application.yaml
配置文件
1 2 3 4 5 spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB
max-file-size
:单个文件的大小
max-request-size
:总上传文件大小
以多文件上传示例,兼容单文件上传。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @PostMapping("/upload") public JsonResponse upload (MultipartFile[] file, @RequestParam("username") String username, @RequestParam("password") String password) { System.out.println("username: " + username + ", password: " + password); String storagePath = "/Users/2f314fb/project/java/demo-spring-boot/upload/" ; try { for (MultipartFile f : file) { String fileName = f.getOriginalFilename(); System.out.println(storagePath + fileName); File newFileObj = new File (storagePath + fileName); f.transferTo(newFileObj); } } catch (Exception e) { e.printStackTrace(); return JsonResponse.error(); } return JsonResponse.ok(); }
静态文件配置 修改 application.yaml
配置文件,追加配置参数。
1 2 3 spring: mvc: static-path-pattern: /static/**
该配置示例表示,配置 classpath
目录下的 static
目录为静态文件的根目录。
比如:该文件夹(static)下存在 index.html
文件,则浏览器中访问应该使用 http://ip:port/static/index.html
异步任务配置 主要使用两个注解来实现。使用 @EnableAsync
标记在类上,表示开启异步支持,使用 @Async
标记在该类的方法上,表示这是一个异步执行的方法。最后在使用 @Component
把该类丢进 Spring 容器,在需要使用的地方声明注入使用即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package site.chilisdy.asyncTask;import org.springframework.scheduling.annotation.Async;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.stereotype.Component;@Component @EnableAsync public class Task1 { @Async public void run () { System.out.println("task1 start" ); try { Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("task1 end" ); } }
中间件配置 Filter Filter是Java Web标准中的组件,可以用于在请求到达Servlet之前或离开Servlet之后执行某些操作。它是基于Servlet规范实现的,可以处理所有的请求和响应,包括静态资源文件(如图片、CSS、JS等)。Filter可以对请求进行一些处理,例如添加请求头、对请求参数进行加密解密等。在Spring Boot中,可以使用@WebFilter注解将Filter注册到应用程序中。
下面我们使用跨域中间件来演示如何在 Spring 中使用该方式编写中间件。
HandlerInterceptor HandlerInterceptor 是 Spring 框架中的组件,用于拦截请求并对其进行处理。它是基于 Spring 框架实现的,只能拦截 Controller 中的请求,无法拦截静态资源文件。HandlerInterceptor 可以在 Controller 方法执行之前、之后或渲染视图之前、之后执行某些操作。它可以用于实现登录验证、日志记录等功能。在 Spring Boot 中,可以使用实现 HandlerInterceptor 接口的类来定义拦截器,并使用 WebMvcConfigurer 的 addInterceptors 方法将其注册到应用程序中。
支持在 Handler 执行前后自定义操作。但是无法修改 Response 返回值,因为返回值在调用 postHandle
之前就已经被写入。
该接口需要实现三个方法:
preHandle
:预处理,执行 Handler 之前调用
postHandle
:后处理,执行 Handler 之后调用,但是在渲染视图之前
afterCompletion
:完成请求后调用,但是在渲染视图之后
实现一个用于 Token 认证的拦截器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package site.chilisdy.middleware;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class Auth implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("预处理 [Auth] 执行..." ); if (request.getHeader("token" ) == null ) { response.getWriter().write("token is null" ); response.setStatus(401 ); return false ; } return HandlerInterceptor.super .preHandle(request, response, handler); } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("后处理 [Auth] 执行..." ); response.setContentType("application/json;charset=utf-8" ); HandlerInterceptor.super .postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("请求完成处理 [Auth] 执行..." ); HandlerInterceptor.super .afterCompletion(request, response, handler, ex); } }
注册该拦截器。创建一个配置类并且其要实现 WebMvcConfigurer
接口的 addInterceptors
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package site.chilisdy.config;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import site.chilisdy.middleware.Auth;import site.chilisdy.middleware.GetSome;@Configuration public class MiddlewareConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new Auth ()).addPathPatterns("/**" ); registry.addInterceptor(new GetSome ()).addPathPatterns("/**" ); WebMvcConfigurer.super .addInterceptors(registry); } }
执行顺序如下,如果预处理阶段其中一个拦截器返回了 false
则后续的拦截器都不会在执行,请求会直接完成。
⚠️注意: 预处理方法中的 response 对象获取一次数据后,后续在 Handler 中将无法在获取。
1 2 3 4 5 6 预处理 [Auth] 执行... 预处理 [GetSome] 执行... 后处理 [GetSome] 执行... 后处理 [Auth] 执行... 请求完成处理 [GetSome] 执行... 请求完成处理 [Auth] 执行...
ResponseBodyAdvice 用于在控制器方法返回结果被写入响应体之前执行的,它的作用是对响应体进行修改或处理。在Spring中,beforeBodyWrite方法通常用来进行统一的响应体处理、加密、压缩、缓存控制等操作。
实现一个修改响应体内容并设置响应头的拦截器,该方法针对所有 Handler 有效。
使用 @ControllerAdvice
注解注册该全局拦截器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package site.chilisdy.middleware;import org.springframework.core.MethodParameter;import org.springframework.http.MediaType;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;@ControllerAdvice public class Resp implements ResponseBodyAdvice <Object> { @Override public boolean supports (MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true ; } @Override public Object beforeBodyWrite (Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { response.getHeaders().set("Content-Type" , "application/json;charset=utf-8" ); if (body instanceof String) { return "Modified response: " + body; } return body; } }
beforeBodyWrite 函数的参数注释说明:
body
:控制器方法返回的结果对象,可以是任意类型的对象,包括Java基本类型、自定义类型、集合类型等。
returnType
:控制器方法返回值的类型信息,包括泛型参数类型等信息。
selectedContentType
:响应体的Content-Type类型,包括text/plain、text/html、application/json等。
selectedConverterType
:响应体的HttpMessageConverter类型,包括StringHttpMessageConverter、MappingJackson2HttpMessageConverter等。
request
:包含了当前请求的相关信息,例如请求方法、请求URL、请求头、请求体等。
response
:包含了当前响应的相关信息,例如响应状态码、响应头、响应体等。
@ExceptionHandler 错误处理注解。
可以写在 Controller 类中,处理该 Controller 类中方法产生的错误,如果产生的错误不是 @ExceptionHandler(错误类型)
注解指定捕获的错误,则会继续向下抛出给全局的错误错误处理方法。
也可以使用 @ControllerAdvice
注解修饰一个类,在其内的某个方法上使用 @ExceptionHandler(错误类型)
注册一个全局的错误处理方法。
默认存在一个全局错误处理方法,其注解为:@ExceptionHandler(Exception.class)
【盲猜,未查看源码】
实现一个全局错误处理拦截器。写在 Controller 中的错误处理使用方式相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package site.chilisdy.middleware;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import site.chilisdy.utils.JsonResponse;@ControllerAdvice public class Error { private final Logger logger = LoggerFactory.getLogger(Error.class); @ExceptionHandler(Exception.class) @ResponseBody public JsonResponse handleException (Exception ex) { String msg = ex.getMessage(); logger.error(msg); return JsonResponse.error(msg); } }
⚠️注意: 错误处理修饰的函数返回数据时,必须使用 @ResponseBody
修饰,否则返回就是视图,客户端无法正确的获取返回的数据。
跨域(CORS)中间件 使用 curl
检测跨域是否生效:
1 curl --verbose -H "Origin: http://localhost:9090" -H "Access-Control-Request-Method: POST" -X OPTIONS http://127.0.0.1:8080/user/form
官网文档中提供的方法: https://docs.spring.io/spring-framework/docs/6.0.8/reference/html/web.html#mvc-cors-global
单个接口跨域可以直接使用 SpringBoot 提供的注解 @CrossOrigin
在接口上添加即可。
以下为全局拦截器实现,且 Filter 的优先级大于 HandlerInterceptor 的优先级,两种实现方式大同小异。
基于 HandlerInterceptor 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package site.chilisdy.middleware;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class CORS implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String method = request.getMethod(); response.setHeader("Access-Control-Allow-Origin" , "*" ); response.setHeader("Access-Control-Allow-Headers" , "Content-Type,AccessToken,X-CSRF-Token, Authorization" ); response.setHeader("Access-Control-Expose-Headers" , "" ); response.setHeader("Access-Control-Allow-Methods" , "POST, GET, OPTIONS, PUT, DELETE, UPDATE" ); response.setHeader("Access-Control-Allow-Credentials" , "true" ); if (method.equals("OPTIONS" )) { response.setStatus(204 ); return false ; } return HandlerInterceptor.super .preHandle(request, response, handler); } }
基于 Filter 实现 只需要在 Configuration
类中创建方法返回 FilterRegistrationBean
对象,并在方法上使用 @Bean
告知 Spring 即可。Spring 会自动加载使用该过滤器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package site.chilisdy.middleware;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.servlet.*;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Configuration public class CORSConfig { @Bean public FilterRegistrationBean<CORSFilter> corsFilter () { FilterRegistrationBean<CORSFilter> filterRegistrationBean = new FilterRegistrationBean <>(new CORSFilter ()); filterRegistrationBean.addUrlPatterns("/*" ); filterRegistrationBean.setName("CORS" ); filterRegistrationBean.setOrder(1 ); return filterRegistrationBean; } } class CORSFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { Filter.super .init(filterConfig); } @Override public void destroy () { Filter.super .destroy(); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setHeader("Access-Control-Allow-Origin" , "*" ); response.setHeader("Access-Control-Allow-Headers" , "Content-Type,AccessToken,X-CSRF-Token, Authorization" ); response.setHeader("Access-Control-Expose-Headers" , "custom-header-key" ); response.setHeader("Access-Control-Allow-Methods" , "POST, GET, OPTIONS, PUT, DELETE, UPDATE" ); response.setHeader("Access-Control-Allow-Credentials" , "true" ); HttpServletRequest request = (HttpServletRequest) servletRequest; String method = request.getMethod(); if (method.equals("OPTIONS" )) { response.setStatus(204 ); return ; } filterChain.doFilter(servletRequest,response); } }
数据库配置 修改 pom.xml
依赖配置。
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.32</version > </dependency >
单数据源配置 修改 spring boot 的配置文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tmp?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: 123456 hikari: connection-timeout: 30000 maximum-pool-size: 10 minimum-idle: 5 idle-timeout: 600000 max-lifetime: 1800000 pool-name: HikariPool-1 connection-test-query: SELECT 1
spring boot 2.x 默认依赖的数据源连接池类型就是 hikari
不需要额外的依赖引入。 使用上述配置查询数据库,默认 spring boot 已经注入好了 Datasource
来让我们获取 Connection
对象,所以可以使用自动注入来获取 DataSource
,以下为代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package site.chilisdy.dao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import javax.sql.DataSource;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;@Component public class TmpDao { private DataSource dataSource; @Autowired public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } public void test () { Connection connection = null ; try { connection = dataSource.getConnection(); } catch (Exception e) { e.printStackTrace(); return ; } try { PreparedStatement query = connection.prepareStatement("select * from tmp" ); ResultSet rs = query.executeQuery(); while (rs.next()) { long id = rs.getLong(1 ); String name = rs.getString(2 ); int age = rs.getInt(3 ); System.out.println(id + " " + name + " " + age); } } catch (Exception e) { e.printStackTrace(); } } }
多数据源配置 同时连接多个数据库使用,分别连接不同的数据库,相比单数据源
配置文件和DataSource
配置稍有不同。
多个数据源的情况下无法直接使用默认的 DataSource
对象了,除非使用 @Primary
标记我们的 DataSourceConfig
类中的其中一个对象,来指定主要使用的数据库对象。
首先修改配置文件,使用以下配置文件 idea
可能会提示 master
下面的参数无法解析之类的,这貌似是因为配置文件不是标准的配置关系,但是该写法是能使用并正确解析的,其中 datasource
下的 master
子项和 slave
子项的名字是可以自定义的,并且不限制数量,你可以写任意数量的数据库配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 spring: datasource: type: com.zaxxer.hikari.HikariDataSource master: jdbc-url: jdbc:mysql://localhost:3306/tmp?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: connection-timeout: 30000 maximum-pool-size: 10 minimum-idle: 5 idle-timeout: 600000 max-lifetime: 1800000 pool-name: HikariPool-1 connection-test-query: SELECT 1 slave: jdbc-url: jdbc:mysql://localhost:3306/tmp2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: connection-timeout: 30000 maximum-pool-size: 10 minimum-idle: 5 idle-timeout: 600000 max-lifetime: 1800000 pool-name: HikariPool-2 connection-test-query: SELECT 1
实例化 DataSource
对象。我们需要新建一个配置类,专门用于管理 DataSource
配置。
以下配置通过 Bean
分配不同的对象名称,通过 ConfigurationProperties
分配不同的数据库配置,SpringBoot 会自动根据配置内容实例化两个 DataSource
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package site.chilisdy.config;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.jdbc.DataSourceBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;@Configuration public class DataSourceConfig { @Bean(name = "db1") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource () { return DataSourceBuilder.create().build(); } @Bean(name = "db2") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDataSource () { return DataSourceBuilder.create().build(); } }
分别使用两个 DataSource
查询数据。比如以下示例,两个数据库中都存在 tmp
数据表,根据请求参数返回不同数据库的第一条数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @GetMapping("/tmp2/{seq}") public String test2 (@PathVariable("seq") String seqStr) { try { Integer seq = Integer.parseInt(seqStr); tmpDao.test2(seq); } catch (Exception e) { return "seq must be a number" ; } return "test" ; } DataSource db1; DataSource db2; @Autowired @Qualifier("db1") public void setDb1 (DataSource db1) { this .db1 = db1; } @Autowired @Qualifier("db2") public void setDb2 (DataSource db2) { this .db2 = db2; } public void test2 (Integer seq) { try { Connection connection; if (seq == 1 ) { connection = db1.getConnection(); } else { connection = db2.getConnection(); } String sql = "select * from tmp limit 1" ; PreparedStatement query = connection.prepareStatement(sql); ResultSet rs = query.executeQuery(); while (rs.next()) { long id = rs.getLong(1 ); String name = rs.getString(2 ); int age = rs.getInt(3 ); System.out.println(id + " " + name + " " + age); } connection.close(); } catch (Exception e) { e.printStackTrace(); } }
MyBatis 配置 单数据源和多数据源在 Mapper 使用上有点区别。
⚠️注意: 当使用 @MapperScan
注解时,MyBatis 就不会扫描全项目中所有被 @Mapper
注解修饰的 Mapper 类了。
多数据源的情况下,需要设置一个数据源为 @Primary
用来当作 SpringBoot 的默认数据源。
依赖配置 1 2 3 4 5 6 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 3.0.0</version > </dependency >
单数据源 单数据源就不用多说了,直接引入 SpringBoot 自动装配的对象使用就完事了。
基于上面的单数据源配置。
在 application.yaml
中追加配置,主要是指定 Mapper 类的 XML 配置文件位置。
1 2 mybatis: mapper-locations: classpath:mappers/db1/*.xml
还可以通过 mybatis.config-location
指定 mybatis
的配置文件。
在项目中编写 Mapper 接口类,并在接口类上使用 @Mapper
注解让 SpringBoot 自动装配。不想每个接口类都写注解,可以在项目启动类上使用 @MapperScan
注解指定 Mapper 接口类的位置。
在需要查询数据库的地方,正常注入 Mapper 即可使用。
多数据源 基于上面的 多数据源
配置继续配置。
需要给不同的数据源分别配置 MyBatis 配置。以下是主数据源配置信息,需要两个 Bean
分别返回 SqlSessionFactory
和 SqlSessionTemplate
。其他数据源配置都是相同的,区别只是 Bean
的名字和使用的 DataSource
不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package site.chilisdy.config;import org.apache.ibatis.session.SqlSessionFactory;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.SqlSessionTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import javax.sql.DataSource;@Configuration public class MyBatisDB1Config { private DataSource dataSource; @Autowired @Qualifier("db1") public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } @Bean("sqlSessionFactoryDB1") @Primary public SqlSessionFactory sqlSessionFactoryDB1 () throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean (); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations( new PathMatchingResourcePatternResolver (). getResources("classpath:mappers/db1/*.xml" ) ); return factoryBean.getObject(); } @Bean("sqlSessionTemplateDB1") @Primary public SqlSessionTemplate sqlSessionTemplateDB1 () throws Exception { return new SqlSessionTemplate (sqlSessionFactoryDB1()); } }
Mapper 类的声明就和正常声明是一样的,对应的 xml 配置文件也是正常编写,可以直接在项目的 dao
层,通过 Spring 的容器获取这里 MyBatis 的 SqlSessionFactory
对象,然后正常的通过工厂函数获取 Session
,之后通过 Session
传入 Mapper 类
获取对应的 Mapper 类对象
,调用其方法即可执行数据库操作。比如以下使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package site.chilisdy.mapper;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import site.chilisdy.pojo.TmpTable;@Mapper public interface TmpMapper { TmpTable selectOneByID (@Param("id") Integer id) ; } package site.chilisdy.dao;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.stereotype.Repository;import site.chilisdy.mapper.TmpMapper;import site.chilisdy.pojo.TmpTable;@Repository public class MyBatisDao { SqlSessionFactory sqlSessionFactoryDB1; @Autowired @Qualifier("sqlSessionFactoryDB1") public void setSqlSessionFactoryDB1 (SqlSessionFactory sqlSessionFactoryDB1) { this .sqlSessionFactoryDB1 = sqlSessionFactoryDB1; } public String test () { SqlSession session = sqlSessionFactoryDB1.openSession(); TmpMapper mapper = session.getMapper(TmpMapper.class); TmpTable res = mapper.selectOneByID(1 ); session.close(); return res.toString(); } }
上述 Mapper 类的信息都写在了 XML 文件中,以下是其内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="site.chilisdy.mapper.TmpMapper" > <resultMap id ="selectOneByIDResult" type ="site.chilisdy.pojo.TmpTable" > <id column ="id" property ="id" /> <result column ="name" property ="name" /> <result column ="age" property ="age" /> </resultMap > <select id ="selectOneByID" resultMap ="selectOneByIDResult" > select id,name,age from tmp where id=#{id} </select > </mapper >
简单的 SQL 操作当然也可以写在 Mapper 类中,如果 MyBatis 配置中指定了 XML 文件位置(任意数据源),并且在 XML 写了该 Mapper 类方法名对应 id 的配置,则不可以在使用注解方式,否则会产生冲突报错。注解和 XML 配置只能二选一。
重复错误提示:Mapped Statements collection already contains value for xxxxxxxx
1 2 3 4 5 6 7 8 9 10 11 @Mapper public interface TmpMapper { @Select("select * from tmp where id = #{id}") @Results({ @Result(property = "id", column = "id"), @Result(property = "name", column = "name"), @Result(property = "age", column = "age") }) TmpTable selectOneByID (@Param("id") Integer id) ; }
如果觉得这种使用 Mapper 方式太麻烦了,可以通过使用 @MapperScan
来让 Spring 帮我们自动装配 Mapper。
这种方式需要给每个数据源指定其生效的 Mapper 存放位置,且 @Mapper
注解会失效,MyBatis 不在扫描全项目中所有被 @Mapper
修饰的类。好处是指定位置的 Mapper 类可以不使用 @Mapper
注解,但是 IDEA 在语法检测时会检测,不写 @Mapper
注解会给你报红线错误,但是程序是依然可以运行的。
我们可以在 MyBatis 的配置上分别使用 @MapperScan
注解。以下配置仅仅相比上面增加了一个 @MapperScan
注解,在注解中:
value
:指定了扫描 Mapper 类的位置
sqlSessionFactoryRef
:指定了该位置的 Mapper 类使用的 MyBatis 配置,也就是 SqlSessionFactory Bean
的名称。
⚠️注意: value 指定的位置不可以重叠,否则将会覆盖子文件路径的配置,比如下面两个配置文件分别指定了 site.chilisdy.mapper.db1
和 site.chilisdy.mapper.db2
,此时是正常的。两个文件夹下的 Mapper 类可以正常的被分配到不同的数据源。如果修改 site.chilisdy.mapper.db1
的 @MapperScan
参数为 site.chilisdy.mapper
,则 site.chilisdy.mapper.db2
的 @MapperScan
参数配置会失效,该位置的 Mapper 类也会使用 db1
的数据源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration @MapperScan(value = "site.chilisdy.mapper.db1", sqlSessionFactoryRef = "sqlSessionFactoryDB1") public class MyBatisDB1Config { private DataSource dataSource; @Autowired @Qualifier("db1") public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } @Bean("sqlSessionFactoryDB1") @Primary public SqlSessionFactory sqlSessionFactoryDB1 () throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean (); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations( new PathMatchingResourcePatternResolver (). getResources("classpath:mappers/db1/*.xml" ) ); return factoryBean.getObject(); } @Bean("sqlSessionTemplateDB1") @Primary public SqlSessionTemplate sqlSessionTemplateDB1 () throws Exception { return new SqlSessionTemplate (sqlSessionFactoryDB1()); } }
第二个数据源的配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Configuration @MapperScan(value = "site.chilisdy.mapper.db2", sqlSessionFactoryRef = "sqlSessionFactoryDB2") public class MyBatisDB2Config { private DataSource dataSource; @Autowired @Qualifier("db2") public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } @Bean("sqlSessionFactoryDB2") public SqlSessionFactory sqlSessionFactoryDB2 () throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean (); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations( new PathMatchingResourcePatternResolver (). getResources("classpath:mappers/db2/*.xml" ) ); return factoryBean.getObject(); } @Bean("sqlSessionTemplateDB2") public SqlSessionTemplate sqlSessionTemplateDB2 () throws Exception { return new SqlSessionTemplate (sqlSessionFactoryDB2()); } }