优点:
缺点:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.7version>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01(){
return "Hello, Spring Boot!";
}
}
MainApplication
类http://localhost:8888/hello
,页面输出Hello, Spring Boot!
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
parent>
上面的父项目又引入了如下父项目,在该项目中声明了几乎所有开发中常用依赖的版本号,所以在引入依赖时不必再声明版本号:<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.3.4.RELEASEversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
在开发中,只要引入了某个场景的 starter,则该场景需要的所有常规依赖都会自动引入。
<properties>
<java.version>11java.version>
<jedis.version>3.8.0jedis.version>
properties>
DataSourceProperties
@Configuration
public class MyConfig {
@Bean
public User user(){
User user= new User("Tom", 18);
return user;
}
}
@Bean 注解:表示当前方法的返回值会被注册到 Ioc 容器中,方法名作为组件 id,且如果该方法有形参,会自动从 Ioc 容器中找到对应的实例注入。
如何选用:
① 配置类组件之间无依赖关系时,用 Lite 模式可以加速容器启动过程
② 配置类组件之间有依赖关系时,用 Full 模式
<beans ...">
<bean id="tom" class="com.boot.bean.User">
<property name="name" value="Tom">property>
<property name="age" value="18">property>
bean>
beans>
导入:@ImportResource("classpath:beans.xml")
public class MyConfig {
...
}
@Component // 需要把该类注册到 Ioc 容器中
@ConfigurationProperties(prefix = "mycar") //设置该类在配置文件中的前缀
public class Car {
private String brand;
private Integer price;
}
在 application.properties 中就可以配置相关属性值:mycar.brand = BYD
mycar.price = 100000
另一种配置绑定方式:@ConfigurationProperties + @EnableConfigurationProperties
@ConfigurationProperties(prefix = "mycar") //设置该类在配置文件中的前缀
public class Car {
private String brand;
private Integer price;
}
@EnableConfigurationProperties(Car.class)
public class MyConfig {
...
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}
标志当前类是 SpringBoot 启动类,重点关注其中的 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan
。
@AutoConfigurationPackage、@Import(AutoConfigurationImportSelector.class)
。@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)//给容器中导入一个组件
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
该注解直译为:自动配置包,指定了默认的包规则,利用 Registrar 将组件注册到 Ioc 容器中。
AutoConfigurationImportSelector:自动配置导入选择器,将需要的自动配置类注册到 Ioc 容器中。
当工程启动时,默认会加载全部 127 个场景的所有自动配置类,但由于自动配置类上有条件注解@Conditional,只有满足条件的自动配置类才会生效,才能被注册到 Ioc 容器中。
如AopAutoConfiguration
类:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = "auto",
havingValue = "true",
matchIfMissing = true
)
public class AopAutoConfiguration {
...
}
IDEA 2020 版本及以后,默认集成了 Lombok 插件,只需引入其依赖即可。
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
作用一:简化 bean 类的代码。
@Data //自动生成当前类所有属性的get、set、toString、equals、hashCode等方法
public class User {
private Integer uid;
private String uname;
}
作用二:简化日志开发。
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01(@RequestParam("name") String name){
log.info("请求进来了....");
return "Hello, Spring Boot 2!"+"你好:"+name;
}
}
Spring Initializr 是创建 SpringBoot 工程的向导。
创建步骤:在 IDEA 中,菜单栏New -> Project -> Spring Initializr。
application.yaml 可以替代 application.proterties,作为工程的配置文件。
age: 18
# 行内写法
k: {k1:v1,k2:v2,k3:v3}
# 或
k:
k1: v1
k2: v2
k3: v3
# 行内写法:
k: [v1,v2,v3]
# 或
k:
- v1
- v2
- v3
当我们在配置文件中设置自定义类的属性值时,一般都没有提示。若需要提示,则在 pom.xml 中引入如下依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
classpath:映射为 maven 工程下的 /resources/ 目录。
localhost:port/静态资源名
,会自动到默认存放目录寻找静态资源,因为内部的映射规则为 /**
,/
表示 resources 目录。spring:
web:
resources:
static-locations: [classpath:/haha/]
spring:
mvc:
static-path-pattern: /res/**
此时的访问路径:localhost:port/res/静态资源名
。localhost:port/
时,会自动加载 index.html。(如果设置了静态资源访问前缀,则该功能失效)rest 风格的请求:请求路径中只包含操作对象,要执行的操作通过请求方法表示。
举例:假设请求路径是 /user
,则请求方法不同,执行的操作也不同。
SpringBoot 默认关闭 rest 风格,若要开启表单的 rest 功能,需添加以下配置:
spring:
mvc:
hiddenmethod:
filter:
enabled: true
其内部是通过HiddenHttpMethodFilter
实现的。
post
;_method
的隐藏域:<input type="hidden" name="_method" value="delete">
或
<input type="hidden" name="_method" value="put">
HiddenHttpMethodFilter
拦截,该过滤器判断请求是否正常以及请求方法是否是post
;_method
的值,然后判断该值是否在预设的请求方法集合内;_method
的值传给一个 request 包装类HttpMethodRequestWrapper
,该包装类重写了getMethod()
方法;getMethod()
时调用的都是HttpMethodRequestWrapper
的。HiddenHttpMethodFilter
源码:
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
...
public static final String DEFAULT_METHOD_PARAM = "_method";
...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
...
@Override
public String getMethod() {
return this.method;
}
}
}
_method
WebMvcAutoConfiguration
源码:
public class WebMvcAutoConfiguration {
...
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
...
}
分析源码可知,当 Ioc 容器内没有 HiddenHttpMethodFilter 组件时,hiddenHttpMethodFilter()
方法才生效,所以我们可以自定义一个 HiddenHttpMethodFilter 作为 Ioc 容器中的组件,代码如下:
@Configuration(proxyBeanMethods = false)
public class WebConfig{
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}
DispatcherServlet:所有的请求都是通过 DispatcherServlet 的doDispatch()
方法进行处理的 。
请求映射原理:遍历所有的HandlerMapping
,找到能够处理当前请求的Handler
。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);
...
}
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
this.handlerMappings 中包含以下几个 HandlerMapping:
- RequestMappingHandlerMapping:表示 @RequestMapping 注解对应的处理器映射器;
- WelcomePageHandlerMapping:欢迎页请求对应的处理器映射器;
- BeanNameUrlHandlerMapping:
- RouterFunctionHandlerMapping:
- SimpleUrlHandlerMapping:
RequestMappingHandlerMapping 中保存了所有@RequestMapping
和handler
的映射规则。
@RestController
public class ParameterTestController {
// car/2/owner/zhangsan?age=18&hobby=run&hobby=sing
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id, @PathVariable("username") String name, @PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent, @RequestHeader Map<String,String> header,
@RequestParam("age") Integer age, @RequestParam("hobby") List<String> hobby, @RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga, @CookieValue("_ga") Cookie cookie){
Map<String,Object> map = new HashMap<>();
...
return map;
}
}
@Controller
public class RequestController {
@GetMapping("/test")
public String test(Map<String,Object> map,
Model model,
HttpServletRequest request){
//无论map还是model,最终都会调用request.setAttribute()将其里边的内容放到请求域
map.put("map","map666");
model.addAttribute("model","model666");
request.setAttribute("request","request666");
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public String success(@RequestAttribute(value = "map",required = false) String s1,
@RequestAttribute(value = "model",required = false) String s2,
@RequestAttribute(value = "request",required = false) String s3){
System.out.println(s1); // map666
System.out.println(s1); // model666
System.out.println(s1); // request666
return "success";
}
}
@RestController
public class ParameterTestController {
@PostMapping("/save")
public String postMethod(@RequestBody String content){
return content;
}
}
WebMvcConfigurer
组件来开启:@Configuration(proxyBeanMethods = false)
public class WebConfig{
@Bean // 关于 MVC 的定制化,都可以通过重写 WebMvcConfigurer 里的相应方法来实现
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
}
}
@MatrixVariable 使用示例:@RestController
public class ParameterTestController {
// 请求路径:/cars/sell;price=9999;brand=byd,audi
@GetMapping("/cars/{path}")
public String carsSell(@MatrixVariable("price") Integer price,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
System.out.println(price); // 9999
System.out.println(brand); // ["byd","audi"]
System.out.println(path); // sell
return "hello";
}
}
HandlerMapping
,找到能够处理当前请求的Handler
;Handler
找一个HandlerAdapter
,用的最多的是RequestMappingHandlerAdapter
;
在 DispatcherServlet 内部,默认加载了所有的 HandlerAdapter:
- RequestMappingHandlerAdapter:支持 @RequestMapping 注解的
- HandlerFunctionAdapter:支持函数式编程的
- HttpRequestHandlerAdapter:
- SimpleControllerHandlerAdapter
HandlerAdapter
的handle()
方法,完成请求处理。
RequestMappingHandlerAdapter 内部初始化了许多
HandlerMethodArgumentResolver(参数解析器)
,handle()方法执行时会遍历所有的参数解析器,寻找一个匹配的完成参数解析。
WebRequest、ServletRequest、MultipartRequest、HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId。
以上类型的参数都是由ServletRequestMethodArgumentResolver
解析的。
举例:
@RestController
public class RequestController {
@GetMapping("/goto")
public String goToPage(HttpServletRequest request){
request.setAttribute("msg","成功了...");
return "success";
}
}
ModelMethodProcessor
进行处理;MapMethodProcessor
进行处理;以上两种解析器在解析参数时,最终都会调用mavContainer.getModel()
,而且返回的是同一个BindingAwareModelMap
对象。
举例:
@RestController
public class ParameterTestController {
@PostMapping("/saveuser")
public Person saveuser(Person person){
return person;
}
}
自定义类型参数使用ServletModelAttributeMethodProcessor
进行处理。在该解析器中,利用WebDataBinder
将请求参数与自定义参数进行绑定。
绑定过程:对于每一个请求参数,WebDataBinder
都会遍历其内部的所有Converter
,找到可以将该请求参数转换到指定类型的Converter
,然后将请求数据转成指定的数据类型并封装到JavaBean中。
ServletModelAttributeMethodProcessor
->WebDataBinder
->Converter
可以通过自定义WebMvcConfigurer
组件,来定制化 SpringMVC 的功能。
举例:将请求参数Tom,3
转换成Pet
对象。
@Configuration(proxyBeanMethods = false)
public class WebConfig{
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
};
}
}
返回值的处理也是在当前HandlerAdapter
的handle()
方法内进行处理的。
RequestMappingHandlerAdapter 内部初始化了许多
HandlerMethodReturnValueHandler(返回值处理器)
,handle()方法执行时会遍历所有的返回值处理器,寻找一个匹配的完成返回值处理,在处理过程中会使用MessageConverters(消息转换器)
进行写出操作。
ReturnValueHandler
的工作流程:
supportsReturnType
);handleReturnValue()
进行处理;RequestResponseBodyMethodProcessor
为例,该处理器可以处理标注了@ResponseBody
注解的方法。返回值处理器以内容协商的形式进行返回值的处理。
accept
字段);MediaType
);HttpMessageConverter
,找到可以将返回值以上述MediaType
类型写出的消息转换器;在获取客户端可以接受的媒体类型时,会先通过内容协商管理器(ContentNegotiationManager
)判断使用哪种内容协商策略,如果请求地址中有format
这个请求参数,则会启用基于请求参数的内容协商策略。不过 SpringBoot 中需要手动开启基于请求参数的内容协商功能:
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
举例:
http://localhost:8080/test/person?format=json
,返回 json 格式的数据;http://localhost:8080/test/person?format=xml
,返回 xml 格式的数据。自定义MessageConverter
:
public class GuiguMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class); //假设要转换 Person 类型的返回值
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-guigu"); // 假设请求头中的 accept 字段是:application/x-guigu
}
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//自定义协议数据的写出
String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth();
//写出去
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
通过对WebMvcConfigurer
组件的定制化,将自定义MessageConverter
添加到 Ioc 容器中:
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
}
}
}
使用 Postman 模拟发送请求(请求头Accept:application/x-guigu),返回值处理器遍历所有的消息转换器,发现 GuiguMessageConverter 可以将返回值以 x-guigu 媒体类型写出。
上面定义的 GuiguMessageConverter 目前只能支持基于请求头的内容协商,因为基于请求参数的协商策略中只定义了format=json
、format=xml
两种请求参数,所以我们需要自定义基于请求参数的内容协商策略,来支持自定义请求参数可以调用自定义 MessageConverter。
@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigurer*/ {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//用于存放请求参数与媒体类型的映射关系
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
//自定义媒体类型
mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu"));
//创建基于请求参数的内容协商策略
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
//还需添加请求头处理策略,否则基于请求头的内容协商策略会失效
HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(parameterStrategy, headeStrategy));
}
}
}
}
在浏览器输入http://localhost:8080/test/person?format=gg
,返回值处理器会先选择内容协商策略(由于请求地址中包含format
请求参数,会选用基于请求参数的内容协商策略),该策略通过解析format=gg
判断客户端要接收的媒体类型是application/x-guigu
,然后找到匹配的消息转换器将返回值写出(即 GuiguMessageConverter)。
注意:当我们自定义组件时,可能会覆盖很多默认的功能,导致一些默认功能失效。
Thymeleaf 是适用于 web 和 独立环境 的现代服务器端Java模板引擎。Thymeleaf 官网
Thymeleaf 的优点:
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
类型 | 示例 |
---|---|
文本值 | ‘one text’ , ‘Another one!’ ,… |
数字 | 0, 5, 2.3, … |
布尔值 | true, false |
空值 | null |
变量 | one,two,… (变量不能有空格) |
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
fieldset>
form>
<img src="../../images/gtvglogo.png"
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onionstd>
<td th:text="${prod.price}">2.41td>
<td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>
<a href="comments.html" th:if="${not #lists.isEmpty(prod.comments)}">viewa>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administratorp>
<p th:case="#{roles.manager}">User is a managerp>
<p th:case="*">User is some other thingp>
div>
Order | Feature | Attributes |
---|---|---|
1 | Fragment inclusion | th:insert、th:replace |
2 | Fragment iteration | th:each |
3 | Conditional evaluation | th:if、th:unless、th:switch、th:case |
4 | Local variable definition | th:object、th:with |
5 | General attribute modification | th:attr、th:attrprepend、th:attrappend |
6 | Specific attribute modification | th:value、th:href、th:src、... |
7 | Text (tag body modification) | th:text、th:utext |
8 | Fragment specification | th:fragment |
9 | Fragment removal | th:remove |
<p>Hello, [[${session.user.name}]]!p>
上述写法可动态修改标签的文本内容。
引用方式 | 效果 |
---|---|
th:include | 替换当前标签内的所有内容 |
th:insert | 插入当前标签 |
th:replace | 替换当前标签 |
假设有公共页面 common.html:
<div th:fragment="test0" id="test1">
假设当前标签内有许多内容
div>
在 test.html 中引用:
<div th:include="common :: test0"> 通过片段名进行引用 div>
<div th:replace="common :: #test1"> 通过id进行引用 div>
视图解析流程(DispatcherServlet
):
HandlerAdapter
执行完handle()
方法后,会返回一个ModelAndView
对象;processDispatchResult()
方法处理派发结果:该方法内部调用render(mv, request, response)
方法进行页面渲染;
ModelAndView
对象获取视图名称;ViewResolver
视图解析器,找到可以解析当前视图名称的视图解析器,并将其解析为View
对象;view.render(mv.getModelInternal(), request, response)
,视图对象调用自己的render()
方法进行最终的页面渲染。HandlerInterceptor
接口public class LoginInterceptor implements HandlerInterceptor {
//目标方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//登录检查逻辑:登录了直接放行,未登录跳转到登录页面
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return true;
}
request.getRequestDispatcher("/").forward(request,response);
return false;
}
//目标方法执行之后
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}
//页面渲染之后
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //拦截所有请求,包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
HandlerExecutionChain
;
HandlerExecutionChain:处理器执行链,内部包含了可以处理当前请求的 handler 以及所有拦截器)
preHandle()
方法;
① 如果当前拦截器的 preHandle() 返回 true,则执行下一个拦截器的preHandle();
② 如果当前拦截器的 preHandle() 返回 false,则从当前拦截器开始,倒序执行所有已经执行了的拦截器的 afterCompletion() 方法;
preHandle()
方法返回 false,都直接跳出DispatcherServlet
,不再执行目标方法;preHandle()
方法都返回 true 时,执行目标方法;postHandle()
方法;afterCompletion()
;afterCompletion()
。<input type="file" name="headImg">
@PostMapping("/upload")
public String upload(@RequestPart("headImg") MultipartFile headImg){
...
}
<input type="file" name="photos" multiple>
@PostMapping("/upload")
public String upload(@RequestPart("photos") MultipartFile[] photos){
...
}
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartProperties
spring:
servlet:
multipart:
max-file-size: 1MB # 修改单个文件的上传大小
max-request-size: 10MB # 修改一次请求可以上传的文件大小
MultipartAutoConfiguration
类中配置好了文件上传解析器:StandardServletMultipartResolver
。
解析流程:
//StandardServletMultipartResolver 重写了 isMultipart() 方法
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}
//StandardServletMultipartResolver 重写了 resolveMultipart() 方法
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler())
时,内部会找出能够处理当前请求的参数解析器RequestPartMethodArgumentResolver
,然后该解析器进行参数解析,解析过程中会将请求参数封装为MultipartFile
类的对象作参数。public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return true;
}
else {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
return false;
}
return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional());
}
}
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional());
String name = getPartName(parameter, requestPart);
parameter = parameter.nestedIfOptional();
Object arg = null;
//封装成MultipartFile类型的对象作参数
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
arg = mpArg;
}
...
return adaptArgumentIfNecessary(arg, parameter);
}
}
{
"timestamp": "2020-11-22T05:53:28.416+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/asadada"
}
/templates/error/
目录下有4xx.html、5xx.html
等错误页面,则出现错误时会自动替换默认的错误页面。DefaultErrorAttributes
:在容器中的 id 为errorAttributes
,用于定义错误页面中可以包含哪些数据(异常明细,堆栈信息等);public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver{
...
}
BasicErrorController
:在容器中的 id 为basicErrorController
,默认的错误处理控制器,处理/error
请求(json+白页 适配响应);@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
...
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
...
}
DefaultErrorViewResolver
:在容器中的 id 为conventionErrorViewResolver
,默认的错误视图解析器,会将HTTP状态码作为视图名称viewName
,并最终将/error/viewName
封装为ModelAndView
的视图名。public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
...
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
...
}
dispatchException
,并且标志当前请求结束;系统默认的处理器异常解析器:
DefaultErrorAttributes:把异常信息保存到 request 域,并且返回 null;
ExceptionHandlerExceptionResolver:处理 @ExceptionHandler 注解;
ResponseStatusExceptionResolver:处理 @ResponseStatus 注解;
DefaultHandlerExceptionResolver:处理常见的 web 异常;
/error
请求,/error
请求会被BasicErrorController
接收处理;BasicErrorController
内部遍历所有的错误视图解析器,看哪个可以解析;DefaultErrorViewResolver
默认错误页的视图名是:error/500
;/templates/error/500.html
。@ControllerAdvice
+@ExceptionHandler
@ControllerAdvice
public class MyExceptionHandler {
// 可以处理哪些异常
@ExceptionHandler({ArithmeticException.class,NullPointerException.class})
public String handleArithException(Exception e){
return "login"; //视图地址
}
}
@ResponseStatus
+自定义异常@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {
public UserTooManyException(){
}
public UserTooManyException(String message){
super(message);
}
}
说明:
① 当目标方法执行期间抛出UserTooManyException
异常时,由于该异常类标注了@ResponseStatus
注解,会被ResponseStatusExceptionResolver
处理。
② 该解析器内部最终会调用response.sendError(statusCode, resolvedReason)
,即让 Tomcat 服务器发送/error
请求。
@Order(value= Ordered.HIGHEST_PRECEDENCE) //优先级:数字越小优先级越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
response.sendError(511,"我喜欢的错误");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
将原生的 Servlet、Filer、Listener 注入 Ioc 容器。
@ServletComponentScan(basePackages = "com.springbootadmin")
@SpringBootApplication
public class SpringbootAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAdminApplication.class, args);
}
}
@WebServlet
:注入原生 Servlet。@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
@WebFilter
:注入原生 Filter。@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
@WebListener
:注入原生 Listener。@Slf4j
@WebListener
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyListener监听到项目销毁");
}
}
Spring 提供的3个类:ServletRegistrationBean
、FilterRegistrationBean
、ServletListenerRegistrationBean
。(将自定义的三大 Web 组件类标注为普通类)
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyListener myListener = new MyListener();
return new ServletListenerRegistrationBean(myListener);
}
}
SpringBoot 默认支持以下3种 Web Server:Tomcat
、Jetty
、Undertow
。
SpringBoot 默认使用 Tomcat 服务器,若要更改为其他服务器,则修改 pom.xml:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jettyartifactId>
dependency>
server.xxx
WebServerFactoryCustomizer
接口的实现类,重写customize()
方法。@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
ConfigurableServletWebServerFactory
组件。导入 JDBC 场景依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>
导入数据库驱动
以 MySQL 为例:SpringBoot 默认对 MySQL 的驱动版本进行了仲裁
,为了与本机的 MySQL 版本匹配,需要手动指明版本号。
8.0.28
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.21version>
dependency>
<properties>
<java.version>11java.version>
<mysql.version>8.0.21mysql.version>
properties>
相关配置类
DataSourceAutoConfiguration
:数据源自动配置类,默认配置HikariDataSource
数据源。
DataSourceTransactionManagerAutoConfiguration
:事务管理自动配置类。
JdbcTemplateAutoConfiguration
:JdbcTemplate 自动配置类。
JndiDataSourceAutoConfiguration
:Jndi 自动配置类。
XADataSourceAutoConfiguration
:分布式事务相关的自动配置类。
填写配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
单元测试
@Slf4j
@SpringBootTest
class SpringbootAdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long count = jdbcTemplate.queryForObject("select count(*) from user", Long.class);
log.info("总记录数:{}", count);
}
}
Druid 的 GitHub 仓库
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
@Configuration
public class MyDataSourceConfig {
@ConfigurationProperties("spring.datasource") //复用配置文件中的数据源配置
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
}
StatViewServlet
:Druid 内置的一个 Servlet,用于配置监控页。主要作用是提供监控信息展示的html页面、提供监控信息的JSON API。WebStatFilter
:用于采集 web-jdbc 关联监控的数据,如SQL监控、URI监控。WallFilter
:防火墙,基于SQL语义分析来实现防御SQL注入攻击。@Configuration
public class MyDataSourceConfig {
/**
* 配置 Druid 数据源
* @return
*/
@ConfigurationProperties("spring.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<StatViewServlet> servletRegistrationBean = new ServletRegistrationBean<>(statViewServlet,"/druid/*");
return servletRegistrationBean;
}
/**
* 用于采集web-jdbc关联监控的数据
* @return
*/
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
//设置拦截路径
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
//设置放行路径
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.6version>
dependency>
spring.datasource
,配置基本的数据库连接信息。spring.datasource.druid
,配置 Druid 的特色功能。spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
druid:
filters: stat,wall #开启相关功能:stat(SQL监控),wall(防火墙)
stat-view-servlet: #配置监控页功能
enabled: true
login-username: admin
login-password: 123456
web-stat-filter: #监控web
enabled: true
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter: #配置具体的某个 filter
stat:
enabled: true
slow-sql-millis: 1000
log-slow-sql: true
wall:
enabled: true
config:
drop-table-allow: false
MyBatis 的 GitHub 仓库
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.3version>
dependency>
@Mapper
public interface UserMapper {
User getUser(Integer id);
}
userMapper.xml
如下:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.admin.mapper.UserMapper">
<select id="getUser" resultType="com.springboot.admin.bean.User">
select * from user where id=#{id}
select>
mapper>
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #全局配置文件的路径
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件的路径
不推荐使用全局配置文件,推荐直接在 application.yaml 中设置 MyBatis 的全局配置信息,所有在mybatis-config.xml 中配置的信息,都可以直接在mybatis.configuration
下配置:spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件的路径
configuration:
map-underscore-to-camel-case: true #开启下划线转驼峰命名规则
@Mapper
public interface RoleMapper {
@Select("select * from role where id=#{id}")
Role getRole(Integer id);
}
注意:简单的 CURD 语句可以通过注解编写,复杂的还是要通过 mapper 配置文件进行编写。
在主程序类上标注 @MapperScan 注解,则 Mapper 接口上就无需再标注 @Mapper 注解。
@Mapper
public interface RoleMapper {
// 简单的 CURD 语句直接通过注解编写
@Select("select * from role where id=#{id}")
Role getRole(Integer id);
//复杂的 CURD 语句通过 mapper 文件编写
Role getRole2(Integer id);
}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.admin.mapper.RoleMapper">
<select id="getRole2" resultType="com.springboot.admin.bean.Role">
select * from role where id = #{id}
select>
mapper>
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件的路径
configuration:
map-underscore-to-camel-case: true #开启下划线转驼峰命名规则
MyBatis Plus 的 GitHub 仓库
MyBatis Plus:简称 MP,是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
mapperLocations
:classpath*:/mapper/**/*.xml
,类路径下 /mapper 目录及其子目录下的所有 .xml 文件。<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
BaseMapper
类:public interface UserMapper2 extends BaseMapper<User> {
}
怎样确定 User 类对应数据库中的哪张表?
① 默认寻找类名首字母小写对应的表,即 user 表;
② 手动指明:在 User 类上面标注@TableName("user")
注解;
@SpringBootTest
class SpringbootAdminApplicationTests {
@Autowired
UserMapper2 userMapper;
@Test
void testUserMapper2(){
User user = userMapper.selectById(1);
System.out.println(user);
}
}
IService
接口:public interface UserService2 extends IService<User> {
}
ServiceImpl
类:@Service
public class UserService2Impl extends ServiceImpl<UserMapper2, User> implements UserService2 {
}
@SpringBootTest
class SpringbootAdminApplicationTests {
@Autowired
UserService2 userService;
@Test
void testUserService2(){
User user = userService.getById(1);
System.out.println(user);
}
}
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//分页拦截器
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// mybatisPlus 拦截器
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor;
}
}
@SpringBootTest
class SpringbootAdminApplicationTests {
@Autowired
UserService2 userService;
@Test
void testPage(){
Page<User> page = new Page<>(1, 2); //获取第1页,每页2条记录
Page<User> userPage = userService.page(page);
System.out.println("当前页数:" + userPage.getCurrent());
System.out.println("每页显示条数:" + userPage.getSize());
System.out.println("总页数:" + userPage.getPages());
System.out.println("总记录数:" + userPage.getTotal());
System.out.println("当前页数据:" + userPage.getRecords());
}
}
ubuntu 18.04 下 安装 redis
Redis 学习笔记
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
LettuceConnectionConfiguration
、JedisConnectionConfiguration
JedisConnectionConfiguration
不生效;StringRedisTemplate
、RedisTemplate
;spring:
redis:
host: ip
port: 6379
password: username:password
@SpringBootTest
class SpringbootAdminApplicationTests {
@Autowired
RedisTemplate redisTemplate;
@Test
void testRedis(){
ValueOperations ops = redisTemplate.opsForValue();
ops.set("hello","world");
Object hello = ops.get("hello");
System.out.println(hello);
}
}
JUnit 5 官方文档
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
JUnit Platform: Junit Platform是在 JVM 上启动测试框架的基础,不仅支持 Junit 自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter提供了 JUnit 5 的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。
JUnit Vintage: 由于JUint 已经发展多年,为了照顾老的项目,JUnit Vintage 提供了兼容 JUnit4.x,JUnit3.x 的测试引擎。
<dependency>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.hamcrestgroupId>
<artifactId>hamcrest-coreartifactId>
exclusion>
exclusions>
dependency>
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@DisplayName("JUnit5 测试方法")
@Test
void testDisplayName(){
System.out.println("测试 @DisplayName 注解");
}
}
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@ParameterizedTest
@ValueSource(ints = {1,2,3})
void testParameterizedTest(int i){
System.out.println("测试 @ParameterizedTest 注解" + i);
}
}
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@RepeatedTest(3)
void testRepeatedTest(){
System.out.println("测试 @RepeatedTest 注解");
}
}
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
System.out.println("测试 @Timeout 注解");
Thread.sleep(600);
}
}
断言(Assertion)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。
方法 | 说明 |
---|---|
assertEquals() | 判断两个对象或两个原始类型是否相等 |
assertNotEquals() | 判断两个对象或两个原始类型是否不相等 |
assertSame() | 判断两个对象引用是否指向同一个对象 |
assertNotSame() | 判断两个对象引用是否指向不同的对象 |
assertTrue() | 判断给定的布尔值是否为 true |
assertFalse() | 判断给定的布尔值是否为 false |
assertNull() | 判断给定的对象引用是否为 null |
assertNotNull() | 判断给定的对象引用是否不为 null |
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@Test
void testSimpleAssert(){
Assertions.assertEquals(1,2,"1不等于2");
Assertions.assertSame(new Object(), new Object(), "你这是两个对象");
Assertions.assertTrue(1 > 2, "1不大于2");
Assertions.assertNull(new Object(), "不为 null");
}
}
当某个断言失败后,则该断言下面的代码就不再执行。
assertArrayEquals():判断两个数组对应位置处的元素是否相等。
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@Test
void testArrayAssert(){
Assertions.assertArrayEquals(new int[]{1,2}, new int[]{2,1});
}
}
assertAll():该方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@Test
void testAssertAll(){
Assertions.assertAll(
() -> Assertions.assertEquals(2, 2),
() -> Assertions.assertTrue(1 > 0)
);
}
}
当组合断言中的某个断言失败时,该断言下面的代码不再执行。
assertThrows():断言是否会抛出指定异常,配合函数式编程使用。
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@Test
void testException(){
//断言会出现数学运算异常,如果不抛出数学运算异常,则断言失败
ArithmeticException arithmeticException = Assertions.assertThrows(ArithmeticException.class, () -> System.out.println(1 / 0));
System.out.println(arithmeticException);
}
}
assertTimeout():断言程序运行时间不会超过指定超时时间,配好函数式编程使用。
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@Test
void testTime(){
//断言程序运行时间不会超过500毫秒,如果超过500毫秒,则断言失败
Assertions.assertTimeout(Duration.ofMillis(500), ()->Thread.sleep(600));
}
}
fail():使当前断言直接失败。
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@Test
void testFail(){
System.out.println("测试 fail() 方法");
Assertions.fail("断言失败");
}
}
Assumption(假设),类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法终止执行。可以把 Assumption 看作是方法的执行前提,不满足该前提,则方法也就不用再执行了。
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@Test
void testAssumption(){
Assumptions.assumeTrue(1 == 1);
Assumptions.assumeFalse(1 > 2);
Assumptions.assumingThat(1 == 1, ()-> System.out.println(666));
}
}
assumingThat():第1个参数是布尔型,第2个参数是 Executable 接口的实现对象。当第1个参数为 true 时,后面的 Executable 对象才会执行。
JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试。
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
List<Integer> list;
@BeforeEach
void createNewList(){
list = new ArrayList<>();
}
@Nested
class NestedTest{
@Test
void isNull(){
Assertions.assertNull(list);
}
}
}
运行 isNull() 方法,断言失败。表示内部类的测试方法可以驱动外部类的 @BeforeEach、@AfterEach、@BeforeAll、@AfterAll 注解标注的方法。
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
List<Integer> list;
@Test
void isNull(){
Assertions.assertNull(list);
}
@Nested
class NestedTest{
@BeforeEach
void createNewList(){
list = new ArrayList<>();
}
}
}
运行 isNull() 方法,断言成功。表示外部类的测试方法不可以驱动内部类的 @BeforeEach、@AfterEach 注解标注的方法。(jdk 11 以后,内部类不允许定义静态成员)
参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用 @ValueSource 等注解,指定入参来源,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
注解 | 说明 |
---|---|
@ValueSource | 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型 |
@NullSource | 表示为参数化测试提供一个null的入参 |
@EnumSource | 表示为参数化测试提供一个枚举入参 |
@CsvFileSource | 表示读取指定CSV文件内容作为参数化测试入参 |
@MethodSource | 表示读取指定静态方法的返回值作为参数化测试入参(注意方法返回的需要是一个流) |
@DisplayName("JUnit5 测试类")
@SpringBootTest
public class JUnit5Test {
@MethodSource("test") //指定方法名
@ParameterizedTest
void testMethodSource(String s){
System.out.println(s);
}
static Stream<String> test(){
return Stream.of("hello","world");
}
}
从 JUnit 4 迁移到 JUnit 5 需注意如下事项:
org.junit.jupiter.api
包中,断言在org.junit.jupiter.api.Assertions
类中,假设在org.junit.jupiter.api.Assumptions
类中。@Before -> @BeforeEach;@After -> @AfterEach
@BeforeClass -> @BeforeAll;@AfterClass -> @AfterAll
@Ignore -> @Disabled
@Category -> @Tag
@RunWith + @Rule + @ClassRule -> @ExtendWith
SpringBoot Actuator 官方文档
SpringBoot Actuator:未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等,SpringBoot 就抽取了 Actuator 场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
http://localhost:8080/actuator
ID | 描述 |
---|---|
health | 显示应用程序运行状况信息 |
metrics | 显示当前应用程序的“指标”信息 |
loggers | 显示和修改应用程序中日志的配置 |
info | 显示应用程序信息 |
auditevents | 暴露当前应用程序的审核事件信息,需要一个 AuditEventRepository 组件 |
beans | 显示应用程序中所有 Spring Bean 的完整列表 |
caches | 暴露可用的缓存 |
conditions | 显示自动配置的所有条件信息,包括匹配或不匹配的原因 |
configprops | 显示所有 @ConfigurationProperties |
env | 暴露Spring的属性ConfigurableEnvironment |
flyway | 显示已应用的所有 Flyway 数据库迁移,需要一个或多个 Flyway 组件 |
httptrace | 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应),需要一个HttpTraceRepository组件 |
integrationgraph | 显示 Spring integrationgraph,需要依赖 spring-integration-core |
liquibase | 显示已应用的所有 Liquibase 数据库迁移,需要一个或多个 Liquibase 组件 |
mappings | 显示所有 @RequestMapping 路径列表 |
scheduledtasks | 显示应用程序中的计划任务 |
sessions | 允许从 Spring Session 支持的会话存储中检索和删除用户会话,需要使用 Spring Session 的基于 Servlet 的 Web 应用程序 |
shutdown | 使应用程序正常关闭,默认禁用 |
startup | 显示由 ApplicationStartup 收集的启动步骤数据,需要使用 SpringApplication 进行配置BufferingApplicationStartup |
threaddump | 执行线程转储 |
如果是Web应用程序(Spring MVC、Spring WebFlux 或 Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump | 返回hprof堆转储文件 |
jolokia | 通过 HTTP 暴露 JMX bean(需要引入Jolokia,不适用于WebFlux),需要引入依赖 jolokia-core |
logfile | 返回日志文件的内容(如果已设置 logging.file.name 或 logging.file.path 属性),支持使用 HTTPRange 标头来检索部分日志文件的内容 |
prometheus | 以 Prometheus 服务器可以抓取的格式公开指标,需要依赖 micrometer-registry-prometheus |
端点的开启与禁用:除了 shutdown 之外的所有端点,默认都是开启的。
management:
endpoint:
beans:
enabled: false
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
端点暴露:只有暴露的端点,才可以被访问。
actuator 支持两种暴露方式:HTTP(默认只暴露 health)、JMX(默认暴露所有的 Endpoint)。
若要修改默认暴露的 Endpoint,请配置以下的包含和排除属性:
Property | Default |
---|---|
management.endpoints.jmx.exposure.exclude | |
management.endpoints.jmx.exposure.include | * |
management.endpoints.web.exposure.exclude | |
management.endpoints.web.exposure.include | health、info |
可以在配置文件中设置端点显示详细信息:
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
给 health 端点新增自定义的检查信息:
HealthIndicator
,并且继承AbstractHealthIndicator
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
...
//自定义检查逻辑
...
//检查完成,定制显示信息
Map<String,Object> 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);
}
}
info 端点默认显示为空,自定义 info 端点显示信息:
方法1:通过配置文件
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以获取pom文件值、系统环境变量...
mavenProjectVersion: @project.version@
方法2:创建自定义类实现InfoContributor
接口
@Component
public class MyInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("hello","你好")
.withDetail(Collections.singletonMap("key", "value"));
}
}
给 metrics 端点新增自定义的指标信息:
MeterRegistry
注册监控指标:假设监控 hello() 方法的执行次数@Service
public class MyService {
Counter counter;
public MyService(MeterRegistry registry){
counter = registry.counter("myservice.hello.count");
}
public double hello(){
counter.increment();
double count = counter.count();
return count;
}
}
@Controller
public class IndexController {
@ResponseBody
@GetMapping("/hello")
public double count(){
return myService.hello();
}
}
@Component
@Endpoint(id = "container") //id 即为端点名
public class DockerEndpoint {
@ReadOperation //读方法,返回当我们访问 /actuator/container 时的页面信息
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation //写方法,可以通过 JMX 调用
private void restartDocker(){
System.out.println("docker restarted....");
}
}
spring-boot-admin 官方文档
Spring Boot Admin Server:codecentric 发起的一个开源项目,将上面的指标监控进行可视化展示。
记得与自己工程中引入的 actuator 依赖版本对应。
<dependency>
<groupId>de.codecentricgroupId>
<artifactId>spring-boot-admin-starter-serverartifactId>
<version>2.6.7version>
dependency>
@EnableAdminServer
注解:@EnableAdminServer
@SpringBootApplication
public class SpringbootAdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAdminServerApplication.class, args);
}
}
server:
port: 8081
http://localhost:8081
,即可看到可视化界面。记得与自己工程中引入的 actuator 依赖版本对应。
<dependency>
<groupId>de.codecentricgroupId>
<artifactId>spring-boot-admin-starter-clientartifactId>
<version>2.3.1version>
dependency>
spring:
application:
name: MyAdminClient # 设置自己的工程名
boot:
admin:
client:
url: http://localhost:8081 # 设置 admin server 的地址
management:
endpoints:
enabled-by-default: true # 设置启用所有端点
web:
exposure:
include: '*' # 设置以 web 模式暴露所有端点
实际开发中,测试环境与生产环境用的配置文件不同,为了方便多环境适配,SpringBoot 简化了 profile 功能。
public interface Person {
String getName();
Integer getAge();
}
@Profile
:该注解即可以标注在类上,又可以标注在方法上。@Profile("test") //表示当加载 application-test.yaml 时,该类生效
@Component
@ConfigurationProperties("person")
@Data
public class Worker implements Person {
private String name;
private Integer age;
}
@Profile(value = {"prod","default"}) //表示当加载 application-prod.yaml 时,该类生效
@Component
@ConfigurationProperties("person")
@Data
public class Boss implements Person {
private String name;
private Integer age;
}
@Controller
public class IndexController {
@Autowired
private Person person;
@GetMapping("/person")
public String person(){
//激活了prod,则返回Boss;激活了test,则返回Worker
return person.getClass().toString();
}
}
person:
name: test-张三
server:
port: 7000
② 生成环境的配置文件 application-prod.yaml:person:
name: prod-张三
server:
port: 8000
spring:
profiles:
active: prod
http://localhost:8080/person
,结果是 “Boss”。配置文件查找路径:
配置文件加载顺序:
总结:指定环境优先,外部优先,后面的可以覆盖前面的同名配置项。
目标:创建HelloService
的 starter。
hello-spring-boot-starter-autoconfigure
pom.xml
:
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.0version>
<relativePath/>
parent>
<groupId>com.atguigugroupId>
<artifactId>hello-spring-boot-starter-autoconfigartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>hello-spring-boot-starter-autoconfigname>
<description>hello-spring-boot-starter-autoconfigdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
dependencies>
project>
HelloServiceAutoConfiguration.java
:@Configuration
@EnableConfigurationProperties(HelloProperties.class) //开启属性与配置文件的绑定
public class HelloServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean(HelloService.class) //配置当容器中没有 HelloService 组件时,我们再往容器中放该组件
public HelloService helloService(){
return new HelloService();
}
}
HelloProperties.java
:@ConfigurationProperties("hello") //与配置文件进行属性绑定,绑定前缀为 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;
}
}
HelloService.java
:/**
* 默认不要放在容器中,让用户按需配置
*/
public class HelloService {
@Autowired
private HelloProperties helloProperties;
public String sayHello(String username){
return helloProperties.getPrefix() + "##" + username + "##" + helloProperties.getSuffix();
}
}
spring.factories
:在 SpringBoot 中注册我们自己的自动配置类。org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.atguigu.hello.auto.HelloServiceAutoConfiguration
工程创建完成,使用 maven 插件 install 到本地。
hello-spring-boot-starter
hello-spring-boot-starter-autoconfig
依赖。pom.xml
:
<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.0modelVersion>
<groupId>org.atguigugroupId>
<artifactId>hello-spring-boot-starterartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<maven.compiler.source>11maven.compiler.source>
<maven.compiler.target>11maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>hello-spring-boot-starter-autoconfigartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
dependencies>
project>
工程创建完成,使用 maven 插件 install 到本地。
hello-spring-boot-starter-test
hello-spring-boot-starter
的依赖:<dependency>
<groupId>org.atguigugroupId>
<artifactId>hello-spring-boot-starterartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
application.properties
:hello.prefix=aaaa
hello.suffix=bbbb
HelloController.java
:@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String hello(){
return helloService.sayHello("张三");
}
}
http://localhost:8080/hello
,浏览器返回aaaa##张三##bbbb
。