# SpringBootLearn **Repository Path**: chmingx/spring-boot-learn ## Basic Information - **Project Name**: SpringBootLearn - **Description**: 学习SpringBoot - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2021-05-18 - **Last Updated**: 2022-11-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringBoot [TOC] ## 1. 第一个SpringBoot程序 - 导入依赖 ```xml org.springframework.boot spring-boot-starter-parent 2.4.5 org.springframework.boot spring-boot-starter-web ``` - 编写主程序 ```java package com.chmingx.boot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } } ``` - 编写Controller ```java package com.chmingx.boot.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; @RestController // @RestController = @Controller + @ResponseBody public class HelloController { @RequestMapping("/hello") public String handle01() { return "Hello, Spring Boot 2!"; } } ``` - 允许主程序并测试 ```shell $ curl http:127.0.0.1:8080 ``` - 如果要将代码打包成jar,需导入配置 ```xml org.springframework.boot spring-boot-maven-plugin ``` - 打包 ```shell $ mvn package ``` - 运行 ```shell $ java -jar target/helloworld-1.0-SNAPSHOT.jar ``` ## 2. SpringBoot 自动装配 ### 2.1 依赖管理 在spring-boot-starter-dependencies中几乎声明了所有开发中常用的依赖的版本号, 自动版本仲裁机制 1. 引入依赖默认都可以不写版本 2. 引入非版本仲裁的jar,要写版本号 对自动仲裁的版本号不满意可以修改 ```xml 5.1.43 ``` ### 2.2 自动装配 - @Configuration 将类转换为配置类,用以取代以前的配置文件 - @Bean ```java /** * 将普通类转换为配置类,用来取代之前的Beans.xml, 在配置类中通过注解@Bean,给容器添加组件 * proxyBeanMethods = true, Full模式,保证每个@Bean方法被调用多少此返回的组件都是单例的, 默认的, 底层通过代理模式获取MyConfig的代理类,然后再创建对象 * proxyBeanMethods = false, Lite模式,保证每个@Bean方法被调用多少此返回的组件都是新创建的, 底层就是用MyConfig创建对象,没有生成代理类 * * 配置类本身也是组件 */ @Configuration(proxyBeanMethods = true) //@EnableConfigurationProperties(Car.class) public class MyConfig { /** * 给容器中添加组件, * 方法名 --> 组件id * 返回值类型 --> 组件类型 * 返回值 --> 组件在容器中的实例 * @return */ @Bean public Pet pet() { return new Pet("miaomiao~"); } @Bean public User user() { return new User("zhangsan", 18, pet()); } } ``` - @Component, @Controller, @Service, @Repository, 直接将类作为组件加入容器中 ```java @Component public class Person { @Value("foobar") private String name; @Value("20") private int age; } ``` - @ComponentScan 指定扫描路径 ```java @ComponentScan("com.chmingx.BootProject") ``` - @Import 给容器导入一组组件 ```java @Import({DBHelper.class}) // 给容器中自动创建出这个数组中指定类型的组件,组件默认名字就是全类名 ``` - Conditional 条件装配,满足条件才装配 ```java @ConditionalOnMissingBean(name="person") // 没有变量名为person的变量时候才进行装配 ``` - @ImportResource 引入原生配置文件 ```java @ImportResource("classpath:beans.xml") // 导入beans.xml, 将xml中的组件注册到容器中 ``` - ConfigurationProperties Java类绑定配置文件 ```java /** * @Component * @ConfigurationProperties("mycar") * * 等价于 * * @EnableConfigurationProperties(Car.class) 在配置类上加注解 * @configurationProperties("mycar") * * 1. 开启Car配置绑定功能 * 2. 把这个Car这个组件自动注册到容器中 */ @Component @ConfigurationProperties(prefix="mycar") // 读取properties文件信息,自动装配 public class Car { private String brand; private double price; } ``` ### 2.3 自动装配原理 - SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration - 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定 - 生效的配置类就会给容器中装配很多组件 - 只要容器中有这些组件,相当于这些功能就有了 - 定制化配置 - - 用户直接自己@Bean替换底层的组件 - 用户去看这个组件是获取的配置文件什么值就去修改。 **xxxxxAutoConfiguration ---> 组件 --->** **xxxxProperties里面拿值 ----> application.properties** ## 3. Yaml - 字面量:单个的、不可再分的值。date、boolean、string、number、null ```yaml k: v ``` - 对象:键值对的集合。map、hash、set、object ```yaml k: {k1:v1,k2:v2,k3:v3} #或 k: k1: v1 k2: v2 k3: v3 ``` - 数组:一组按次序排列的值。array、list、queue ```yaml 行内写法: k: [v1,v2,v3] #或者 k: - v1 - v2 - v3 ``` 注意: __字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义__ 添加配置文件提示,打包时排除 ```xml org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-maven-plugin org.springframework.boot spring-boot-configuration-processor ``` ## 4. Web ### 4.1 静态资源 只要将静态资源放在类路径下: - /static - /public - /resource - /META_INF/resource 那么就可以通过 `当前项目根路径/静态资源名` 访问 改变静态资源路由和存储路径 ```yaml spring: mvc: static-path-pattern: /image/** # 指定静态资源路由 web: resources: static-locations: classpath:images # 指定静态资源存储路径 ``` ### 4.2 自定义SpringMVC功能 - 方式一: 添加配置类并继承WebMvcConfigurer, 重写其中需要自定义的方法 ```java @Configuration public class WebConfig implements WebMvcConfigurer { /** * SpringBoot默认关闭矩阵变量,这是另一种开启方式 * @param configurer */ @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } } ``` - 方式二: 添加配置类,向容器中注入WebMvcConfigurer实例,重写其中需要自定义的方法 ```java package com.chmingx.boot.config; @Configuration public class WebConfig { /** * 自定义SpringMVC功能 * @return */ @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { /** * SpringBoot默认关闭矩阵变量,这是一种开启方式 * @param configurer */ @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter() { @Override public Pet convert(String s) { // miao,3 if (!s.isEmpty()) { String[] split = s.split(","); Pet pet = new Pet(); pet.setName(split[0]); pet.setAge(Integer.parseInt(split[1])); return pet; } return null; } }); } }; } } ``` ### 4.3 请求参数 #### 4.3.1 请求路径与RESTful - @RequestMapping - @GetMapping - @PostMapping - @PutMapping - @DeleteMapping ```java @RequestMapping(value = "/user", method = RequestMethod.GET) @GetMapping("/user") ``` ```java @Configuration(proxyBeanMethods = false) public class WebConfig /*implements WebMvcConfigurer*/ { /** * 表单请求方式只有get, post,对于put和delete,需要传递_method来指定请求方式 * 此配置将 _method 改为 _m * @return */ @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter() { HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; } } ``` _自定义Handler Mapping处理api/v1/, api/v2 不同请求_ #### 4.3.2 普通参数与基本注解: - @PathVariable 路径参数 - @RequestHeader 请求头参数 - @RequestParam 请求参数 - @CookieValue Cookie参数 - @RequestAttribute 请求属性参数 - @ReqeustBody 请求体参数 - @MatrixVariable 矩阵变量 ```java // car/2/owner/zhangsan?age=30 @GetMapping("/car/{id}/owner/{username}") public Map getCar(@PathVariable("id") Integer id, @PathVariable("username") String username, @PathVariable Map pv, @RequestHeader("User-Agent") String userAgent, @RequestHeader Map header, @RequestParam("age") Integer age, @RequestParam("inters") List inters, @RequestParam Map params, @CookieValue("_ga") String _ga, @CookieValue("_ga") Cookie cookie ) { Map map = new HashMap<>(); map.put("id", id); map.put("username", username); map.put("pv", pv); map.put("userAgent", userAgent); map.put("header", header); map.put("age", age); map.put("inters", inters); map.put("params", params); map.put("_ga", _ga); System.out.println(cookie.getName() + "===>" + cookie.getValue()); return map; } ``` ##### RequestBody ```java @PostMapping("/save") public Map postMethod(@RequestBody String content) { Map map = new HashMap<>(); map.put("content", content); return map; } ``` ##### RequestAttribute ```java /** * 普通的controller用来转发请求 */ @Controller public class RequestController { @GetMapping("/goto") public String goToPage(HttpServletRequest request) { // 为请求设置属性 request.setAttribute("msg", "成功转发"); request.setAttribute("code", "200"); return "forward:/success"; // 转发到 /success 请求 } @ResponseBody @GetMapping("/success") public Map success(@RequestAttribute("msg") String msg, @RequestAttribute("code") Integer code, HttpServletRequest request) { Map map = new HashMap<>(); // 通过request.getAttribute 获取请求域属性 map.put("request_method", request.getAttribute("msg")); // 利用注解 @RequestAttribute 获取请求域属性 map.put("annotation", msg); return map; } } ``` ##### 矩阵变量MatrixVariable - `/car/{path}?xxx=xxx&aaa=aaa&bbb=bbb` 查询字符串, 使用@RequestParam `/car/1?low=30&brand=bmw&band=benz&brand=audi` - `/car/{path};xxx=xxx;yyy=yyy;zzz=zzz` 矩阵变量,使用@MatrixVariable `/car/1;low=30;brand=bmw;brand=benz;brand=audi` `/car/1;low=30;brand=bmw,benz,audi` 常用于cookie被禁用,将cookie中的数据放到矩阵变量中,形成 `/abc;jsessionid=xxx` , 这被称为url重写 ```java // 获取请求中的矩阵变量 // 访问路径 /car/sell;low=30;brand=bmw,benz,audi // springboot 中默认时禁用矩阵变量的,需要手动开启 // 矩阵变量必须要有url路径变量才能被解析 @GetMapping("/car/{path}") public Map sell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List brand, @PathVariable("path") String path) { Map map = new HashMap<>(); map.put("low", low); map.put("brand", brand); map.put("path", path); return map; } ``` `/boss/1/2` 添加矩阵变量变为 `/boss/1;age=20/2;age=30` ```java // 不同路径下的矩阵变量,需要指明所在路径 // /boss/1;age=30/2;age=20 @GetMapping("/boss/{bossId}/{empId}") public Map boss(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age", pathVar = "empId") Integer empAge) { Map map = new HashMap<>(); map.put("bossAge", bossAge); map.put("empAge", empAge); return map; } ``` #### 4.3.3 复杂参数Map, Model Map和Model中的数据会被放在原生request的请求域中,再SpringBoot底层会调用request.setAttribute(),最终可与通过reqeust.getAttribute()获取数据 ```java /** * 普通的controller用来转发请求 */ @Controller public class RequestController { @GetMapping("/params") public String testParams(Map map, Model model, HttpServletRequest request, HttpServletResponse response) { map.put("aaa", "aaa"); model.addAttribute("bbb", "bbb"); request.setAttribute("ccc", "ccc"); Cookie cookie = new Cookie("c1", "v1"); response.addCookie(cookie); return "forward:/testparams"; } @ResponseBody @GetMapping("/testparams") public Map testComplictParams(@RequestAttribute(value = "msg", required = false) String msg, @RequestAttribute(value = "code", required = false) Integer code, HttpServletRequest request) { Map map = new HashMap<>(); Object aaa = request.getAttribute("aaa"); Object bbb = request.getAttribute("bbb"); Object ccc = request.getAttribute("ccc"); map.put("aaa", aaa); map.put("bbb", bbb); map.put("ccc", ccc); return map; } } ``` #### 4.3.4 自定义类型参数 ```java /** * 自定义类型参数 * * 页面提交的请求数据GET/POST都可以和对象属性进行绑定 */ @PostMapping("/saveuser") public Person saveUser(Person person) { System.out.println(person); return person; } ``` ```html

自定义类型

姓名: 年龄: 生日: 宠物姓名: 宠物年龄:
``` - 自定义类型转换器, 需要自定义SpringMVC功能 ```html

自定义类型

姓名: 年龄: 生日: 宠物:
``` ```java /** * 自定义SpringMVC功能 * @return */ @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter((Converter) s -> { // miao,3 if (!s.isEmpty()) { String[] split = s.split(","); Pet pet = new Pet(); pet.setName(split[0]); pet.setAge(Integer.parseInt(split[1])); return pet; } return null; }); } }; } ``` ### 4.4 响应 #### 4.4.1 @ResponseBody 标注@ResponseBody即可响应数据, 数据返回后,返回值处理器会处理数据,调用MessageConverter,进行数据类型转换 ```java public class ResponseTestController { @ResponseBody @GetMapping("/test/person") public Person getPerson() { Person person = new Person(); person.setName("foobar"); person.setAge(20); person.setBirth(new Date()); return person; } } ``` #### 4.4.2 内容协商 - 浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型 - 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据 - SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理 #### 4.4.3 响应JSON和XML __依赖__ ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-json 2.3.4.RELEASE compile com.fasterxml.jackson.dataformat jackson-dataformat-xml ``` ##### 4.4.3.1 通过请求头确定响应JSON还是XML 请求头的Accept的值为 `application/xml`,则响应xml数据;如果为 `application/json`, 则响应json数据 ```json {"name":"foobar","age":20,"birth":"2021-06-09T14:25:10.890+00:00","pet":null} ``` ```xml foobar 20 2021-06-10T00:00:28.253+00:00 ``` ##### 4.4.3.2 通过请求参数确定响应数据类型 为了方便内容协商,开启基于请求参数的内容协商功能。默认只支持json和xml ```yaml spring: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式 ``` 发请求: - http://localhost:8080/test/person?format=json - http://localhost:8080/test/person?format=xml #### 4.4.4 自定义MessageConverter **实现多协议数据兼容。json、xml、x-diy** 1. @ResponseBody 响应数据出去 调用 **RequestResponseBodyMethodProcessor** 处理 2. Processor 处理方法返回值。通过 **MessageConverter** 处理 3. 所有 **MessageConverter** 合起来可以支持各种媒体类型数据的操作(读、写) 4. 内容协商找到最终的 **messageConverter** ##### 4.4.4.1 请求头方式完成自定义内容协商 - 说明 ```java @Controller public class ResponseTestController { /** * 自定义协议数据MessageConverter * * 1. 浏览器法请求返回xml, [application/xml] jacksonXmlConverter * 2. 如果是ajax请求 返回json [application/json] jacksonJsonConverter * 3. 如果app发请求,返回自定义协议数据 [application/x-diy] DiyMessageConverter 属性值1;属性值2;属性值3 * * 步骤: * 1. 添加自定义的MessageConverter进系统底层 * 2. 系统底层就会统计出所有MessageConverter能操作哪些类型 * 3. 客户端内容协商 [x-diy --> DiyMessageConverter --> 处理返回数据] * * @return */ @ResponseBody // 利用返回值处理器里面的消息转换器进行数据处理 @GetMapping("/diy/person") public Person diyPerson() { Person person = new Person(); person.setName("foobar"); person.setAge(20); person.setBirth(new Date()); return person; } } ``` - 自定义MessageConverter ```java package com.chmingx.boot.converter; import com.chmingx.boot.bean.Person; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import java.io.IOException; import java.io.OutputStream; import java.util.List; public class DiyMessageConverter implements HttpMessageConverter { // 能否读取RequestBody中传过来的自定义数据类型 比如controller的参数中有 @RequestBody Person person @Override public boolean canRead(Class aClass, MediaType mediaType) { return false; } @Override public boolean canWrite(Class aClass, MediaType mediaType) { return aClass.isAssignableFrom(Person.class); // 方法返回值类型时Person类型,就能写 } /** * 服务器要统计所有MessageConverter都能处理那些媒体类型 * * 我们自定义的是 application/x-diy * @return */ @Override public List getSupportedMediaTypes() { return MediaType.parseMediaTypes("application/x-diy"); } @Override public Person read(Class aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException { return null; } /** * 自定义协议数据的写出 * @param person * @param mediaType * @param httpOutputMessage * @throws IOException * @throws HttpMessageNotWritableException */ @Override public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException { String data = person.getName() + ";" + person.getAge() + ";" + person.getBirth(); // 写出去 OutputStream body = httpOutputMessage.getBody(); body.write(data.getBytes()); } } ``` - 向converter中添加自定义的MessageConverter ```java @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { /** * 自定义数据返回时的内容协商策略 * * 如果请求头中的Accept值为 x-diy 则使用自定义的MessageConverter * @param converters */ @Override public void extendMessageConverters(List> converters) { converters.add(new DiyMessageConverter()); } }; } ``` ##### 4.4.4.2 以参数方式完成内容协商 `?format=diy` --> `x-diy`, 需要自定义内容协商管理器 ```java @Configuration public class WebConfig /*implements WebMvcConfigurer*/ { /** * 自定义SpringMVC功能 * @return */ @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { /** * 自定义协商策略 */ @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // 指定支持解析哪些参数对应的哪些媒体类型 Map mediaTypes = new HashMap<>(); mediaTypes.put("json", MediaType.APPLICATION_JSON); mediaTypes.put("xml", MediaType.APPLICATION_XML); mediaTypes.put("diy", MediaType.parseMediaType("application/x-diy")); // 参数协商策略 ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(mediaTypes); // parameterContentNegotiationStrategy.setParameterName("ff"); // 请求头协商策略 HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy, headerContentNegotiationStrategy)); } }; } } ``` ### 4.5 视图和模板 #### 4.5.1 视图解析 - 引入依赖 ```xml org.springframework.boot spring-boot-starter-thymeleaf ``` - SpringBoot的自动配置 ```java public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; //xxx.html ``` - 页面开发 ```html Title

哈哈

去百度
去百度2

``` - controller ```java @Controller public class ViewTestController { @GetMapping("/hello") public String hello(Model model) { // model中的数据会被放在请求域中 request.setAttribute("aaa", AAA) model.addAttribute("msg", "你好"); model.addAttribute("link", "www.taobao.com"); return "hello"; } } ``` #### 4.5.2 thymeleaf模板引擎 #### 4.5.3 后台 使用正则全部替换html中的路由 ``` src="(.*?)" ``` ``` th:src="@\{/$1\}" ``` ```html ``` ### 4.6 拦截器 拦截器编写步骤 - 编写一个拦截器实现HandlerInterceptor接口 ```java /** * 登录检查拦截器 * * 配置好拦截器的拦截规则 * 哪些放行;哪些拦截,拦截后可以添加如何跳转登录页面 */ public class LoginInterceptor implements HandlerInterceptor { /** * 目标方法执行前 * * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); System.out.println("preHandle拦截的请求路径是: " + requestURI); // 登录逻辑检查 HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user != null) { // 放行 return true; } // 拦截住, 未登录,跳转到登录页面 // session.setAttribute("msg", "请登录"); // response.sendRedirect("/"); // 或者使用request 转发请求 request.setAttribute("msg", "请登录"); request.getRequestDispatcher("/login").forward(request, response); return false; } /** * 目标方法执行后 * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } /** * 页面渲染以后 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } } ``` - 拦截器注册到容器中, 实现WebConfigurer的addIntercepter - 指定拦截规则 ```java @Configuration public class AdminWebConfig implements WebMvcConfigurer { /** * 配置拦截器,将拦截器注入到容器中 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") //拦截所有请求,包括静态资源请求 .excludePathPatterns("/login", "/css/**", "/fonts/**", "/js/**", "/images/**"); // 要放行的请求 } } ``` ### 4.7 文件上传 - form ```html Title

file upload

username
email
avatar
photos
``` - MultipartFile自动封装文件 ```java package com.chmingx.admin.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; @Controller public class FileController { @GetMapping("/file") public String filePage() { return "form/file"; } /** * MultipartFile 自动封装了上传过来的文件 * @return */ @PostMapping("/file") public String upload(@RequestParam("username") String username, @RequestParam("email") String email, @RequestPart("avatar") MultipartFile avatar, @RequestPart("photos") MultipartFile[] photos) throws IOException { System.out.println(username); System.out.println(email); if (!avatar.isEmpty()) { String originalFilename = avatar.getOriginalFilename(); avatar.transferTo(new File("D:\\tmp\\" + originalFilename)); } if (photos.length > 0) { for (MultipartFile photo : photos) { if (!photo.isEmpty()) { photo.transferTo(new File("D:\\tmp\\" + photo.getOriginalFilename())); } } } return "index"; } } ``` - 修改允许上传文件大小上限 ```yaml spring: servlet: multipart: max-file-size: 10MB max-request-size: 100MB ``` ### 4.8 错误处理 - 默认情况下,Spring Boot提供`/error`处理所有错误的映射 - 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息; 对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据 #### 4.8.1 自定义错误页面 `resources/templates/error/`下的4xx.html,5xx.html页面会被自动解析 有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页 #### 4.8.2 @ControllerAdvice+@ExceptionHandler处理全局异常 底层是 **ExceptionHandlerExceptionResolver 支持的** ```java /** * 处理全局异常 */ @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({NullPointerException.class}) // 能处理的异常 public String handleNullPointerException(Exception e) { System.out.println(e.getMessage()); return "index"; // 需要返回一个ModelAndView, 可以直接指定页面,即index.html } } ``` #### 4.8.3 @ResponseStatus处理自定义异常 ```java /** * 一般用于处理项目中自定义的异常, 比如项目中哪里 throw new DiyException() */ @ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "页面被紧张访问") public class DiyException extends RuntimeException { public DiyException() { } public DiyException(String message) { super(message); } } ``` #### 4.8.4 自定义实现 HandlerExceptionResolver 处理异常 ```java @Order(value = Ordered.HIGHEST_PRECEDENCE) // 可以指定自定义异常处理器的优先级 @Component public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) { try { httpServletResponse.sendError(511, "自定义异常处理"); } catch (IOException ioException) { ioException.printStackTrace(); } return new ModelAndView(); // 异常处理需要返回一个ModelAndView // return new ModelAndView("5xx.html"); // 异常处理需要返回一个ModelAndView, 可以指定一个页面 } } ``` ### 4.9 Web原生组件注入 三大原生组件 Servlet, Filter, Listener #### 4.9.1 通过@ServletComponentScan + @WebServlet/@WebFilter/@WebListener(推荐) - Servlet ```java /** * 注入Servlet */ @WebServlet(urlPatterns = "/my") // 指定处理的路由 public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("原生组件服务"); } } ``` - Filter ```java /** * 注入Filter */ @WebFilter(urlPatterns = {"/css/*", "/images/*"}) // 要拦截的路由数组 public class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("放行"); filterChain.doFilter(servletRequest, servletResponse); // 放行 } @Override public void destroy() { Filter.super.destroy(); } } ``` - Listener ```java /** * 注入Listener */ @WebListener public class MyServletContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("原生Listener监听到项目初始化完成"); } @Override public void contextDestroyed(ServletContextEvent sce) { System.out.println("原生Listener监听到项目销毁"); } } ``` - 添加ServletComponentScan ```java @ServletComponentScan(basePackages = "com.chmingx.admin") // 指定扫描包, 默认为文件所在包 @SpringBootApplication public class Boot03WebadminApplication { public static void main(String[] args) { SpringApplication.run(Boot03WebadminApplication.class, args); } } ``` #### 4.9.2 使用RegistrationBean - 创建号原生组件,不要加@WebServlet等注解,如上小节 - 编写Bean即可 ```java @Configuration public class MyRegisterConfig { @Bean public ServletRegistrationBean myServlet() { MyServlet myServlet = new MyServlet(); // 创建原生组件对象 return new ServletRegistrationBean(myServlet, "/my", "your"); // 将原生组件注入,并指定能处理的路由 } @Bean public FilterRegistrationBean myFilter() { MyFilter myFilter = new MyFilter(); FilterRegistrationBean myFilterFilterRegistrationBean = new FilterRegistrationBean<>(); myFilterFilterRegistrationBean.setFilter(myFilter); myFilterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/my", "/css")); return myFilterFilterRegistrationBean; } @Bean public ServletListenerRegistrationBean myListener() { return new ServletListenerRegistrationBean<>(new MyServletContextListener()); } } ``` #### 4.9.3 原理 SpringBoot核心的DispathServlet和定义的原生的MyServlet都是通过ServletRegistrationBean注入进Tomcat-Servlet的,因此目前在Tomcat中有两个服务分别是DispaServlet处理`/`下请求,MyServlet处理`/my`下请求。在多个Servlet能处理同一路径下,精确优先原则,所以`/my`下的请求都直接由MyServlet处理,所以DispatchServlet的拦截器不会拦截`/my`下请求 ### 4.10 嵌入式Servlet容器 默认支持的webServer `Tomcat`, `Jetty`, `Undertow` ### 4.11 定制化 定制化常见方式 - 修改配置文件 - xxxCustomizer 定制化器 - 编写自定义配置类 xxxConfiguration + @Bean - __Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能__ ## 5. 数据访问 ### 5.1 HikaraDataSource - 导入JDBC ```xml org.springframework.boot spring-boot-starter-data-jdbc ``` 会自动导入Spring-jdbc、Hikari数据源、和Spring-tx事务,所以还需要导入驱动 ```xml mysql mysql-connector-java 8.0.22 ``` 修改版本的方式: - 直接指定版本 ```xml mysql mysql-connector-java 5.1.47 ``` - pom中重写生命仲裁的版本 ```xml 1.8 5.1.47 ``` - 配置数据库连接和数据库连接池相关属性 ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/test username: admin password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # type: com.zaxxer.hikari.HikariDataSource # 默认的 jdbc: template: query-timeout: 3 ``` - 使用Spring自带的JdbcTemplate操作数据库 ```java @SpringBootTest class Boot04DataApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads() { Integer integer = jdbcTemplate.queryForObject("select count(*) from user", int.class); System.out.println(integer); } } ``` ### 5.2 Druid - 依赖 ```xml com.alibaba druid 1.1.17 ``` - 将数据源注入容器 ```java @Configuration public class DataSourceConfig { @ConfigurationProperties("spring.datasource") // 将DataSource与配置文件中的内容绑定 @Bean public DataSource dataSource() { return new DruidDataSource(); } } ``` ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/test username: admin password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver ``` - 可以对Druid数据源进行配置监控页面, 详情见监控页面 ```java @Configuration public class MyDataSourceConfig { @ConfigurationProperties("spring.datasource") // 将DataSource与配置文件中的内容绑定 @Bean public DataSource dataSource() throws SQLException { DruidDataSource druidDataSource = new DruidDataSource(); // 加入监控功能 druidDataSource.setFilters("stat,wall"); return druidDataSource; } /** * 配置druid的监控页功能 * @return */ @Bean public ServletRegistrationBean statViewServlet() { StatViewServlet statViewServlet = new StatViewServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*"); // 添加账号和密码 registrationBean.addInitParameter("loginUsername", "admin"); registrationBean.addInitParameter("loginPassword", "123456"); return registrationBean; } /** * WebStatFilter 用于采集web-jdbc关联监控的数据 * @return */ @Bean public FilterRegistrationBean webStatFilter() { WebStatFilter webStatFilter = new WebStatFilter(); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter); // 添加监控的路径 filterRegistrationBean.setUrlPatterns(Arrays.asList("/*")); // 排除监控的路径 filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } } ``` #### 使用官方starter ```xml com.alibaba druid-spring-boot-starter 1.1.17 ``` 配置示例 ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.chmingx.data.* #监控SpringBean filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: # 对上面filters里面的stat的详细配置 slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false ``` ### 5.3 MyBatis #### 5.3.1 配置版 - 依赖 ```xml org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.4 ``` - 编写Mapper接口,并标注@Mapper ```java @Mapper public interface UserMapper { public User getUserById(@Param("id") int id); } ``` 如果不标注@Mapper注解,需要在某个配置类上标注@MapperScan("com.chmingx.data.mapper"), 表示扫描这个包下的类为Mapper - 编写sql映射文件并绑定mapper接口 ```xml ``` - 在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信:可以编写Mybatis全局配置;也可以直接在application.yaml中指定(推荐) ```yaml mybatis: # config-location: classpath:mybatis/mybatis-config.xml # 与 configuration不兼容, 推荐不写全局配置文件,直接编写configuration mapper-locations: classpath:mybatis/mapper/*.xml configuration: map-underscore-to-camel-case: true ``` ```xml ``` - 编写service调用mapper接口 ```java @Service public class UserService { @Autowired UserMapper userMapper = null; public User getUserById(int id) { return userMapper.getUserById(id); }; } ``` - 编写controller返回数据给调用方 ```java @RestController public class UserController { @Autowired UserService userService; @GetMapping("/user") public User getUserById(@RequestParam("id") int id) { return userService.getUserById(id); } } ``` #### 5.3.2 注解版 ```java @Mapper public interface CityMapper { @Select("select * from city where id=#{id}") public City getCityById(@Param("id") Long id); //@Insert("insert into city(`name`, `state`, `country`) values (#{name}, #{state}, #{country})") //@Options(useGeneratedKeys = true, keyProperty = "id") public void insert(City city); } ``` ```xml insert into city(`name`,`state`,`country`) values(#{name},#{state},#{country}) ``` #### 5.3.3 总结 - 引入Mybatis-starter - 配置application.yaml, 指定mapper-location位置 - 编写Mapper接口并标注@Mapper注解 - 简单SQL直接注解方式,复杂SQL编写mapper.xml进行绑定映射 - 编写Service调用Mapper方法 - 编写Controller调用Service - @MapperScan("com.chmingx.data.mapper"), 简化, 不用标注@Mapper注解 ### 5.4 Mybatis-Plus ```xml com.baomidou mybatis-plus-boot-starter 3.4.1 ``` - 添加JavaBean ```java public class Account { private Long id; private String name; private Integer age; private String email; // ... } ``` - 添加AccountMapper接口继承BaseMapper ```java @Mapper public interface AccountMapper extends BaseMapper { } ``` - 测试 ```java @SpringBootTest class Boot04DataApplicationTests { @Autowired AccountMapper accountMapper; @Test void accountMapperTest() { List accounts = accountMapper.selectList(null); accounts.forEach(System.out::println); } } ``` ### 5.5 CRUD - 编写Mapper ```java public interface UserMapper extends BaseMapper { } ``` - 编写Service接口 ```java public interface UserService extends IService { } ``` - 编写Service接口实现类 ```java @Service public class UserServiceImpl extends ServiceImpl implements UserService { } ``` - 编写Controller ```java @RestController public class UserController { @Autowired UserService userService; @GetMapping("/user") public String getAllUser() { List userList = userService.list(); // 使用fastjson将列表转为json return JSON.toJSONString(userList); } // 如果逻辑复杂,则在Service中实现 @GetMapping("/userpage") public String getUserByPage(@RequestParam("currentPage") int currentPage, @RequestParam("pageSize") int pageSize) { // 构造分页参数 Page page = new Page<>(currentPage, pageSize); // 调用page进行分页 Page userPage = userService.page(page, null); System.out.println("当前页数: " + userPage.getCurrent()); System.out.println("总页数: " + userPage.getPages()); System.out.println("总记录数: " + userPage.getTotal()); return JSON.toJSONString(userPage.getRecords()); // 获取当前页记录 } } ``` - 添加Mybatis-Plus分页插件 ```java @Configuration public class MybatisConfig { // 向容器中注入Mybatis-Plus分页插件支持 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } } ``` ### 5.6 NoSQL - 引入Redis依赖 ```xml org.springframework.boot spring-boot-starter-data-redis ``` - 配置Redis连接 ```yaml spring: redis: url: redis://127.0.0.1:6379/0 ``` - 测试 ```java @SpringBootTest class Boot05CrudApplicationTests { @Autowired StringRedisTemplate stringRedisTemplate; @Test void testRedis() { ValueOperations stringStringValueOperations = stringRedisTemplate.opsForValue(); stringStringValueOperations.set("hello", "world"); System.out.println(stringStringValueOperations.get("hello")); } } ``` - 引入Jedis作为客户端 ```xml redis.clients jedis ``` - 配置Jedis ```yaml spring: redis: host: 127.0.0.1 port: 6379 # url: redis://127.0.0.1:6379/0 database: 0 client-type: jedis # 显示配置Redis连接工具为Jedis jedis: pool: max-active: 10 # 配置jedis连接池大小 ``` - 使用(略) ## 6 单元测试 SpringBoot2.4以后的测试模块时JUnit5 ### 6.1 常用注解 - **@Test :**表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试 - **@ParameterizedTest :**表示方法是参数化测试,下方会有详细介绍 - **@RepeatedTest :**表示方法可重复执行,下方会有详细介绍 - **@DisplayName :**为测试类或者测试方法设置展示名称 - **@BeforeEach :**表示在每个单元测试之前执行 - **@AfterEach :**表示在每个单元测试之后执行 - **@BeforeAll :**表示在所有单元测试之前执行 - **@AfterAll :**表示在所有单元测试之后执行 - **@Tag :**表示单元测试类别,类似于JUnit4中的@Categories - **@Disabled :**表示测试类或测试方法不执行,类似于JUnit4中的@Ignore - **@Timeout :**表示测试方法运行如果超过了指定时间将会返回错误 - **@ExtendWith :**为测试类或测试方法提供扩展类引用 ### 6.2 断言(assertions) 断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告 | assertion | meaning | | -------------------------- | ------------------------------------------------------------ | | assertEquals | 判断两个对象或两个原始类型是否相等 | | assertNotEquals | 判断两个对象或两个原始类型是否不相等 | | assertSame | 判断两个对象引用是否指向同一个对象 | | assertNotSame | 判断两个对象引用是否指向不同的对象 | | assertTrue | 判断给定的布尔值是否为 true | | assertFalse | 判断给定的布尔值是否为 false | | assertNull | 判断给定的对象引用是否为 null | | assertNotNull | 判断给定的对象引用是否不为 null | | assertArrayEquals | 判断两个对象或原始类型的数组是否相等 | | assertAll | 接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言 | | Assertions.assertThrows() | 判断抛出的异常是否时想要的异常 | | Assertions.assertTimeout() | 如果执行超时则抛出异常 | | fail() | 通过 fail 方法直接使得测试失败 | ```java public class Junit5Test { // 组合断言 @Test @DisplayName("assert all") public void all() { assertAll("Math", () -> assertEquals(2, 1 + 1), () -> assertTrue(1 > 0) ); } @Test @DisplayName("异常测试") public void exceptionTest() { ArithmeticException exception = Assertions.assertThrows( //扔出断言异常 ArithmeticException.class, () -> System.out.println(1 % 0)); } @Test @DisplayName("超时测试") public void timeoutTest() { //如果测试方法时间超过1s将会异常 Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); } @Test @DisplayName("fail") public void shouldFail() { fail("This should fail"); } } ``` ### 6.3 前置条件(assumptions) JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。 ```java @DisplayName("前置条件") public class AssumptionsTest { private final String environment = "DEV"; @Test @DisplayName("simple") public void simpleAssume() { assumeTrue(Objects.equals(this.environment, "DEV")); assumeFalse(() -> Objects.equals(this.environment, "PROD")); } @Test @DisplayName("assume then do") public void assumeThenDo() { assumingThat( Objects.equals(this.environment, "DEV"), () -> System.out.println("In DEV") ); } } ``` ### 6.4 嵌套测试 JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起 ### 6.5 参数化测试 参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利 - **@ValueSource**: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型 - **@NullSource**: 表示为参数化测试提供一个null的入参 - **@EnumSource**: 表示为参数化测试提供一个枚举入参 - **@CsvFileSource**:表示读取指定CSV文件内容作为参数化测试入参 - **@MethodSource**:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流) ```java @ParameterizedTest @ValueSource(strings = {"one", "two", "three"}) @DisplayName("参数化测试1") public void parameterizedTest1(String string) { System.out.println(string); Assertions.assertTrue(StringUtils.isNotBlank(string)); } @ParameterizedTest @MethodSource("method") //指定方法名 @DisplayName("方法来源参数") public void testWithExplicitLocalMethodSource(String name) { System.out.println(name); Assertions.assertNotNull(name); } static Stream method() { return Stream.of("apple", "banana"); } ``` ## 7 指标监控 ### 7.1 Actuator - 引入Actuator的Starter ```xml org.springframework.boot spring-boot-starter-actuator ``` - 配置 ```yaml # management 是所有actuator的配置 # management.endpoint.端点名.xxxx 对某个端点的具体配置 management: endpoints: enabled-by-default: true #默认开启所有监控端点 true web: exposure: include: '*' # 以web方式暴露所有端点 endpoint: #对某个端点的具体配置 health: show-details: always enabled: true info: enabled: true beans: enabled: true metrics: enabled: true info: appName: boot-admin appVersion: 1.0.0 mavenProjectName: @project.artifactId@ # 获取pom中的信息 mavenProjectVersion: @project.version@ ``` - 访问 ```shell $ curl http://localhost:8080/actuator/** ``` ### 7.2 常用的Endpoint - Health: 监控状况 - Info: 项目信息 - Metrics: 运行时指标, 提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到。可与Prometheus配合使用 - Loggers:日志 ### 7.3 定制Endpoint #### 7.3.1 Health - 方法1:配置文件 ```yaml # management 是所有actuator的配置 # management.endpoint.端点名.xxxx 对某个端点的具体配置 management: endpoint: #对某个端点的具体配置 health: show-details: always enabled: true ``` - 方法2:自定义HealthIndicator ```java public class MyComHealthIndicator extends AbstractHealthIndicator { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { Map map = new HashMap<>(); // 检查完成 if(1 == 1){ // builder.up(); //健康 builder.status(Status.UP); map.put("count",1); map.put("ms",100); }else { // builder.down(); builder.status(Status.OUT_OF_SERVICE); map.put("err","连接超时"); map.put("ms",3000); } builder.withDetail("code",100) .withDetails(map); } } ``` #### 7.3.2 Info - 方法1:配置文件 ```yaml info: appName: boot-admin version: 2.0.1 mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值 mavenProjectVersion: @project.version@ ``` - 方法2:自定义InfoContributor ```java public class AppInfoContributor implements InfoContributor { @Override public void contribute(Info.Builder builder) { builder.withDetail("msg","你好") .withDetail("hello","atguigu") .withDetails(Collections.singletonMap("world","666600")); } } ``` #### 7.3.3 Metrics 常用于自定义指标,比如某个url访问的次数,比如在某个服务中某个方法被调用一次,指标就+1 ```java class MyService{ Counter counter; public MyService(MeterRegistry meterRegistry){ // 指标注册 counter = meterRegistry.counter("myservice.method.running.counter"); } public void hello() { // 每调用一次该方法,指标就加1 counter.increment(); } } ``` #### 7.3.4 Endpoint 官方提供的Endpoint没有满足需求,则自定义新的端点 ```java @Component @Endpoint(id = "container") public class MyServiceEndpoint { @ReadOperation public Map getDockerInfo(){ //端点的读操作 http://localhost:8080/actuator/myservice return Collections.singletonMap("dockerInfo","docker started....."); } @WriteOperation public void stopDocker(){ System.out.println("docker stopped....."); } } ``` ### 7.4 spring-boot-admin https://github.com/codecentric/spring-boot-admin - 创建一个新的监控服务项目,引入spring-boot-admin-starter-server ```xml de.codecentric spring-boot-admin-starter-server ``` ```pro server.port=8888 # 配置端口 ``` - SpringBoot启动类添加@EnableAdminServer注解,作为监控后台 ```java @EnableAdminServer @SpringBootApplication public class Boot06AdminApplication { public static void main(String[] args) { SpringApplication.run(Boot06AdminApplication.class, args); } } ``` - 需要监控的服务引入spring-boot-admin-starter-client ```xml de.codecentric spring-boot-admin-starter-client ``` - 需要监控的服务配置要将监控数据汇报给哪个服务 ```yaml spring: boot: admin: client: url: http://localhost:8888 # 要将监控数据汇报给哪个服务器 ``` ## 8 高级特性 ### 8.1 指定配置文件 - 默认配置文件 application.yaml;任何时候都会加载 - 指定环境配置文件 application-{env}.yaml - applicaiton-test.yaml - application-production.yaml - application-dev.yaml - ... - 激活指定环境 - 配置文件激活 - 命令行激活:`java -jar xxx.jar --spring.profiles.active=prod --person.name=haha`, 还修改配置文件的任意值,命令行优先 - 默认配置与环境配置同时生效 - 同名配置项,profile配置优先 ```java @Configuration(proxyBeanMethods = false) @Profile("production") // 只有指定为production环境时,该配置才生效 public class ProductionConfiguration { // ... } ``` 还可以对配置文件进行分组 ```yaml spring.profiles.group.production[0]=proddb spring.profiles.group.production[1]=prodmq spring.profiles.group.test[0]=test ``` 使用:--spring.profiles.active=production 激活production组 ### 8.2 外部配置 外部配置源: **Java属性文件**、**YAML文件**、**环境变量**、**命令行参数**等 配置文件查找位置: - classpath 根路径 - classpath 根路径下config目录 - jar包当前目录 - jar包当前目录的config目录 - `/config`子目录的直接子目录 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项 ### 8.3 自定义starter