2、Web开发
2.1、SpringMVC自动化配置概述
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上)
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
2.2、简单功能分析
2.2.1、静态资源访问
① 静态资源目录
静态资源目录:/static (or /public or /resources or /META-INF/resources)
访问:当前项目的根路径/ + 静态资源名
原理:静态映射 /**
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
修改默认的静态资源路径
spring: mvc: static-path-pattern: /resources/** web: resources: static-locations: [classpath:/haha/]
② 静态资源访问前缀
spring: mvc: static-path-pattern: "/resources/**"
访问:当前项目 + static-path-pattern + 静态资源名
③ webjar
自动映射 /webjars/**
<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.2.2、欢迎页支持
静态资源路径下 index.html
可以配置静态资源路径
但是不能配置静态资源的访问前缀。否则导致 index.html 页面不能被默认访问
spring: # mvc: # static-path-pattern: /resources/** web: resources: static-locations: [classpath:/haha/]
controller 处理 /index
2.2.3、自定义Favicon
favicon.ico放在静态资源目录下
2.2.4、静态资源配置原理
SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
@AutoConfiguration( after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class} ) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) @AutoConfigureOrder(-2147483638) public class WebMvcAutoConfiguration { }
给容器中配置了什么
@Configuration( proxyBeanMethods = false ) @Import({EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { }
配置文件的相关属性和xxx进行绑定
WebMvcProperties==spring.mvc
WebProperties==spring.web
① 资源处理的默认规则
public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); } else { this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (this.servletContext != null) { ServletContextResource resource = new ServletContextResource(this.servletContext, "/"); registration.addResourceLocations(new Resource[]{resource}); } }); } }
spring: web: resources: add-mappings: false # 禁用所有静态资源
@ConfigurationProperties("spring.web") public class WebProperties { public static class Resources { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"}; private String[] staticLocations; private boolean addMappings; private boolean customized; private final Chain chain; private final Cache cache; public Resources() { this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS; this.addMappings = true; this.customized = false; this.chain = new Chain(); this.cache = new Cache(); } } }
② 欢迎页的处理规则
@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; }
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) { if (welcomePage != null && "/**".equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage); this.setRootViewName("forward:index.html"); } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { logger.info("Adding welcome page template: index"); this.setRootViewName("index"); } }
③ favicon
2.3、请求参数处理
2.3.1、请求映射
① rest使用与原理
@xxxMapping;
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
以前:**/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
现在: /user *GET-*获取用户 *DELETE-*删除用户 *PUT-*修改用户 *POST-*保存用户
核心Filter;HiddenHttpMethodFilter
用法: 表单method=post,隐藏域 _method=put
SpringBoot中手动开启
扩展:如何把_method 这个名字换成我们自己喜欢的
@RequestMapping(value = "/user",method = RequestMethod.GET) public String getUser(){ return "GET-张三"; } @RequestMapping(value = "/user",method = RequestMethod.POST) public String saveUser(){ return "POST-张三"; } @RequestMapping(value = "/user",method = RequestMethod.PUT) public String putUser(){ return "PUT-张三"; } @RequestMapping(value = "/user",method = RequestMethod.DELETE) public String deleteUser(){ return "DELETE-张三"; }
@Bean @ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"} ) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }
自定义filter
@Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; } }
Rest原理(表单提交要使用REST的时候)
表单提交会带上**_method=PUT**
请求过来被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功能
spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能
② 请求映射原理
SpringMVC功能分析都从 org.springframework.web.servlet.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 { try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = this.checkMultipart(request); multipartRequestParsed = processedRequest != request; mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null) { this.noHandlerFound(processedRequest, response); return; } HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } this.applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException("Handler dispatch failed", var21); } this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); } catch (Exception var22) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22); } catch (Throwable var23) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23)); } } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else if (multipartRequestParsed) { this.cleanupMultipart(processedRequest); } } }
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
所有的请求映射都在HandlerMapping中
SpringBoot自动装配欢迎页的 WelcomePageHandlerMapping 。访问 / 能访问到index.html
SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
pringBoot自动配置了默认 的 RequestMappingHandlerMapping
如果有就找到这个请求对应的handler
如果没有就是下一个 HandlerMapping
我们需要一些自定义的映射处理,我们也可以自己给容器中放 HandlerMapping
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; }
2.3.2、普通参数与基本注解
① 注解
@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){ 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; } }
② 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); }
③ 复杂参数
Map、**Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、**Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据
request.getAttribute() 进行获取
Map、Model类型的参数,会返回 mavContainer.getModel()—> BindingAwareModelMap 是Model 也是Map
mavContainer.getModel(); 获取到值的
④ 自定义对象参数
@Data public class Person { private String userName; private Integer age; private Date birth; private Pet pet; }
@Data public class Pet { private String name; private Integer age; }
2.3.3、POJO封装过程
ServletModelAttributeMethodProcessor
2.3.4、参数处理原理
HandlerMapping 中找到能够处理请求的Handler (Controller.method())
为当前的 Handler 找到一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
适配器执行目标方法并确定方法参数的每一个值
① HandlerAdapter
0 — 支持方法上面标注 @RequestMapping
1 — 支持函数式编程
……
② 执行目标方法
//DispatcherServlet -- doDispatch mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法 //ServletInvocableHandlerMethod Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); //获取方法的参数值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
③ 参数解析器 HandlerMethodArgumentResolver
确定将要执行的目标方法的每一个参数的值是什么
SpringMVC目标方法能写多少种参数类型。取决于参数解析器
当前解析器是否支持解析这种参数
支持就才调用 resolveArgument
④ 返回值处理器
⑤ 如何确定目标方法每一个参数的值
// InvocableHandlerMethod 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 (logger.isDebugEnabled()) { String exMsg = var10.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw var10; } } } return args; } }
挨个判断所有参数解析器那个支持解析这个参数
@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; }
解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
自定义类型参数 封装POJO
ServletModelAttributeMethodProcessor 这个参数处理器支持
是否为简单类型
// ModelAttributeMethodProcessor public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()); }
// BeanUtils public static boolean isSimpleProperty(Class<?> type) { Assert.notNull(type, "'type' must not be null"); return isSimpleValueType(type) || type.isArray() && isSimpleValueType(type.getComponentType()); } public static boolean isSimpleValueType(Class<?> type) { return Void.class != type && Void.TYPE != type && (ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type); }
@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 = (ModelAttribute)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 { try { attribute = this.createAttribute(name, parameter, binderFactory, webRequest); } catch (BindException var10) { if (this.isBindExceptionRequired(parameter)) { throw var10; } if (parameter.getParameterType() == Optional.class) { attribute = Optional.empty(); } else { attribute = var10.getTarget(); } bindingResult = var10.getBindingResult(); } } if (bindingResult == null) { WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { if (!mavContainer.isBindingDisabled(name)) { this.bindRequestParameters(binder, webRequest); } this.validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); } } if (!parameter.getParameterType().isInstance(attribute)) { attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } bindingResult = binder.getBindingResult(); } Map<String, Object> bindingResultModel = bindingResult.getModel(); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); return attribute; }
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
byte – > file
未来我们可以给WebDataBinder里面放自己的Converter;
private static final class StringToNumber<T extends Number> implements Converter<String, T>
自定义Converter
@Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除分号后面的内容,矩阵变量才能生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter<String, Pet>() { @Override public Pet convert(String source) { if (!StringUtils.isEmpty(source)){ String[] split = source.split(","); Pet pet = new Pet(); pet.setName(split[0]); pet.setAge(Integer.parseInt(split[1])); return pet; } return null; } }); } }; }
⑥ 目标方法执行完成
将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据
⑦ 处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
// InternalResourceView protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { this.exposeModelAsRequestAttributes(model, request); this.exposeHelpers(request); String dispatcherPath = this.prepareForRendering(request, response); RequestDispatcher rd = this.getRequestDispatcher(request, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + this.getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } else { if (this.useInclude(request, response)) { response.setContentType(this.getContentType()); if (this.logger.isDebugEnabled()) { this.logger.debug("Including [" + this.getUrl() + "]"); } rd.include(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Forwarding to [" + this.getUrl() + "]"); } rd.forward(request, response); } } }
暴露模型作为请求域属性
exposeModelAsRequestAttributes(model, request);
// AbstractView protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception { model.forEach((name, value) -> { if (value != null) { request.setAttribute(name, value); } else { request.removeAttribute(name); } }); }
2.4、数据响应与内容协商
2.4.1、响应JSON
① jackson.jar + @ResponseBody
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- web场景自动引入了json场景 -->
给前端自动返回 json
a、返回值解析器
源码太难了……
b、返回值解析器原理
返回值处理器是否支持这种类型返回值 supportReturnType
返回值处理器调用 handleReturnValue 进行处理
RequestResponseBodyMethodProcessor 可以处理返回值标了 @Response 注解的
利用 MessageConverters 进行处理将数据写为json
服务器最终根据自己自身的能力,决定服务器能生产出什么样的内容类型的数据
SpringMVC 会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
得到 MappingJackson2HttpMessageConverter 可以将对象写为 json
利用 MappingJackson2HttpMessageConverter 将对象转为 json 再写出去
② SpringMVC到底支持那些返回值
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 —> RequestResponseBodyMethodProcessor;
③ HttpMessageConverter原理
a、MessageConverter规范
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person
b、默认的MessageConvverter
……
2.4.2、内容协商
根据客户端接收能力不同,返回不同媒体类型的数据
① 引入xml依赖
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
② postman分别测试返回json和xml
只需要改变请求头中方中的 Accept 字段,Http协议中规定的,告诉服务器本客户端可以接收的数据类型
③ 开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商协议
spring: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式
发请求:
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
确定客户端接收什么类型的内容类型
Parameter 策略优先确定是要返回 json 数据(获取请求头中的 format 的值)
最终进行内容协商返回给客户端 json 即可
④ 内容协商原理
判断当前响应头中是否有确定的媒体类型 MediaType
获取客户端(Postman、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】
contentNegotationMannager 内容协商管理器 默认使用基于请求头的策略
HeaderConventNegotationStrategy 确定客户端可以接受的内容类型
遍历循环所有当前系统的 MessageConverter 看谁支持操作这个对象(Person)
找到支持操作 Person 的converter 把 converter 支持的媒体类型统计出来
客户端需要【application/xml】服务器能力【10种,json、xml】
进行内容协商的最佳匹配媒体类型
用支持将对象转换为最佳媒体类型的 converter,调用它进行转换
⑤ 自定义MessageConverter
实现多协议数据兼容 json、xml、x-jingchao
@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor
Processor 处理方法返回值 通过 MessageConverter 处理
所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
内容协商找到最终的 messageConverter
SpringMVC 的什么功能 一个入口给容器中添加一个 WebMvcConfigurer
@Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new JingChaoMessageConverter()); } } }
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】
2.5、视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染
2.5.1、视图解析
① 视图解析原理流程
目标方法处理的过程中,所有的数据都会被放在 ModelAndViewContainer 里面,包括数据和视图地址
方法的参数是一个自定义类型对象(从请求参数中确定的)。把他重新放在 ModelAndViewContainer
任何目标方法执行完成以后都会返回 ModelAndVIew(数据和视图地址)
processDispatchResult 处理派发结果(页面该如何响应)
1.
2.5.2、模板引擎-Thymeleaf
① thymeleaf简介
现代化、服务端 Java 模板引擎
② 基本语法
表达式
名字 | 语法 | 描述 |
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
字面量
文本值:‘one text’,‘Another one!’,……
数字:0,44,3.14,……
布尔值:false、true
空值:null
变量:one,two,……,变量不能有空格
文本操作
字符串拼接:+
变量替换**|the name is ${name}|**
数学运算
运算符:+、-、*、/、%
布尔运算
运算符:and、or
一元运算:!、not
比较运算
比较:>、<、>=、<=(gt、lt、ge、le)
等式:==、!=(eq、ne)
条件运算
if-then:(if) ? (then)
if-then-else:(if) ? (then) : (else)
Default:(value) ?: (defaultvalue)
特殊运算
无操作:_
③ 设置属性值
设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}"> <fieldset> <input type="text" name="email" /> <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/> </fieldset> </form>
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
④ 迭代
<tr th:each="prod : ${prods}"> <td th:text="${prod.name}">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>
<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>
⑥ 属性优先级
2.5.3、thymeleaf 使用
① 引入 starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
② 自动配置好了 thymeleaf
@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class }) @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration { }
自动配置的策略
1.所有 thyemleaf 的配置值都在 ThymeleafProperties
2.配置好了 SpringTemplateEngine
3.配好了 ThymeleafViewResolver
4.我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
③ 页面开发
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Hello</title> </head> <body> <h1 th:text="${msg}">Hello页面</h1> <a href="www.baidu.com" th:href="${link}">去百度</a><br> </body> </html>
2.5.4、构建后台管理系统
① 创建项目
thymeleaf、web-starter、devtools、lombok
② 静态资源管理
自动配置好,将静态资源放到 static 文件夹xia
③ 路径构建
th:action=“@{/login}”
④ 模板抽取
th:insert/replace/include
⑤ 页面跳转
@PostMapping("/login") public String main(User user, HttpSession session, Model model){ if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){ //把登陆成功的用户保存起来 session.setAttribute("loginUser",user); //登录成功重定向到main.html; 重定向防止表单重复提交 return "redirect:/main.html"; }else { model.addAttribute("msg","账号密码错误"); //回到登录页面 return "login"; } }
⑥ 数据渲染
@GetMapping("/dynamic_table") public String dynamic_table(Model model){ //表格内容的遍历 List<User> users = Arrays.asList(new User("zhangsan", "123456"), new User("lisi", "123444"), new User("haha", "aaaaa"), new User("hehe ", "aaddd")); model.addAttribute("users",users); return "table/dynamic_table"; }
<table class="display table table-bordered" id="hidden-table-info"> <thead> <tr> <th>#</th> <th>用户名</th> <th>密码</th> </tr> </thead> <tbody> <tr class="gradeX" th:each="user,stats:${users}"> <td th:text="${stats.count}">Trident</td> <td th:text="${user.userName}">Internet</td> <td >[[${user.password}]]</td> </tr> </tbody> </table>
2.6、拦截器
2.6.1、HandlerInterceptor 接口
/** * 登录检查 * 1、配置好拦截器要拦截那些请求 * 2、把这些配置放在容器中 */ @Slf4j public class LoginInterceptor implements HandlerInterceptor { /** * 目标方法执行之前 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); log.info("preHandle拦截的路径为:"+ requestURI); // 登录检查逻辑 HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser"); if (loginUser != null){ return true; } /* session.setAttribute("msg", "请先登录!"); response.sendRedirect("/"); */ request.setAttribute("msg","请先登录!"); request.getRequestDispatcher("/").forward(request, response); return false; } /** * 目标方法执行完成以后 * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("postHandle执行"+ modelAndView); } /** * 页面渲染以后 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("afterCompletion执行异常"+ ex); } }
2.6.2、配置拦截器
@Configuration public class AdminWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) //所有资源都会被拦截,包括静态资源 .addPathPatterns("/**") // 放行资源 .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); } }
2.6.3、拦截器原理
根据当前请求,找到 HandlerExecutionChain 【可以处理请求的 handler 以及 handler 的所有拦截器】
先来顺序执行 所有拦截器的 preHandle 方法
如果当前拦截器 prehandler返回为 ture,则执行下一个拦截器的 preHandle
如果当前拦截器返回为 false,直接倒序执行所有以及执行了的拦截器的 afterCompletion
如果任何一个拦截器返回 false。直接跳出不执行目标方法
所有拦截器都返回 ture,执行目标方法
倒序执行所有拦截器的 postHandle 方法
前面的步骤有任何异常都会直接倒序触发 afterCompletion
页面成功渲染完成以后,也会倒序触发 afterCompletion
2.7、文件上传
2.7.1、页面表单
<form method="post" action="/upload" enctype="multipart/form-data"> <input type="file" name="file"><br> <input type="submit" value="提交"> </form>
2.7.2、文件上传代码
/** * MultipartFile 自动封装上传过来的文件 * @param email * @param username * @param headerImg * @param photos * @return */ @PostMapping("/upload") public String upload(@RequestParam("email") String email, @RequestParam("username") String username, @RequestPart("headerImg") MultipartFile headerImg, @RequestPart("photos") MultipartFile[] photos) throws IOException { log.info("上传消息: email={}, username={}, headerImg={}, photos={}", email, username, headerImg.getSize(),photos.length); if(!headerImg.isEmpty()){ // 保存到文件服务器 String filename = headerImg.getOriginalFilename(); headerImg.transferTo(new File("E:\\jingchao\\"+filename)); } if (photos.length > 0){ for (MultipartFile photo : photos) { if (!photo.isEmpty()){ String photoOriginalFilename = photo.getOriginalFilename(); photo.transferTo(new File("E:\\jingchao\\"+photoOriginalFilename)); } } } return "index"; }
2.7.3、自动配置原理
文件上传自动配置类 MultipartAutoConfiguration MultipartProperties
自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
原理步骤
请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
参数解析器来解析请求中的文件内容封装成MultipartFile
将request中文件信息封装为一个 Map MultiValueMap<String, MultipartFile>
FileCopyUtils 实现文件流的拷贝
2.8、异常处理
2.8.1、错误处理
① 默认规则
默认情况下,Spring Boot提供 /error 处理所有错误的映射
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“whitelabel”错误视图,以HTML格式呈现相同的数据
要对其进行自定义,添加 View 解析为 error
要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件 以实现现有机制但替换其内容
error/ 下的 4xx,5xx 页面会被自动解析;
② 定制错误处理逻辑
自定义错误页
error/404.html error/5xx.html 有精确的错误状态码页面就匹配精确,没有就找4xx.html,如果都没有就触发白页
@ControllerAdvice + @ExceptionHandler 处理全局异常,底层是 ExceptionHandlerExeptionResolver 支持的
@ResponseStatus + 自定义异常,底层是 ResponseStatusExceptionResolver,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolverReason), tomcat发送的 /error
Spring 底层的异常,如参数类型转换异常,DefaultHandlerExceptionResolver 处理框架底层的异常
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
自定义实现 HandlerExceptionResolver 处理异常,可以作为默认的全局异常处理规则
ErrorViewResolver 实现自定义处理异常
response.sendError error 请求就会转给 controller
你的异常没有任何人能处理,tomcat底层 response.sendError error请求就会转给controller
basicErrorController 要去的页面地址是 ErrorViewController
③ 异常处理自动配置原理
ErrorMvcAutoConfiguration 自动配置异常处理规则
容器中的组件:类型:DeafultErrorAttributes —> id:errorAttributes
public class DefaultErrorAttributes implements ErrorAttribubtes, HandlerExceptionResolver
DefaultErrorAttributes: 定义错误页面中包含那些数据
容器中的组件:类型:BasicErrorController —> id: basicErrorController(json + 白页 适配响应)
容器中有组件 View —> id 是 error(响应默认错误页)
容器中放组件 BeanNameViewResolver (视图解析器);按照返回的视图名作为组件的 id 去容器中找到 View 对象
容器中的组件:类型:DefaultErrorViewResolver —> id: conventionErrorViewResolver
如果发生错误,会以 HTTP 的状态码 作为视图页地址(viewName)找到真正的页面
error/404、5xx.html
如果想要返回页面,就会找 error 视图【StaticView】,默认是一个白页
④ 异常处理步骤流程
执行目标方法,目标方法运行期间有任何异常都会被 catch、而且标志当前请求结束;并且用 dispatchException
进入视图解析流程 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
mv = processHandlerException;处理 handler 发生的异常, 处理完成后返回 ModelAndVIew
遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
系统默认的 异常解析器
DefaultErrorAttributes 先来处理异常,把异常信息保存到 request 域,并返回 null
默认没有任何人能处理异常,所以异常将会被抛出
如果没有任何人能处理最终底层就会 发送 /error 请求。会被底层的 BasicErrorController 处理
解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析
默认的 DefaultErrorViewResolver,作用是把响应状态码作为错误页的地址,error/500.html
模板引擎最终响应这个页面 error/500.html
2.9、Web原生组件注入(Servlet、Filter、Listener)
2.9.1、使用Servlet API
指定原生 Servlet 组件放置的位置
@ServletComponentScan(basePackages = "com.jingchao.admin")
使用
@WebServlet(urlPatterns = "/my") public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("666666666666"); } }
直接响应,没有经过 Spring 的拦截器
解决办法
@WebFilter(urlPatterns = {"/css/*","/images/*"}) public class MyFilter implements Filter {}
@WebListener public class MyServletContextListener implements ServletContextListener {}
2.9.2、使用RegistrationBean
ServletRegistrationBean, FilterRegistrationBean, ServletListenerRegistrationBean
@Configuration public class MyRegistryConfig { @Bean public ServletRegistrationBean myServlet(){ MyServlet myServlet = new MyServlet(); return new ServletRegistrationBean(myServlet, "/my","/my1"); } @Bean public FilterRegistrationBean myFilter(){ MyFilter myFilter = new MyFilter(); // return new FilterRegistrationBean(myFilter, myServlet()); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/images/*")); return filterRegistrationBean; } @Bean public ServletListenerRegistrationBean myListener(){ MyServletContextListener myServletContextListener = new MyServletContextListener(); return new ServletListenerRegistrationBean(myServletContextListener); } }
2.10、嵌入式Servlet容器
2.10.1、切换嵌入式Servlet容器
默认支持的 webServer
Tomcat、Jetty、Undertow
ServletWebServerApplicationContext 容器自动寻找 ServletWebServerFactory 并引导创建服务器
切换服务器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>
原理
SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂—> Servlet 的web服务器)
SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize—this.tomcat.start()
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
2.10.2、定制Servlet容器
实现
WebServerFactoryCustomizer
把配置文件的值和 ServletWebServerFactory 进行绑定
修改配置文件 server.xxx
直接自定义 ConfigurableServletWebServerFactory
xxxCustomizer:定制化器,可以改变 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> { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); } }
2.11、定制化原理
2.11.1、定制化的常见方式
修改配置文件
xxxxCustomizer
编写自定义组件的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;视图解析器
Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件
@Configuration public class AdminWebConfig implements WebMvcConfigurer
@EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能
原理
WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…
一旦使用 @EnableWebMvc 会 @Import(DelegatingWebMvcConfiguration.class)
DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
2.11.2、原理分析的套路
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项