SpringBoot 2.x 时代 – 核心功能篇

视频学习地址:https://www.bilibili.com/video/BV1Et411Y7tQ?p=131&spm_id_from=pageDriver

在上一篇文章中我们已学会SpringBoot的基本使用,并且明白了SpringBoot的自动配置原理!接下来就是SpringBoot的核心功能,此篇文章会根据视频内容整理SpringBoot的配置文件、web开发、数据访问等一些重要知识!帮助我们快速掌握SpringBoot !

1、配置文件详解

1.1、两种文件类型 properties&yaml

配置文件的作用:修改SpringBoot自动配置的默认值,SpringBoot在底层都给我们自动配置好。

SpringBoot使用一个全局的配置文件,并且配置文件名是固定的:

  • application.properties
  • application.yml

application.properties是SpringBoot初始化向导默认生成的,前面我们也已经接触过了,采用key=value的语法结构去描述配置信息!

如果两种配置文件同时存在并且配置了相同的key!yml的优先级会大于properties,因为properties是后加载的,所以此时yml就没有生效。

SpringBoot就用application.properties覆盖了application.yml的配置信息。如:

application.properties

# 指定端口号
server.port=8010

application.yml

server:port: 8888# 指定端口号

项目启动日志:Tomcat started on port(s): 8010 (http) with context path ''

properties需要使用大量的路径来描述层级(或者属性),比如environments.dev.urlenvironments.dev.name。其次,对于较为复杂的结构,比如数组(my.servers),写起来更为复杂。而对应的YAML格式文件就简单很多。

关于更多的properties配置项与使用这里就不多介绍了。

1.2、yaml 文件

YAML是 “YAML Ain’t a Markup Language” (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)

这种语言以数据作为中心,而不是以标记语言为重点!非常适合来做配置文件。

标记语言:

​ 以前的配置文件,大多都使用的是 xxxx.xml文件。

​ YAML:以数据为中心,比json、xml等更适合做配置文件。

如:xml 与 yaml 的对比,配置项目端口号!

xml

<server><port>8081</port>
</server>

yaml

server:port: 8081

1.3、yaml的基本语法

语法结构:key:(空格)value表示一对键值对(空格必须有)

如:

server:port: 8888# 指定端口号servlet:context-path: /hello # 指定根路径

说明:语法要求严格!

  1. 空格不能省略
  2. 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
  3. 属性和值的大小写都是十分敏感的。
  4. 缩进不允许使用tab,只允许使用空格。
  5. #表示注释。
  6. 关于key:userNameuser-name是等效的。

字面量:普通的值 [ 数字,布尔值,字符串 ]

字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号

""双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思。

比如 :name: "kuang \n shen" 输出 :kuang 换行 shen

''单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出。

比如 :name: 'kuang \n shen' 输出 :kuang \n shen

1.4、yaml不同数据类型的书写

  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
# 语法
key: value
# 如
debug: true
  • 对象:键值对的集合。map、hash、set、object
# 语法
k: {k1:v1,k2:v2,k3:v3} # 行内写法
# 或
k: k1: v1k2: v2k3: v3
# 如
spring:datasource:username: rootpassword: root
  • 数组:一组按次序排列的值。array、list、queue
# 行内写法
k: [v1,v2,v3]
# 或者
k:- v1- v2- v3
  • 示例

不同数据类型Java代码:

@Data
@Component
@ConfigurationProperties(prefix = "person")
public class Person {private String userName;private Boolean boss;private Date birth;private Integer age;private Pet pet;private String[] interests;private List<String> animal;private Map<String, Object> score;private Set<Double> salarys;private Map<String, List<Pet>> allPets;
}@Data
public class Pet {private String name;private Double weight;
}

对应的yaml配置

person:userName: zhangsanboss: falsebirth: 2019/12/12 20:12:33age: 18pet: name: tomcatweight: 23.4interests: [篮球,游泳]animal: - jerry- marioscore:english: first: 30second: 40third: 50math: [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}]

1.5、自定义类绑定的配置提示问题

当我们自定义的类,绑定SpringBoot的全局配置后,编写相关配置时,是没有提示信息的,并且idea会爆出一些警告!如下图所示:

这是因为缺少配置处理器,我们只需添加一组依赖即可解决:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional>
</dependency>

官方文档地址:https://docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/appendix-configuration-metadata.html#configuration-metadata

引入配置处理器依赖后,我们在项目打包时不应该打包配置处理器,所以还需要改变打包的配置!

<project><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>
</project>

2、Web开发

2.1、SpringMVC自动配置概览

官网说明:https://docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/spring-boot-features.html#boot-features-developing-web-applications

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 and BeanNameViewResolver beans.
    翻译:内容协商视图解析器和BeanName视图解析器

  • Support for serving static resources, including support for WebJars (covered later in this document)).
    翻译:静态资源(包括webjars)

  • Automatic registration of Converter, GenericConverter, and Formatter 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上)

自定义SpringMVC组件的三种方式:

① 官网原文:If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

说明:不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer自定义规则

② 官网原文:If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

说明:声明 WebMvcRegistrations 改变默认底层组件

③ 官网原文:If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

说明:使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC

以上都是基于官网介绍:SpringMVC的自动配置。当然在开发web应用时,我们随时都需要使用SpringMVC所以这里简单的了解一下SpringMVC的自动配置,关于详细的内容,后面会涉及到。

2.2、静态资源的映射规则

2.2.1、静态资源目录

静态资源是指JS、CSS、Image等,当一个http请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。

默认情况下静态资源处理器又通过 项目根路径+静态资源名称/路径 处理静态资源!值得注意的是静态资源只要放在类路径下,可放在如下目录下(/表示根目录):

  • /static
  • /public
  • /resources
  • /META-INF/resources

就可以通过当前项目根路径/ + 静态资源名去访问!

"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/":当前项目的根路径

例如我在static目录下放一张图片c.jpg,如下图所示:

访问:http://localhost:8080/c.jpg就可访问得到这张图片!

改变默认的静态资源路径:

我们还可以使用spring.resources.static-locations属性来自定义静态资源位置(用目录位置列表替换默认值)。根Servlet上下文路径/也会自动添加为位置。

spring:resources:static-locations: [classpath:/haha/] # 自定义静态资源位置为类路径下的haha目录

2.2.2、静态资源访问的前缀

默认是无前缀的,静态资源映射到/**,但是我们可以使用spring.mvc.static-path-pattern属性对其进行调整。例如,将所有静态资源重新定位到以下位置/app/**可以实现:

spring:mvc:static-path-pattern: /app/** # 指定访问静态资源前缀为app

在访问静态资源时:当前项目根路径 + static-path-pattern + 静态资源名 = 静态资源文件夹下找。

如:http://localhost:8080/app/c.jpg

2.2.3、webjar(了解即可)

自动映射 /webjars/**

https://www.webjars.org/

<!--jQuery依赖-->
<dependency><groupId>org.webjars</groupId><artifactId>jquery</artifactId><version>3.5.1</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

2.3、欢迎页的支持

Spring Boot支持静态和模板欢迎页面。它首先index.html在配置的静态资源目录中查找文件。如果找不到,它将寻找一个index模板。如果找到任何一个,它将自动用作应用程序的欢迎页面。

注意:如果设置了静态资源路径的访问前缀,SpringBoot对欢迎页的支持会失效!

spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致welcome page功能失效

2.4、自定义favicon

favicon.ico 放在静态资源目录下即可。

spring:
#  mvc:
#    static-path-pattern: /res/**   这个也会导致favicon功能失效

更多favicon.ico图标的制作与使用,可查看我前端的博客。

2.5、静态资源配置原理

1、SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)。

2、SpringMVC功能的自动配置类 WebMvcAutoConfiguration 生效。

WebMvcAutoConfiguration.java 配置生效条件:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) // 当容器中没有这个类型的组件以下配置才生效
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {...
}

WebMvcAutoConfiguration.java 给容器中配了什么?

@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

知识扩展:如果配置类中只有一个有参构造器!

那么构造器中所有参数都在IOC容器中获取!言外之意所有参数都是组件实例!如:

// WebMvcAutoConfigurationAdapter类的有参构造器
/*
ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
ListableBeanFactory beanFactory Spring的beanFactory
HttpMessageConverters 找到所有的HttpMessageConverters
ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
DispatcherServletPath
ServletRegistrationBean   给应用注册Servlet、Filter....
*/
public WebMvcAutoConfigurationAdapter(WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,ObjectProvider<HttpMessageConverters> messageConvertersProvider,ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,ObjectProvider<DispatcherServletPath> dispatcherServletPath,ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {this.mvcProperties = mvcProperties;this.beanFactory = beanFactory;this.messageConvertersProvider = messageConvertersProvider;this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();this.dispatcherServletPath = dispatcherServletPath;this.servletRegistrations = servletRegistrations;
}

3、资源处理的默认规则

@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {super.addResourceHandlers(registry);if (!this.resourceProperties.isAddMappings()) {logger.debug("Default resource handling disabled");return;}// 1、webjars资源的处理规则addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");// 2、其他静态资源的映射规则addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(),this.resourceProperties.getStaticLocations());
}

方法点进去后就可查看其他静态资源的映射规则:

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

4、欢迎页的映射规则

// 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类:

WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {// 要使用欢迎页功能,必须是 /**if (welcomePage != null && "/**".equals(staticPathPattern)) {logger.info("Adding welcome page: " + welcomePage);setRootViewName("forward:index.html");}else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {logger.info("Adding welcome page template: index");// 调用 Controller /indexsetRootViewName("index");}
}

2.6、请求参数处理

2.6.1、请求映射

1、Rest 风格的使用与原理

  • @xxxMapping注解
  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET-获取用户DELETE-删除用户PUT-修改用户POST-保存用户

使用POST、DELETE、PUT、GET去操作不同的资源,使得代码更简洁,更有层次,更易于实现缓存等机制,这就是Rest风格!那么在SpringBoot中我们应该怎样使用Rest风格呢?

默认情况下,从form表单提交一个DELETE等请求,SpringBoot是不能正常处理的,当然使用Postman工具是可以的!所以想要表单支持Rest风格请求,我们需要配合hiddenHttpMethodFilter组件与表单隐藏域的书写!

  • 核心Filter:HiddenHttpMethodFilter

    • 用法: 表单method=post,隐藏域 _method=put
    • SpringBoot中手动开启
  • 扩展:如何把_method 这个名字换成我们自己喜欢的。详见以下内容!

Rest风格使用示例与总结:

// 页面提交Rest风格的使用示例:发送一个 put 请求// 1、后端代码,编写一个处理 PUT 请求的方法
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser() {return "PUT - Howie";
}// 2、修改配置文件(application.yml)
spring:mvc:hiddenmethod:filter:enabled: true // 开启隐藏域功能,来支持表单可以提交Rest风格请求// 3、前端代码
<form action="/user" method="post">// 注意隐藏域的写法<input type="hidden" name="_method" value="PUT"><input type="submit" value="REST-PUT 提交">
</form>// 为什么需要手动配置,才能支持form提交REST风格请求?
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
// 如果没有在配置文件里配置spring.mvc.hiddenmethod.filter就默认是false,hiddenHttpMethodFilter组件也就默认不生效!
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {return new OrderedHiddenHttpMethodFilter();
}

为什么表单隐藏域中name要等于_method ??我们可以自定义_method的名字吗?

答案是肯定的,我们来看看HiddenHttpMethodFilter组件的源码:

public class HiddenHttpMethodFilter extends OncePerRequestFilter {private static final List<String> ALLOWED_METHODS =Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));/** Default method parameter: {@code _method}. */public static final String DEFAULT_METHOD_PARAM = "_method"; // 组件指定的默认值private String methodParam = DEFAULT_METHOD_PARAM;...
}

不知道小伙伴们有没有发现呢?我们只需自定义自己的HiddenHttpMethodFilter组件,就可改变表单隐藏域name属性的值!

//自定义filter@Beanpublic HiddenHttpMethodFilter hiddenHttpMethodFilter(){HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();methodFilter.setMethodParam("_m"); // 指定表单隐藏域name属性的值。return methodFilter;}

Rest原理(表单提交要使用REST的时候):

1、表单提交会带上_method=PUT

2、请求过来被HiddenHttpMethodFilter拦截

  • 请求是否正常,并且是POST

    • 获取到_method的值。
    • 兼容以下请求;PUT.DELETE.PATCH
    • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
    • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。

Rest使用客户端工具:

  • 如PostMan直接发送Put、delete等方式请求,无需Filter。
  • 也可以省略如下配置。
spring:mvc:hiddenmethod:filter:enabled: true#开启页面表单的Rest功能

2、请求映射原理

我们都知道servlet任务规范中,http服务器能调用的【动态资源文件】必须是一个servlet接口实现类,然后重写doGet()doPost()或者是doService()去处理不同的网络请求。他们的继承关系如下:

经过分析发现,重写了doGet()doPost()service()(注意不是doService())等方法的类是FrameworkServlet这个抽象类!这个抽象类中doGet()doPost()方法里又都调用了processRequest()方法,如:

// FrameworkServlet抽象类中重写的service()方法
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());if (httpMethod == HttpMethod.PATCH || httpMethod == null) {processRequest(request, response); // 调用 processRequest() 方法}else {super.service(request, response);}
}
// FrameworkServlet抽象类中重写的doGet()方法
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {processRequest(request, response); // 调用 processRequest() 方法
}
// FrameworkServlet抽象类中重写的doPost()方法
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {processRequest(request, response); // 调用 processRequest() 方法
}

所以,每个过来的请求都会调用processRequest()方法!而此方法里又调用了doService()方法:

然后我们继续看doService()的实现:

最终是DispatcherServlet这个类继承了FrameworkServlet抽象类并重写了doService方法,重写的时候又调用了doDispatch()这个方法!可以自行查看,于是得出结论:SpringMVC的功能分析都要从DispatcherServlet类中的doDispatch()这个方法开始!

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// 找到当前请求使用哪个Handler(Controller的方法)处理mappedHandler = getHandler(processedRequest);// HandlerMapping:处理器映射。/xxx->>xxxx

5 个HandlerMapping(处理器映射)

第0个处理器映射 RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。

结论:所有的请求映射都在HandlerMapping中,也就是处理器映射。

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping。访问/能访问到index.html
  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping。
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;
}

2.6.2、请求参数与基本注解

1、基本注解

关于请求参数的绑定,SpringMVC中我们都已经接触过了,所以在这里我们来简单的回顾一下:

@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody

测试:@PathVariable

// 1、controller 代码
/*** API:http://localhost:8080/car/2/owner/lisi* 注解 @PathVariable:绑定路径变量,如果请求参数是 key=value 的形式,还可以封装成一个 map*/
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCarInfo(@PathVariable("id") Integer id,@PathVariable("username") String username,@PathVariable Map<String,Object> params) {System.out.println("debug: carId = " + id);System.out.println("debug: username = " + username);System.out.println("debug: params = " + params);Map<String,Object> map = new HashMap<>();map.put("id",id);map.put("username",username);map.put("car","迈凯伦");return map;
}// 2、前端代码
<li><a href="/car/1/owner/lisi">测试@PathVariable注解</a>
</li>// 3、测试
{car: "迈凯伦",id: 1,username: "lisi"
}

测试:@RequestHeader

// 1、controller代码
/*** API:http://localhost:8080/car/header* 注解 @RequestHeader:获取请求头信息,我们可以传一个map、请求头名、MultiValueMap,没有参数就是提取所以请求头。*/
@GetMapping("/car/header")
public Map<String,Object> requestHeaderTest(@RequestHeader("host") String host,@RequestHeader("User-Agent") String userAgent) {System.out.println("host: " + host);System.out.println("User-Agent: " + userAgent);Map<String,Object> map = new HashMap<>();map.put("host",host);map.put("User-Agent",userAgent);return map;
}// 2、前端代码
<li><a href="/car/header">测试@RequestHeader注解</a>
</li>// 3、测试
{host: "localhost:8080",User-Agent: "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}

测试:@RequestParam

// 1、controller 代码
/*** API:http://localhost:8080/owner?id=1&name=alex&age=22&like=eat&like=code* 注解 @RequestParam:主要解决前台参数名称与后台接收参数变量名称不一致的问题,我们还可以封装成一个 list 或 map*/
@GetMapping("/owner")
public Map<String,Object> setUserInfo(@RequestParam("id") Integer userId,@RequestParam("name") String userName,@RequestParam("age") String userAge,@RequestParam("like") List<String> likes,@RequestParam Map<String,Object> params) {System.out.println(params);Map<String,Object> map = new HashMap<>();map.put("user_id",userId);map.put("username",userName);map.put("user_age",userAge);map.put("like",likes);return map;
}// 2、前端代码
<li><a href="/owner?id=1&name=alex&age=22&like=eat&like=code">测试@RequestParam</a>
</li>// 3、测试
{user_id: 1,like: ["eat","code"],user_age: "22",username: "alex"
}

测试:@CookieValue

// 1、controller 代码
/*** API:http://localhost:8080/cookie* 注解 @CookieValue:获取cookie的值*/
@GetMapping("/cookie")
public Map<String,Object> getCookie(@CookieValue("ts_uid") String ts_uid){Map<String,Object> map = new HashMap<>();map.put("cookie_ts_uid",ts_uid);return map;
}// 2、前端代码
<li><a href="/cookie">测试@CookieValue注解</a>
</li>// 3、测试
{cookie_ts_uid: "1520892790"
}

测试:@RequestBody 获取请求体数据,例如post请求!

// 1、Book 实体类
/*** @description: 书籍实体类* @author: laizhenghua* @date: 2021/3/29 14:32*/
public class Book {private Integer id;private String bookName;private String bookAuthor;...// getter and setter// toString()
}// 2、controller 代码
/*** API:http://localhost:8080/book*/
@PostMapping("/book")
public Map<String,Object> saveBook(Book book,@RequestBody String content) {System.out.println("debug => " + book);System.out.println("debug => " + content);Map<String,Object> map = new HashMap<>();map.put("status","success");map.put("book",book);map.put("content",content);return map;
}// 3、前端代码
<li><form action="/book" method="post"><input type="hidden" name="id" value="1">书籍名称:<input type="text" name="bookName">书籍作者:<input type="text" name="bookAuthor"><input type="submit" value="提交"></form>
</li>// 4、测试
{book: {id: 1,bookName: "Java编程思想",bookAuthor: "Alex"},content: "id=1&bookName=Java%E7%BC%96%E7%A8%8B%E6%80%9D%E6%83%B3&bookAuthor=Alex",status: "success"
}

另外还有@RequestAttribute@MatrixVariable注解这里就不测试了!可自行学习。

  • @RequestAttribute:获取request域属性
  • @MatrixVariable:矩阵变量

2、Servlet API

WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

// ServletRequestMethodArgumentResolver 类参数解析器支持的参数类型@Override
public boolean supportsParameter(MethodParameter parameter) {Class<?> paramType = parameter.getParameterType();return (WebRequest.class.isAssignableFrom(paramType) ||ServletRequest.class.isAssignableFrom(paramType) ||MultipartRequest.class.isAssignableFrom(paramType) ||HttpSession.class.isAssignableFrom(paramType) ||(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||Principal.class.isAssignableFrom(paramType) ||InputStream.class.isAssignableFrom(paramType) ||Reader.class.isAssignableFrom(paramType) ||HttpMethod.class == paramType ||Locale.class == paramType ||TimeZone.class == paramType ||ZoneId.class == paramType);
}

3、复杂参数

MapModel(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

验证mapmodelrequest参数所传的值最终都保存到request域里:

/*** @description: 浏览器访问:http://localhost:8080/param 即可查看效果* @author: laizhenghua* @date: 2021/3/30 9:44*/
@Controller
public class RequestController {@GetMapping("/param")public String testParam(Map<String,Object> params, Model model,HttpServletRequest request,HttpServletResponse response) {params.put("message","hello world"); // 使用 map 参数存值model.addAttribute("code",200); // 使用 model 参数存值request.setAttribute("status","success"); // 原生 request 存值Cookie cookie = new Cookie("k1","v1");cookie.setDomain("localhost");response.addCookie(cookie);return "forward:/success";}@GetMapping("/success")@ResponseBodypublic Map<String,Object> success(@RequestAttribute("code") Integer code, HttpServletRequest request) {Map<String,Object> map = new HashMap<>();// 验证 map,model 参数里面的数据存放到 request请求域里Object message = request.getAttribute("message"); // 获取 map 参数所传的值map.put("message",message);map.put("code",code); // 注意 code 是 model参数 所传的值Object status = request.getAttribute("status"); // 获取原生 request 参数所传的值map.put("status",status);return map;}
}

原理解析:

Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map
mavContainer.getModel(); 获取到值

最后怎样存到request域里的原理这里不好描述,推荐看视频讲解:https://www.bilibili.com/video/BV1Et411Y7tQ?p=145&spm_id_from=pageDriver

2.6.3、POJO封装过程

在开发时,我们经常使用实体类作为参数去接收前端或客户端传过来的请求参数!如:

// 1、实体类的书写
public class Book {private Integer id;private String bookName;private String bookAuthor;...// getter and setter// toString()
}// 2、ctroller 代码
/*** 使用实体类接收参数* 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定,还支持级联属性的写法(pet.name)*/
@PutMapping("/book")
public Map<String,Object> updateBook(Book book) {Map<String,Object> map = new HashMap<>();map.put("book",book);map.put("status","success");return map;
}// 3、前端代码
<li><form action="/book" method="post"><input type="hidden" name="_method" value="put"><input type="hidden" name="id" value="1">书籍名称:<input type="text" name="bookName">书籍作者:<input type="text" name="bookAuthor"><input type="submit" value="提交"></form>
</li>// 4、测试

那么传过来的参数在SpringBoot中是如何封装成一个实体类的呢?经过分析发现,自定义类型参数(实体类接收参数)是通过如下步骤解析与封装为POJO

  1. 通过ServletModelAttributeMethodProcessor这个参数解析器解析传过来的参数。
  2. 判断是否为简单的参数(String、Number等)。this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()))
  3. 创建一个空的实体类对象。Object attribute = null
  4. 创建web数据绑定器。WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);,将请求参数的值绑定到指定的JavaBean属性里。
  5. 利用web数据绑定器的类型转换器Converters把参数转换成JavaBean需要的类型,再次封装到JavaBean中。
  6. 在设置每一个值的时候,GenericConversionService找它里面的所有converter那个可以将这个数据类型转换到指定的类型(request带来参数的字符串转换到指定的类型。JavaBean --> Integer、byte --> file)。
  7. 如此循环,逐一绑定。

核心方法:

@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");String name = ModelFactory.getNameForParameter(parameter);ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);if (ann != null) {mavContainer.setBinding(name, ann.binding());}Object attribute = null; // 创建一个空对象BindingResult bindingResult = null;if (mavContainer.containsAttribute(name)) {attribute = mavContainer.getModel().get(name);}else {// Create attribute instancetry {attribute = createAttribute(name, parameter, binderFactory, webRequest);}catch (BindException ex) {if (isBindExceptionRequired(parameter)) {// No BindingResult parameter -> fail with BindExceptionthrow ex;}// Otherwise, expose null/empty value and associated BindingResultif (parameter.getParameterType() == Optional.class) {attribute = Optional.empty();}bindingResult = ex.getBindingResult();}}if (bindingResult == null) {// Bean property binding and validation;// skipped in case of binding failure on construction.// 创建 web 数据绑定器WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);if (binder.getTarget() != null) {if (!mavContainer.isBindingDisabled(name)) {bindRequestParameters(binder, webRequest);}validateIfApplicable(binder, parameter);if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {throw new BindException(binder.getBindingResult());}}// Value type adaptation, also covering java.util.Optionalif (!parameter.getParameterType().isInstance(attribute)) {attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);}bindingResult = binder.getBindingResult();}// Add resolved attribute and BindingResult at the end of the modelMap<String, Object> bindingResultModel = bindingResult.getModel();mavContainer.removeAttributes(bindingResultModel);mavContainer.addAllAttributes(bindingResultModel);return attribute;
}

类型转换器:

2.6.4、参数处理原理

核心:

  • HandlerMapping中找到能处理请求的Handler(Controller.method())
  • 为当前Handler 找一个适配器 HandlerAdapter;大多数请求都得到RequestMappingHandlerAdapter
  • 适配器执行目标方法并确定方法参数的每一个值

1、4 种 HandlerAdapter

0 - 支持方法上标注@RequestMapping

1 - 支持函数式编程的controller方法。

等等最常用的是以上两个!

2、执行目标方法

// Actually invoke the handler.
// 在 DispatcherServlet 类的 doDispatch() 方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod); // 执行目标方法// ServletInvocableHandlerMethod 类
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); // 在这里真正执行controller方法,拿到返回值
// 获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);

3、参数解析器 - HandlerMethodArgumentResolver

确定将要执行的目标方法的每一个参数的值是什么。

SpringMVC目标方法能写多少种参数类型。取决于参数解析器。

参数解析器的原生接口,也就是说,以上26个解析器是接口实现类:

两个接口方法解读:

  • 当前解析器是否支持解析这种参数
  • 支持就调用 resolveArgument

4、返回值处理器

5、如何确定目标方法每一个参数的值

// InvocableHandlerMethod 类中
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {MethodParameter[] parameters = getMethodParameters();if (ObjectUtils.isEmpty(parameters)) {return EMPTY_ARGS;}Object[] args = new Object[parameters.length];for (int i = 0; i < parameters.length; i++) {MethodParameter parameter = parameters[i];parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);args[i] = findProvidedArgument(parameter, providedArgs);if (args[i] != null) {continue;}// 判断那个参数解析器支持解析参数if (!this.resolvers.supportsParameter(parameter)) {throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));}try {args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);}catch (Exception ex) {// Leave stack trace for later, exception may actually be resolved and handled...if (logger.isDebugEnabled()) {String exMsg = ex.getMessage();if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {logger.debug(formatArgumentError(parameter, exMsg));}}throw ex;}}return args;
}// 5.1、挨个判断所有参数解析器那个支持解析器这个参数
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);if (result == null) {for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {if (resolver.supportsParameter(parameter)) {result = resolver;this.argumentResolverCache.put(parameter, result);break;}}}return result;
}// 5.2、解析这个参数的值
// 调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可

2.7、响应数据与内容协商

2.7.1、响应JSON数据

以前的开发,我们给请求响应JSON数据,都需要自己手动转换,但是在SpringBoot中我们已不需要自己转换只需使用一些简单的注解(@ResponseBody / @RestController)就可实现JSON数据的转换。

引入的依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><!--web场景自动引入了json场景,点进去查看即可-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-json</artifactId><version>2.3.9.RELEASE</version><scope>compile</scope>
</dependency>

底层使用 jackson

例如有这样一个方法:

@ResponseBody
@GetMapping("/test/book")
public Book getBook() {Book book = new Book();book.setId(2);book.setBookName("大话数据结构");book.setBookAuthor("程杰");return book;
}
// 只需发起 http://localhost:8080/test/book 请求,就能给浏览器响应json数据。

那么响应 json 数据的原理又是什么呢?

  1. 获取所有返回值解析器
  2. 利用返回值解析器处理返回值(把返回值转换成json格式的数据)

所有返回值解析器:

处理返回值:

try {// 利用返回值解析器this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}

handleReturnValue()方法内部寻找那个返回值解析器可以处理返回值

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {// 获取返回值与返回值类型,并寻找可以处理返回值的返回值解析器 selectHandler()HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);if (handler == null) {throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());}handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}

最终是RequestResponseBodyMethodProcessor类中的此方法将数据写为 json 。

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest)throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {mavContainer.setRequestHandled(true);ServletServerHttpRequest inputMessage = createInputMessage(webRequest);ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);// Try even with null return value. ResponseBodyAdvice could get involved.// 使用消息转换器进行写出操作writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

返回值解析原理:

通过这个接口实现类确定方法返回值使用那种返回值解析器!

  • 1、返回值处理器判断是否支持这种类型返回值 supportsReturnType
  • 2、返回值处理器调用 handleReturnValue 进行处理
  • 3、RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
1.利用 MessageConverters 进行处理 将数据写为json1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?1、得到MappingJackson2HttpMessageConverter可以将对象写为json2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。

SpringMVC到底支持哪些返回值:

ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor(返回值解析器);

HTTPMessageConverter(消息转换器)原理

1、HTTPMessageConverter接口

HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。

例子:Person对象转为JSON。或者 JSON转为Person

2、HTTPMessageConverter接口实现类,有着不同的功能!

0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true 支持所有类型
8 - true 支持所有类型

9 - 支持注解方式xml处理的。

最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)

2.7.2、内容协商

根据客户端接收能力不同,返回不同媒体类型的数据。

返回xml格式数据测试:

1、引入依赖

<!--xml依赖--><dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId>
</dependency>

2、postman分别测试返回 json 和 xml

只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
json

xml

我们并没有改变代码,只是引入了xml的支持依赖,为什么SpringBoot就拥有了返回json和xml数据两种不同的能力??SpringBoot内容协商??

内容协商原理:

  1. 判断当前响应头中是否已经有确定的媒体类型。MediaType
  2. 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段 —> application/xml )
  3. 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Book),最后两个支持解析xml数据。
  4. 找到支持操作Book的converter,把converter支持的媒体类型统计出来。
  5. 客户端需要【application/xml】。服务端能力【10种、json、xml】
  6. 通过for循环进行内容协商的最佳匹配媒体类型
// 也就是匹配客户端需要的类型(application/xml)和服务端提供的类型(10种类型)List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {for (MediaType producibleType : producibleTypes) {if (requestedType.isCompatibleWith(producibleType)) {// 匹配成功mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));}}
}
/* 对于浏览器没有写明json情况下,是响应xml格式的数据,因为在Accept请求头字段里,xml的权重更高!所以想要响应json格式的数据,需要开启浏览器参数方式内容协商功能。
*/

匹配的类型:

最后一步:

  1. 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化

开启浏览器参数方式内容协商功能

为了方便内容协商,开启基于请求参数的内容协商功能

application.yml

spring:mvc:contentnegotiation:favor-parameter: true# 通过 format 请求参数指定内容协商类型

浏览器访问:http://localhost:8080/test/book?format=json或是http://localhost:8080/test/book?format=xml即可指定协商类型。

原理:

确定客户端接收什么样的内容类型:

  1. Parameter策略优先确定是要返回json数据(获取请求头中的format的值)。
  2. 最终进行内容协商返回给客户端json即可。

自定义 MessageConverter

  • 实现多协议数据兼容。json、xml、x-guigu
  1. @ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
  2. Processor 处理方法返回值。通过 MessageConverter 处理
  3. 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
  4. 内容协商找到最终的 messageConverter
/*
现有这样一个开发场景(多协议数据兼容):1. 浏览器发起请求直接返回 xml。  [application/xml]   jacksonXmlConverter2. 如果是 ajax 请求,返回 json。   [application/json]  jacksonJsonConverter3. 如果硅谷app 发起的请求,返回自定义协议数据。  [application/x-guigu]   jacksonXGuiguConverter4. 自定义协议数据就为:属性值1;属性值2;*/
@ResponseBody
@GetMapping("/test/book")
public Book getBook() {Book book = new Book();book.setId(2);book.setBookName("大话数据结构");book.setBookAuthor("程杰");return book;
}/*
实现步骤:1.添加自定义的 MessageConverter 进系统底层2.系统底层就会统计出所有 MessageConverter 能操作那些类型3.内容协商,不同请求端,响应不同格式的数据
*/

添加或修改SpringMVC的功能。只需从一个入口给容器中添加一个 WebMvcConfigurer。

自定义 MessageConverter实现类:

/*** @description: 自定义 MessageConverter* @author: laizhenghua* @date: 2021/3/31 19:54*/
public class GuiguMessageConverter implements HttpMessageConverter<Book> {@Overridepublic boolean canRead(Class<?> clazz, MediaType mediaType) {return false;}@Overridepublic boolean canWrite(Class<?> clazz, MediaType mediaType) {return false;}/** SpringBoot底层需要统计所有 MessageConverter 都能写出哪些内容类型。* application/x-guigu*/@Overridepublic List<MediaType> getSupportedMediaTypes() {return MediaType.parseMediaTypes("application/x-guigu");}@Overridepublic Book read(Class<? extends Book> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {return null;}@Overridepublic void write(Book book, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {// 自定义协议数据的写出String data = book.getId() + ";" + book.getBookName() + ";" + book.getBookAuthor();// 写出去OutputStream out = outputMessage.getBody();out.write(data.getBytes());}
}

给容器中添加一个 WebMvcConfigurer:

/*** @description: WebMvcConfigurer 定制化 SpringMVC的功能* @author: laizhenghua* @date: 2021/3/31 19:50*/
@Configuration
public class WebMvcConfig {@Beanpublic WebMvcConfigurer webMvcConfigurer() {return new WebMvcConfigurer() {@Overridepublic void extendMessageConverters(List<HttpMessageConverter<?>> converters) {converters.add(new GuiguMessageConverter()); // 添加自己消息转换器}};}
}

2.8、视图解析与模板引擎

2.8.1、视图解析


视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。

2.8.2、模板引擎Thymeleaf

前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。

jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,并且我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。

那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?

SpringBoot推荐我们使用模板引擎:

模板引擎:我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf。

注意引入Thymeleaf的依赖才能使用Thymeleaf:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

1、thymeleaf是现代化、服务端Java模板引擎 – 摘自官网

官网地址:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

2、基本语法

注意页面上使用Thymeleaf语法时,需要加上名称空间:

<html xmlns:th="http://www.thymeleaf.org">
表达式名字 语法 用途
变量取值 ${…} 获取请求与、session域、对象等值
选择变量 *{…} 获取上下文对象值
消息 #{…} 获取国际化等值
链接 @{…} 生成连接
片段表达式 ~{…} 与jsp:include作用一致。引入公共页面片段

1、字面量

文本值: one text'Another one! 等。数字: 0 , 34 , 3.0 , 12.3等。布尔值: true , false
空值: null
变量:one,two,....变量不能有空格

2、文本操作

字符串拼接: +
变量替换: |The name is ${name}|

3、数学运算

运算符: + , - , * , / , %

4、布尔运算

运算符: and , or
一元运算: ! , not

5、比较运算

比较: > , < , >= , <= ( gt , lt , ge , le ) 等式: == , != ( eq , ne )

6、条件运算

If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)

7、特殊操作

无操作: _

3、设置属性值 – th:attr

设置单个属性:

<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><!--简写方式-->
<form action="subscribe.html" th:action="@{/subscribe}"><fieldset><input type="text" name="email" /><input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/></fieldset>
</form>

设置多个属性:

<img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

所有h5兼容的标签写法

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes

4、循环

<tr th:each="prod : ${prods}"><td th:text="${prod.name}">Onions</td><td th:text="${prod.price}">2.41</td><td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'"><td th:text="${prod.name}">Onions</td><td th:text="${prod.price}">2.41</td><td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

5、条件运算

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}"><p th:case="'admin'">User is an administrator</p><p th:case="#{roles.manager}">User is a manager</p><p th:case="*">User is some other thing</p>
</div>

6、属性优先级

7、Thymeleaf的自动配置

引入依赖后,我们可以直接使用了。除了基本语法外,还需要知道Thymeleaf的使用规则。首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则:

// Thymeleaf的自动配置类
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {...
}

自动配好的策略:

  • 所有Thymeleaf的配置值都在ThymeleafProperties
  • 配好了Thymeleaf的模板引擎SpringTemplateEngine
  • 配好了Thymeleaf的视图解析器ThymeleafViewResolver
  • 我们只需要直接开发页面
// ThymeleafProperties里的默认配置public static final String DEFAULT_PREFIX = "classpath:/templates/"; // 前缀public static final String DEFAULT_SUFFIX = ".html"; // 后缀// 例如
@GetMapping("/index")
public String toIndexPage() {return "index"; // 返回 "classpath:/templates/index.html" 页面
}

2.8.3、视图解析原理

1、目标方法处理的过程中,所有数据都会被放在ModelAndViewContainer里面。包括数据和视图地址。

2、方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在ModelAndViewContainer

3、任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。

4、processDispatchResult处理派发结果(页面改如何响应)

// 进入到 render 方法
1render(mv,request,response)  // 进行页面渲染逻辑
/*
根据方法的String返回值得到 View 对象(定义了页面的渲染逻辑):1、所有的视图解析器尝试是否能根据当前返回值得到 View 对象2、得到了  redirect:/main.html --> Thymeleaf new RedirectView()3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。4、view.render(mv.getModelInternal(), request, response);   视图对象调用自定义的render进行页面渲染工作RedirectView 如何渲染【重定向到一个页面】??1、获取目标url地址2、response.sendRedirect(encodedURL);
*/

视图解析器:

视图解析:

  • 返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);
  • 返回值以 redirect: 开始: new RedirectView() —> render就是重定向
  • 返回值是普通字符串: new ThymeleafView()—> render()方法进行页面逻辑渲染

2.9、拦截器

1、SpringMVC中的拦截器:将堆内存SpringMVC使用拦截器对请求进行拦截处理,以实现特定的功能。在AOP中,拦截器用于在某个方法或者字段被访问之前,进行拦截,然后再之前或者之后加入某些操作。

2、拦截器的特点:

  • 可以定义拦截器链,连接器链就是将拦截器按着一定的顺序结成一条链,在访问被拦截的方法时,拦截器链 中的拦截器会按着定义的顺序执行。

  • 拦截器和过滤器的功能比较类似,有区别:

    1. 过滤器是Servlet规范的一部分,任何框架都可以使用过滤器技术。

    2. 拦截器是SpringMVC框架独有的。

    3. 过滤器 url-pattern 配置了/*,可以拦截任何资源。

    4. 拦截器只会对控制器中的方法进行拦截。

  • 拦截器也是AOP思想的一种实现方式

  • 要自定义拦截器,需要实现HandlerInterceptor接口。

3、HandlerInterceptor 接口

拦截器的使用:

1、编写一个拦截器实现 HandlerInterceptor 接口

package com.laizhenghua.admin.interceptor;import lombok.extern.slf4j.Slf4j;
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;/*** @description: 登录拦截器* @author: laizhenghua* @date: 2021/4/3 22:00*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {/*** 目标方法执行之前*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("拦截器拦截的请求路径 : {}",request.getRequestURI());// 登录检查逻辑HttpSession session = request.getSession();Object admin = session.getAttribute("admin");if(admin != null) {return true; // 放心请求}request.setAttribute("message","亲!您还未登录网站,不可以访问哦!");request.getRequestDispatcher("/to_login").forward(request,response);return false;}/*** 目标方法执行完成以后*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}/*** 页面渲染以后*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}

2、拦截器注册到容器中(实现 WebMvcConfigureraddInterceptors() 方法),并制定拦截规则

package com.laizhenghua.admin.config;import com.laizhenghua.admin.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @description: 自定义 WebMVC 组件* @author: laizhenghua* @date: 2021/4/3 22:11*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// registry 拦截器注册中心registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") // 所有请求都会拦截,包括静态资源.excludePathPatterns("/admin/css/**","/admin/fonts/**","/admin/images/**","/admin/js/**") // 放行静态资源.excludePathPatterns("/","/to_login","/admin/login"); // 不用拦截的请求/*1、编写一个拦截器实现 HandlerInterceptor 接口2、拦截器注册到容器中(实现 WebMvcConfigurer 的 addInterceptors() 方法)3、指定拦截器规则(如果是拦截所有,静态资源也会被拦截)*/}
}

拦截器原理:

1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】

2、先来顺序执行所有拦截器的 preHandle方法

  1. 如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
  2. 如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;

3、如果任何一个拦截器返回false。直接跳出不执行目标方法

4、所有拦截器都返回True。执行目标方法

5、倒序执行所有拦截器的postHandle方法。

6、前面的步骤有任何异常都会直接倒序触发 afterCompletion

7、页面成功渲染完成以后,也会倒序触发 afterCompletion

拦截器链:

2.10、文件上传

1、页面表单

<!--注意 enctype="multipart/form-data" 属性的书写-->
<form role="form" th:action="@{upload}" method="post" 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="name" class="form-control" id="exampleInputPassword1"placeholder="Name"></div><div class="form-group"><label for="exampleInputFile">头像</label><input type="file" name="headerImg" id="exampleInputFile"><p class="help-block">Example block-level help text here.</p></div><div class="form-group"><label for="exampleInputFile">生活照</label><!--多文件上传--><input type="file" name="photos" 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">Submit</button>
</form>

2、文件上传代码

/*** MultipartFile 自动封装上传过来的文件*/
@PostMapping("/admin/upload")
public String upload(@RequestParam("email") String email,@RequestParam("name") String name,@RequestPart("headerImg") MultipartFile headerImg,@RequestPart("photos") MultipartFile[] photos) throws IOException {log.info("上传了文件 : headerImg : {}",headerImg.getSize());log.info("上传了文件 : photos : {}", photos.length);String basePath = null;if(!headerImg.isEmpty()) {// 保存到文件服务器或者是oss服务器String filename = headerImg.getOriginalFilename();// 根路径,在 resources/static/uploadbasePath = ResourceUtils.getURL("classpath:").getPath() + "static/upload/";assert filename != null;File file = new File(basePath);// 文件夹不存在,则新建if(!file.exists()){file.mkdirs();}// 保存文件headerImg.transferTo(new File(basePath,filename));}if(photos.length > 0) {for(MultipartFile file : photos) {if(!file.isEmpty()) {String filename = file.getOriginalFilename();assert filename != null;file.transferTo(new File(basePath,filename));}}}return "admin/index";
}

3、自动配置原理

文件上传自动配置类是MultipartAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class) // 属性配置类
public class MultipartAutoConfiguration {...
}

SpringBoot自动配好了文件上传解析器StandardServletMultipartResolver。如果是以流的方式上传文件,我们需要自定义文件上传解析器

文件上传原理:

  1. 请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
  2. 参数解析器来解析请求中的文件内容封装成MultipartFile
  3. 将request中文件信息封装为一个MapMultiValueMap<String, MultipartFile>

2.11、SpringBoot错误处理

1、错误处理

默认情况下,Spring Boot提供/error处理所有错误的映射:

  • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。
  • 对于浏览器客户端,响应一个 “whitelabel” 错误视图,以HTML格式呈现相同的数据。


  • 要对其进行自定义,添加View解析为error
  • 要完全替换默认行为,可以实现ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制并替换其内容。
  • error/下的4xx,5xx页面会被自动解析。


2、定制错误处理逻辑

  • 自定义错误页

    • error/404.htmlerror/500.html 有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
  • @ControllerAdvice + @ExceptionHandler处理全局异常。底层是 ExceptionHandlerExceptionResolver 支持的。所有controller产生的异常都由我们指定的方法处理!

  • @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error

  • Spring底层的异常,如参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。

    1. response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());此次请求立即结束。
    2. 因为此次请求已经结束;Tomcat得不到响应,Tomcat就响应原生错误页
  • 自定义异常解析器:实现HandlerExceptionResolver接口;可以作为默认的全局异常处理规则。

package com.laizhenghua.admin.exception;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;/*** @description: 自定义的异常解析器* @author: laizhenghua* @date: 2021/4/6 9:28*/
@Order(value = Ordered.HIGHEST_PRECEDENCE) // 数字越小优先级越高(枚举类是一个数字)
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {@Overridepublic ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {// 可定制自己的解析规则try {response.sendError(520,"我喜欢的错误!");} catch (IOException e) {e.printStackTrace();}return new ModelAndView();}
}

  • ErrorViewResolver实现自定义异常处理

    • response.sendError 。error请求就会转给controller
    • 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
    • basicErrorController 要去的页面地址是 ErrorViewResolver ;

3、异常处理自动配置原理

ErrorMvcAutoConfiguration自动配置类 —> 自动配置异常处理规则

// 重要组件1
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {return new DefaultErrorAttributes();
}
// 我们发现 DefaultErrorAttributes类 实现的接口有:
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {...// 定义错误页面中可以包含哪些数据
}
// 重要组件2
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,ObjectProvider<ErrorViewResolver> errorViewResolvers) {return new BasicErrorController(errorAttributes, this.serverProperties.getError(),errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
// BasicErrorController映射的规则
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // 默认处理 /error 路径的请求
public class BasicErrorController extends AbstractErrorController {...// 根据内容协商响应不同的数据(json 或者是 白页面whitelabel)
}
/*白页面响应:new ModelAndView("error", model)
*/
// 重要组件3(id为 error 的组件) --> 响应默认的错误页
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {return this.defaultErrorView;
}// 容器中还存放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象
// 如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)// 重要组件4(错误视图解析器)
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);/*功能描述:如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面error/404、5xx.html*/
}

4、异常处理流程

1、请求过来时,找到可以处理请求的处理器映射!处理器映射执行目标方法。目标方法运行期间有任何异常都会被catch、而且标志当前请求结束,并且用 dispatchException记录目标方法出现的异常。

2、拿到dispatchException记录的异常后,执行此方法:processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);进入到视图解析流程。

3、mv = processHandlerException(request, response, handler, exception);处理handler发生的异常,处理完成返回ModelAndView

  1. 遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【接口HandlerExceptionResolver处理器异常解析器】
  2. 系统默认的异常解析器
  3. 异常解析器如何处理?
/*
1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
2、默认没有任何人能处理异常,所以异常会被抛出1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的 BasicErrorController 处理2、解析错误视图;遍历所有的  ErrorViewResolver  看谁能解析。3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html 4、模板引擎最终响应这个页面 error/500.html
*/

2.12、Web原生组件注入(Servlet、Filter、Listener)

1、使用Servlet API

/*** @description: 新建一个类并用 @WebServlet() 标注* @author: laizhenghua* @date: 2021/4/6 10:16*/
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.getWriter().write("66666");}
}

主程序类:

@SpringBootApplication
@ServletComponentScan(basePackages = "com.laizhenghua.admin.servlet") // 指定原生 Servlet组件位置
public class AdminApplication {public static void main(String[] args) {SpringApplication.run(AdminApplication.class, args);}}

当我们在浏览器中访问:http://localhost:8080/my,会发现SpringBoot中的拦截器并没有进行拦截,而是直接响应!这是为什么呢?

/*
容器中有两个servlet:1.MyServlet --> /my2.DispatcherServlet(统一派发器) --> /DispatchServlet 如何注册进来呢? DispatcherServletAutoConfiguration 自动配置类中1.容器中自动配置了  DispatcherServlet  属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。2.通过 ServletRegistrationBean<DispatcherServlet> 把 DispatcherServlet  配置进来。3.默认映射的是 / 路径。Tomcat-Servlet;多个 Servlet 都能处理到同一层路径,精确优选原则A: /my/B: /my/1
*/

使用Servlet3.0配置自定义Filter:

/*** @description: 过滤器 WebFilter 可以指定要拦截的资源* @author: laizhenghua* @date: 2021/4/6 10:33*/
@WebFilter(urlPatterns = {"/admin/css/*"}) // 注意 * 和 ** 的写法,*为servlet写法,**为Spring写法
@Slf4j
public class MyFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {log.info("MyFilter初始化完成");}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {log.info("MyFilter开始工作");chain.doFilter(request,response);}@Overridepublic void destroy() {log.info("MyFilter已销毁");}
}

使用Servlet3.0的注解自定义原生Listener监听器:

/*** @description: 监听器* @author: laizhenghua* @date: 2021/4/6 10:43*/
@WebListener
@Slf4j
public class MyServletContextListener implements ServletContextListener {@Overridepublic void contextInitialized(ServletContextEvent sce) {log.info("MyServletContextListener监听到项目初始化完成");}@Overridepublic void contextDestroyed(ServletContextEvent sce) {log.info("MyServletContextListener监听到项目已销毁");}
}

以上都是Servlet 3.0提供的注解,用于注册原生的三大web组件。我们还有更简便的注册方式,这也是比较推荐的方式,通过RegistrationBean注册配置配置原生组件。

2、通过RegistrationBean注册原生组件

ServletRegistrationBeanFilterRegistrationBean、 and ServletListenerRegistrationBean

/*** @description:* @author: laizhenghua* @date: 2021/4/6 12:18*/
@Configuration
public class MyRegistrationConfig {@Beanpublic ServletRegistrationBean<MyServlet> myServlet() {MyServlet myServlet = new MyServlet();return new ServletRegistrationBean<>(myServlet,"/my");}@Beanpublic FilterRegistrationBean<MyFilter> myFilter() {MyFilter myFilter = new MyFilter();FilterRegistrationBean<MyFilter> filterRegistrationBean = new FilterRegistrationBean<>(myFilter);filterRegistrationBean.setUrlPatterns(Arrays.asList("/admin/css/*","/my"));return filterRegistrationBean;}@Beanpublic ServletListenerRegistrationBean<MyServletContextListener> myListener() {MyServletContextListener myListener = new MyServletContextListener();return new ServletListenerRegistrationBean<>(myListener);}
}

2.13、嵌入式 Servlet 容器

1、切换嵌入式的 Servlet 容器

我们都知道 SpringBoot 默认使用Tomcat作为嵌入式的Servlet容器。SpringBoot也支持切换不同的web 服务器。如TomcatJetty、or Undertow

ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器。

我们想要切换服务器,只需排除tomcat服务器并引入相应服务器的依赖场景即可:

<!-- 引入web模块 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><artifactId>spring-boot-starter-tomcat</artifactId><groupId>org.springframework.boot</groupId></exclusion></exclusions>
</dependency><!--引入其他的Servlet容器(jetty)-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

原理:

  • SpringBoot应用启动发现当前是Web应用。web场景包 - 导入tomcat
  • web应用会创建一个 web 版的 ioc 容器 ServletWebServerApplicationContext
  • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂—> Servlet 的web服务器)
  • SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
  • 底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
  • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
  • ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
  • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize — this.tomcat.start();
  • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat 核心 jar 包存在)

2、定制servlet容器

  • 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>接口

    • 把配置文件的值和ServletWebServerFactory 进行绑定
    • 官网地址:https://docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/spring-boot-features.html#boot-features-embedded-container-application-context
  • 修改配置文件 server.xxx(比较推荐)
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {@Overridepublic void customize(ConfigurableServletWebServerFactory server) {server.setPort(9000);}}

xxxxxCustomizer:SpringBoot中称为定制化器,可以改变xxxx的默认规则。

3、定制化原理

/*
定制化常见的方式:1.修改配置文件;2.使用xx定制化器:xxxxxCustomizer;3.编写自定义的配置类  xxxConfiguration;+ @Bean 替换、增加容器中默认组件;视图解析器 4.Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
*/// 如自定义 WebMVC 组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {// 定制格式化器// 定制拦截器等...
}
/*
全面接管 SpringMVC:1.@EnableWebMvc + WebMvcConfigurer —— @Bean  可以全面接管SpringMVC2.所有规则全部自己重新配置; 实现定制和扩展功能原理:1、WebMvcAutoConfiguration  默认的SpringMVC的自动配置功能类。静态资源、欢迎页.....2、一旦使用 @EnableWebMvc 会 @Import(DelegatingWebMvcConfiguration.class)3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用1.把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer  合起来一起生效2.自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport4、WebMvcAutoConfiguration 里面的配置要能生效 必须  @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)5、@EnableWebMvc  导致了 WebMvcAutoConfiguration  没有生效。
*/

原理分析套路:场景starter —> xxxxAutoConfiguration —> 导入xxx组件 —> 绑定xxxProperties —> 绑定配置文件项

3、数据访问

3.1、SQL

1、数据源的自动配置 - HikariDataSource

我们先引入 jdbc 的场景:

 <!-- jdbc 场景--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId></dependency>

jdbc 场景自动引入的依赖:

值得注意的是:JDBC场景并没有为我们导入数据库的连接驱动!因为官方不知道我们接下来要操作什么数据库。

MySQL数据库驱动(数据库版本和驱动版本对应):

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.38</version>
</dependency>

JDBC的自动配置类:

  • DataSourceAutoConfiguration:数据源的自动配置
  • DataSourceTransactionManagerAutoConfiguration:事务管理器的自动配置
  • JdbcTemplateAutoConfigurationJdbcTemplate自动配置,可以来对数据库进行crud
  • JndiDataSourceAutoConfiguration: jndi的自动配置 – 了解
  • XADataSourceAutoConfiguration: 分布式事务相关的 – 了解

1、DataSourceAutoConfiguration 自动配置类

  • 修改数据源相关的配置:spring.datasource
  • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
  • 底层配置好的连接池是:HikariDataSource
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {...
}

2、JdbcTemplateAutoConfiguration 自动配置类

  • 可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate
  • @Bean@Primary, JdbcTemplate;容器中有这个组件

3、修改数据库连接信息

spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://180.76.178.21:3306/ssm_grxz?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: laizhenghuapassword: 123456jdbc:template:query-timeout: 3# 3s超时

4、测试

@SpringBootTest
class AdminApplicationTests {@Autowiredprivate JdbcTemplate jdbcTemplate;@Testpublic void jdbcCRUDTest() {String sql = "select count(*) from admin";Long count = jdbcTemplate.queryForObject(sql, Long.class);System.out.println("记录数:" + count);}
}

2、使用 Druid 数据源

Druid是阿里巴巴开源平台上一个数据库连接池实现,它结合了C3P0、DBCP、Proxool等DB池的优点,同时加入了日志监控,可以很好的监控DB池连接和SQL的执行情况,可以说是针对监控而生的DB连接池,可以说是目前最好的连接池之一。

druid官方github地址:https://github.com/alibaba/druid

我们使用 druid 作为数据源属于整合第三方技术!我们可以使用两种方式进行整合:

  1. 自定义(编写xml配置文件或编写配置类)
  2. 使用场景启动器 starter

1、自定义方式

① 创建数据源

<!-- Druid数据源依赖 -->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.10</version>
</dependency><!--编写配置文件(在SpringBoot中不推荐编写xml,推荐编写配置类)-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"destroy-method="close"><property name="url" value="${jdbc.url}" /><property name="username" value="${jdbc.username}" /><property name="password" value="${jdbc.password}" /><property name="maxActive" value="20" /><property name="initialSize" value="1" /><property name="maxWait" value="60000" /><property name="minIdle" value="1" /><property name="timeBetweenEvictionRunsMillis" value="60000" /><property name="minEvictableIdleTimeMillis" value="300000" /><property name="testWhileIdle" value="true" /><property name="testOnBorrow" value="false" /><property name="testOnReturn" value="false" /><property name="poolPreparedStatements" value="true" /><property name="maxOpenPreparedStatements" value="20" />
</bean>

编写 druid 配置类:

/*** @description: Druid 数据源的配置* @author: laizhenghua* @date: 2021/4/7 9:41*/
@Configuration
public class DruidDataSourceConfig {/**1、默认的自动配置是判断容器中没有才会配@ConditionalOnMissingBean(DataSource.class)2、将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效注解 @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中*/@Bean@ConfigurationProperties(prefix = "spring.datasource")public DataSource dataSource() {DruidDataSource dataSource = new DruidDataSource();return dataSource;}
}

② StatViewServlet

Druid内置提供了一个StatViewServlet用于展示Druid的统计信息<StatViewServlet的用途包括:

  • 提供监控信息展示的html页面
  • 提供监控信息的JSON API

以前的配置方式

<!--StatViewServlet是一个标准的javax.servlet.http.HttpServlet,需要配置在你web应用中的WEB-INF/web.xml中。-->
<servlet><servlet-name>DruidStatView</servlet-name><servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping><servlet-name>DruidStatView</servlet-name><url-pattern>/druid/*</url-pattern>
</servlet-mapping>

我们使用RegistrationBean注册的方式注入com.alibaba.druid.support.http.StatViewServlet

/*
配置 druid 的监控页功能*/
@Bean
public ServletRegistrationBean<StatViewServlet> statViewServlet() {StatViewServlet statViewServlet = new StatViewServlet();return new ServletRegistrationBean<>(statViewServlet,"/druid/*");
}

配好以后,我们访问:http://localhost:8080/druid/index.html就可以进入 druid 的监控页。

③ StatFilter

要打开SQL的监控功能,还需要通过 StatFilter 打开Druid的监控统计功能。StatFilter 用于统计监控信息;如SQL监控、URI监控。

以前的配置:

<!--需要给数据源中配置如下属性;可以允许多个filter,多个用,分割;如:--><property name="filters" value="stat,slf4j" /><bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">... ...<property name="filters" value="stat,log4j" /></bean>

系统中所有filter:

别名 Filter类名
default com.alibaba.druid.filter.stat.StatFilter
stat com.alibaba.druid.filter.stat.StatFilter
mergeStat com.alibaba.druid.filter.stat.MergeStatFilter
encoding com.alibaba.druid.filter.encoding.EncodingConvertFilter
log4j com.alibaba.druid.filter.logging.Log4jFilter
log4j2 com.alibaba.druid.filter.logging.Log4j2Filter
slf4j com.alibaba.druid.filter.logging.Slf4jLogFilter
commonlogging com.alibaba.druid.filter.logging.CommonsLogFilter

修改配置:

/*配置 druid 的监控页功能
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() throws SQLException {DruidDataSource dataSource = new DruidDataSource();dataSource.setFilters("stat,wall"); // 别名配置,打开监控统计功能return dataSource;
}

配好 StatFilter 我们可以自己编写访问SQL的controller接口,测试SQL的监控功能。

Web应用 监控:

/*
webStatFilter 用于采集 web-jdbc 关联监控的数据*/
@Bean
public FilterRegistrationBean<WebStatFilter> webStatFilter() {WebStatFilter webStatFilter = new WebStatFilter();FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<>(webStatFilter);registrationBean.setUrlPatterns(Arrays.asList("/*"));HashMap<String, String> map = new HashMap<>();map.put("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");registrationBean.setInitParameters(map);return registrationBean;
}

2、Druid 提供的场景启动器 starter

相对于上面自定义的方式,我们需要各种配置,而starter的方式更加简洁,可以简化我们的开发。


引入场景依赖:

<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.17</version>
</dependency>

starter里的自动配置类:

DruidDataSourceAutoConfigure

@Configuration
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class) // 提前生效
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class, //监控SpringBean的;配置项:spring.datasource.druid.aop-patternsDruidStatViewServletConfiguration.class, // 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启DruidWebStatFilterConfiguration.class, // web监控配置;spring.datasource.druid.web-stat-filter;默认开启DruidFilterConfiguration.class}) // 所有Druid自己filter的配置
public class DruidDataSourceAutoConfigure {private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);// 数据源@Bean(initMethod = "init")@ConditionalOnMissingBeanpublic DataSource dataSource() {LOGGER.info("Init DruidDataSource");return new DruidDataSourceWrapper();}
}

默认配好的filter:

private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";

明白这些配置好的组件后,我们只需根据需要在全局配置文件中配置即可。配置示例:

spring:servlet:multipart:max-file-size: 10MB # 指定文件上传大小max-request-size: 100MB # 指定文件上传请求大小datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://180.76.178.21:3306/ssm_grxz?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: laizhenghuapassword: 123druid:filter:stat:log-slow-sql: trueslow-sql-millis: 1000enabled: truewall:enabled: trueconfig:drop-table-allow: falseaop-patterns: com.laizhenghua.admin.*stat-view-servlet:enabled: true# 是否开启Druid监控页功能login-username: druidlogin-password: druidresetEnable: false# 是否开启一键清空监控信息web-stat-filter:enabled: true# 是否打开SQL URL等监控功能url-pattern: /*exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'jdbc:template:query-timeout: 3# 3s超时

更多可配置项:

  1. SpringBoot配置示例
    https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
  2. 配置项列表https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8

3、整合 MyBatis

MyBatis GitHub地址:https://github.com/mybatis

在 SpringBoot 中 MyBatis 属于第三方技术,所以在starter的命名上遵循*-spring-boot-starter

<!--MyBatis starter-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version>
</dependency>

引入的依赖:

在以前持久层我们使用 MyBatis 需要编写全局配置文件、SqlSessionFactory、SqlSession、Mapper映射文件等一堆东西,然而引入MyBatis场景启动器后,我已不需要在编写复杂的SqlSessionFactory或配置文件,MybatisAutoConfiguration都给我们自动配置好了。

MybatisAutoConfiguration自动配置类:

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class) // MyBatis配置项绑定的类
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {...// 1、自动配置好了 SqlSessionFactory// 2、自动配置了 SqlSessionTemplate 组合了 SqlSession/*3、@Import(AutoConfiguredMapperScannerRegistrar.class) 找到 @Mapper 标注的接口并自动扫描注入,所以只要我们写的操作MyBatis的接口标注了 @Mapper 就会被自动扫描进来*/
}// MybatisProperties配置类
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {public static final String MYBATIS_PREFIX = "mybatis"; // 配置文件前缀...
}
// 可以修改全局配置文件中 mybatis 开头的所有配置;

一切已准备就绪,我们来简单测试一下MyBatis是否整合成功:

1、编写实体类

/*** @description: admin 实体类* @author: laizhenghua* @date: 2021/4/7 20:00*/
@Data
public class AdminEntity {private String adminId;private String username;private String password;private String realName;private String contact;
}

2、编写dao接口

/*** @description: dao 接口* @author: laizhenghua* @date: 2021/4/7 20:10*/
@Mapper
public interface AdminMapper {public AdminEntity getAdminEntityById(@Param("id") String id);
}

3、编写Mapper映射文件(AdminMapper.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.laizhenghua.admin.dao.AdminMapper"><!-- public AdminEntity getAdminEntityById(String id); --><select id="getAdminEntityById" resultType="com.laizhenghua.admin.bean.AdminEntity">select adminid adminId,username,password,realname realName,contact from admin where adminid = #{id}</select>
</mapper>

4、修改全局配置文件(application.yml)

# 配置 MyBatis 的规则
mybatis:config-location: classpath:mybatis/mybatis-config.xml # myBatis全局配置文件位置mapper-locations: classpath:mybatis/mapper/*.xml # SQL映射文件configuration:map-underscore-to-camel-case: true# 开启驼峰命名规则

配置了 private Configuration configuration;。mybatis.configuration下面的所有,就是相当于改mybatis全局配置文件中的值。例如开启驼峰命名规则。注意:config-locationconfiguration不能同时存在!意思就是要么使用myBatis全局配置文件修改相关配置,要么就在SpringBoot的全局配置文件中修改MyBatis的相关配置。

# 配置 MyBatis 的规则
mybatis:# config-location: classpath:mybatis/mybatis-config.xml # myBatis全局配置文件位置mapper-locations: classpath:mybatis/mapper/*.xml # SQL映射文件configuration:map-underscore-to-camel-case: true# 开启驼峰命名规则

5、编程测试API

/*** @description: 测试MyBatis* @author: laizhenghua* @date: 2021/4/7 20:38*/
@RestController
public class MyBatisController {@Autowiredprivate AdminMapper adminMapper;@GetMapping("/mybatis/{id}")public AdminEntity getAdminEntity(@PathVariable("id") String id){AdminEntity adminEntityById = adminMapper.getAdminEntityById(id);return adminEntityById;}
}

浏览器访问:http://localhost:8080/mybatis/1并查看SQL监控信息。

小结:

  • 导入mybatis官方starter
  • 编写mapper接口。标准@Mapper注解
  • 编写sql映射文件并绑定mapper接口
  • 在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信息 (建议;配置在mybatis.configuration

4、整合 MyBatis-plus

很喜欢 MyBatis-plus 的一句话:我们不做改变只做增强,笔者也很喜欢MyBatis-plus,MyBatis与MyBatis-plus是亲兄弟,两者互相配合才能发挥1+1大于2的效果。

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。这里主要介绍与SpringBoot整合相关知识。更多有关MyBatis-plus的知识(逻辑删除、自动填充、乐观锁与悲观锁,分页查询等),可自行学习。

MyBatis-plus官网:https://mp.baomidou.com/

我们将通过一个简单的 Demo 来阐述 MyBatis-Plus 的强大功能。准备数据库脚本:

DROP TABLE IF EXISTS user;CREATE TABLE user
(id BIGINT(20) NOT NULL COMMENT '主键ID',name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',age INT(11) NULL DEFAULT NULL COMMENT '年龄',email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',PRIMARY KEY (id)
);DELETE FROM user;INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');


好了数据库,已经准备完毕,然后添加 MyBatis-plus 依赖:

<!-- MyBatis-plus依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.2</version>
</dependency>

自动配置类:

MybatisPlusAutoConfiguration

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisPlusProperties.class) // 配置项绑定的类
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {...// 1、自动配置好了 SqlSessionFactory。底层是容器中默认的数据源// 2、private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"}; SQL映射文件的默认位置。// 任意包的类路径下的所有 mapper 文件夹下任意路径下的所有xml都是sql映射文件。  建议以后sql映射文件,放在 mapper下// 3、自动配好了SqlSessionTemplate// 4、@Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan("com.atguigu.admin.mapper") 批量扫描就行// 优点:开发时只需要我们的Mapper继承 BaseMapper 就可以拥有crud能力,无需编写SQL语句,根据条件构造器,可满足大多数场景。
}// 配置项绑定的类
@Data
@Accessors(chain = true)
@ConfigurationProperties(prefix = Constants.MYBATIS_PLUS) // 前缀是 mybatis-plus
public class MybatisPlusProperties {...
}

1、编写实体类(user)

/*** @description: 用户实体类* @author: laizhenghua* @date: 2021/4/1 13:47*/
@Data
@TableName("user")
public class User {private Long id;private String name;private Integer age;private String email;
}

2、编写dao接口

/*** @description: dao接口* @author: laizhenghua* @date: 2021/4/7 22:51*/
@Mapper
public interface UserMapper extends BaseMapper<User> {// 继承BaseMapper该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
}

3、编写业务层接口与实现类

/*** @description: 业务层接口* @author: laizhenghua* @date: 2021/4/8 8:24*/
public interface UserService extends IService<User> {// 继承 IService 后,拥有很多crud方法,也可以自定义接口方法,非常方便
}

业务层接口实现类

/*** @description: 业务层接口实现类* @author: laizhenghua* @date: 2021/4/8 8:26*/
@Service("userService")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {// UserMapper 相当于自动注入了 UserMapper 类型组件
}

4、controller层API

@Autowired
private UserService userService;/**API:http://localhost:8080/admin/dynamic_table展示用户信息(user表)*/
@GetMapping("/dynamic_table")
public String toDynamicTablePage(Model model) {List<User> userList = userService.list();model.addAttribute("userList",userList);return "admin/table/dynamic_table";
}

5、前端 thymeleaf 模板语法渲染

<tbody><tr class="gradeX odd" th:each="user : ${userList}"><td th:text="${user.id}"></td><td th:text="${user.name}"></td><td th:text="${user.age}">Win 95+</td><td class="center hidden-phone" th:text="${user.email}">4</td><td class="center hidden-phone"><a href="#">编辑</a><a href="#">删除</a></td></tr>
</tbody>

效果:

以上操作,我们只完成了 的操作,更多增删该可自己练习完成。

MyBatis-plus分页:

1、添加分页插件

/*** @description: MyBatis-plus的配置* @author: laizhenghua* @date: 2021/4/8 10:33*/
@Configuration
public class MyBatisPlusConfig {/*** 注入分页插件*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}

2、修改controller层响应的数据

/**API:http://localhost:8080/admin/dynamic_table展示用户信息(user表)*/
@GetMapping("/dynamic_table")
public String toDynamicTablePage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model) {Page<User> userPage = new Page<>(pageNum,3); // 分页模型// 分页查询的结果Page<User> page = userService.page(userPage, null);model.addAttribute("page",page);return "admin/table/dynamic_table";
}

3、前端代码

<ul><li class="prev disabled"><a href="#">← 上一页</a></li><li th:class="${num == page.current ? 'active' : ''}" th:each="num : ${#numbers.sequence(1,page.pages)}"><a th:href="@{'/admin/dynamic_table?pageNum=' + ${num}}" th:text="${num}"></a></li><li class="next disabled"><a href="#">下一页 → </a></li>
</ul>

3.2、NoSQL

NoSQL= Not Only SQL (不仅仅是SQL)。泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0时代的产物,尤其是大规模、高并发的数据社区。暴露出来很多难以客服的问题,NoSQL在当今大数据环境卞发展的十分迅速,Redis是发展最快的,而且是我们当下必须要掌握的一个技术。

所以我们以redis为例进行NoSQL的整合,首先引入starter:

<!-- redis starter -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>


1、redis的自动配置:

RedisAutoConfiguration自动配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class) // Redis配置项绑定的类,spring.redis.xxx 是对redis的配置
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
// 连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration。也就是说支持两种客户端操作redis
public class RedisAutoConfiguration {...// 1、自动注入了 RedisTemplate<Object, Object>组件 : key 与 value 都是 Object 类型// 2、自动注入了 StringRedisTemplate类型的组件;k:v都是String// 3、底层只要我们使用 StringRedisTemplate、RedisTemplate 就可以操作 redis
}

修改SpringBoot的全局配置文件:

spring:# Redis 配置redis:host: 49.234.92.74 # 换成自己的服务器 ip 地址port: 6379

简单测试:

@Slf4j
@SpringBootTest
public class RedisTest {@Autowiredprivate StringRedisTemplate redisTemplate;@Testpublic void redisTest() {ValueOperations<String, String> ops = redisTemplate.opsForValue();ops.set("hello","hello redis");String hello = ops.get("hello");log.info("hello -> {}",hello);}
}

2、jedis 与 Lettuce

说明:在 /SpringBoot 2.0以后,原来使用的jedis被替换成为了 lettuce,我们也可以手动切换至 jedis。

两种客户端:

  • jedis:采用的直连,多个线程操作不安全,若要避免,则要使用jedis pool连接池,更像Bio模式

  • lettuce:采用netty,可以在多个线程中共享,不存在线程不安全的情况,可以减少线程数量,更像Nio模式

切换至 jedis:

引入 jedis 依赖

<!-- 导入jedis-->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>

修改配置

spring:redis:host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.comport: 6379password: lfy:Lfy123456client-type: jedis # 切换客户端jedis:pool:max-active: 10

4、单元测试

4.1、JUnit5 的变化

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  1. JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。

  2. JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。

  3. 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。

现在版本(@SpringBootTest + @Test):

/*** @description:* @author: laizhenghua* @date: 2021/4/9 9:45*/
@Slf4j
@SpringBootTest
public class RedisTest {@Autowiredprivate StringRedisTemplate redisTemplate;@Testpublic void redisTest() {ValueOperations<String, String> ops = redisTemplate.opsForValue();ops.set("hello","hello redis");String hello = ops.get("hello");log.info("hello -> {}",hello);}
}

以前版本(@SpringBootTest + @RunWith(SpringTest.class)

现在版本官方引入的依赖(starter):

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions>
</dependency>

依赖:

pringBoot整合Junit以后:

  • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  • Junit类具有Spring的功能,@Autowired、比如 @Transactional标注测试方法,测试完成后自动回滚

4.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 :为测试类或测试方法提供扩展类引用

4.3、断言(assertions)

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:

  • 检查业务逻辑返回的数据是否合理。
  • 所有的测试运行结束以后,会有一个详细的测试报告;

1、简单断言

用来对单个值进行简单的验证。如:

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为ture
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 表达式很容易的提供这些断言。

@DisplayName("组合断言")
@Test
public void allTest() {Assertions.assertAll("test",() -> Assertions.assertEquals(1,1 + 2),() -> Assertions.assertTrue(2 > 1));
}

4、异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。

@DisplayName("异常断言")
@Test
public void exceptionTest() {Assertions.assertThrows(ArithmeticException.class,() -> {int a = 1/0;},"业务逻辑正常运行了");
}

5、快速失败

通过 fail 方法直接使得测试失败

@Test
@DisplayName("fail")
public void shouldFail() {fail("This should fail");
}

5、指标监控

5.1、SpringBoot Actuator

未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。这些功能需要引入场景依赖才能生效:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

关于指标监控 1.x 与 2.x 的比较:

官网地址:https://docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/production-ready-features.html#production-ready

监控指标的可视化web开源项目:https://github.com/codecentric/spring-boot-admin

使用指标监控功能的方式:

  1. 引入依赖
  2. 访问 http://localhost:8080/actuator/**
  3. 暴露所有监控信息为HTTP(默认只暴露了healthinfo等端点),例如http://localhost:8080/actuator/health
# 官网都有详细介绍
management:endpoints:enabled-by-default: true#暴露所有端点信息web:exposure:include: '*'  #以web方式暴露
  1. 测试

http://localhost:8080/actuator/beans:查看当前容器中的所有组件
http://localhost:8080/actuator/configprops:显示所有@ConfigurationProperties的配置
http://localhost:8080/actuator/metrics:查看当前应用的指标列表
http://localhost:8080/actuator/metrics/jvm.gc.pause:根据指标名字查看当前应用的具体指标信息
http://localhost:8080/actuator/endpointName/detailPath

等等更多可查看端点信息可查看官网描述。

5.2、Actuator Endpoint

1、常用端点

ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
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.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

2、Health Endpoint

健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

重要的几点:

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
  • 可以很容易的添加自定义的健康检查机制

当我们访问http://localhost:8080/actuator/health只响应了健康状态,并没有响应详细信息!所以我们还需要修改配置,让SpringBoot响应详细健康信息汇总报告。

application.yml

management:endpoints:enabled-by-default: trueweb:exposure:include: '*'endpoint:health:show-details: always # 响应详细健康信息

再次访问:http://localhost:8080/actuator/health即可获得详细健康信息报告。

3、Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被 push(主动推送)或者 pull(被动获取)方式得到:

  • 通过Metrics对接多种监控系统
  • 简化核心Metrics开发
  • 添加自定义Metrics或者扩展已有Metrics

一次请求:http://localhost:8080/actuator/metrics

根据名字二次请求:http://localhost:8080/actuator/metrics/jvm.gc.live.data.size

4、管理Endpoints

在实际应用中,我们不应该在 web 端暴露所有端点,这是非常危险的。根据需要暴露指定的端点,才是正确的做法。

开启与禁用Endpoints:

  • 默认所有的Endpoint除过shutdown都是开启的。
  • 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint.<endpointName>.enabled = true
management:endpoint:beans:enabled: true

或者禁用所有的Endpoint然后手动开启指定的Endpoint

management:endpoints:enabled-by-default: falseendpoint:beans:enabled: truehealth:enabled: true

5.3、定制 Endpoint

1、定制 health 信息

package com.laizhenghua.admin.health;import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;/*** @description: 定制 health 信息* @author: laizhenghua* @date: 2021/4/10 10:41*/
@Component
public class MyComponentHealthIndicator extends AbstractHealthIndicator {@Overrideprotected void doHealthCheck(Health.Builder builder) throws Exception {boolean isHealth = true;Map<String,Object> map = new HashMap<>();// 检查逻辑// ...if(isHealth) {// builder.up(); // 表示健康builder.status(Status.UP);map.put("count",1);map.put("connectTime","100ms");}else {// builder.down(); 不健康builder.status(Status.OUT_OF_SERVICE);map.put("error","连接超时");map.put("connectTimeOut","3000ms");}builder.withDetail("code",500).withDetails(map);}
}

访问:http://localhost:8080/actuator/health

2、定制 info 信息

默认情况下,info 并没有展示什么信息。所以我们需要自定 info 信息。

常用两种方式自定义 info 信息::

  1. 编写配置文件
info:appName: boot-adminversion: 2.0.1mavenProjectName: @project.artifactId@  #使用@@可以获取maven的pom文件值mavenProjectVersion: @project.version@
  1. 编写InfoContributor
package com.laizhenghua.admin.acutuator.info;import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;import java.util.Collections;/*** @description: 编写 InfoContributor* @author: laizhenghua* @date: 2021/4/10 18:52*/
@Component
public class AppInfoInfoContributor implements InfoContributor {@Overridepublic void contribute(Info.Builder builder) {builder.withDetail("message","hello world").withDetails(Collections.singletonMap("hello","SpringBoot"));}
}

http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息

3、定制Metrics信息

SpringBoot支持自动适配的Metrics:

  • JVM metrics, report utilization of:

    • Various memory and buffer pools
    • Statistics related to garbage collection
    • Threads utilization
    • Number of classes loaded/unloaded
  • CPU metrics

  • File descriptor metrics

  • Kafka consumer and producer metrics

  • Log4j2 metrics: record the number of events logged to Log4j2 at each level

  • Logback metrics: record the number of events logged to Logback at each level

  • Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time

  • Tomcat metrics (server.tomcat.mbeanregistry.enabled must be set to true for all Tomcat metrics to be registered)

  • Spring Integration metrics

增加定制Metrics:

class MyService{Counter counter;public MyService(MeterRegistry meterRegistry){counter = meterRegistry.counter("myservice.method.running.counter");}public void hello() {counter.increment();}
}//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}

6、Profile 功能

我们都知道一个产品的开发有开发、测试、生产等环境,为了方便多环境适配,SpringBoot简化了proflie功能。

官方地址:https://docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/spring-boot-features.html#boot-features-profiles

6.1、application-profile功能

  1. 默认配置文件 application.yaml;任何时候都会加载
  2. 指定环境配置文件 application-{env}.yaml
  3. 激活指定环境
    • 配置文件激活
    • 命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
  4. 修改配置文件的任意值,命令行优先
  5. 默认配置与环境配置同时生效
  6. 同名配置项,profile配置优先


配置文件激活:

spring:profiles:active: dev # 指定激活环境为 devspring:profiles:active: test # 指定激活环境为 test

命令行激活:

# 指定激活环境
java -jar profile-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod# 指定配置属性
java -jar profile-0.0.1-SNAPSHOT.jar --server.port=8999

6.2、@Profile条件装配功能

@Profile不仅能标注在类上,还能标注在方法上。

@Profile("test") // 表示此组件在 test 环境上才能生效
@Component
@ConfigurationProperties("person")
@Data
public class Boss implements Person {private String name;private Integer age;
}@Profile("dev")
@Component
@ConfigurationProperties("person")
@Data
public class Worker implements Person {private String name;private Integer age;
}

6.3、profile分组

spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq# 使用:--spring.profiles.active=production  激活

7、外部化配置

我们要修改已上线的产品的配置信息,我们可能会这样操作:关闭上线的应用 -> 修改配置 -> 重新打包部署。但是这样的流程相对比较繁琐。

而在SpringBoot中允许你外化(externalize)你的配置,这样你能够在不同的环境下使用相同的代码。你可以使用properties文件,YAML文件,环境变量和命令行参数来外化配置。使用@Value注解,可以直接将属性值注入到你的beans中,并通过Spring的Environment抽象或绑定到结构化对象来访问。

官网地址:https://docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/spring-boot-features.html#boot-features-external-config

1、常用的外部配置源

官方说明的外部配置源十多种,但是我们最常用的还是Java属性文件、YAML文件、环境变量、命令行参数;

2、配置文件查找位置

  1. classpath 根路径
  2. classpath 根路径下config目录
  3. jar包(项目打包好的jar包。以下同理)当前目录
  4. jar包当前目录的config目录
  5. /config子目录的直接子目录

3、配置文件加载顺序:

  1. 当前jar包内部的application.properties和application.yml(优先级最高,所以后面的都会覆盖优先级高的)
  2. 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
  3. 引用的外部jar包的application.properties和application.yml
  4. 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

4、重点记住这句话:指定环境优先,外部优先,后面的可以覆盖前面的同名配置项

8、自定义starter

SpringBoot的一大优势就是Starter,由于SpringBoot有很多开箱即用的Starter依赖,使得我们开发变得简单,我们不需要过多的关注框架的配置。

在日常开发中,我们也会自定义一些Starter,特别是现在微服务框架,我们一个项目分成了多个单体项目,而这些单体项目中会引用公司的一些组件,这个时候我们定义Starter,可以使这些单体项目快速搭起,我们只需要关注业务开发。

在此之前我们再深入的了解下SpringBoot启动原理。而后再将如何自定义starter。

8.1、starter启动原理

  1. starter-pom 引入 autoconfigure 包

  2. autoconfigure包中配置使用 META-INF/spring.factories 中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类

  3. 编写自动配置类 xxxAutoConfiguration -> xxxxProperties

  4. @Configuration
    @Conditional
    @EnableConfigurationProperties
    @Bean

  5. 引入starter — xxxAutoConfiguration — 容器中放入组件 — 绑定xxxProperties — 配置项

8.2、自定义starter

模式:

  1. 启动器只用来做依赖导入;

  2. 专门来写一个自动配置模块;

  3. 启动器依赖自动配置;别人只需要引入启动器(starter)

  4. mybatis-spring-boot-starter;命名规范:自定义启动器名-spring-boot-starter

也就是说,我们只需编写好启动器(starter)和自动配置(autoconfigurer)就能完成自定义starter。

如何编写自动配置?

@Configuration  // 指定这个类是一个配置类
@ConditionalOnXXX  // 在指定条件成立的情况下自动配置类生效
@AutoConfigureAfter  // 指定自动配置类的顺序
@Bean  //给容器中添加组件@ConfigurationPropertie // 结合相关xxxProperties类来绑定相关的配置
@EnableConfigurationProperties // 让xxxProperties生效加入到容器中
/*
自动配置类要能加载
将需要启动就加载的自动配置类,配置在META-INF/spring.factories
*/
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\

步骤1:编写启动器(starter)

通过maven创建一个工程,命名为howie-spring-boot-starter,不需要有任何源代码,未来给别人使用的。

得到一个 pom.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.howie</groupId><artifactId>howie-spring-boot-starter</artifactId><version>1.0-SNAPSHOT</version><!-- 这个坐标就作为场景启动器!后面还需要引入 自动配置模块(autoconfigure) 的依赖 -->
</project>

步骤2:编写自动配置模块(autoconfigure)

使用 SpringBoot 的初始化向导,命名为howie-spring-boot-starter-autoconfigure。无需选中任何开发场景。

自动配置模块编写的详细内容,请看步骤4。

步骤3:在场景启动器中引入自动配置模块的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.howie</groupId><artifactId>howie-spring-boot-starter</artifactId><version>1.0-SNAPSHOT</version><!-- 这个坐标就作为场景启动器!后面还需要引入 自动配置模块(autoconfigure) 的依赖 --><dependencies><!--引入自动配置模块--><dependency><groupId>com.howie</groupId><artifactId>howie-spring-boot-starter-autoconfigure</artifactId><version>0.0.1-SNAPSHOT</version></dependency></dependencies>
</project>

步骤4:就是编写自己需要的服务,并把服务安装到本机仓库中(maven 执行 install 命令,注意去掉springboot的maven插件才能打包成功)

如自己写了个sayHello服务:

HelloServiceAutoConfiguration.java

/*** @description: 自动配置类* @author: laizhenghua* @date: 2021/4/11 9:55*/
@Configuration
@EnableConfigurationProperties(HelloProperties.class) // 除了会绑定配置类以外还会将 HelloProperties 类型的组件放在容器中
public class HelloServiceAutoConfiguration {@Bean@ConditionalOnMissingBean(HelloService.class)public HelloService helloService() {return new HelloService();}
}

HelloProperties.java

/*** @description: 属性配置类* @author: laizhenghua* @date: 2021/4/11 9:50*/
@ConfigurationProperties("howie.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

/*** @description: 默认不要放在组件中* @author: laizhenghua* @date: 2021/4/11 9:46*/
public class HelloService {@Autowiredprivate HelloProperties helloProperties;public String sayHello(String name) {return helloProperties.getPrefix() + " : " + name + " : " + helloProperties.getSuffix();}
}

自动配置类要能加载,需要将启动就加载的自动配置类,配置在META-INF/spring.factories

spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.howie.autoconfigure.auto.HelloServiceAutoConfiguration

步骤5:在其他项目中引入自定义的 starter 依赖,并测试服务。

<!--确保自动配置包(jar包)已经在本机仓库中存在-->
<dependency><groupId>com.howie</groupId><artifactId>howie-spring-boot-starter</artifactId><version>1.0-SNAPSHOT</version>
</dependency>


引入依赖后,我们修改SpringBoot全局配置文件,指定sayHello服务的响应的前缀与后缀。注意是在其他测试项目中。

application.yml

howie:hello:prefix: '这里是对抗路'suffix: '很高兴您的到来'

编写controller访问API:

/*** @description:* @author: laizhenghua* @date: 2021/4/10 20:03*/
@RestController
public class HelloController {@Autowiredprivate HelloService helloService;@GetMapping("/sayHello/{username}")public String sayHello(@PathVariable("username") String name) {return helloService.sayHello(name);}
}

测试:

ok 完美!需要注意的是如果我们在容器中注入了自己的HelloService类型的组件,就能覆盖我们之前自己编写的sayHello服务。

9、SpringBoot启动配置原理

9.1、SpringBoot启动过程

SpringBoot的启动,又分为创建SpringApplication对象运行run()方法两个步骤!

/*** Static helper that can be used to run a {@link SpringApplication} from the* specified sources using default settings and user supplied arguments.* @param primarySources the primary sources to load* @param args the application arguments (usually passed from a Java main method)* @return the running {@link ApplicationContext}*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {// 创建 SpringApplication对象 并 调用run()方法return new SpringApplication(primarySources).run(args);
}

1、创建SpringApplication对象

// 创建SpringApplication对象时的构造器
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {// 给 SpringApplication对象保存一些基础信息this.resourceLoader = resourceLoader;Assert.notNull(primarySources, "PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 使用ClassUtils抽象类判定当前应用的类型 reactive ? servlet ?this.webApplicationType = WebApplicationType.deduceFromClasspath(); // 判定当前应用是 servlet应用// bootstrappers是初始启动引导器(List<Bootstrapper>)集合列表。作用:去spring.factories文件中找 org.springframework.boot.Bootstrapperthis.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));// 结论:getSpringFactoriesInstances() 方法都是去spring.factories文件中找指定类型的配置值// 去 spring.factories 找 ApplicationContextInitializer(初始化器)。List<ApplicationContextInitializer<?>> initializers;setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// listeners是ApplicationListener(应用监听器)类型的集合列表,去spring.factories找 ApplicationListener。List<ApplicationListener<?>> listeners;setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 决定主程序this.mainApplicationClass = deduceMainApplicationClass();
}

SpringApplication对象的初始化器:

SpringApplication对象的应用监听器:

2、运行run方法

run() 方法源码:

/*** Run the Spring application, creating and refreshing a new* {@link ApplicationContext}.* @param args the application arguments (usually passed from a Java main method)* @return a running {@link ApplicationContext}*/
public ConfigurableApplicationContext run(String... args) {StopWatch stopWatch = new StopWatch();stopWatch.start(); // 记录应用的启动时间// 创建引导上下文(Context环境)createBootstrapContext()。// 作用:获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置DefaultBootstrapContext bootstrapContext = createBootstrapContext();ConfigurableApplicationContext context = null;// 让当前应用进入headless模式。java.awt.headlessconfigureHeadlessProperty();// 获取所有 RunListener(运行监听器)【为了方便所有Listener进行事件感知】// 作用1:getSpringFactoriesInstances 去spring.factories找 SpringApplicationRunListenerSpringApplicationRunListeners listeners = getRunListeners(args);// 回调所有的获取SpringApplicationRunListener.starting()方法。相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。listeners.starting(bootstrapContext, this.mainApplicationClass);try {// 封装命令行参数ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 准备环境 prepareEnvironment()// 作用:返回或者创建基础环境信息对象。StandardServletEnvironmentConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);/*同时在放里面:1.配置了环境信息信息对象(读取所有的配置源的配置属性值)2.绑定环境信息3.监听器调用 listener.environmentPrepared(); 通知所有的监听器当前环境准备完成*/configureIgnoreBeanInfo(environment);Banner printedBanner = printBanner(environment); // 控制台打印 Banner 信息// 创建IOC容器(createApplicationContext())context = createApplicationContext();/*1.根据项目类型(Servlet)创建容器2.当前会创建 AnnotationConfigServletWebServerApplicationContext*/context.setApplicationStartup(this.applicationStartup);// 准备ApplicationContext IOC容器的基本信息   prepareContext()prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);/*1.保存环境信息2.IOC容器的后置处理流程。3.应用初始化器;applyInitializers(context);1.遍历所有的 ApplicationContextInitializer 调用 initialize.。来对ioc容器进行初始化扩展功能2.遍历所有的 listener 调用 contextPrepared EventPublishRunListenr;通知所有的监听器contextPrepared4.所有的监听器 调用 contextLoaded。通知所有的监听器 contextLoaded;*/// 刷新IOC容器。refreshContext。(创建容器中的所有组件(Spring注解))refreshContext(context);// 容器刷新完成后工作?afterRefreshafterRefresh(context, applicationArguments);stopWatch.stop(); // 记录应用启动成功时间if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}// 所有监听器 调用 listeners.started(context); 通知所有的监听器 startedlisteners.started(context);// 调用所有runners;callRunners()callRunners(context, applicationArguments);/*1.获取容器中的 ApplicationRunner 2.获取容器中的  CommandLineRunner3.合并所有runner并且按照@Order进行排序4.遍历所有的runner。调用 run 方法*/}catch (Throwable ex) {// 如果以上有异常,调用Listener 的 failedhandleRunFailure(context, ex, listeners);throw new IllegalStateException(ex);}try {// 调用所有监听器的 running 方法  listeners.running(context); 通知所有的监听器 runninglisteners.running(context);}catch (Throwable ex) {// running如果有问题。继续通知 failed (调用所有 Listener 的 failed;通知所有的监听器 failed)handleRunFailure(context, ex, null);throw new IllegalStateException(ex);}return context;
}
public interface Bootstrapper {/*** Initialize the given {@link BootstrapRegistry} with any required registrations.* @param registry the registry to initialize* @since 2.4.4*/default void initialize(BootstrapRegistry registry) {intitialize(registry);}/*** Initialize the given {@link BootstrapRegistry} with any required registrations.* @param registry the registry to initialize* @deprecated since 2.4.4 in favor of* {@link Bootstrapper#initialize(BootstrapRegistry)}*/@Deprecatedvoid intitialize(BootstrapRegistry registry);}

9.2、Application Events and Listeners

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners

几个重要的事件回调机制。配置在META-INF/spring.factories

  1. ApplicationContextInitializer
  2. ApplicationListener
  3. SpringApplicationRunListener

End

Thank you for watching

End

经典再现,看到就是赚到。尚硅谷雷神 - SpringBoot 2.x 学习笔记 - 核心功能篇相关推荐

  1. 经典再现,看到就是赚到。尚硅谷雷神 - SpringBoot 2.x 学习笔记 - 基础入门篇

    SpringBoot 2.x 时代 – 基础入门篇 视频学习地址:https://www.bilibili.com/video/BV1Et411Y7tQ?p=112&spm_id_from=p ...

  2. 经典再现,看到就是赚到。尚硅谷雷神 - SpringBoot 2.x 学习笔记 -高级与场景整合篇

    SpringBoot 2.x 场景整合 在上一篇核心功能篇里,我们已了解SpringBoot的配置文件.web开发.数据访问.JUnit5单元测试.生产指标监控.SpringBoot启动流程等.然而S ...

  3. 尚硅谷JavaWeb_2020idea_王振国_学习笔记

    文章目录 基本操作 阶段一.使用JS正则表达式检查输入 阶段二.实现登陆和注册功能 阶段三.做一些优化 阶段四.使用EL**表达式修改表单回显** 阶段五.图书模块 阶段五.下.分页的实现 阶段六.登 ...

  4. 【Vue实践】尚硅谷张天禹Vue学习笔记(087-135)-20221212~20221218

    (任意组件通信)084-086_全局事件总线 全局事件总线SOP 086_TodoList案例_事件总线 src/mian.js: import Vue from 'vue' import App f ...

  5. 尚硅谷_JS DOM编程_学习笔记

    DOM DOM:Document Object Model(文本对象模型) D:文档 – html 文档 或 xml 文档 O:对象 – document 对象的属性和方法 M:模型 DOM 是针对x ...

  6. 尚硅谷 天禹老师 Vue学习笔记总计(自己复习用)

    004 Vue环境搭建 如何关掉这2个提示? 关闭第一个提示,下载Vue的开发者工具 安装 - Vue.js Vue开发者工具GitHub下载地址: https://github.com/vuejs/ ...

  7. 【尚硅谷】Vue2.x核心学习笔记--渐进式的JS框架

    Vue核心 在这里插入图片描述 一.Vue的基本认识 1.1 Vue特点 1.2 与其他的前端Js框架的关联 1.3 Vue的扩展插件 二.Vue的基本使用 2.1 效果 2.2 如何引入Vue.js ...

  8. 尚硅谷以太坊区块链学习之NFT智能合约(6)

    尚硅谷以太坊区块链学习之NFT智能合约(6) 前言 一.NFT智能合约 1.智能合约代码 2.智能合约推送 3.具体调用 二.具体使用 三.NFT商家智能合约 前言 提示:服务外包区块链学习 5被ba ...

  9. 尚硅谷大数据技术Zookeeper教程-笔记01【Zookeeper(入门、本地安装、集群操作)】

    视频地址:[尚硅谷]大数据技术之Zookeeper 3.5.7版本教程_哔哩哔哩_bilibili 尚硅谷大数据技术Zookeeper教程-笔记01[Zookeeper(入门.本地安装.集群操作)] ...

  10. 尚硅谷大数据技术Scala教程-笔记04【集合】

    视频地址:尚硅谷大数据技术之Scala入门到精通教程(小白快速上手scala)_哔哩哔哩_bilibili 尚硅谷大数据技术Scala教程-笔记01[Scala课程简介.Scala入门.变量和数据类型 ...

最新文章

  1. python3.6.5安装tensorflow_ubuntu下python3.6.5import tensorflow显示非法指令(核心已转储)
  2. Unix Regex
  3. 实时内核(Core)和实时操作系统(RTOS)有何不同?
  4. Android官方开发文档Training系列课程中文版:键盘输入处理之控制输入法的显示方式
  5. 双重差分模型能做固定效应吗_互助问答第53期:控制时间效应、交互项等问题...
  6. VSCode拓展插件推荐(HTML、Node、Vue、React开发均适用)
  7. EasyUI的-表格设置
  8. 龙将加速浏览器_360安全浏览器正式进军政企市场 积极适配中国芯
  9. Qt Designer怎样加入资源文件
  10. 2023秋招--广州光娱--游戏客户端--面经
  11. 大一计算机课如何做表格,电脑做表格的基本操作教程
  12. 剽悍一只猫《一年顶十年》读书笔记
  13. 直方图均衡化取整怎么计算_玩转直方图处理之直方图均衡化、规定化
  14. 1999年中国省、自治区的城市规模结构特征的一些数据,可通过聚类分析将这些省、自治区进行分类_BeansSuperman_新浪博客
  15. 环信网页端客服集成用户体系
  16. 游戏美术设计怎么入门?场景建模该如何学习?
  17. linux edb 运行不了,运行 Linux 的 IBM Power Systems 上的 EDB Postgres Advanced Server 9.5 入门...
  18. 计算机组装所需要的部件,组装一台电脑需要哪些配件【详细列举】
  19. 10个常用的汇总公式,拿来即用
  20. 水果礼包拼手气(仿红包拼手气)含拼手气红包算法

热门文章

  1. walking机器人入门教程-工具-命令管理器
  2. 生育医疗费用出院结算流程
  3. 【技术博客】当蒸馏遇上GAN
  4. 详解关键路径法,这可能是你能找到的最详尽的了
  5. 效率是绳,质量是命!——浅析如何运用UML来提高手游团队的绳命质量
  6. 网曝悼念牌汤圆吃出创可贴(图)
  7. 注册表(安全 活动桌面)
  8. 基于若依框架项目点击登录时出现TypeError: Cannot read properties of undefined (reading ‘user‘)
  9. manjaro 安装 tim 后无法输入中文
  10. SQL注入-waf绕过