#
1、SpringMVC自动配置概览
在 Spring Boot Reference Documentation —》Web 中
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- 内容协商视图解析器和BeanName视图解析器
- Support for serving static resources, including support for WebJars (covered later in this document)).
- 静态资源(包括webjars)
- Automatic registration of
Converter
,GenericConverter
, andFormatter
beans.- 自动注册
Converter,GenericConverter,Formatter
- 自动注册
- Support for
HttpMessageConverters
(covered later in this document).- 支持
HttpMessageConverters
(后来我们配合内容协商理解原理)
- 支持
- Automatic registration of
MessageCodesResolver
(covered later in this document).- 自动注册
MessageCodesResolver
(国际化用)
- 自动注册
- Static
index.html
support.- 静态index.html 页支持
- Custom
Favicon
support (covered later in this document).- 自定义
**Favicon(小图标)**
- 自定义
- Automatic use of a
ConfigurableWebBindingInitializer
bean (covered later in this document).- 自动使用
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上)
- 自动使用
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 typeWebMvcConfigurer
but without@EnableWebMvc
. 不用@EnableWebMvc注解。使用**@Configuration**
+**WebMvcConfigurer**
自定义规则If you want to provide custom instances of
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrations
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
-annotatedDelegatingWebMvcConfiguration
as described in the Javadoc of@EnableWebMvc
. 使用**@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC**
2、简单功能分析
2.1、静态资源访问
1、静态资源目录
只要静态资源放在类路径(class:path)下: called /static
(or /public
or /resources
or /META-INF/resources
访问 : 当前项目根路径/ + 静态资源名
原理: 静态映射/**。
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
spring:
mvc:
static-path-pattern: /res/**
# 配置改变默认的静态资源路径
resources:
static-locations: [classpath:/haha/]
2、静态资源访问前缀
为了让拦截时能区分出静态资源和动态资源,所以一般在静态资源前面加个前缀,拦截器在看到指定前缀时就放行,从而达到动态静态分开的目的。
默认无前缀
static-path-patten是虚拟的路径 不是真实存在的
spring:
mvc:
static-path-pattern: /res/**
当前项目 + static-path-pattern + 静态资源名 = 默认从静态资源文件夹下找
3、webjar
webjar相当于把一些库资源打包,相当于JS的库
自动映射 /webjars/**
https://www.webjars.org/
<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、欢迎页支持
- 静态资源路径下 index.html
- 可以配置静态资源路径
- 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
resources:
static-locations: [classpath:/haha/]
- controller能处理/index
- controller中配置@RequestMapping(“/“) return 返回的页面为欢迎页
2.3、自定义 Favicon
favicon.ico 放在静态资源目录下即可。
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
2.4、静态资源配置原理
- 1.SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
- 2.SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
- 给容器中配了什么。
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
- 配置文件的相关属性和xxx进行了绑定。WebMvcProperties==spring.mvc、ResourceProperties==spring.resources
1、配置类只有一个有参构造器
配置类的有参构造器的方法参数,默认从容器中寻找组件来注入
//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath
//ServletRegistrationBean 给应用注册Servlet、Filter....
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
2、资源处理的默认规则
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) { // 是否禁用所有静态资源规则
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); // .getPeriod();静态资源存放的时间
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
//webjars的规则
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
//
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
// resourceProperties.getStaticLocations()对应静态资源映射的默认位置
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
spring:
# mvc:
# static-path-pattern: /res/**
resources:
add-mappings: false 禁用所有静态资源规则
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
3、欢迎页的处理规则
HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
// 已经在底层写死了,因此对于欢迎页不可以指定映射前缀
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
//要用欢迎页功能,必须是/**
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 调用Controller /index
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
4、favicon
由于favicon.ico图标是由浏览器自动发送请求/favicon.ico获取并保存在session域中。因此,如果我们在配置文件中设置了静态资源访问前缀,那么浏览器发送的/favicon.ico由于不符合访问前缀要求,就会获取不到相对应的图标了(图标也是静态资源的一种)。
3、请求参数处理
0、请求映射
1、rest使用与原理
- @xxxMapping;@GetMapping();@PostMapping();@PutMapping();@DeleteMapping(); @PatchMapping,它是对Put的补充,区别是Patch是部分更新,Put是全部更新,这些注解都是Spring4.3引入的。等价于 @RequestMapping(method = RequestMethod. XXXX)
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
- 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
核心Filter;HiddenHttpMethodFilter
- 实现其他请求用法: 表单method=post,隐藏域 _method=xxx
- 但是需要在SpringBoot中手动开启,开启配置yaml在下面
WebMvcAutoConfiguration中
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)// 需要手动配置为true
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
- 实现其他请求用法: 表单method=post,隐藏域 _method=xxx
扩展:如何把_method 这个名字换成我们自己喜欢的。自定义filter
@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-张三";
}
自己写一个config class 如 Myconfig 然后写上重定义filter
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
Rest原理(表单提交要使用REST的时候)
- 表单提交会带上_method=PUT
- 请求过来被HiddenHttpMethodFilter拦截 org.springframework.web.filter.HiddenHttpMethodFilter
- 请求是否正常,并且是POST
- 获取到_method的值。
- 兼容以下请求;PUT.DELETE.PATCH
- 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
- 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
- 请求是否正常,并且是POST
Rest使用客户端工具,
- 如PostMan直接发送Put、delete等方式请求,无需Filter。
- 也就是如果只是当做提供接口,此时只传输json数据,无需配置这一项!
- 自己用别人开发好的接口时需要加上配置
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能
2、请求映射原理
doGet(HttpServletBean中) 调用-> processRequest(FrameworkServlet) 调用-> doService (调用 DispatchServlet中的doService实现 ) doService中每个请求都会调用->doDispatch
也就是说SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet-》doDispatch()
doDispatch遍历HandleMapping匹配handle(Controlle)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// getHandler遍历HandlerMapping找到当前请求使用哪个Handler(Controller的方法)处理 通过HandlerMapping
mappedHandler = getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx 保存有所有的路径和controller的映射
当前存在的HandlerMapping
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。 是在应用启动时保存的 2.5.3 是 pathLookup
所有的请求映射都在各个HandlerMapping中。
- SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
- SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
遍历查找handler方法:请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
@PathVariable(用于获取路径变量)、@RequestHeader(用于请求头)、@RequestParam(获取请求参数 get等)、@MatrixVariable(矩阵变量)、@CookieValue(获取Cookie值)、@RequestBody (获取请求体[POST])、@RequestAttribute(获取request域属性)、@RequestPart主要用来处理content-type为multipart/form-data或multipart/mixed stream发起的请求,可以获取请求中的参数,包括普通文本、文件或复杂对象比如JSON、XML等,针对复杂对象,需要明确对应的content-type。
PathVariable、RequestHeader、RequestParam、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, // 用来存放路径变量到的所有的键值对 必须用String,String
@RequestHeader("User-Agent") String userAgent, // 用于请求头中的User-Agent
@RequestHeader Map<String,String> header, // 用来存放Header所有的键值对 必须用String,String
@RequestParam("age") Integer age, // 获取age
@RequestParam("inters") List<String> inters, //获取集合
@RequestParam Map<String,String> params, // 用来存放请求参数所有的键值对 必须用String,String
@CookieValue("_ga") String _ga, // 获取Cookie _ga
@CookieValue("_ga") Cookie cookie){ // // 获取Cookie _ga 对象
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;
}
@RequestAttribute
@Controller // 普通Controller做跳转,不是返回json的接口
public class RequestController {
@GetMapping("/goto")
// HttpServletRequest request 为传入的request数据
public String goToPage(HttpServletRequest request){
// 添加信息
request.setAttribute("msg","写入Attribute");
request.setAttribute("code",200);
// 跳转到/success
return "forward:/success";
}
@ResponseBody // 返回json数据
@GetMapping("/success")
// @RequestAttribute("msg") 接受request中的msg
// HttpServletRequest request 为传入的request数据
public Map success(@RequestAttribute("msg") String msg,
@RequestAttribute("code") Integer code,
HttpServletRequest request){
// 获取request中的msg
Object msg1 = request.getAttribute("msg");
Map<String, Object> map = new HashMap<>();
map.put("msg1", msg1);
map.put("msg2", msg);
return map;
}
}
@MatrixVariable
1、SpringBoot默认是禁用了矩阵变量的功能 需要手动开启
2、矩阵变量应当绑定在路径变量中
若是有多个矩阵变量,应当使用英文符号 ; 进行分割
若是一个矩阵变量有多个值,应当用英文符号 , 进行分割或者命名多个key即可
/car/{path}?xxx=xxx&aaa=ccc 是queryString 查询字符串 用 @ReqeustParam 获取
/car/{path;low=34;brand=byd,audi,yd} 是矩阵变量 用 @MatrixVariable 获取 第一个分号前是网络路径
3、使用场景示例
页面开发,cookie被禁用了,session里面的内容怎么使用;
什么是Session(会话):
服务器为了保存用户状态而创建的一个特殊的对象。
当浏览器第一次访问服务器时,服务器创建一个session对象(该
对象有一个唯一的id,一般称之为sessionId),服务器会将sessionId
以cookie的方式发送给浏览器。
当浏览器再次访问服务器时,会将sessionId发送过来,服务器依据
sessionId就可以找到对应的session对象。
用于在 无连接(HTTP)协议基础之上实现在用户状态管理。
正常session使用过程:设定session session.set(a, b),生成一个jsessionid存在cookie中,每次发请求都带上cookie中的jsessionid从服务端获取对应的session数据
cookie被禁:
url重写:/abc;jsessionid = xxxx 把cookie的值使用矩阵变量的方式进行传递
//1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用了矩阵变量的功能
//路径的处理原理。对于路径的处理。UrlPathHelper进行解析。
// UrlPathHelper 中 默认开启removeSemicolonContent(移除分号内容)
//3、 手动开启:在自定义配置类中重新,关闭removeSemicolonContent, 有两种方法
//4、矩阵变量必须有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 分号前是网络路径,分号后是矩阵变量
// 多个路径变量时,指定 pathVar 避免混乱
@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;
}
}
1.2、Servlet API:
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
ServletRequestMethodArgumentResolver 可以解析以上的部分参数
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
1.3、复杂参数:
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里面的数据会被放在request的请求域 request.setAttribute
Map、Model类型的参数,会返回 mavContainer.getModel();getModel 获得一个BindingAwareModelMap 是Model 也是Map Map和Model的底层都是同一个对象
mavContainer.getModel(); 获取到值的
map和model是一个对象,因为id都一样 都是5845
将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据。
1.4、自定义对象参数:
可以自动类型转换与格式化,可以级联封装。
/** 希望将用户提交的东西直接封装成对象
* 用户提交:
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
result
2、POJO封装过程
- ServletModelAttributeMethodProcessor
3、参数处理原理
- HandlerMapping中找到能处理请求的Handler(Controller.method())
- 为当前Handler 找一个适配器 HandlerAdapter; 如RequestMappingHandlerAdapter
- 适配器执行目标方法并确定方法参数的每一个值
1、HandlerAdapter
0 - 支持方法上标注@RequestMapping
1 - 支持函数式编程的
3 - 访问静态资源
xxxxxx
2、执行目标方法
// Actually invoke the handler.
//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);
3、参数解析器-HandlerMethodArgumentResolver
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
即参数解析器对入参进行解析,能否解析取决于参数解析器的种类是否支持入参
参数解析器接口中结构
- 判断当前解析器是否支持解析这种参数
- 支持就调用 resolveArgument
4、返回值处理器
5、如何确定目标方法每一个参数的值
============InvocableHandlerMethod==========================
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
5.1、挨个判断所有参数解析器那个支持解析这个参数
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
5.2、解析这个参数的值
为什么能够支持那么多参数类型,就是对这个接口的实现有很多种,每一种针对不同的参数类型(注解)进行支持、实现,然后解析。HandlerMethodArgumentResolver 接口
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
5.3、自定义类型参数 封装POJO
ServletModelAttributeMethodProcessor 这个参数处理器支持
是否为简单类型。
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != 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));
}
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
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
@FunctionalInterface public interface Converter
未来我们可以给WebDataBinder里面放自己的Converter;
private static final class StringToNumber
自定义 Converter示例
前端代码:
将宠物的字符串成功转换为Pet对象
重写config中的convert代码
//1、WebMvcConfigurer定制化SpringMVC的功能
@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>() {// 添加一个Converter 将String转为Pet
@Override
public Pet convert(String source) {
// 啊猫,3
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
};
}
6、目标方法执行完成
将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据。
7、处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
InternalResourceView:
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
rd.forward(request, response);
}
}
暴露模型作为请求域属性
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
下面为exposeModelAsRequestAttributes的核心方法
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {
//model中的所有数据遍历挨个放在请求域中
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}
4、数据响应与内容协商
1、响应JSON
1.1、jackson.jar+@ResponseBody
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
web场景自动引入了json场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
给前端自动返回json数据;
1、返回值解析器
利用返回值解析器处理
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
2、返回值解析器原理
- 1、返回值handler判断是否支持这种类型返回值 supportsReturnType
- 2、如果支持,返回值handler调用 handleReturnValue 进行处理,遍历查找合适的处理程序Processor
3、RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
RequestResponseBodyMethodProcessor
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
// 使用消息转换器进行写出操作
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
1. 利用 MessageConverters 进行处理 将数据写为json
- 1、处理内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型) accept里边的类型
- 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
- 3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
- 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
- 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
1.2、SpringMVC到底支持哪些返回值
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
标注@ResponseBody 注解的会利用 RequestResponseBodyMethodProcessor;处理返回json
1.3、HTTPMessageConverter原理
1、MessageConverter规范
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person
2、默认的MessageConverter
0 - 只支持Byte类型的
1 - String
2 - String 并不是重复了,里面字符集不一样,第一个是转UTF-8 第二个是转ISO
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class _StreamSource._class Source.class
6 - MultiValueMap
7 - true 无论什么类都返回true,换句话说,就是可以将任何类型的对象转换为浏览器所想要的数据类型;
8 - true
9 - 支持注解方式xml处理的。
最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)
2、内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
1、引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2、postman分别测试返回json和xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
3、开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能。
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
发请求: http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
确定客户端接收什么样的内容类型;
1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
2、最终进行内容协商返回给客户端json即可。
4、内容协商原理
- 1、判断当前响应头中是否已经有确定的媒体类型。MediaType
- 2、如果没有确定的媒体类型,获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】
- contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
- HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
- 3、遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
- 4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。
- 5、客户端需要【application/xml】。服务端能力【10种、json、xml】
- 6、进行内容协商的最佳匹配媒体类型
- 7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
导入了jackson处理xml的包,xml的converter就会自动进来
WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
5、自定义 MessageConverter
实现多协议数据兼容。json、xml、x-guigu
spring:
mvc:
contentnegotiation:
media-types:
gg: application/x-guigu
favor-parameter: true
0、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
1、Processor 处理方法返回值。通过 MessageConverter 处理
2、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter;
自定义Converter,重写各种方法
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());//添加扩展Converter
}
}
}
基于参数的内容协商策略默认只支持下面两种 :localhost:8080?format=json
可重写配置
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】
spring.mvc.contentnegotiation.media-types.* 应该配置文件的是这个属性
5、视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
1、视图解析
1、视图解析原理流程
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址 (通过ViewNameMethodReturnValueHandler这个handler进行处理,这个handler判断返回值是void或者是字符序列。如果是字符序列会转为String,然后将返回值放进mav中)
2、方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
3、任何目标方法执行完成以后都会返回 ModelAndView 对象(存在数据和视图地址)。
4、processDispatchResult 处理派发结果(确定页面改如何响应)
- 1、render(mv, request, response); 进行页面渲染逻辑
- 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
- 1、遍历所有的视图解析器尝试是否能根据当前返回值得到View对象,也就是找到合适的视图解析器(用于得当对应的View()对象)
- 2、 视图解析器 根据 redirect:/main.html —> 得到了 RedirectView(Thymeleaf new RedirectView() ) 对象
- 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
- 4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
- RedirectView 如何渲染【重定向到一个页面】
- 1、获取目标url地址
- 2、response.sendRedirect(encodedURL);
- RedirectView 如何渲染【重定向到一个页面】
- 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
视图解析:
- 返回值以 forward: 开始: new InternalResourceView(forwardUrl); —> 转发request.getRequestDispatcher(path).forward(request, response);
- 返回值以 redirect: 开始: new RedirectView() —》 render就是重定向
- 返回值是普通字符串: new ThymeleafView()—->
2、模板引擎-Thymeleaf
1、thymeleaf简介
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎
2、基本语法
1、表达式
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
2、字面量
文本值: ‘one text’ , ‘Another one!’ ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false
空值: null
变量: one,two,…. 变量不能有空格
3、文本操作
字符串拼接: +
变量替换: |The name is ${name}|
4、数学运算
运算符: + , - , * , / , %
5、布尔运算
运算符: and , or
一元运算: ! , not
6、比较运算
比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )
7、条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
8、特殊操作
无操作: _
3、设置属性值-th:attr
设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
设置多个值
<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
4、迭代
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
5、条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
6、属性优先级
3、thymeleaf使用
1、引入Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、自动配置好了thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }
自动配好的策略
- 1、所有thymeleaf的配置值都在 ThymeleafProperties
- 2、配置好了 SpringTemplateEngine
- 3、配好了 ThymeleafViewResolver
- 4、我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
3、页面开发
xmlns:th=”http://www.thymeleaf.org” 这声明thymeleaf的方式
yaml配置服务器访问路径为world,也就是说所有的请求必须在world下,如localhost:8080/hello.html变为localhost:8080/world/hello.html
controller:因为thtmeleaf配置中已经给定了路径和后缀,所以不需要加路径和后缀,直接success就可以找到templates文件夹下的success.html跳转
view:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<a href="www.atguigu.com" th:href="${link}">去百度</a> <br/>
<a href="www.atguigu.com" th:href="@{link}">去百度2</a>
</h2>
</body>
</html>
4、构建后台管理系统
1、项目创建
thymeleaf、web-starter、devtools、lombok
2、静态资源处理
自动配置好,我们只需要把所有静态资源放到 static 文件夹下
3、路径构建
th:action=”@{/login}”
4、模板抽取
使用thymeleaf动态链接css和js资源,避免后期升级修改html代码
th:insert/replace/include
区别:
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
// 引入
<body>
...
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
</body>
// 结果:
<body>
...
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
5、页面跳转
login.html:
<!-- 跳转action thymeleaf实现 -->
<form class="form-signin" action="main.html" method="post" th:action="@{/login}">
<div class="form-signin-heading text-center">
<h1 class="sign-title">Sign In</h1>
<img src="images/login-logo.png" alt=""/>
</div>
<div class="login-wrap">
<!--model提示信息-->
<label style="color: #b94a48" th:text="${msg}"></label>
<!--name为User对应的属性-->
<input type="text" name="userName" class="form-control" placeholder="User ID" autofocus>
<input type="password" name="password" class="form-control" placeholder="Password">
<button class="btn btn-lg btn-login btn-block" type="submit">
<i class="fa fa-check"></i>
</button>
main.html:
通过thymeleaf显示登录名
<a href="#" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<img src="images/photos/user-avatar.png" alt="" />
[[${session.loginUser.userName}]]! <!---->
<span class="caret"></span>
</a>
controller:
package com.gongwf.admin.controller;
import com.gongwf.admin.bean.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.HttpSession;
@Controller
public class IndexController {
/**
* 来到登录页
* @return
*/
@GetMapping(value = {"/", "/login"})
public String loginPage(){
return "login";
}
/**
* 重定向防止表单重复提交
* 登录成功,重定向到main页面,防止直接转发到main页面,在该页面刷新需要重新提交表单
* 如果直接转发到main,地址栏地址为login.com:8080/login,那么刷新还是请求当前这个方法,需要提交表单
* 如果重定向到”main.html“, 地址栏地址为login.com:8080/main.html,刷新时请求的时main.html的方法,不需要提交表单
* main.html可以随意起名
*/
@PostMapping("login")
public String main(User user, HttpSession session, Model model){
if(!StringUtils.isEmpty(user.getUserName()) && "123456".equals(user.getPassword())){
session.setAttribute("loginUser", user);
return "redirect:main.html";
}else {
model.addAttribute("msg","账户密码错误");
return "login";
}
}
/**
* 去main页面
* @return
*/
@GetMapping("main.html")
public String mainPage(HttpSession session, Model model){
// 是否登录 最好是拦截器/过滤器
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "main";
}else {
model.addAttribute("msg", "请重新登录");
return "login";
}
}
}
6、数据渲染
@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>
6、拦截器
1、HandlerInterceptor 接口
这个handler指的是controller的方法
/**
* 登录检查
* 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;
}
//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");
// 跳转时携带请求域数据
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、配置拦截器
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors),也就是配置拦截器
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override //重写
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求 需要放行静态资源
// 放行静态资源也可以在yaml中配置static路径
}
}
3、拦截器原理
preHandle
调用时间:Controller方法处理之前
执行顺序:链式Intercepter情况下,Intercepter按照声明的顺序一个接一个执行
若返回false,则中断执行,注意:不会进入afterCompletion
postHandle
调用前提:preHandle返回true
调用时间:Controller方法处理完之后,DispatcherServlet进行视图的渲染之前,也就是说在这个方法中你可以对ModelAndView进行操作
执行顺序:链式Intercepter情况下,Intercepter按照声明的顺序倒着执行。
备注:postHandle虽然post打头,但post、get方法都能处理
afterCompletion
调用前提:preHandle返回true
调用时间:DispatcherServlet进行视图的渲染之后
多用于清理资源
1、根据当前请求 mappedHandler = getHandler(processedRequest);,找到HandlerExecutionChain中【可以处理请求的handler以及handler的所有 拦截器】
2、先来**所有拦截器的 preHandle方法
- 1、如果当前拦截器prehandler返回为true(执行成功)。则执行下一个拦截器的preHandle
- 2、如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、所有拦截器都返回True。执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接触发所有已经执行了的拦截器的倒序 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
7、文件上传
1、页面表单
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="headImg" multiple><br> // multiple 表示是多文件上传
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>
2、文件上传代码
修改配置文件,对于上传文件大小的限制
文件对象类型: MultipartFile
spring:
servlet:
multipart:
max-file-size: 10MB # 单个上传文件大小上线
max-request-size: 100MB # 一次请求上传所有文件大小上线
/**
* 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()){
//保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename(); //获取原始文件名
headerImg.transferTo(new File("H:\\cache\\"+originalFilename)); // 保存
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();//获取原始文件名
photo.transferTo(new File("H:\\cache\\"+originalFilename)); // 保存
}
}
}
return "main";
}
3、自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
- 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
- 原理步骤
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成MultipartFile
- 3、将request中文件信息封装为一个Map;MultiValueMap
FileCopyUtils类。实现文件流的拷贝
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos)
8、异常处理
1、错误处理
1、默认规则
- 默认情况下,Spring Boot提供
/error
处理所有错误的映射 - 对于机器客户端(postman),它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
- 要对其进行自定义,添加
View
解析为error
- 要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制但替换其内容。 - error/下的4xx,5xx页面会被自动解析;templates下或者public下都可以
2、定制错误处理逻辑
- 自定义错误页
- error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
- @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
- @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error
- Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
- 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
- ErrorViewResolver 实现自定义处理异常;
- response.sendError 。error请求就会转给controller
- 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
- basicErrorController 要去的页面地址是 ErrorViewResolver ; error请求会被basicErrorController处理,里面使用了ErrorViewResolver进行解析。ErrorViewResolver 会转到一个页面或者返回json数据。
3、异常处理自动配置原理
- ErrorMvcAutoConfiguration 自动配置异常处理规则
- 容器中的组件:类型:DefaultErrorAttributes(自定义错误页面属性的类) -> id:errorAttributes
- public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
- DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
- 容器中的组件:类型:BasicErrorController —> id:basicErrorController(json+白页 适配响应)
- 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);
- 容器中有组件 View->id是error;(响应默认错误页)
- 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
- 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
- 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
- error/404、5xx.html
- 容器中的组件:类型:DefaultErrorAttributes(自定义错误页面属性的类) -> id:errorAttributes
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
写出去json
错误页
4、异常处理步骤流程
1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException 来保存catch到的异常
2、进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); 有异常时,mv为空,异常保存在了dispatchException中
3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;
- 1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
- 2、系统默认的 异常解析器;
- 1、DefaultErrorAttributes先来处理异常。把异常信息保存到request域,并且返回null;
- 2、默认没有任何人能处理异常,所以异常会被抛出
- 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
- 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
- 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
- 4、模板引擎最终响应这个页面 error/500.html
9、Web原生组件注入(Servlet、Filter、Listener)
1、方法一:使用Servlet API
1、注入原生组件必须配置@ServletComponentScan(basePackages = “com.atguigu.admin”) :写在主配置类上面,指定原生组件都放在那里
2、配置Servlet组件 @WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器?
根据精确优先原则,DispatcherServlet处理”/“请求,MyServlet处理”/my”请求,更精确,选择MyServlet处理”/my”请求
所以由原生的servlet(Tomcat处理),只有由DispatcherServlet(Spring)处理的请求才会经过spring的拦截器
3、配置Filter组件@WebFilter(urlPatterns={“/css/“,”/images/“}) 当请求css、images相关的url时运行当前filter
4、配置@WebListener组件
推荐可以这种方式;
扩展:DispatchServlet 如何注册进来
- 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
- 通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。
- 默认映射的是 / 路径。
Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则
A: /my/
B: /my/1
2、方法二:使用RegistrationBean注入
ServletRegistrationBean
, FilterRegistrationBean
, and ServletListenerRegistrationBean
防止重新生成依赖对象,改变对象内容
@Configuration
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// 传入myServlet(), 则myServlet()设置的路径会传给FilterRegistrationBean
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
// 设置过滤路径
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
10、嵌入式Servlet容器
1、切换嵌入式Servlet容器
- 默认支持的webServer
Tomcat
,Jetty
, orUndertow
ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
- 切换服务器,有以下几个服务器供选择
- 1、导出默认的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>
- 原理
- SpringBoot应用启动发现当前是Web应用。web场景包-默认导入了tomcat
- web应用会创建一个web版的ioc容器叫
ServletWebServerApplicationContext
ServletWebServerApplicationContext
启动的时候寻找**ServletWebServerFactory**``(Servlet 的web服务器工厂 --(生成)-> Servlet 的web服务器)
- SpringBoot底层默认有很多的WebServer工厂;
TomcatServletWebServerFactory
,JettyServletWebServerFactory
, orUndertowServletWebServerFactory
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了哪个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---》this.tomcat.start();
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
2、定制Servlet容器
- 推荐直接修改配置文件 server.xxx
- 实现 WebServerFactoryCustomizer
- 把配置文件的值和
**ServletWebServerFactory 进行绑定**
- 把配置文件的值和
- 直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
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);
}
}
11、定制化原理
1、定制化的常见方式
- 修改配置文件(推荐);
- xxxxxCustomizer(如定制化Servlet容器);
- 编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;如视图解析器
- Web应用 推荐编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer
- @EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能
- 配置方法示例:
- 原理
- 1、WebMvcAutoConfiguration 中默认的SpringMVC的自动配置功能类。自动配置了静态资源、欢迎页等官方的自动配置
- 2、一旦使用 @EnableWebMvc 。会 @Import(DelegatingWebMvcConfiguration. class)
- 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
- 把所有系统中的 WebMvcConfigurer 集合拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
- 自动配置了一些非常底层的组件。如RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
- DelegatingWebMvcConfiguration 的完整声明为 public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport,也就是说继承实现了WebMvcConfigurationSupport
- 4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 满足,也就是容器中没有这个组件WebMvcAutoConfiguration 才生效。
- 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
- … …
2、原理分析套路
场景starter —》 引入一系列xxxxAutoConfiguration -》 导入xxx组件(@Import、@Bean、@Configuration等方法导入) -》 组件中的默认属性绑定在xxxProperties —》 又绑定配置文件项
默认情况下spring boot就可以正常工作,如果想修改配置,直接修改配置文件项即可