Spring Boot 快速入门

官网文档: 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

<?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 标签,有必要的话可以和下面的自定义版本号合并
	-->
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!--
	    SpringBoot项目必须引入该父工程,
	    父工程中包含了很多依赖,所以下面的 dependencies就不用写版本了.版本号交给父工程维护了
	    父工程中不存在的依赖,就需要在 dependencies中写版本号了
    -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <!-- 如果想使用特定的依赖包版本,可以在本 pom 文件中覆盖掉父工程的版本 -->
    <!--<properties>-->
    <!--    <mysql.version>5.1.43</mysql.version>-->
    <!--</properties>-->

    <dependencies>
        <!--
	        spring web 依赖,开发 web 项目引入
	        包含 logback, log4j, slf4j, tomcat, jackson, spring-webmvc
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--
	        spring boot 开发工具
	        提供热部署,自动重启,远程调试等功能
	        大项目不建议使用,因为会影响启动速度
        -->
        <!--<dependency>-->
        <!--    <groupId>org.springframework.boot</groupId>-->
        <!--    <artifactId>spring-boot-devtools</artifactId>-->
        <!--    <optional>true</optional>-->
        <!--</dependency>-->

        <!--
	        spring boot 配置处理器
	        实现从配置文件载入对象信息
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>

        <!-- 使用默认的数据源(HicariCP) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- mysql 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.32</version>
        </dependency>

        <!-- mybatis 依赖配置 -->
        <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 项目。

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) {
        // 启动SpringBoot应用
        SpringApplication.run(Application.class, args);
    }
}

HelloWorld

在 Main 类的同级创建 controller 包,当然你不叫这个名字也行,你可以随意定义。

controller 包内新建类 Hello,并参考下方代码填写内容。

@RestController
public class Hello {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

现在回到 Main 类,右键 Run,注意 idea 控制台刷新的消息,观察下 tomcat 的端口号,默认为 8080

...
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 这个是不包含项目依赖的包,无法运行,内部主要是用户写的类,用途是提供给其他项目依赖。
<!-- 注意标签结构! -->
<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。 使用步骤也很简单:

  1. 把对象交给 Spring 容器
  2. 告诉 Spring 这里需要某个对象
  3. 使用对象

依赖注入的几种方式

  • 直接在类的属性字段上使用 @Autowired,让 Spring 注入。
  • 在 setter 方法上使用 @Autowired,让 Spring 注入。
  • 在类上使用 @Component 标记,然后通过该类的构造函数给属性字段赋值
@RestController
public class GetContainerObj {

    // 直接注入
    @Autowired
    CustomBeanConfig customBeanConfig;

    private User user;

    // setter方式注入
    @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 容器中管理,可以指定对象的名称方法
@JsonPropertyjackson 包提供的注解,当处理 Json 数据时,标记在类的属性上,解决属性字段名和 Json 的 key 名称不一致的问题。属性字段
@ControllerAdvice高级控制器,用于实现全局注册拦截器、错误处理等操作
@ExceptionHandler标记在方法上,被标记的方法为本类中其他方法的错误处理函数。当在被 @ControllerAdvice 标记的类内部时,表示注册全局错误拦截器。方法
@MapperMyBatis 提供的注解,被标记的类为数据库操作类
@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 方法来设置对象的值。

@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;

	// 此处 get/set 方法被省略,但是必须存在。
	// form 表单的 key 和属性字段名不一致时,需要按照 form 表单的 key 编写 setter 方法。
}

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 字段。

@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;

    // 省略 get/set 方法,但是必须要有
}

URL-PATH 参数接口

在 URL 上传递数据。

// URL 传递参数
@GetMapping("/get/{id}")
public JsonResponse getUserByID(@PathVariable("id") int id) {
	String res = "user id: " + id;
	return JsonResponse.ok((Object) res);
}

URL 参数接口

在 URL 上使用 间隔,参数之间使用 & 符号相连的普通 GET 方式传递参数。

@GetMapping("/get")
public String getUserByName(@RequestParam("name") String name) {
	return "user name: " + name;
}

获取最原始的 Request 和 Response

@GetMapping("/test")
public String test(HttpServletRequest request, HttpServletResponse response) {
	return "ok";
}

使用注解 @RequestHeader@CookieValue 分别获取请求头和 Cookie 中的数据。

@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 对象所属的包:import javax.servlet.http.Cookie;

获取单个 Cookie 的值使用 @CookieValue

获取所有 Cookie 的值。

@GetMapping("/test1")
public String test1(@RequestParam("name") String name,
					@CookieValue("aaa") String value,
					HttpServletRequest request,
					HttpServletResponse response) {
	// 获取所有的 Cookie 参数
	Cookie[] allCookies = request.getCookies();
	for (Cookie cookie : allCookies) {
		System.out.println(cookie.getName() + ": " + cookie.getValue());
	}

	// 设置Cookie
	Cookie newCookie = new Cookie("client_user", name);
	// 设置Cookie的有效路径
	newCookie.setPath("/");
	// 设置Cookie的有效期为7天,单位为秒
	newCookie.setMaxAge(60 * 60 * 24 * 7);

	// 将Cookie添加到响应中
	response.addCookie(newCookie);

	return "test1";
}

上传文件

修改最大上传文件大小配置:编辑 SpringBoot 的 application.yaml 配置文件

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
  • max-file-size:单个文件的大小
  • max-request-size:总上传文件大小

以多文件上传示例,兼容单文件上传。

@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 配置文件,追加配置参数。

spring:
  mvc:
    static-path-pattern: /static/**

该配置示例表示,配置 classpath 目录下的 static 目录为静态文件的根目录。

比如:该文件夹(static)下存在 index.html 文件,则浏览器中访问应该使用 http://ip:port/static/index.html

异步任务配置

主要使用两个注解来实现。使用 @EnableAsync 标记在类上,表示开启异步支持,使用 @Async 标记在该类的方法上,表示这是一个异步执行的方法。最后在使用 @Component 把该类丢进 Spring 容器,在需要使用的地方声明注入使用即可。

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 认证的拦截器。

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 方法。

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 中将无法在获取。

预处理 [Auth] 执行...
预处理 [GetSome] 执行...
后处理 [GetSome] 执行...
后处理 [Auth] 执行...
请求完成处理 [GetSome] 执行...
请求完成处理 [Auth] 执行...

ResponseBodyAdvice

用于在控制器方法返回结果被写入响应体之前执行的,它的作用是对响应体进行修改或处理。在Spring中,beforeBodyWrite方法通常用来进行统一的响应体处理、加密、压缩、缓存控制等操作。

实现一个修改响应体内容并设置响应头的拦截器,该方法针对所有 Handler 有效。

使用 @ControllerAdvice 注解注册该全局拦截器。

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 函数的参数注释说明:

  1. body:控制器方法返回的结果对象,可以是任意类型的对象,包括Java基本类型、自定义类型、集合类型等。
  2. returnType:控制器方法返回值的类型信息,包括泛型参数类型等信息。
  3. selectedContentType:响应体的Content-Type类型,包括text/plain、text/html、application/json等。
  4. selectedConverterType:响应体的HttpMessageConverter类型,包括StringHttpMessageConverter、MappingJackson2HttpMessageConverter等。
  5. request:包含了当前请求的相关信息,例如请求方法、请求URL、请求头、请求体等。
  6. response:包含了当前响应的相关信息,例如响应状态码、响应头、响应体等。

@ExceptionHandler

错误处理注解。

  • 可以写在 Controller 类中,处理该 Controller 类中方法产生的错误,如果产生的错误不是 @ExceptionHandler(错误类型) 注解指定捕获的错误,则会继续向下抛出给全局的错误错误处理方法。
  • 也可以使用 @ControllerAdvice 注解修饰一个类,在其内的某个方法上使用 @ExceptionHandler(错误类型) 注册一个全局的错误处理方法。
  • 默认存在一个全局错误处理方法,其注解为:@ExceptionHandler(Exception.class) 【盲猜,未查看源码】

实现一个全局错误处理拦截器。写在 Controller 中的错误处理使用方式相同。

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 检测跨域是否生效:

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 实现

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");
        // 跨域请求允许携带 cookie
        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 会自动加载使用该过滤器。

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");
        // 跨域请求允许携带 cookie
        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 依赖配置。

<!-- 使用默认的数据源(HicariCP) -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-jdbc</artifactId>  
</dependency>  
  
<!-- mysql 驱动 -->  
<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>8.0.32</version>  
</dependency>

单数据源配置

修改 spring boot 的配置文件。

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 ,以下为代码示例:

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;
    }

    /*
    * 测试数据库连接
    * 使用原始 JDBC API 操作数据库
    * */
    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();
            // 遍历数据,rs.next()用于判断是否有下一行记录
            while (rs.next()) {
                long id = rs.getLong(1); // 注意:索引从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 子项的名字是可以自定义的,并且不限制数量,你可以写任意数量的数据库配置。

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

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 数据表,根据请求参数返回不同数据库的第一条数据。

// 接口
@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";
}

// dao 操作
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();
		// 遍历数据,rs.next()用于判断是否有下一行记录
		while (rs.next()) {
			long id = rs.getLong(1); // 注意:索引从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 的默认数据源。

依赖配置

<!-- mybatis 依赖配置 -->
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>3.0.0</version>
</dependency>

单数据源

单数据源就不用多说了,直接引入 SpringBoot 自动装配的对象使用就完事了。

基于上面的单数据源配置。

application.yaml 中追加配置,主要是指定 Mapper 类的 XML 配置文件位置。

mybatis:
  mapper-locations: classpath:mappers/db1/*.xml

还可以通过 mybatis.config-location 指定 mybatis 的配置文件。

在项目中编写 Mapper 接口类,并在接口类上使用 @Mapper 注解让 SpringBoot 自动装配。不想每个接口类都写注解,可以在项目启动类上使用 @MapperScan 注解指定 Mapper 接口类的位置。

在需要查询数据库的地方,正常注入 Mapper 即可使用。

多数据源

基于上面的 多数据源 配置继续配置。

需要给不同的数据源分别配置 MyBatis 配置。以下是主数据源配置信息,需要两个 Bean 分别返回 SqlSessionFactorySqlSessionTemplate。其他数据源配置都是相同的,区别只是 Bean 的名字和使用的 DataSource 不同。

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 类对象,调用其方法即可执行数据库操作。比如以下使用示例:

// 定义一个 Mapper 类并使用 @Mapper 注解让其被扫描到
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);
}

// 定义 dao 层的操作
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 {

	// 注入 mybatis 的 SqlSessionFactory 对象
    SqlSessionFactory sqlSessionFactoryDB1;

    @Autowired
    @Qualifier("sqlSessionFactoryDB1")
    public void setSqlSessionFactoryDB1(SqlSessionFactory sqlSessionFactoryDB1) {
        this.sqlSessionFactoryDB1 = sqlSessionFactoryDB1;
    }

    public String test() {
	    // 获取 session
        SqlSession session = sqlSessionFactoryDB1.openSession();
        // 获取 mapper
        TmpMapper mapper = session.getMapper(TmpMapper.class);
        // 调用 mapper 的方法
        TmpTable res = mapper.selectOneByID(1);
        // 关闭 session
        session.close();
        return res.toString();
    }
}

上述 Mapper 类的信息都写在了 XML 文件中,以下是其内容:

<?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

@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.db1site.chilisdy.mapper.db2,此时是正常的。两个文件夹下的 Mapper 类可以正常的被分配到不同的数据源。如果修改 site.chilisdy.mapper.db1@MapperScan 参数为 site.chilisdy.mapper,则 site.chilisdy.mapper.db2@MapperScan参数配置会失效,该位置的 Mapper 类也会使用 db1 的数据源。

@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());
    }
}

第二个数据源的配置如下:

@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());
    }
}