文章目录
SpringBoot2整篇
- SpringBoot2(上篇) 爆干三万字,只为博你一赞
- SpringBoot2 (中篇) Web访问和数据访问,简单上手,通俗底层,深刻理解
- SpringBoot2 (终极篇里的高级特性) 你觉得你玩过单元测试?你知道什么指标监控?还是运行原理?怎么去自定义事件监听?
Web开发
静态资源的访问
- 当静态资源在类路径下的/static(或/public或/resources或/META-INF/resources),都可以直接访问,比如:我在/public 目录下放了一张图片,如果要浏览该图片,只需要访问localhost:8080/图片就可以了,而不用带上/public.这是因为,springboot默认访问静态资源是匹配类路径下的所有静态文件(/**)
- 如果要指定如何访问,比如指定localhost:8080/picture/图片,说明我们的匹配路径要为/picture/**,这个时候,只需要配置以下属性就可以了,这个设置到我们开发拦截器的时候用的很多
spring.mvc.static-path-pattern=/resources/**
- 当然我们也可以指定springboot访问哪些静态资源
spring:
mvc:
# 指定访问路径必须带上res
static-path-pattern: /res/**
resources:
# 这里与上面不同,这里是指定可以访问的资源资源在哪个包下,如果有多个包,直接用逗号隔开就可以了
static-locations: [classpath:/haha/]
webjar
- 这是一种特殊的静态资源,如:将jQuery的js文件打包成的jar便是webjar,当我们要访问这些js文件的时候,只需要进入https://www.webjars.org/ 网址,然后导入相关的静态资源jar.
- 例如:我们导入Jquery的jar包
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
导入后边可以访问http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面的路劲是按照我们的jar路径来的
首页访问
- 可以将index页面访问springboot默认或自己配置的的静态资源访问的包中.
spring:
# mvc:
# static-path-pattern: /tmp/** # 这里不能指定,不然首页访问失效
resources:
static-locations: [classpath:/hello/] # 指定静态资源路径
- 其次,除了这个方法,我们在Controller中也可以进行首页访问
自定义图标
- 和index页面一样,只要放在静态资源目录下即可,这个目录是默认的也可以你自己指定的.
- 但和首页访问一样 static-path-pattern: /tmp/** 一样不能配置.
- 注意:图标的名字为favicon.ico
静态资源配置原理
- SpringMVC生效的自动配置类为spring-boot-autoconfigure-2.3.4.RELEASE.jar org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
//多实例
@Configuration(proxyBeanMethods = false)
//典型的Servlet应用
@ConditionalOnWebApplication(type = Type.SERVLET)
// 容器中存在这三个组件的时候
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
// 容器中不存在这个组件的时候该类生效
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
- 看看该类给容器中配了什么?
- hiddenHttpMethodFilter 兼容rest风格的请求
- formContentFilter 表单内容过滤器
- 静态内部类 :WebMvcAutoConfigurationAdapter ,该类便是我们此次研究的核心.
我们先看@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
WebMvcProperties:
@ConfigurationProperties(
prefix = "spring.mvc"
)
public class WebMvcProperties {}
ResourceProperties:
@ConfigurationProperties(
prefix = "spring.resources",
ignoreUnknownFields = false
)
public class ResourceProperties {}
在该静态内部类中,只有一个有参构造器,这说明,该构造器里的所有属性值都从配置文件(也是从容器中,因为配置文件映射成一个类就处于容器中)中获取:
//ResourceProperties resourceProperties 获取spring.resources配置文件绑定
//WebMvcProperties mvcProperties 与spring.mvc配置文件绑定
//ListableBeanFactory beanFactory spring的bean工厂
//HttpMessageConverters 找到所有的HttpMessageConverters
//resourceHandlerRegistrationCustomizerProvider 找到资源处理器的自定义器
//servletRegistrations 给应用注册Servlet和Filter,Listener
public WebMvcAutoConfigurationAdapter(
ResourceProperties resourceProperties,
WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory,
ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
给所有的属性设置值后,我们就来到了该静态类的静态资源处理的核心代码:
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
//处理webjars
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
//处理静态资源
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
}
可以看到,处理webjars时,其会到/META-INF/resources/webjars/寻找文件,这就是为什么我们访问http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面的路径是按照我们的jar路径来的,从下图也可以看出,将Jquery打包成jar后,其静态资源确实在/META-INF/resources/webjars/中
同样的,当我们处理静态资源访问的时候,是通过WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())来获取路径,这个路径就是.resourceProperties.getStaticLocations(),而.resourceProperties.getStaticLocations()又是从ResourceProperties这个类中的映射出来的,这个类对于我们是properties配置文件,可以指定访问规则.当然其本身也有默认值:
public class ResourceProperties {
private static final String[]
CLASSPATH_RESOURCE_LOCATIONS =
new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
}
下面我们来看看首页访问的处理核心代码:
欢迎页的处理在另一个静态内部类EnableWebMvcConfiguration中:
HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping =
new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext),
applicationContext, this.getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
可以看到,该方法主要是new了一个WelcomePageHandlerMapping对象,然后返回,并将一些规则设置进去,这些规则要么是从形参拿入(即从IOC中拿入),要么是从this.mvcProperties获取,这个this.mvcProperties也对应着一个配置文件,所以this.mvcProperties.getStaticPathPattern()就是我们配置的spring.mvc.static-path-pattern,该配置是为了自定义访问静态资源路径.
这里就和我们前面的首页访问出现bug的地方对应上了!
点开WelcomePageHandlerMapping的有参构造器会发现:
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage.get());
this.setRootViewName("forward:index.html");
} else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 如果第一个条件不成立,跳转到这里,让Controller处理有index请求的页面
logger.info("Adding welcome page template: index");
this.setRootViewName("index");
}
}
可以看到,要想this.setRootViewName(“forward:index.html”)生效,就要使得"/".equals(staticPathPattern),也就是静态资源访问路径必须配置成/,这里springboot底层已经写死,这就是为什么我们在前面设置了访问路径是/tmp/**的时候,首页访问不了.
Rest风格使用原理
- 在Rest风格中,我们期望用同一个请求地址不同的请求方法来达到不同的请求.例如:
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
在这几个请求中,我们都知道GET和POST是表单默认支持的,但PUT和DELETE请求是不支持的,而要想支持这两个请求,在表单提交的时候,要做以下操作
<form method="POST">
<input name="_method" type="hidden" value="PUT" />
</form>
只有指定了请求方式是POST并且带有 隐藏的input才行,这个input里有两个重要属性,一个是name=_method和value=PUT/DELETE.
在前面的SpringMVC生效的自动配置类中,注册的hiddenHttpMethodFilter 这个bean兼容了rest风格的请求,所以,在请求过来的时候,会被该bean拦截,我们进入该类中可以看到:
public class OrderedHiddenHttpMethodFilter extends HiddenHttpMethodFilter
其集成了HiddenHttpMethodFilter,我们点击这个类进去,会发现一个核心的处理请求方法:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//得到一个原生请求
HttpServletRequest requestToUse = request;
//判断请求方式是否是POST,这就是我们要指定为POST的原因,且判断没有错误
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
//获取input标签的value属性值,this.methodParam就是_method
String paramValue = request.getParameter(this.methodParam);
//如果name=_mthod的input标签的value属性的值不为空
if (StringUtils.hasLength(paramValue)) {
//将其进行转换为大写,这就是为什么在页面的请求可以写成大小写
String method = paramValue.toUpperCase(Locale.ENGLISH);
//如果这个请求是允许的
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter((ServletRequest)requestToUse, response);
}
该方法利用了原生的Servlet方式进行请求处理和放行.首先,在if条件中,表示,我们的请求如果是POST(这就是写其他请求的时候要指定POST的原因),然后就request.getParameter(this.methodParam)获取名字是_method的值,即带有name=_method 的input标签的value属性值(put/delete),可以点击this.methodParam,其默认值就是_method.
之后,再判断是否为空hasLength,如果不为空,先进行一次大写转换,转换完后,再判断这个请求是否允许,然后进行包装:requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);在这里,HttpServletRequest类型的requestToUse 既然能被后者赋值,说明HiddenHttpMethodFilter一定实现了该接口或者其父类实现了该接口,点击进去我们可以看到,该方法将method传入,然后进行有参构造包装之后(并且重写了getMethod方法[返回的是修改后的method],重写的该方法会被后面映射地址的时候调用),再将requestTpUse替代,然后再利用doFilter方法进行放行.
注意:
在注册hiddenHttpMethodFilter这个bean的方法之上,是有条件限制的:
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.hiddenmethod.filter",
name = {"enabled"},
matchIfMissing = false
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
ConditionalOnProperty表示我们要在配置文件中开启过滤功能,才能在表单中进行PUT/DELETE等非GET,post请求.
但这里还有一个条件注解:@ConditionalOnMissingBean,该注解表示只要我们有这个类型的类就用我们自己的,没有才进行注册,所以我们可以通过配置文件的方式进行hiddenHttpMethodFilter这个bean的注册.
@Bean
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
OrderedHiddenHttpMethodFilter orderedHiddenHttpMethodFilter = new OrderedHiddenHttpMethodFilter();
orderedHiddenHttpMethodFilter.setMethodParam("_m");
return orderedHiddenHttpMethodFilter;
}
setMethodParam 方法是通过原理知晓的,在我们寻找OrderedHiddenHttpMethodFilter类的集成父类HiddenHttpMethodFilter时,会发现在HiddenHttpMethodFilter类中:
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS;
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = "_method";
public HiddenHttpMethodFilter() {
}
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}
...
}
可以发现DEFAULT_METHOD_PARAM属性指定了隐藏的input的name属性值_method,虽然这个值是final类型的不可改变,但是setMethodParam却可以设置,因为将_method赋值给了methodParam
请求映射基本原理
- 在表单进行请求的时候,springboot底层是如何映射到Controller层的某个方法执行呢?
首先,我们先进行一个Controller的书写
@RequestMapping("/user")
public String user(){
return "hello World";
}
- 首先,所有的请求都会进入到org.springframework.web.servlet.DispatcherServlet中,在该类中,每个请求进来的时候都会去到doService方法,该方法中处理请求的核心方法是doDispatch,该方法里的核心处理方法是getHandler(processedRequest)
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
handlerMappings:保存所有的请求映射
RequestMappingHandlerMapping:保存了所有@ReuqestMapping和handler的映射信息.
在getHandler方法中,如果这些请求映射不为空,就遍历各大映射Mapping,然后调用对传入的请求/user调用mapping.getHandler(request)方法,只要能获取到,就说明存在该请求方法:
我们可以看到,调用该方法后返回的handler信息是:com.hyb.sprinqboot2sprinqweb.controller.UserController #user(),该信息就保存了我们要调用的Controller下的user()方法.
之后将该handler返回,就去执行调用哪一步.
参数注解
- @PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
@RestController
public class ParameterTestController {
// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie){
Map<String,Object> map = new HashMap<>();
// map.put("id",id);
// map.put("name",name);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
map.put("_ga",_ga);
System.out.println(cookie.getName()+"===>"+cookie.getValue());
return map;
}
@PostMapping("/save")
public Map postMethod(@RequestBody String content){ //只有post请求才有请求体
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
//1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
// 第一个分号之前是路径,后面则为参数,各个参数又以分号隔开
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
// removeSemicolonContent(移除分号内容)支持矩阵变量的
//3、矩阵变量必须有url路径变量才能被解析
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}
// /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}
}
注意:springboot底层有个名为UrlPathHelper的类,该类的下的removeSemicolonContent属性默认是true的,意思是移除掉矩阵变量中分号的后面所有参数,所以springboot是禁用了矩阵变量的,必须要自己配置一个bean
package com.hyb.springboot2springweb.config;
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;
@Configuration
public class MethodFilter{
//implements WebMvcConfigurer
@Bean
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
OrderedHiddenHttpMethodFilter orderedHiddenHttpMethodFilter = new OrderedHiddenHttpMethodFilter();
orderedHiddenHttpMethodFilter.setMethodParam("_m");
return orderedHiddenHttpMethodFilter;
}
//没有实现接口,就用@Bean注入
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
//不移除分号后面的内容,在底层中,这是默认移除的
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
//实现接口便不用1@Bean注入
/*@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
//不移除分号后面的内容,在底层中,这是默认移除的
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}*/
}
参数处理原理简单版
- 我们适当修改前面的user请求,这里我们模拟一个请求过来的id是1
@GetMapping("/user/{id}")
public String user(@PathVariable("id") Integer id){
return "hello World";
}
前面请求映射的原理基本了解过,下面了解一下参数处理的原理:
还是在org.springframework.web.servlet.DispatcherServlet的doService方法中的doDispatch方法,在使用getHandler(processedRequest)找到能处理请求的Handler后,会为Handler找一个适配器 HandlerAdapter,该适配器执行目标方法并进行参数处理:this.getHandlerAdapter().
进入该方法,会发现这个方法和getHandler差不多一样,这也是遍历,只不过这里遍历的是Adapter(适配器),在handlerAdapters中,有四个适配器:
其中第一个便是我们处理RequestMapping的适配器,第二是支持函数式编程的适配器,得到应有的适配器后返回.之后继续执行,会继续下一个核心方法ha.handle()方法,一步步进去,就会来到RequestMappingHandlerAdapter类中的handleInternal()方法里,最后一步步执行,就会来到mav = this.invokeHandlerMethod(request, response, handlerMethod);方法中,该方法就是用来调用映射方法的,进入该方法,会发现也是RequestMappingHandlerAdapter的方法,在该方法中,有一个参数名为:argumentResolvers,该参数包含了有多参数解析器,解析器会解析执行目标方法的参数每个值是什么,我们可以查看这些解析器:
从这些解析器的名字中不难看出,每个解析器其实是一一对应一个能写在形参上的注解.我们可以查看包含了这么多个解析器参数argumentResolvers的定义:private HandlerMethodArgumentResolverComposite argumentResolvers; 会发现,其是HandlerMethodArgumentResolverComposite 类型,这个类型去实现了HandlerMethodArgumentResolver接口,该接口有两个方法:
boolean supportsParameter(MethodParameter var1);
@Nullable
Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
HandlerMethodArgumentResolverComposite会实现这两个接口,从语义上可以看出,supportsParameter方法表示传过来的参数是否被支持,那么另一个方法就是分析目标参数的方法了.
这里我们便知道了传过来的参数是被某个参数解析器解析了.
和argumentResolvers一样的位置,我们会发现另一个类型功能的属性:returnValueHandlers,该属性其实就是包含了所有返回类型解析器.
从size可以看出,该属性一共包含了十五种返回类型解析器,每个解析器都对应一个返回类型,如第一个就表示支持ModelAndView返回类型.
了解了这两个解析器,我们继续执行,便来到了核心方法invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);进入到该方法后,继续执行,在第一行中,有一个this.invokeForRequest(…)方法,该方法就是执行目标方法的终极方法,如何证明?你可以在此处打一个断点,然后在RequestMapping的目标方法打一个断点,执行后会发现,跳过invokeForReuqest方法后,会直接来到了RequestMapping的目标方法.
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 获取形参中的参数
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Arguments: " + Arrays.toString(args));
}
//执行目标方法,再进入该方法就涉及java的反射代码了
return this.doInvoke(args);
}
执行getMethodArgumentValues后发现args这个变量得到的就是我们的id=1.
getMethodArgumentValues()又是如何确定目标方法每一个参数的值呢?
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 获取方法中所有的参数列表
MethodParameter[] parameters = this.getMethodParameters();
//判断这些参数不为空
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
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) {
//判断当前解析器是否解析该类型的参数
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 var10) {
if (this.logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
this.logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
1)判断是否支持该类型的参数解析器
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator();
while(var3.hasNext()) {
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, resolver);
break;
}
}
}
return result;
}
可以看到,直接是将二十六个参数解析器遍历出来,然后用supportsParameter这个方法判断是否支持该注解
public boolean supportsParameter(MethodParameter parameter) {
//判断是否有RequestParam这个注解
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
return true;
} else {
RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
return requestParam != null && StringUtils.hasText(requestParam.name());
}
} else if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
} else {
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
} else {
return this.useDefaultResolution ? BeanUtils.isSimpleProperty(parameter.getNestedParameterType()) : false;
}
}
}
遍历完成后,如果合适,就会执行argumentResolverCache.put(parameter, resolver)将该参数保存在缓存中,这就是为什么springboot在加载过一次请求后,后面的多次会很快的原因.
2)可以解析这个参数后,在真正解析这个参数(resolveArgument方法):
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
//获取参数名字,比如这里是id
AbstractNamedValueMethodArgumentResolver.NamedValueInfo namedValueInfo = this.getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
} else {
//解析该参数名字id,得到该参数值
Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
} else if (namedValueInfo.required && !nestedParameter.isOptional()) {
this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = this.handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
} else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
} catch (ConversionNotSupportedException var11) {
throw new MethodArgumentConversionNotSupportedException(arg, var11.getRequiredType(), namedValueInfo.name, parameter, var11.getCause());
} catch (TypeMismatchException var12) {
throw new MethodArgumentTypeMismatchException(arg, var12.getRequiredType(), namedValueInfo.name, parameter, var12.getCause());
}
}
this.handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
}
resolveName方法:
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
直接调用原生ServletAPI,获取该参数名字的值,比如,获取出的id=1
Map<String, String> uriTemplateVars = (Map)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, 0);
return uriTemplateVars != null ? uriTemplateVars.get(name) : null;
}
thymeleaf
设置属性值-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>
设置多个值
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法 th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
所有h5兼容的标签写法
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
迭代
<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>
条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
springboot整合
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 配置好了thymeleaf
@Configuration(
proxyBeanMethods = false
)
//thymeleaf模板值配置文件
@EnableConfigurationProperties({ThymeleafProperties.class})
@ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class})
@AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class})
public class ThymeleafAutoConfiguration {
}
- 在配置类中以静态内部类的方式配置好了模板引擎SpringTemplateEngine
protected static class ThymeleafDefaultConfiguration {
protected ThymeleafDefaultConfiguration() {
}
@Bean
@ConditionalOnMissingBean({ISpringTemplateEngine.class})
SpringTemplateEngine templateEngine(ThymeleafProperties properties, ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
dialects.orderedStream().forEach(engine::addDialect);
return engine;
}
}
- 同样的,也配置好了视图解析器
@Bean
@ConditionalOnMissingBean(
name = {"thymeleafReactiveViewResolver"}
)
ThymeleafReactiveViewResolver thymeleafViewResolver(ISpringWebFluxTemplateEngine templateEngine, ThymeleafProperties properties) {
ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver();
resolver.setTemplateEngine(templateEngine);
this.mapProperties(properties, resolver);
this.mapReactiveProperties(properties.getReactive(), resolver);
resolver.setOrder(2147483642);
return resolver;
}
该视图解析器也是从模板配置文件中拿到值的:
public class ThymeleafProperties {
...
//页面存放的包
public static final String DEFAULT_PREFIX = "classpath:/templates/";
//匹配后缀是html的页面
public static final String DEFAULT_SUFFIX = ".html";
...
}
入门
- 前面看到springboot整合thymeleaf的时候,其底层的视图解析器,是默认在template里面的,所以我们要在这个包下创建的页面才能生效,而是html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>第一个thymeleaf</title>
</head>
<body>
<h1 th:text="${msg}"></h1>
<a th:href="${href}">这是一个百度链接</a>
<a th:href="@{href}">这是一个百度链接</a>
</body>
</html>
注意:xmlns:th="http://www.thymeleaf.org"必须要加上.
- 编写Controller
@GetMapping("/hyb")
public String hyb(Model model){
model.addAttribute("msg","这是我的第一个模板引擎代码!");
model.addAttribute("href","http://www.baidu.com");
return "helloThymeleaf";
}
这里我们向Model域中放了两个数据,测试后你会发现h1是可以拿到值的,并且第一个a标签也是可以拿到值的,并跳转地址的,但是第三个a标签就不行,这是因为@{href}会被解析成一个字符串href,这个字符串会自动拼接上当前工程地址,如果有这个页面就会跳转,没有就不会跳转,这个@一般用在比如表单提交的action要跳转的Controller请求.
抽取公共页面
假如我们有一个公共页面需要被抽取
<footer th:fragment="copy"> <!--th:fragment或者用id替代也表示唯一-->
© 2011 The Good Thymes Virtual Grocery
</footer>
我们有三种引用方式:
th:insert:将公共片段整个插入到声明引入的元素中
<div th:insert="页面 :: copy"></div>
<!--结果 将footer整个标签都插入到了该div中-->
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
th:replace:将声明引入的元素替换为公共片段
<div th:replace="页面 :: copy"></div>
<!--结果 将footer整个标签代替了这个div-->
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
th:include:将被引入的片段的内容包含进这个标签中
<div th:include="页面 :: copy"></div>
<!--结果 将footer标签里的内容插入到div中-->
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
注意:
- 在使用 页面 :: copy 的时候,这个页面是公共页面的页面名字,该页面的匹配也是在template包下的,这个名字不用带.html结尾,也会被视图解析器解析.
- 公共页面中的th:fragment="copy"表示某公共部分的唯一标识,可以用id直接代替.但是在使用 页面::copy 的时候必须换成 页面::#copy
视图解析器原理
这里我们模拟一个请求,并返回一个视图名字让底层去解析,并跳转页面,这个时候注意,RestController得用Controller代替了.
@Controller
public class ThymeleafController {
@GetMapping("/map")
public String map(){
return "helloThymeleaf";
}
}
前面请求映射的原理中说过,请求会经过DispatcherServlet,然后被参数解析器和返回值解析器去解析.首先是经过参数解析器,确定了是map()被调用:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
this.setResponseStatus(webRequest);
if (returnValue == null) {
if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {
this.disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(this.getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception var6) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.formatErrorForReturnValue(returnValue), var6);
}
throw var6;
}
}
this.invokeForRequest这个方法便是执行目标方法map(),执行完毕后,会返回一个值returnValue,这个值就是map()方法的返回值:helloThymeleaf,我们继续执行下去,会来到this.returnValueHandlers.handleReturnValue方法,该方法便是处理返回值的方法,深入handleReturnValue方法查看:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
} else {
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
1)该方法首先第一步是选择返回值处理器selectHandler:
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = this.isAsyncReturnValue(value, returnType);
Iterator var4 = this.returnValueHandlers.iterator();
HandlerMethodReturnValueHandler handler;
do {
do {
if (!var4.hasNext()) {
return null;
}
handler = (HandlerMethodReturnValueHandler)var4.next();
} while(isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler));
} while(!handler.supportsReturnType(returnType));
return handler;
}
会发现,其选择返回值解析器也是通过遍历的方式,找到适合的解析器,这里遍历后你会发现,若是返回字符串,会被ViewNameReturnValueHandler处理.
2)该方法的第二步,就是进行处理:handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest),进入处理方法:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
//如果返回值是一个字符串
if (returnValue instanceof CharSequence) {
//就将返回值转为字符串
String viewName = returnValue.toString();
//将数据放在ModelAndViewContainer mavContainer 容器中,包括数据和地址
mavContainer.setViewName(viewName);
判断是否是重定向视图,拥有redirect字符串,
if (this.isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
} else if (returnValue != null) {
throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
保存进入ModelAndViewContainer里后,一直返回,直到跳出invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);进一步执行,就会来到ModelAndView var15 = this.getModelAndView(mavContainer, modelFactory, webRequest);方法:
@Nullable
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
modelFactory.updateModel(webRequest, mavContainer);
if (mavContainer.isRequestHandled()) {
return null;
} else {
ModelMap model = mavContainer.getModel();
//核心步骤,将ModelAndViewContainer的数据封装成ModelAndView
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
if (!mavContainer.isViewReference()) {
mav.setView((View)mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes)model).getFlashAttributes();
HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
}
//之后就返回这个ModelAndView
return mav;
}
}
得到ModelAndView之后,一直返回出去,直到mv = ha.handle(processedRequest, response, mappedHandler.getHandler());处,继续执行,便会将ModelAndView传入到一个方法去执行:
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException),这个方法就是真实执行视图跳转功能:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException)exception).getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
//ModelAndView不为空,也没有被清理过
if (mv != null && !mv.wasCleared()) {
//既然不为空,就进行视图渲染
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace("No view rendering, null ModelAndView returned.");
}
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
}
}
}
- this.render(mv, request, response)进行视图渲染:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
Locale locale = this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale();
response.setLocale(locale);
//得到视图名称
String viewName = mv.getViewName();
View view;
if (viewName != null) {
//根据视图名称得到View对象,该对象保存了一些渲染逻辑
view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + this.getServletName() + "'");
}
} else {
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'");
}
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
} catch (Exception var8) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Error rendering view [" + view + "]", var8);
}
throw var8;
}
}
this.resolveViewName()获取视图解析器:
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
Iterator var5 = this.viewResolvers.iterator();
while(var5.hasNext()) {
ViewResolver viewResolver = (ViewResolver)var5.next();
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
但值得注意的是,如果你继续执行下去,会发现,这里使用的视图解析器是ContentNegotiatingViewResolver,而不是ThymeleafViewResolver,但其实这本身不影响,因为在ContentNegotiatingViewResolver里就包含了1-4索引的视图解析器:
虽然使用了ContentNegotiatingViewResolver解析器,但在这个解析器里,解析当前这个视图还是用ThymeleafViewResolver.
return view后,得到这个view
它会调用自己的render方法,view.render()
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
this.renderFragment(this.markupSelectors, model, request, response);
}
renderFragment(this.markupSelectors, model, request, response):
protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
...
viewTemplateEngine.process(templateName, processMarkupSelectors, context, (Writer)templateWriter);
..
}
process便是核心方法,因为我们这个是普通的页面跳转,springboot底层用该方法直接与html的模板引擎对接,就渲染出了页面.
而如果是我们的forward跳转和rediect跳转:大致流程都一样,不过这些在最底层是直接拿Servlet方法调用就渲染出了页面.
拦截器
例子
public interface HandlerInterceptor {
//在请求之前
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
//在请求之后还未渲染之前
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
//在渲染页面之后
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
package com.hyb.springboot2springweb.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;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("拦截器方法执行了");
Object user = request.getAttribute("user");
if (user!=null){
return true;
}
request.getRequestDispatcher("/login").forward(request,response);
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
// 所有定制web功能的都可以实现该接口
@Configuration
public class LoginConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
// 拦截所有请求
.addPathPatterns("/**")
// 排除的请求,注意,将静态资源放行
.excludePathPatterns("/static/**","/","/login");
}
}
拦截器原理
假如我们请求地址为:http://localhost:8080/map,然后被我们自定义的拦截器拦截.
前面说过请求映射,一旦我们发生/map的请求,会请求到DispatcherServlet->doService->doDispatch中,获取请求映射处理器是mappedHandler = this.getHandler(processedRequest);方法,其实该方法获取到的handler中,还包含我们自定义的拦截器:
得到我们的拦截器后,就会到执行getHandlerAdapter获取适配器,然后才会到真正的执行方法ha.handle(),但我们要在请求之前拦截,所以这个执行方法先不能执行,所以,在这个方法之前,有一个applyPreHandle()方法:
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
如果该方法返回为false,便直接return,之后就不执行handle方法,这就是为什么我们自定义拦截器时,在predHandle方法返回false的时候会请求失败.
我们可以进入该判定方法看看:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
}
return true;
}
该方法主要的原理在于,我们遍历所有拦截器,然后执行里面的preHandle()方法,这个时候因为我们实现了拦截器接口,所以这个方法会跳转到我们实现的方法当中来,然后我们进行判断,是否返回true,如果返回true,说明不拦截,请求成功,便执行返回了,而如果返回失败,便会执行triggerAfterCompletion(request, response, (Exception)null)方法,深入该方法:
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = this.interceptorIndex; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable var8) {
logger.error("HandlerInterceptor.afterCompletion threw exception", var8);
}
}
}
}
可以发现,该方法中,也是遍历所有拦截器,但是这是反向遍历,而且调用拦截器中afterCompletion方法.
前面失败就到此为止,如果成功,便会执行doDispatch中mv = ha.handle(processedRequest, response, mappedHandler.getHandler());方法,执行完该方法之后,又会来到applyPostHandle方法:
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = interceptors.length - 1; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
该方法也是通过遍历拦截器的方式,然后调用拦截器的postHandle()方式.执行完applyPostHandle方法之后,会进行页面渲染,页面渲染的方法是:this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);进入该方法会发现,在该方法最后,也就是页面渲染完之后,会执行一个叫mappedHandler.triggerAfterCompletion(request, response, (Exception)null);的方法,进入该方法:
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = this.interceptorIndex; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable var8) {
logger.error("HandlerInterceptor.afterCompletion threw exception", var8);
}
}
}
}
该方法可以看出,原理也是通过遍历拦截器去执行afterCompletion(request, response, this.handler, ex)的.
所以,我们可以画个图,这里假如有多个拦截器:
![8ef835f5635d6747faa4c9516bae5b1.png](https://img-blog.csdnimg.cn/img_convert/46ad49e0a1e5422e423e17e586d7997c.png#clientId=ufb2285fc-9ba9-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u95e13cd2&margin=[object Object]&name=8ef835f5635d6747faa4c9516bae5b1.png&originHeight=653&originWidth=1046&originalType=binary&ratio=1&rotation=0&showTitle=false&size=55139&status=done&style=none&taskId=u1cb8a3d6-3e31-4594-9826-c6356542c4d&title=)
1、根据当前请求,找到**HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 **所有拦截器的 preHandle方法
- 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、所有拦截器都返回True。执行目标方法
5、倒序执行所有拦截器的postHandle方法。
**6、前面的步骤有任何异常都会直接倒序触发 **afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
文件上传
<form th:action="@{/upFile}" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="singleFile"/>
<input type="file" name="files" id="multiFile" multiple/>
<button type="submit">提交</button>
</form>
@PostMapping("/upFile")
public String upFile(@RequestPart("file")MultipartFile file,@RequestPart("files")MultipartFile[] files)throws Exception{
if (!file.isEmpty()){
System.out.println("上传了单个文件->"+file.getOriginalFilename());
}
if (files.length>0){
for (MultipartFile m :
files) {
System.out.println("上传的多个文件的->"+m.getOriginalFilename()+"文件");
}
}
return "helloThymeleaf";
}
文件上传要注意的点是:
- 表单必须是post请求
- 单个文件不能超过1m,多个文件一起总不能超过10m,这是由于其底层决定的.
@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 {}
从这里我们可以看见,文件配置类的属性值绑定是来自MultipartProperties类的,所以可以深入该类看看:
private DataSize maxFileSize = DataSize.ofMegabytes(1L);
private DataSize maxRequestSize = DataSize.ofMegabytes(10L);
可以清楚的看到,单个文件限制为1m,而多个文件总限制为10m
如果要改变默认限制,可以看到@ConditionalOnProperty的属性prefix的值,该属性表示匹配配置文件前缀为spring.servlet.multipart的key,所以我们修改默认限制得从这里下手:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
错误处理
默认处理
在springboot中,如果存在于静态资源文件夹或template文件夹下的/error包,都是存放默认错误页面,当页面出现错误,springboot会自动跳转到该页面
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
/error下,4xx和5xx页面表示匹配所有错误码为4开头或5开头的页面都将跳转至该错误页面.
![](https://img-blog.csdnimg.cn/img_convert/bad9cc522a2432ab5565c8804615f1a3.png#crop=0&crop=0&crop=1&crop=1&from=url&id=VNMua&margin=[object Object]&originHeight=135&originWidth=325&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
默认处理原理
底层组件
在spring-boot-autoconfigure-2.3.7.RELEASE.jar中org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration便是错误处理自动化配置类,我们可以看看该配置类又在容器中配置了哪些组件:
- DefaultErrorAttributes类型的errorAttributes组件
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {}
该组件主要是用于定义返回的错误具有哪些内容:
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (Boolean.TRUE.equals(this.includeException)) {
options = options.including(new Include[]{Include.EXCEPTION});
}
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
- BasicErrorController类型的 basicErrorController 组件:
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {}
从这里便可以发现,其默认处理的路径是/error,同时你也可以知道server.error.path这个key可以修改默认处理路径.
在该类中,也注册了一些常用组件:
- ModelAndView类型的errorHtml组件,该组件返回了一个ModelAndView对象进行页面渲染,即错误以页面的形式展示
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
该视图名字为error
2) ResponseEntity<Map<String, Object>>类型的 error 组件,该组件会返回一段json字符串.
- 静态内部类WhitelabelErrorViewConfiguration中注册了一个View类型的 error组件
@Bean(
name = {"error"}
)
@ConditionalOnMissingBean(
name = {"error"}
)
public View defaultErrorView() {
return this.defaultErrorView;
}
- 静态内部类WhitelabelErrorViewConfiguration中也注册了beanNameViewResolver类型的 beanNameViewResolver 组件.该组件是视图解析器,会根据errorHtml() 返回的ModelAndView(error)的视图找View对象.
- 找到的对象名为error,通过defaultErrorView()方法返回this.defaultErrorView得到,该返回值是ErrorMvcAutoConfiguration.StaticView类型的:
private static class StaticView implements View {
...
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (response.isCommitted()) {
String message = this.getMessage(model);
logger.error(message);
} else {
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(this.getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(this.htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
}
....
}
可以看到StaticView是一个静态内部类,其视图渲染方法render便渲染了一个默认的错误页面.
- 该配置类中还有一个静态内部类DefaultErrorViewResolverConfiguration,在该类中DefaultErrorViewResolver 类型的conventionErrorViewResolver组件,进入DefaultErrorViewResolverConfiguration类,找到一个错误页面视图解析方法:
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
在该方法中,传入了一个HttpStatus类型的页面响应状态码(404,505),和其用到的视图名字一直是SERIES_VIEWS有一定关联,我们可以看看该值的定义:
private static final Map<Series, String> SERIES_VIEWS;
再来寻找给该变量赋值的代码:
static {
Map<Series, String> views = new EnumMap(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
那么再返回resolve方法中可以看到,其errorViewName = “error/” + viewName,该viewName是从resolve((String)SERIES_VIEWS.get(status.series()), model)传入的,也就是说,比如页面出现了404错误,该错误就会寻找在/error包的下的4xx或者404开头的错误页面.
从这里我们便可以知道为什么我们的错误页面命名是页面响应状态码的时候会被自动分析响应.
错误处理流程
假设我们的请求方法中有一个错误:
@GetMapping("/map")
public String map(){
int i=10/0;
return "helloThymeleaf";
}
- 在DispatcherServlet中,我们执行目标方法的代码是mv = ha.handle(processedRequest, response, mappedHandler.getHandler()),在该方法的下面几行,会发现,一旦目标方法有任何错误都会被catch掉,并且被dispatchException捕捉:
catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
- 捕捉后,虽然目标方法执行不成功,但是还是会进行视图解析流程,但是这里因为执行目标方法不成功,这个ModelAndView(mv)是空的:
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
//异常是否不为空?
if (exception != null) {
//异常是否是ModelAndView定义异常
if (exception instanceof ModelAndViewDefiningException) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException)exception).getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
//处理异常,处理完还是返回ModelAndView
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
if (mv != null && !mv.wasCleared()) {
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace("No view rendering, null ModelAndView returned.");
}
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
}
}
}
2.1 如何处理发生的异常:
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
ModelAndView exMv = null;
// 如果异常处理不为空,遍历所有的异常处理器,看谁能处理该异常
if (this.handlerExceptionResolvers != null) {
Iterator var6 = this.handlerExceptionResolvers.iterator();
while(var6.hasNext()) {
//处理器异常解析器
HandlerExceptionResolver resolver = (HandlerExceptionResolver)var6.next();
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
} else {
if (!exMv.hasView()) {
String defaultViewName = this.getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Using resolved error view: " + exMv, ex);
} else if (this.logger.isDebugEnabled()) {
this.logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, this.getServletName());
return exMv;
}
} else {
throw ex;
}
}
HandlerExceptionResolver:处理器异常解析器有多个:
![](https://img-blog.csdnimg.cn/img_convert/de8c2dee1405ab998071a126166e8407.png#crop=0&crop=0&crop=1&crop=1&from=url&id=oWDkm&margin=[object Object]&originHeight=297&originWidth=608&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
![](https://img-blog.csdnimg.cn/img_convert/640cfb11aa18204e551ad07c4ece0808.png#crop=0&crop=0&crop=1&crop=1&from=url&id=BGOus&margin=[object Object]&originHeight=110&originWidth=966&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
2.1.1 DefaultErrorAttributes如何处理异常?(遍历第一个异常处理器的时候,深入异常处理方法resolveException)
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
this.storeErrorAttributes(request, ex);
return null;
}
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
request.setAttribute(ERROR_ATTRIBUTE, ex);
}
可以看到,DefaultErrorAttributes处理异常很简单,直接将异常信息保存到请求域中然后返回为null.
2.1.2 轮到下一个异常解析器解析
因为第一个异常解析器返回为null,所以这里exMv为空,不能break,继续循环,轮到下一个异常HandlerExceptionResolverComposite,该解析器中又有三个异常解析器,我们一个个慢慢遍历,但遍历完后你会发现,这些值返回还是为null,表明没有处理器能处理我们的异常,前面的DefaultErrorAttributes也只是将信息保存到请求域中了而已,所以全部返回为空后,到processHandlerException末尾的throw ex;又会将异常抛出去.
但虽然抛出去了,也没有人能解析,所以我们一直放行,放行多次后会发现,这个有错误异常的请求会变成了/error请求,再来执行一遍DispatcherServlet.
/error会被当做正常的地址被解析,首先是handle()执行目标方法:
->handleInternal->invokeHandlerMethod->invokeAndHandle->invokeForRequest:
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Arguments: " + Arrays.toString(args));
}
return this.doInvoke(args);
}
NativeWebRequest request:
->doInvoke(args):
@Nullable
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(this.getBridgedMethod());
try {
return this.getBridgedMethod().invoke(this.getBean(), args);
} catch (IllegalArgumentException var4) {
this.assertTargetBean(this.getBridgedMethod(), this.getBean(), args);
String text = var4.getMessage() != null ? var4.getMessage() : "Illegal argument";
throw new IllegalStateException(this.formatInvokeError(text, args), var4);
} catch (InvocationTargetException var5) {
Throwable targetException = var5.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException)targetException;
} else if (targetException instanceof Error) {
throw (Error)targetException;
} else if (targetException instanceof Exception) {
throw (Exception)targetException;
} else {
throw new IllegalStateException(this.formatInvokeError("Invocation failure", args), targetException);
}
}
}
args:
->this.getBridgedMethod().invoke(this.getBean(), args)[反射]->BasicErrorController:errorHtml:
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
设置状态码等一些错误信息,然后解析错误视图->resolveErrorView:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
Iterator var5 = this.errorViewResolvers.iterator();
ModelAndView modelAndView;
do {
if (!var5.hasNext()) {
return null;
}
ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
modelAndView = resolver.resolveErrorView(request, status, model);
} while(modelAndView == null);
return modelAndView;
}
该方法遍历所有的errorViewResolvers,看谁能解析:
拿到该解析器,执行->resolveErrorView:
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
获取ModelAndView->resolve:
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
这里的viewName通过状态码或者是4xx,5xx传入:
这里的viewName值是500:
继续执行->resolveResource:
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
String[] var3 = this.resourceProperties.getStaticLocations();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
}
}
return null;
}
-> createRelative 拼接页面(resourceProperties存在的情况下,这里是不存在的):
资源文件不存在,值不存在,所以返回空,这里的ModelAndView为空,便执行resolveErrorView->第二个resolve:
刚才得到的是500状态码,因为返回为空后,所以这个时候找我们/error存在的页面,找一个5开头的5xx页面,找不到,还是空,继续返回,最后resolveErrorView返回空ModelAndView来到errorHtml,最后执行return modelAndView != null ? modelAndView : new ModelAndView(“error”, model);方法,因为为空,所以执行 new ModelAndView(“error”, model)构造名字为error的ModelAndView对象,一直返回,执行完handle方法后,封装ModelAndView:
得到ModelAndView后,会进入视图渲染环节:processDispatchResult->render->resolveViewName得到的视图解析器是:
返回视图解析器后-> 执行render中的render:
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (response.isCommitted()) {
String message = this.getMessage(model);
logger.error(message);
} else {
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(this.getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(this.htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
}
这个时候页面就渲染出来了!
自定义错误Controller
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler({ArithmeticException.class,NullPointerException.class})
public String HandlerException1(){
return "login";
}
}
该原理是如何进行的?
在前面咱们说过四个错误异常解析器:
![](https://img-blog.csdnimg.cn/img_convert/c4f9377c23a14b359209134c74494acf.png#crop=0&crop=0&crop=1&crop=1&from=url&id=gGk7C&margin=[object Object]&originHeight=297&originWidth=608&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
在第一个里,只是往域中保存了一些数据,并不能处理异常,而第二个处理器又包含了很多处理器,其包含的处理器当中,第一个处理器ExceptionHandlerExceptionResolver便会识别标注了ExceptionHandler注解的方法去处理异常.
所以上面我们的Controller便会被识别.
ResponseStatus自定义状态码
@ResponseStatus(value= HttpStatus.FOUND,reason = "用户数量太多")
public class OverUsersExceptionHandler extends RuntimeException{
public OverUsersExceptionHandler(String msg){
super(msg);
}
public OverUsersExceptionHandler(){
}
}
@ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver 解析器解析,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);这个虽然解析了,但是没有被包装成ModelAndView所有tomcat会再次发送/error请求,然后继续处理.
自定义异常处理器
package com.hyb.springboot2springweb.controller;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ExceptionHandler;
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;
// 最高优先级
@Order(value = Ordered.HIGHEST_PRECEDENCE) //数字越小优先级越高
@Component
public class CustomExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
try {
httpServletResponse.sendError(501,"自定义错误状态码");
} catch (IOException ex) {
ex.printStackTrace();
}
return new ModelAndView();
}
}
该自定义的异常解析器会在解析器遍历的时候加进入遍历,但有可能我们自定义的解析器优先级太低,没有遍历到,已经被系统的解析器解析了,所以要加上优先级,表示最高优先级,这个value值是最小的int数据.
原生注入Servlet组件
三大组件注入
@WebServlet(urlPatterns = "/sv")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("gbk");
resp.getWriter().write("这是一个注解版的Servlet");
}
}
@Slf4j
@WebFilter(urlPatterns = "/sv")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("init.....");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("doFilter....");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
log.info("destroy....");
}
}
@Slf4j
@WebListener
public class Listener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("项目启动完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("项目销毁完成");
}
}
注意:在启动类上一定加上以下注解:
@ServletComponentScan(basePackages = "com.hyb.springboot2springweb.servlet")
表示扫描的Servlet路径
另一种写法,去掉@Webxxx,直接从容器中注册组件的方式去注册纯Servlet组件:
@Configuration
public class MyRegister {
@Bean
public ServletRegistrationBean<MyServlet> myServlet(){
return new ServletRegistrationBean<MyServlet>(new MyServlet(),"/sv");
}
@Bean
public FilterRegistrationBean<MyFilter> myFilterServletRegistrationBean(){
//第二个参数可以直接传入myServlet(),表示使用该方法的路径匹配,但是必须保证是单实例的
FilterRegistrationBean<MyFilter> myFilterFilterRegistrationBean = new FilterRegistrationBean<>(new MyFilter());
myFilterFilterRegistrationBean.setUrlPatterns(Collections.singletonList("/sv"));
return myFilterFilterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean<Listener> servletListenerRegistrationBean(){
return new ServletListenerRegistrationBean<>(new Listener());
}
}
DispatcherServlet注入原理
- 在spring-boot-autoconfigure-2.3.7.RELEASE.jar->org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration类中:
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
该方法注入了一个DispatcherServlet,而绑定的配置文件为WebMvcProperties,又因为:
@ConfigurationProperties(
prefix = "spring.mvc"
)
public class WebMvcProperties {}
所以该组件对应的配置key是spring.mvc
- 除了DispatcherServlet,还配置了DispatcherServletRegistrationBean:
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
registration.setName("dispatcherServlet");
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
查看其继承的类,发现是ServletRegistrationBean,前面我们通过这个类可以将三大组件注入进去,这里的道理也是一样,DispatcherServlet正是通过DispatcherServletRegistrationBean将其配置进来的,而且从传入的参数webMvcProperties.getServlet().getPath()中可以知道,其默认的匹配路径是/(直接进入getPath方法查看属性便可以知道),所以我们可以修改其默认的匹配路径:
spring:
mvc:
servlet:
path: /自定义默认匹配路径
原生注入Servlet组件的时候,容器里会有两个Servlet组件,一个是我们原生创建的Servlet,一个是springboot底层的DispatcherServlet,当我们发送/sv请求的时候,其实也有可能走DispatcherServlet,但是我们在自己创建的原生Servlet的时候,指定了匹配路径_urlPatterns = “/sv”,_这个时候,按照精确匹配原则,/sv的请求就会走我们的Servlet,这个时候时被Tomcat处理的,所有会走我们自己的过滤器,而DispatcherServlet的请求会被springboot的拦截器HandlerInterceptor拦截,这两个不是一回事.
嵌入式Servlet容器原理
- springboot启动时,如果发现当前应用是web应用,会创建出一个特殊的IOC容器ServletWebServerApplicationContext 该容器也是ApplicationContext类型.
- ServletWebServerApplicationContext 启动的时候会寻找ServletWebServerFactory(Servlet的webServer工厂)
- SpringBoot底层默认有很多的WebServer工厂;
TomcatServletWebServerFactory
,JettyServletWebServerFactory
, orUndertowServletWebServerFactory
这些工厂不用我们自己配置,在spring-boot-autoconfigure-2.3.7.RELEASE.jar->org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration 类会为这些工厂配置. - 该自动配置类导入了EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class三个类:
@Import({ EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {
}
这三个类是ServletWebServerFactoryConfiguration 配置类中的静态内部类.
- 在springboot底层,如果是web应用,ServletWebServerFactoryConfiguration便会默认配置Tomcat web服务器工厂:TomcatServletWebServerFactory:
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory(ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers().addAll((Collection)connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers().addAll((Collection)contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers().addAll((Collection)protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
该服务器工厂会给我们创建出Tomcat服务器并启动.
- 所以,我们可以在ServletWebServerApplicationContex:onRefresh->createWebServer方法中,打一个断点,一旦项目启动,该断点就会停住:
private void createWebServer() {
//创建一个WebServer web服务器
WebServer webServer = this.webServer;
//创建一个ServletContext容器
ServletContext servletContext = this.getServletContext();
if (webServer == null && servletContext == null) {
//获取WebServerFactory(web服务器工厂),因为默认内置了Tomcat,所以首次这里得到Tomcat服务器工厂
ServletWebServerFactory factory = this.getWebServerFactory();
//该工厂创建的Tomcat服务器
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
this.getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer));
this.getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, this.webServer));
} else if (servletContext != null) {
try {
this.getSelfInitializer().onStartup(servletContext);
} catch (ServletException var4) {
throw new ApplicationContextException("Cannot initialize servlet context", var4);
}
}
this.initPropertySources();
}
工厂创建出web服务器Tomcat->factory.getWebServer():
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();
while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}
this.prepareContext(tomcat.getHost(), initializers);
return this.getTomcatWebServer(tomcat);
}
其底层也是代码启动Tomcat.
- 我们创建出的是Tomcat,所以该Web服务器类是TomcatWebServer,进入该类会发现其构造器:
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
this.monitor = new Object();
this.serviceConnectors = new HashMap();
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = shutdown == Shutdown.GRACEFUL ? new GracefulShutdown(tomcat) : null;
this.initialize();
}
会调用一个初始化方法->initialize:
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + this.getPortsDescription(false));
synchronized(this.monitor) {
try {
this.addInstanceIdToEngineName();
Context context = this.findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && "start".equals(event.getType())) {
this.removeServiceConnectors();
}
});
this.tomcat.start();
this.rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), this.getClass().getClassLoader());
} catch (NamingException var5) {
}
this.startDaemonAwaitThread();
} catch (Exception var6) {
this.stopSilently();
this.destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", var6);
}
}
}
初始化方法的核心会调用一个start方法.
所以,web服务器工厂创建出web服务器的时候,还会给我们启动web服务器.
- 创建Tomcat后,返回一个WebServer,进入该接口,可以见到其有一个start()方法,查看其实现类:
会发现其一共支持四种Web服务器.
那么我们如何切换默认服务器呢?
首先,Tomcat为默认服务器是因为导入了spring-boot-starter-web依赖,而该依赖便内置了spring-boot-starter-tomcat,所以我们要切换到其他服务器,只需要将该依赖替换掉就可以了.
首先,我们需要spring-boot-starter-web依赖,但又不需要其内置的Tomcat依赖,所以我们需要排除其内部的Tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
然后在https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters查看对应的服务器依赖即可:
dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
定制化原理
常见的定制化方式
- 修改配置文件某个配置项,如我们可以定制端口号,可以在properties文件中用server.port来修改
- 实现xxxCustomizer接口,比如如果要定制化嵌入式Servlet容器,你可以实现WebServerFactoryCustomizer接口:
@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
上面是以变成的方式设置端口号
或者你可以自定义服务器工厂:
import java.time.Duration;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class MyTomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory server) {
server.addConnectorCustomizers((connector) -> connector.setAsyncTimeout(Duration.ofSeconds(20).toMillis()));
}
}
- Web应用 编写一个配置类实现 WebMvcConfigurer 接口即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer
如果你在实现该接口时还加上@EnableWebMvc,该注解会让你全面接管SpringMVC,这个时候Springboot对SpringMVC的自动配置全部失效.
这个功能得益于Springboot对SpringMVC的自动配置类WebMvcAutoConfiguration ,在该自动配置类上,有一个另其生效的条件@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),这里说明只要容器里没有WebMvcConfigurationSupport这个类的时候,对SpringMVC的自动化配置才会生效,所以当我们用上@EnableWebMvc注解的时候,WebMvcConfigurationSupport这个类就存在了,进入该注解可以看到:
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
其导入了DelegatingWebMvcConfiguration这个类,而这个类刚好又继承WebMvcConfigurationSupport这个类,所以,当我们用上这个注解的时候,自动化配置就会失效.
数据访问
MySQL
数据源的自动配置-HikariDataSource
导入依赖
- 导入jdbc依赖: 进入https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters寻找有关spring-boot-starter-data-*的场景,spring-boot-starter-data-jdbc便是jdbc的场景:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
- 配置数据库连接驱动:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
注意:数据库连接驱动必须和电脑安装的数据库版本大概一致,版本号不能相差太大,如果你需要修改版本,可以直接引入标签的方式将版本写死,这样做的原理是Maven依赖的就近原则.
其次,你也可以指定中的<mysql.version>指定mysql版本
分析自动化配置
spring-boot-autoconfigure-2.3.7.RELEASE.jar->org.springframework.boot.autoconfigure.jdbc包下:
- DataSourceAutoConfiguration 数据源的自动配置
- @EnableConfigurationProperties({DataSourceProperties.class}) 开启与DataSourceProperties类属性配置绑定功能:
@ConfigurationProperties(
prefix = "spring.datasource"
)
public class DataSourceProperties {}
- 底层配置好数据库连接池HikariDataSource:
@Configuration(
proxyBeanMethods = false
)
@Conditional({DataSourceAutoConfiguration.PooledDataSourceCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({ Hikari.class, Tomcat.class, Dbcp2.class, Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration() {
}
}
- DataSourceTransactionManagerAutoConfiguration 事务管理器自动配置
- JdbcTemplateAutoConfiguration JdbcTemplate的自动化配置,可以对数据库进行CRUD
- 注入了JdbcTemplateConfiguration
- JdbcTemplateConfiguration 注入了JdbcTemplate:
@Bean
@Primary
JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
Template template = properties.getTemplate();
jdbcTemplate.setFetchSize(template.getFetchSize());
jdbcTemplate.setMaxRows(template.getMaxRows());
if (template.getQueryTimeout() != null) {
jdbcTemplate.setQueryTimeout((int)template.getQueryTimeout().getSeconds());
}
return jdbcTemplate;
}
所以jdbcTemplate可以自动注入.
- 传入JdbcProperties ,那么在配置文件中可以修改其属性:
- XADataSourceAutoConfiguration 分布式事务的相关配置
配置数据库连接
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hyb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 15717747056HYb!
测试
@Slf4j
@SpringBootTest
class SpringBoot2SpringWebApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long aLong = jdbcTemplate.queryForObject("select count(*) from t_user", Long.class);
log.info("总数是{}",aLong);
}
}
Druid数据源
GitHub地址:https://github.com/alibaba/druid ,可以滑到最下面找中文版手册看讲解.
- 为什么我们可以配置额外的数据源?
因为在DataSourceConfiguration数据源自动配置类中,配置每个springboot内置的数据源的时候都会标上一个@ConditionalOnMissingBean({DataSource.class})注解,这个注解决定了当容器当中没有DataSource这个类型的组件的时候才会配置其内置的数据源.所以一旦我们配置了自己的数据源,这些内置的数据源就不会被配置.
手动配置
- 导入Druid依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
- 配置数据源:
/*
* 配置Druid数据源
* */
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource(){
return new DruidDataSource();
}
- 配置StatViewServlet
- 提供监控信息展示的html页面
- 提供监控信息的JSON API
注意:使用StatViewServlet,建议使用druid 0.2.6以上版本。
如果是纯Servlet编写,则在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>
这里我们也可以利用@Bean的方式注册StatViewServlet:
@Bean
public ServletRegistrationBean<StatViewServlet> statView(){
StatViewServlet statViewServlet = new StatViewServlet();
return new ServletRegistrationBean<>(statViewServlet,"/druid/*");
}
注意,如果在某些场景使用StatViewServlet无法生效,在1.2.5版本之后,提供了Filter的配置方式:
<filter>
<filter-name>DruidStatViewFilter</filter-name>
<filter-class>com.alibaba.druid.support.http.StatViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>DruidStatViewFilter</filter-name>
<url-pattern>/druid/*</url-pattern>
</filter-mapping>
所以,我们可以用@Bean的方式去注册一个Filter来替代StatViewServlet:
@Bean
public FilterRegistrationBean<StatViewFilter> statViewFilterFilter(){
StatViewFilter statViewFilter = new StatViewFilter();
FilterRegistrationBean<StatViewFilter> statViewFilterFilterRegistrationBean = new FilterRegistrationBean<>(statViewFilter);
statViewFilterFilterRegistrationBean.setUrlPatterns(Collections.singleton("/druid/*"));
return statViewFilterFilterRegistrationBean;
}
访问http://localhost:8080/druid/index.html可以到达请求监控页面:
- 监控页面配置好了,要想能监控sql执行的信息,需要开启监控统计功能:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
... ...
<property name="filters" value="stat" />
</bean>
如果使用ssm的方式,则是在DataSource这个bean中加上一个Filters的属性,那么我们利用@Bean添加DataSource的,所以要在该方法里设置:
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setFilters("stat");
return druidDataSource;
}
设置好后,可以模仿一个请求,该请求直接访问数据库:
@Autowired
JdbcTemplate jdbcTemplate;
@ResponseBody
@GetMapping("/sql")
public String query(){
Long aLong = jdbcTemplate.queryForObject("select count(*) from t_user", Long.class);
return "查询的数量是"+aLong;
}
一旦执行该请求后,在Druid Monitor的sql监控中,便会出现下图:
后面的[]里的数字代表执行时间分布:
- 刚才开启的是SQL监控功能,下面来开启监控Web应用功能:
<filter>
<filter-name>DruidWebStatFilter</filter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
<init-param>
<param-name>exclusions</param-name>
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DruidWebStatFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
从这里我们可以看到,我们要配置一个WebStatFilter,这里的拦截路径是/*,还要配出一些资源:
@Bean
public FilterRegistrationBean<WebStatFilter> webStatFilterFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> webStatFilterFilterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
webStatFilterFilterRegistrationBean.setUrlPatterns(Collections.singleton("/*"));
webStatFilterFilterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return webStatFilterFilterRegistrationBean;
}
测试:发送/sql请求,发现Web监控有变化,UR监控也有变化.
- 如何添加SQL防火墙:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
...
<property name="filters" value="wall"/>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
...
<property name="filters" value="wall,stat"/>
</bean>
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setFilters("stat,wall");
return druidDataSource;
}
像这样配置的功能还包含日志输入,慢SQL查询,版本迁移,数据库加密等等.
自动配置
- starter依赖为我们提供了很多自动化配置.
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
导入该依赖后,我们可以来到druid-spring-boot-starter-1.2.6.jar下的com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure 中:
@Import({DruidSpringAopConfiguration.class,
DruidStatViewServletConfiguration.class,
DruidWebStatFilterConfiguration.class,
DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {
}
DruidSpringAopConfiguration:监控Spring组件,其配置文件配置项可以深入该类可以知道是:spring.datasource.druid.aop-patterns
DruidStatViewServletConfiguration:监控页配置,配置项是spring.datasource.druid.stat-view-servlet.enabled,默认是false,不开启.
DruidWebStatFilterConfiguration:Web监控配置,配置项spring.datasource.druid.web-stat-filter.enabled默认不开启
DruidFilterConfiguration:Druid所有的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 = "spring.datasource.druid.filter.wall.config";
所有Filter在该配置类中以@Bean的方式生成,可一一对应查看配置项,例如:
@Bean
@ConfigurationProperties("spring.datasource.druid.filter.stat")
@ConditionalOnProperty(
prefix = "spring.datasource.druid.filter.stat",
name = {"enabled"}
)
@ConditionalOnMissingBean
public StatFilter statFilter() {
return new StatFilter();
}
下面根据底层进行一段配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hyb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 15717747056HYb!
druid:
stat-view-servlet:
enabled: true
login-username: admin
login-password: 123
# 禁用重置操作
reset-enable: false
web-stat-filter: #监控web
enabled: true
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
#对filters进行详细配置
filter:
stat:
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
aop-patterns: com.hyb.* #监控springbean
filters: stat,wall,slf4j #开启sql监控,防火墙,日志(日志要和start依赖导入的一致,不然就自己导入)
整合Mybatis
- 导入*-spring-boot-starter:
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
在mybatis-spring-boot-autoconfigure-2.1.4.jar->org.mybatis.spring.boot.autoconfigure->MybatisAutoConfiguration 为mybatis的自动配置类,该自动配置类省去了我们自动配置SqlSessionFactory,SqlSession,Mapper文件自动扫描等等:
SqlSessionFactory:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
}
SqlSession:
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}
AutoConfiguredMapperScannerRegistrar:只要我们在mybatis接口处标注@Mapper,mapper文件就会被自动扫描进来.
- 编写Mapper接口:
@Mapper
public interface UserMapper {
public Integer count();
}
该接口会被自动扫描.
- 指定全局配置文件和sql映射文件:
mybatis:
config-location: classpath:/mapper/mybatis-config.xml
mapper-locations: classpath:/mapper/*.xml
注意,指定了全局配置文件,如果mybatis-starter底层存在的组件就不用配置,但如果底层不存在,还是要配置,比如驼峰命名规则,mybatis-starter底层是没有自动配置的.
除了用config-location指定配置文件外,还可以直接写配置,不用指定配置文件:
mybatis:
# config-location: classpath:/mapper/mybatis-config.xml
mapper-locations: classpath:/mapper/*.xml
configuration: #指定mybatis全局配置文件
map-underscore-to-camel-case: true # 启动驼峰命名规则
所以整合mybatis:
- 导入官方starter
- 编写mapper接口,标注上@Mapper接口.
- 在yaml文件中指定全局配置文件的位置或者直接用mybatis.configuration配置项配置.
- 编写sql映射文件.
- 前面我们指定了sql映射文件,其实只要我们引入了starter的包,可以直接使用注解的方式而省去sql映射文件:
@Mapper
public interface UserMapper {
@Select("select count(*) from t_user")
public Integer count();
}
- 我们也可以使用混合模式:在Mapper接口中,使用注解注入sql语句,还可以多写一个映射文件映射到执行的接口.
- 多写一个映射文件主要是因为有些sql语句的映射信息有些复杂,但也可以使用注解去解决这个问题:
@Insert("insert into t_user(`name`,`age`,`email`) values(#{name},#{age},#{email})")
@Options(useGeneratedKeys = true,keyProperty = "id")
public void insert(tUser user);
@Options 就是为了在insert标签中加上其他属性.useGeneratedKeys表示使用自增的组件id,映射的属性是id这一栏,该设置可以让id绑定到tUser中作为返回.
- Mapper接口不用每个都写@Mapper,可以在启动类的上方进行统一Mapper文件的扫描:@MapperScan
整合MP
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
- MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。mybatis-plus:xxx 就是对mybatis-plus的定制
- SqlSessionFactory 自动配置好。底层是容器中默认的数据源
- mapperLocations 自动配置好的。有默认值。classpath:/mapper//*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下*
- 容器中也自动配置好了 SqlSessionTemplate
- @Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan(“com.atguigu.admin.mapper”) 批量扫描就行
整合Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
自动配置:
- RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx是对redis的配置
- 连接工厂是准备好的。LettuceConnectionConfiguration(默认)、JedisConnectionConfiguration
- 自动注入了RedisTemplate<Object, Object> : xxxTemplate;
- 自动注入了StringRedisTemplate;k:v都是String
- key:value
- 底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis
redis环境搭建
1、阿里云按量付费redis。经典网络
2、申请redis的公网连接地址
3、修改白名单 允许0.0.0.0/0 访问
4 使用完记得释放!
- 配置文件:
spring:
redis:
host: ip
port: 6379
password: lfy:Lfy123456
client-type: jedis #指定客户端类型,默认是Lettuce
jedis: #调节客户端线程池 (Lettuce)
pool:
max-active: 10
- 切换Jedis:
<!-- 导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>