一、配置文件:
1、yaml的使用
YAML 是 "YAML Ain't Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件
基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- '#'表示注释
- 字符串无需加引号,如果要加,双引号与单引号表示字符串内容 会被 转义/不转义
示例:
Person类:
1 @Data 2 @NoArgsConstructor 3 @AllArgsConstructor 4 @Component 5 @ConfigurationProperties(prefix = "person") 6 public class Person { 7 private String userName; 8 private Boolean boss; 9 private Date birth; 10 private Integer age; 11 private Pet pet; 12 private String[] interests; 13 private List<String> animal; 14 private Map<String, Object> score; 15 private Set<Double> salarys; 16 private Map<String, List<Pet>> allPets; 17 } 18 19 @Data 20 @NoArgsConstructor 21 @AllArgsConstructor 22 class Pet { 23 private String name; 24 private Double weight; 25 }
application.yaml配置文件:
# yaml表示以上对象 person: userName: "zhangsan 李四" boss: false birth: 2019/12/12 20:12:33 age: 18 pet: name: tomcat weight: 23.4 interests: [篮球,游泳] animal: - jerry - mario score: english: first: 30 second: 40 third: 50 math: [131,140,148] chinese: {first: 128,second: 136} salarys: [3999,4999.98,5999.99] allPets: sick: - {name: tom} - {name: jerry,weight: 47} health: [{name: mario,weight: 47}]
controller测试,输出数据:
@Resource Person person; @RequestMapping({"/index", "/"}) public String test1() { System.out.println("=========="+person.getUserName()); return person.toString(); }
2、配置提示:
在一般我们自己定义的组件在配置yaml的时候,是没有提示的,如果我们想要编写yaml的时候有提示的话应该加上一个依赖:
<!--依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--设置在打包的时候不将这个插件进行打包-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
加上了依赖就有了提示:
二、Web开发
1、概览
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.
- 内容协商视图解析器和BeanName视图解析器
- Support for serving static resources, including support for WebJars (covered later in this document)).
- 静态资源(包括webjars)
- Automatic registration of
Converter
,GenericConverter
, andFormatter
beans.
- 自动注册
Converter,GenericConverter,Formatter
- Support for
HttpMessageConverters
(covered later in this document).
- 支持
HttpMessageConverters
(后来我们配合内容协商理解原理)
- Automatic registration of
MessageCodesResolver
(covered later in this document).
- 自动注册
MessageCodesResolver
(国际化用)
- Static
index.html
support.
- 静态index.html 页支持
- Custom
Favicon
support (covered later in this document).
- 自定义
Favicon
- Automatic use of a
ConfigurableWebBindingInitializer
bean (covered later in this document).
- 自动使用
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上)
2、简单功能分析:
2.1.1、静态资源访问目录
只要静态资源放在类路径下: called /static
(or /public
or /resources
or /META-INF/resources
访问 : 当前项目根路径/ + 静态资源名
源码中可以看到springboot默认的资源访问目录设置:
2.1.2、静态资源访问前缀:
在yaml中可以配置静态资源的访问目录,跟访问资源的前缀:
spring: mvc: # 资源访问前缀 static-path-pattern: /res/**
2.2、欢迎页面:
将index.html放在资源访问目录下即可,也可以通过controller进行访问!
2.3、网站图标设置:自定义favicon.ico
将favicon.ico放在static资源目录下,即可看到网站图标。需要注意的是,如果刷新页面没有看到网站图标,可以ctrl+F5强行刷新缓存、
或者重启idea即可!
成功访问:
2.4、【原理分析】静态资源配置原理
- SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
- SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
proxyBeanMethods = false) (type = Type.SERVLET) (Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) ({ WebMvcConfigurationSupport.class) (Ordered.HIGHEST_PRECEDENCE + 10) (DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ({ ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration {}
- 给容器中配了什么。
@Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
- 配置文件的相关属性和xxx进行了绑定。WebMvcProperties==spring.mvc、ResourceProperties==spring.resources
1、配置类只有一个有参构造器
//有参构造器所有参数的值都会从容器中确定 //ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象 //WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象 //ListableBeanFactory beanFactory Spring的beanFactory //HttpMessageConverters 找到所有的HttpMessageConverters //ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。========= //DispatcherServletPath //ServletRegistrationBean 给应用注册Servlet、Filter.... public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) { this.resourceProperties = resourceProperties; this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.dispatcherServletPath = dispatcherServletPath; this.servletRegistrations = servletRegistrations; }
2、资源处理的默认规则
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); //webjars的规则 if (!registry.hasMappingForPattern("/webjars/**")) { customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } // String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } } spring: # mvc: # static-path-pattern: /res/** resources: add-mappings: false 禁用所有静态资源规则 @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) public class ResourceProperties { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; /** * Locations of static resources. Defaults to classpath:[/META-INF/resources/, * /resources/, /static/, /public/]. */ private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
3、欢迎页的处理规则
HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。 @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations()); return welcomePageHandlerMapping; } WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) { if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) { //要用欢迎页功能,必须是/** logger.info("Adding welcome page: " + welcomePage.get()); setRootViewName("forward:index.html"); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { // 调用Controller /index logger.info("Adding welcome page template: index"); setRootViewName("index"); } }
4、favicon
3、请求参数处理
3.1、请求映射
3.1.1、rest使用与原理
- @xxxMapping;
- Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
- 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
- 核心Filter;HiddenHttpMethodFilter
- 用法: 表单method=post,隐藏域 _method=put
- SpringBoot中手动开启
- 扩展:如何把_method 这个名字换成我们自己喜欢的。
- 开启页面表单的rest
spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能
Controller请求:
@RequestMapping(value = "/user",method = RequestMethod.GET) public String getUser(){ return "GET-张三"; } @RequestMapping(value = "/user",method = RequestMethod.POST) public String saveUser(){ return "POST-张三"; } @RequestMapping(value = "/user",method = RequestMethod.PUT) public String putUser(){ return "PUT-张三"; } @RequestMapping(value = "/user",method = RequestMethod.DELETE) public String deleteUser(){ return "DELETE-张三"; } @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } //自定义filter @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; }
表单提交:使用隐藏域进行提交请求类型(表单的提交方式要得是post)
<form action="/user" method="get"> <input type="submit" value="get提交"> </form> <form action="/user" method="post"> <input type="submit" value="post提交"> </form> <form action="/user" method="post"> <input type="submit" value="delete提交"> <!--使用隐藏域提交请求--> <input name="_method" type="hidden" value="delete"> </form> <form action="/user" method="post"> <input type="submit" value="put提交"> <!--使用隐藏域提交请求--> <input name="_method" type="hidden" value="put"> </form>
测试:
Rest原理(表单提交要使用REST的时候)
- 表单提交会带上_method=PUT
- 请求过来被HiddenHttpMethodFilter拦截
- 请求是否正常,并且是POST
- 获取到_method的值。
- 兼容以下请求;PUT.DELETE.PATCH
- 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
- 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
3.2、普通参数与基本注解
3.2.1、注解
@PathVariable:restful风格的注解
@RequestHeader:获取请求头的值,可以获取所有的请求头的值、
@CookieValue:获取请求cookie的值
@RequestBody:获取请求体的值(POST请求才有请求体)
@RequestParam:获取请求参数的值
先来在html页面写一个请求,用户测试获取到请求的值等信息~
<a href="/car/2/owner/zhangsan">跳转请求</a>
controller代码:
@RestController public class MyRequestController { @GetMapping("/car/{id}/owner/{username}") public Map<String, Object> getCar( // 获取RestFull请求指定参数的值 @PathVariable("id") Integer id, @PathVariable("username") String name, // 获取RestFull请求所有参数的值 @PathVariable Map<String, String> pv, // 获取指定名称请求的值 @RequestHeader("User-Agent") String userAgent, // 获取所有的请求参数 @RequestHeader Map<String, String> header, /*获取请求参数中指定参数的值:?age=18 @RequestParam("age") Integer age*/ // 获取到的是请求cookie的值(指定cookie的名称即可获取到) @CookieValue("__utma") String cookie, @CookieValue("__utma") Cookie cookies ) { Map<String, Object> map = new HashMap<>(); map.put("id", id); map.put("username", name); map.put("pv", pv); map.put("userAgent", userAgent); map.put("header", header); map.put("__utma", cookie); System.out.println("=============" + cookies); return map; } }
@ModelAttribute:
@MatrixVariable:矩阵变量
================conroller======================= //1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd //2、SpringBoot默认是禁用了矩阵变量的功能 // 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。 // removeSemicolonContent(移除分号内容)支持矩阵变量的 //3、矩阵变量必须有url路径变量才能被解析 @GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path) { Map<String, Object> map = new HashMap<>(16); map.put("low", low); map.put("brand", brand); map.put("path", path); return map; } =============config/配置请求路径变量======================= @Configuration public class MyWebConfig { /** * 配置路径变量 */ @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 设置是否为“;” (分号)内容应从请求URI中删除。 //默认值为“ true” urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }; } }
3.2.2、复杂参数
视频链接:https://www.bilibili.com/video/BV19K4y1L7MT?p=34
Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、
Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据, request.getAttribute();
Map、Model类型的参数,会返回 mavContainer.getModel();---> BindingAwareModelMap 是Model 也是Map
mavContainer.getModel(); 获取到值的
3.2.3、自定义对象参数
可以自动类型转换与格式化,可以级联封装。
我们通过表单提交数据,通常都是下面这一种:
如果公司业务发生改变,直接通过,分割属性,又该怎么操作呢?我们需要告诉框架这种规则:
需要在配置文件中指定将什么数据类型的数据转为什么数据类型:按照什么进行分割数据:
/** * 配置路径变量 */ @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 设置是否为“;” (分号)内容应从请求URI中删除。 //默认值为“ true” urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } @Override public void addFormatters(FormatterRegistry registry) { /// 将表单提交的类型转为指定的类型 registry.addConverter(new Converter<String, Pet>() { /** * * @param source 提交的是前端页面传过来的值 * @return */ @Override public Pet convert(String source) { if (!source.isEmpty()) { Pet pet = new Pet(); String[] split = source.split(","); pet.setName(split[0]); pet.setAge(split[1]); return pet; } return null; } }); } }; }
表单提交数据,就可以看到数据提交成功:
4、视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
4.1、视图解析
4.2、模板引擎-Thymeleaf
4.2.1、thymeleaf简介
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎
4.2.2、基本语法
1、表达式
2、字面量
文本值: 'one text' , 'Another one!' ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false
空值: null
变量: one,two,.... 变量不能有空格
3、文本操作
字符串拼接: +
变量替换: |The name is ${name}|
4、数学运算
运算符: + , - , * , / , %
5、布尔运算
运算符: and , or
一元运算: ! , not
6、比较运算
比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )
7、条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
8、特殊操作
无操作: _
5、拦截器
5.1、配置拦截器
1、编写一个拦截器实现HandlerInterceptor接口 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors) 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
1、编写拦截器实现HandlerInterceptor接口:/interceptor/MyInterceptor
package com.zhixi.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * @author zhangzhixi * @date 2021/3/31 9:13 */ public class MyInterceptor 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("拦截的请求路径是:" + requestURI); // 执行拦截逻辑 HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user != null) { // 放行 return true; } // 错误消息 request.setAttribute("msg", "请先登录!"); // 失败了转发到首页 request.getRequestDispatcher("/").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 { } /** * 执行后 * * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
2、将拦截器注册到容器中---指定拦截器规则
package com.zhixi.config; import com.zhixi.interceptor.MyInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author zhangzhixi * @date 2021/3/31 9:19 */ @Configuration public class MyInterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyInterceptor()) // 拦截所有请求 .addPathPatterns("/**") // 放行哪些请求 .excludePathPatterns("/", "/login", "/js/*", "/images/*", "/css/*", "/fonts/**"); } }
5.2、拦截器原理
1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
- 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、所有拦截器都返回True。执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
6、文件上传功能
6.1、文件上传代码
1、前端表单:
<form role="form" method="post" th:action="@{/upload}" enctype="multipart/form-data"> <div class="form-group"> <label for="exampleInputEmail1">邮箱</label> <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email"> </div> <div class="form-group"> <label for="exampleInputPassword1">用户名</label> <input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password"> </div> <!--单文件上传--> <div class="form-group"> <label for="exampleInputFile">头像</label> <input name="headImg" type="file" id="exampleInputFile"> <p class="help-block">Example block-level help text here.</p> </div> <!--多文件上传--> <div class="form-group"> <label for="exampleInputFile">生活照</label> <input name="photos" type="file" multiple> <p class="help-block">Example block-level help text here.</p> </div> <div class="checkbox"> <label> <input type="checkbox"> Check me out </label> </div> <button type="submit" class="btn btn-primary">提交</button> </form>
2、controller层请求代码:设置时间戳防止文件名重复
1 /** 2 * 文件上传请求 3 * 4 */ 5 @PostMapping("/upload") 6 public String toUpLoad(@RequestParam("email") String email, 7 @RequestParam("username") String username, 8 // 单文件上传 9 @RequestPart("headImg") MultipartFile headImg, 10 // 多文件上传 11 @RequestPart("photos") MultipartFile[] photos) throws IOException { 12 System.out.println("email" + email); 13 System.out.println("用户名:" + username); 14 System.out.println("单文件的文件大小:" + headImg.getSize()); 15 System.out.println("多文件的文件个数:" + photos.length); 16 // 保存单文件的上传地址 17 if (!headImg.isEmpty()) { 18 // 获取文件名 19 String filename = headImg.getOriginalFilename(); 20 // 将收到的文件传输到给定的目标文件。 21 headImg.transferTo(new File("H:\cache\" + System.currentTimeMillis() + filename)); 22 } 23 // 保存多文件的上传地址 24 if (photos.length > 0) { 25 for (MultipartFile photo : photos) { 26 if (!photo.isEmpty()) { 27 // 获取文件名 28 String filename = photo.getOriginalFilename(); 29 // 将收到的文件传输到给定的目标文件。 30 photo.transferTo(new File("H:\cache\" + System.currentTimeMillis() + filename)); 31 } 32 } 33 } 34 35 // 表单提交成功跳转到主页面 36 return "main"; 37 }
3、application中设置单个文件的上传大小以及请求上传的总容量
spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=50MB
6.2、文件上传原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
- 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
- 原理步骤
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成MultipartFile
- 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝
1 @PostMapping("/upload") 2 public String upload(@RequestParam("email") String email, 3 @RequestParam("username") String username, 4 @RequestPart("headerImg") MultipartFile headerImg, 5 @RequestPart("photos") MultipartFile[] photos)
7、异常处理
1、默认规则
- 默认情况下,Spring Boot提供
/error
处理所有错误的映射 - 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
- 要对其进行自定义,添加
View
解析为error
- 要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制但替换其内容。 - error/下的4xx,5xx页面会被自动解析;
2、定制错误处理逻辑
- 自定义错误页
- error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
- @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
- @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error
- Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
- 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
- ErrorViewResolver 实现自定义处理异常;
- response.sendError 。error请求就会转给controller
- 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
- basicErrorController 要去的页面地址是 ErrorViewResolver ;
3、异常处理自动配置原理
- ErrorMvcAutoConfiguration 自动配置异常处理规则
- 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
- public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
- DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
- 容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)
- 处理默认 /error 路径的请求;页面响应 new ModelAndView("error", model);
- 容器中有组件 View->id是error;(响应默认错误页)
- 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
- 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
- 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
- error/404、5xx.html
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
写出去json
错误页
4、小结:error的错误处理
看上面的源码肯定是会很头大,这里就精简一些说一说我们在项目中该如何使用error定制错误:
1.自定义4xx.html 跟5xxhtml
因为在springboot底层,比如我们请求一个地址,它找不到肯定是报404错误,但是如果你有可能是402,403错误呢,那么springboot就不会走精确匹配,就会给你报一个默认的错误页面、
所以我们想4开头的错误就放在4xx页面中:
发送请求:
错误页面
拿到对应的错误数据(状态码、错误信息)
测试,比一个大屏的error数字界面友好
2、@ControllerAdvice+@ExceptionHandler处理全局异常
写一个类,用来处理整个系统的异常:
1 package com.zhixi.exception; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.web.bind.annotation.ControllerAdvice; 5 import org.springframework.web.bind.annotation.ExceptionHandler; 6 7 /** 8 * @author zhangzhixi 9 * @date 2021/3/31 16:50 10 * 处理MVC的全局异常 11 */ 12 13 @Slf4j 14 @ControllerAdvice // 实际上是一个组件,被增强 15 public class HandlingGlobalExceptions { 16 /** 17 * 在请求中有可能不止一个出现错误,我们写个方法用来处理错误 18 * 处理数学错误 19 */ 20 @ExceptionHandler({ArithmeticException.class, NullPointerException.class}) 21 public String handlerArithmeticException(Exception exc) { 22 log.error("捕获到的异常是:", exc); 23 // 视图地址(如果发生了数学运算异常,跟空指针异常,就会跳转到视图地址) 24 return "login"; 25 } 26 }
3、@ResponseStatus+自定义异常
请求页面
准备一个自定义的异常类
package com.zhixi.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; /** * @author zhangzhixi * @date 2021/3/31 16:32
HttpStatus.FORBIDDEN:表示请求的状态码
*/ @ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量超出异常") public class UserExceededException extends RuntimeException { public UserExceededException() { } public UserExceededException(String message) { super(message); } }
测试:
4、使用架底层使用的错误处理机制自定义处理
注意:自定义的这个处理error的错误是最高的,无论出了什么错误总是会走这个异常处理,而并不会走上面定义的一些异常处理~
实现底层框架接口:HandlerExceptionResolver。进行异常的处理
package com.zhixi.exception; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author zhangzhixi * @date 2021/3/31 17:52 * 自定义异常,实现框架底层接口 */ @Order(value = Ordered.HIGHEST_PRECEDENCE) // 设置优先级的等级最高 @Component public class DiyException implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { /** * 参数: * sc –错误状态代码 * msg –描述性消息 */ response.sendError(521, "这是一个喜欢你的自定义异常"); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } }
5xx.html页面设置取值:
测试:
8、web原生组件注入(servlet、filter、listener)
8.1.使用Servlet API
@ServletComponentScan(basePackages = "com.atguigu.admin") :指定原生Servlet组件都放在那里
@WebServlet(urlPatterns = "/my"):效果:直接响应,没有经过Spring的拦截器?
@WebFilter(urlPatterns={"/css/*","/images/*"})
@WebListener
推荐可以这种方式(以下为简单示例);
servlet:
1 package com.zhixi.servlet; 2 3 import javax.servlet.ServletException; 4 import javax.servlet.annotation.WebServlet; 5 import javax.servlet.http.HttpServlet; 6 import javax.servlet.http.HttpServletRequest; 7 import javax.servlet.http.HttpServletResponse; 8 import java.io.IOException; 9 10 /** 11 * @author zhangzhixi 12 * @date 2021/3/31 19:46 13 */ 14 @WebServlet("/wel") 15 public class MyServlet extends HttpServlet { 16 @Override 17 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 18 doPost(req, resp); 19 } 20 21 @Override 22 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 23 request.setCharacterEncoding("utf-8"); 24 response.setContentType("text/html;charset=UTF-8"); 25 response.getWriter().write("你好啊!"); 26 } 27 }
filter:过滤静态资源等
1 package com.zhixi.servlet; 2 3 import javax.servlet.*; 4 import javax.servlet.annotation.WebFilter; 5 import java.io.IOException; 6 7 /** 8 * @author zhangzhixi 9 * @date 2021/3/31 20:02 10 */ 11 // 拦截静态资源 12 @WebFilter(urlPatterns = {"/css/*", "/images/*"}) 13 public class MyFilter implements Filter { 14 @Override 15 public void init(FilterConfig filterConfig) throws ServletException { 16 System.out.println("过滤器初始化"); 17 } 18 19 @Override 20 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 21 System.out.println("过滤器工作"); 22 chain.doFilter(request, response); 23 } 24 25 @Override 26 public void destroy() { 27 System.out.println("过滤器销毁"); 28 } 29 }
listener:监听器
1 package com.zhixi.servlet; 2 3 import javax.servlet.ServletContextEvent; 4 import javax.servlet.ServletContextListener; 5 import javax.servlet.annotation.WebListener; 6 7 /** 8 * @author zhangzhixi 9 * @date 2021/3/31 20:07 10 */ 11 @WebListener 12 public class MyListener implements ServletContextListener { 13 @Override 14 public void contextInitialized(ServletContextEvent sce) { 15 System.out.println("监听到项目初始化完成"); 16 } 17 18 @Override 19 public void contextDestroyed(ServletContextEvent sce) { 20 System.out.println("监听到项目初始化销毁"); 21 } 22 }
主启动类进行扫描servlet组件:
测试:
扩展:DispatchServlet 如何注册进来
- 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
- 通过 ServletRegistrationBean<DispatcherServlet> 把 DispatcherServlet 配置进来。
- 默认映射的是 / 路径。
Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则
A: /my/
B: /my/1
8.2、使用RegistrationBean
ServletRegistrationBean
,
FilterRegistrationBean
, and
ServletListenerRegistrationBean
把刚刚创建的servlet、filter、listener上面的注解都删除,来测试使用ReistrationBean的方式
1 package com.zhixi.config; 2 3 import com.zhixi.servlet.MyFilter; 4 import com.zhixi.servlet.MyListener; 5 import com.zhixi.servlet.MyServlet; 6 import org.springframework.boot.web.servlet.FilterRegistrationBean; 7 import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; 8 import org.springframework.boot.web.servlet.ServletRegistrationBean; 9 import org.springframework.context.annotation.Bean; 10 import org.springframework.context.annotation.Configuration; 11 12 import java.util.Arrays; 13 14 /** 15 * @author zhangzhixi 16 * @date 2021/3/31 20:19 17 */ 18 @Configuration 19 public class MyRegistration { 20 @Bean 21 public ServletRegistrationBean myServlet() { 22 MyServlet myServlet = new MyServlet(); 23 24 // 参数1:创建一个新的实例、参数2:被映射的URL 25 return new ServletRegistrationBean(myServlet, "/my", "/my02"); 26 } 27 28 29 @Bean 30 public FilterRegistrationBean myFilter() { 31 32 MyFilter myFilter = new MyFilter(); 33 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); 34 // 设置过滤请求的资源 35 filterRegistrationBean.setUrlPatterns(Arrays.asList("/my", "/css/*")); 36 return filterRegistrationBean; 37 } 38 39 @Bean 40 public ServletListenerRegistrationBean myListener() { 41 MyListener MyListener = new MyListener(); 42 return new ServletListenerRegistrationBean(MyListener); 43 } 44 }
测试:
9、嵌入式Servlet容器
1、切换嵌入式Servlet容器
- 默认支持的webServer
Tomcat
,Jetty
, orUndertow
ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
- 切换服务器
禁用tomcat启动器,以及可以在https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot
找到其他的启动器进行使用,不过不建议更改服务容器。了解一下即可!
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>
- 原理
- SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
- web应用会创建一个web版的ioc容器
ServletWebServerApplicationContext
ServletWebServerApplicationContext
启动的时候寻找ServletWebServerFactory
(Servlet 的web服务器工厂---> Servlet 的web服务器)
- SpringBoot底层默认有很多的WebServer工厂;
TomcatServletWebServerFactory
,JettyServletWebServerFactory
, orUndertowServletWebServerFactory
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
2、定制Servlet容器
- 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
- 把配置文件的值和
ServletWebServerFactory 进行绑定
- 修改配置文件 server.xxx
- 直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
1 import org.springframework.boot.web.server.WebServerFactoryCustomizer; 2 import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; 3 import org.springframework.stereotype.Component; 4 5 @Component 6 public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> { 7 8 @Override 9 public void customize(ConfigurableServletWebServerFactory server) { 10 server.setPort(9000); 11 } 12 }
10、定制化原理
1、定制化的常见方式
- 1.修改配置文件;
- xxxxxCustomizer;
- 2.编写自定义的配置类 加上@Configuration;+ @Bean替换、增加容器中默认组件;视图解析器
- 3.Web应用 编写一个配置类实现 WebMvcConfigurer ,即可定制化web功能;+ @Bean给容器中再扩展一些组件
- @EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能
- 原理
- 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页.....
- 2、一旦使用 @EnableWebMvc 、。会 @Import(DelegatingWebMvcConfiguration.class)
- 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
- 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
- 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
- public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
- 4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
- 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
- ... ...
2、原理分析套路
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties -- 绑定配置文件项
三、数据访问
1、数据源的自动配置-HikariDataSource
1、导入JDBC场景
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency>
数据库驱动?
为什么导入JDBC场景,官方不导入驱动?官方不知道我们接下要操作什么数据库。
数据库版本和驱动版本对应
默认版本:<mysql.version>8.0.22</mysql.version>
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
<!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>
2、重新声明版本(maven的属性的就近优先原则)
<properties> <java.version>1.8</java.version> <mysql.version>5.1.49</mysql.version> </properties>
2、分析自动配置
1、自动配置的类
- DataSourceAutoConfiguration : 数据源的自动配置
- 修改数据源相关的配置:spring.datasource
- 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
- 底层配置好的连接池是:HikariDataSource
- DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置
- JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud
- 可以修改这个配置项@ConfigurationProperties(prefix = "spring.jdbc") 来修改JdbcTemplate
- @Bean@Primary JdbcTemplate;容器中有这个组件
- JndiDataSourceAutoConfiguration: jndi的自动配置
- XADataSourceAutoConfiguration: 分布式事务相关的
3、修改配置项
spring: datasource: url: jdbc:mysql://localhost:3306/donglijiedian?useSSL=true&serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 username: root password: zhixi158 driver-class-name: com.mysql.jdbc.Driver
4、测试
@Slf4j @SpringBootTest class Boot05WebAdminApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads() { // jdbcTemplate.queryForObject("select * from account_tbl") // jdbcTemplate.queryForList("select * from account_tbl",) Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class); log.info("记录总数:{}",aLong); } }
2、整合Druid跟mybatis看这篇博客
https://i.cnblogs.com/posts/edit;postId=14335345
3、SpringBoot操作Redis数据库看这篇博客
https://www.cnblogs.com/zhangzhixi/p/14446050.html#_label5
NoSQL
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
1、Redis自动配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
自动配置:
- RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx是对redis的配置
- 连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration
- 自动注入了RedisTemplate<Object, Object> : xxxTemplate;
- 自动注入了StringRedisTemplate;k:v都是String
- key:value
- 底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis
四、单元测试
1、JUnit5 的变化
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。
注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)
JUnit 5’s Vintage Engine Removed from spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage
<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
2、JUnit5常用注解
JUnit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
- @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
- @RepeatedTest :表示方法可重复执行,下方会有详细介绍
- @DisplayName :为测试类或者测试方法设置展示名称
- @BeforeEach :表示在每个单元测试之前执行
- @AfterEach :表示在每个单元测试之后执行
- @BeforeAll :表示在所有单元测试之前执行
- @AfterAll :表示在所有单元测试之后执行
- @Tag :表示单元测试类别,类似于JUnit4中的@Categories
- @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
- @ExtendWith :为测试类或测试方法提供扩展类引用
import org.junit.jupiter.api.Test; //注意这里使用的是jupiter的Test注解!! public class TestDemo { @Test @DisplayName("第一次测试") public void firstTest() { System.out.println("hello world"); }
3、断言(assertions)
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后,会有一个详细的测试报告;
1、简单断言
用来对单个值进行简单的验证。如:
方法 | 说明 |
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
@Test @DisplayName("simple assertion") public void simple() { assertEquals(3, 1 + 2, "simple math"); assertNotEquals(3, 1 + 1); assertNotSame(new Object(), new Object()); Object obj = new Object(); assertSame(obj, obj); assertFalse(1 > 2); assertTrue(1 < 2); assertNull(null); assertNotNull(new Object()); }
2、数组断言
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等
@Test @DisplayName("array assertion") public void array() { assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); }
3、组合断言
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
@Test @DisplayName("assert all") public void all() { assertAll("Math", () -> assertEquals(2, 1 + 1), () -> assertTrue(1 > 0) ); }
4、异常断言
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。
@Test @DisplayName("异常测试") public void exceptionTest() { ArithmeticException exception = Assertions.assertThrows( //扔出断言异常 ArithmeticException.class, () -> System.out.println(1 % 0)); }
5、超时断言
Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间
@Test @DisplayName("超时测试") public void timeoutTest() { //如果测试方法时间超过1s将会异常 Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); }
6、快速失败
通过 fail 方法直接使得测试失败
@Test @DisplayName("fail") public void shouldFail() { fail("This should fail"); }
7、前置条件(assumptions)
JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
@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") ); } }
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
8、参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。
如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
@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<String> method() { return Stream.of("apple", "banana"); }
五、指标监控
1、SpringBoot Actuator
1.1、简介
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
1.2、1.x与2.x的不同
1.3、如何使用
- 引入场景
- 访问 http://localhost:8080/actuator/**
- 暴露所有监控信息为HTTP
management: endpoints: enabled-by-default: true #暴露所有端点信息 web: exposure: include: '*' #以web方式暴露
- 测试
http://localhost:8080/actuator/beans
http://localhost:8080/actuator/configprops
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/jvm.gc.pause
http://localhost:8080/actuator/endpointName/detailPath
。。。。。。
2、Actuator Endpoint
2.1、最常使用的端点
ID | 描述 |
|
暴露当前应用程序的审核事件信息。需要一个 |
|
显示应用程序中所有Spring Bean的完整列表。 |
|
暴露可用的缓存。 |
|
显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
|
显示所有 |
|
暴露Spring的属性 |
|
显示已应用的所有Flyway数据库迁移。 |
|
显示应用程序运行状况信息。 |
|
显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个 |
|
显示应用程序信息。 |
|
显示Spring |
|
显示和修改应用程序中日志的配置。 |
|
显示已应用的所有Liquibase数据库迁移。需要一个或多个 |
|
显示当前应用程序的“指标”信息。 |
|
显示所有 |
|
显示应用程序中的计划任务。 |
|
允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 |
|
使应用程序正常关闭。默认禁用。 |
|
显示由 |
|
执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
|
返回 |
|
通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖 |
|
返回日志文件的内容(如果已设置 |
|
以Prometheus服务器可以抓取的格式公开指标。需要依赖 |
最常用的Endpoint
- Health:监控状况
- Metrics:运行时指标
- Loggers:日志记录
2.2、Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
- health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
- 可以很容易的添加自定义的健康检查机制
2.3、Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或者扩展已有Metrics
2.4、管理Endpoints
1、开启与禁用Endpoints
- 默认所有的Endpoint除过shutdown都是开启的。
- 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint.<endpointName>.enabled = true
management: endpoint: beans: enabled: true
- 或者禁用所有的Endpoint然后手动开启指定的Endpoint
management: endpoints: enabled-by-default: false endpoint: beans: enabled: true health: enabled: true
2、暴露Endpoints
支持的暴露方式
- HTTP:默认只暴露health和info Endpoint
- JMX:默认暴露所有Endpoint
- 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
ID | JMX | Web |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
Yes |
|
N/A |
No |
|
Yes |
No |
|
Yes |
Yes |
|
Yes |
No |
|
N/A |
No |
|
N/A |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
N/A |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
3、指标监控(可视化):Boot Admin Server
项目地址:https://github.com/codecentric/spring-boot-admin
快速开始:https://codecentric.github.io/spring-boot-admin/2.3.1/#getting-started
服务端配置:
- 1.新建一个监控模块,先导入场景依赖
<!--web启动场景--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--监控项目的启动场景--> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.3.1</version> </dependency>
- 2.开启监控功能+更改监控模块的端口,防止与项目端口冲突
端口号更改:server.port=8888
- 3.测试访问监控页面、
客户端配置
- 1.引入客户端场景依赖
<!--客户端场景依赖--> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.3.1</version> </dependency>
- 2.设置服务端的URL
# 服务端地址 spring.boot.admin.client.url=http://localhost:8888 # 以web方式暴露所有端点 management.endpoints.web.exposure.include=*
- 3.启动客户端
- 4.更改实例IP信息
在刚刚的监控中,可以看到使用的是计算机名称作为域名,这是因为在我的电脑上面有本机规则,但是如果放到了linux环境下,肯定会出错的,所以我们要更改成对应的IP信息进行访问
# 当前实例地址是用host(false)还是server(true) spring.boot.admin.client.instance.prefer-ip=true
再次重启客户端应用进行监控测试:
六、原理解析
1、Profile功能
为了方便多环境适配,springboot简化了profile功能。
1.1、application-profile功能
- 默认配置文件 application.yaml;任何时候都会加载
- 指定环境配置文件 application-{env}.yaml
- 激活指定环境
- 配置文件激活:spring.profile.active=test
- 命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
- 修改配置文件的任意值,命令行优先
- 默认配置与环境配置同时生效
- 同名配置项,profile配置优先
1.2、@Profile条件装配功能
@Configuration(proxyBeanMethods = false) @Profile("production") public class ProductionConfiguration { // ... }
2、外部化配置
2.1、外部配置源
常用:Java属性文件、YAML文件、环境变量、命令行参数;
2、配置文件查找位置
- (1) classpath 根路径
- (2) classpath 根路径下config目录
- (3) jar包当前目录
(4) jar包当前目录的config目录
- (5) config子目录的直接子目录
注意:后面的配置会覆盖前面的配置,并不是优先级的问题(是覆盖问题)
3、配置文件加载顺序:
- 当前jar包内部的application.properties和application.yml
- 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
- 引用的外部jar包的application.properties和application.yml
- 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
4、指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
3、自定义starter(掌握+理解)
3.1、starter启动原理
- starter-pom引入 autoconfigurer 包
- autoconfigure包中配置使用 META-INF/spring.factories 中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类
- 编写自动配置类 xxxAutoConfiguration -> xxxxProperties
- @Configuration
- @Conditional
- @EnableConfigurationProperties
- @Bean
- ......
引入starter --- xxxAutoConfiguration --- 容器中放入组件 ---- 绑定xxxProperties ---- 配置项
3.2、自定义starter
atguigu-hello-spring-boot-starter(启动器)
atguigu-hello-spring-boot-starter-autoconfigure(自动配置包)
步骤:
- 1.创建场景启动器start
想让别人来引入我们的场景,先要有一个场景启动器,一个场景启动器只是说明当前场景有多少个依赖,并没有代码
拿我们常用的场景启动器:spring-boot-starter-test、而对于一个场景的自动配置则是在当中的spring-boot-test-autoconfigure中
编写maven工程: zhixi-hello-spring-boot-starter(启动器)。
- 2.创建自动配置包
创建springboot项目:zhixi-hello-spring-boot-starter-autoconfigure。
- 3.场景启动器中引入自动配置的包信息
- 4.自动配置包中:编写业务逻辑业务组件+配置组件+自动配置类
业务组件:service/HelloService
package com.zhixi.service; import com.zhixi.beans.HelloProperties; import org.springframework.beans.factory.annotation.Autowired; /** * @author zhangzhixi * @date 2021/4/2 22:24 * 默认不要放在组件中 */ public class HelloService { @Autowired HelloProperties helloProperties; /** * 打招呼的方法 * * @return */ public String sayHello(String name) { return helloProperties.getPrefix() + "," + name + "," + helloProperties.getSuffix(); } }
配置组件:benas/HelloProperties
package com.zhixi.beans; import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author zhangzhixi * @date 2021/4/2 22:26 */ @ConfigurationProperties("zhixi.hello") public class HelloProperties { private String prefix; private String suffix; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; } }
自动配置类:auto/HelloServiceAutoConfiguration
package com.zhixi.auto; import com.zhixi.beans.HelloProperties; import com.zhixi.service.HelloService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author zhangzhixi * @date 2021/4/2 22:33 */ @Configuration /** * 没有配置HelloService,下面配置才生效 */ @ConditionalOnMissingBean(HelloService.class) /** * 开启属性自动绑定,默认会将HelloProperties放在容器当中 */ @EnableConfigurationProperties(HelloProperties.class) public class HelloServiceAutoConfiguration { @Bean public HelloService helloService() { return new HelloService(); } }
- 5.安装到本地maven仓库中
场景启动器依赖自动配置包,所以要将自动配置包安装得到本地仓库
把场景启动器也放到本地仓库中,这样别的项目就可以引入了
- 6.创建开发场景目录,进行测试
引入我们写好的场景启动器 zhixi-hello-spring-boot-starter(启动器):
<dependency> <groupId>com.zhixi</groupId> <artifactId>zhixi-hello-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
自动配置类不可能默认生效,因为默认生效的规则是:
- autoconfigure包中配置使用 META-INF/spring.factories 中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类
- 7.更改默认配置
在自动配置包:zhixi-hello-spring-boot-starter-autoconfigure的resources目录下新建META-INF目录,创建spring.factories配置文件
在配置文件中指定要加载的自动配置类
- 8.继续测试
编写controller:controller/MyController
@RestController public class MyController { @Autowired private HelloService helloService; @GetMapping("/hello") public String helloController() { return helloService.sayHello("张志喜"); } }
在配置文件中定义前后缀信息:
zhixi.hello.prefix=你好 zhixi.hello.suffix=666
访问:localhost:8080/hello、成功