Web开发
SpringMVC自动配置概览
SpringBoot自动配置了SpringMVC(大多场景我们都无需自定义配置)
- 内容协商试图解析器和BeanName视图解析器
- 静态资源(包括webjars)
- 自动注册Converter,GenericConverter,Formatter
- 支持HttpMessageConverters
- 自动注册
MessageCodesResolver
(国际化用) - 静态index.html 页支持
- 自定义
Favicon
- 自动使用
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上)
简单功能
静态资源访问
静态资源目录
类路径下:called/static or /resources or /public or /META-INF/resources
访问: 当前项目的根路径/ + 静态资源名
原理:静态映射 /**
收到请求,先去找Controller,没找到再去交给静态资源处理器.
静态资源访问前缀
默认无前缀
spring:
mvc:
static-path-pattern: /res/**
当前项目 + static-path-pattern + 资源名
webjar
自动映射 /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 后面地址要按照依赖里面的包路径
欢迎页支持
- 静态资源路径下 index.html
- 可以配置静态资源路径
- 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
resources:
static-locations: [classpath:/haha/]
- controller能处理/index
自定义Favicon
favicon.ico 放在静态资源目录下即可。
静态资源配置原理
- Springboot启动默认加载xxxAutoConfiguration类(自动配置类)
- 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();
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();
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 禁用所有静态资源规则
默认的静态资源文件夹/META-INF/resources/,/resources/, /static/, /public/
@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");
}
}
请求参数处理
请求映射
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
-
以前:**/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
-
现在: /user *GET-*获取用户 *DELETE-*删除用户 *PUT-*修改用户 *POST-*保存用户
-
核心Filter;HiddenHttpMethodFilter
-
用法: 表单method=post,隐藏域 _method=put
-
SpringBoot中手动开启
spring: mvc: hiddenmethod: filter: enabled: true
@Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)//没有配置这个属性,不生效 public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }
Rest原理(表单提交要使用REST的时候)
-
表单提交会带上_method=PUT
-
请求过来被HiddenHttpMethodFilter拦截
-
请求是否正常,并且是POST
-
获取到_method的值。
-
兼容以下请求:PUT.DELETE.PATCH
-
原生request(post),装饰器模式requesWrapper重写了getMethod方法,返回的是传入的值。
-
过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的
-
在网页中只有post和get,以上操作只是后端代码的实现,在浏览器中依然是post
Rest使用客户端工具,可以直接以put,delete等方式发送请求
- 如PostMan直接发送Put、delete等方式请求,无需Filter。
-
-
-
扩展:如何把_method 这个名字换成我们自己喜欢的。
-
修改默认参数名即可
-
package com.qin.boot.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.HiddenHttpMethodFilter; @Configuration public class Myconfig { @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter(); filter.setMethodParam("_m"); return filter; } }
-
请求映射原理
-
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 { ModelAndView mv = null; Exception dispatchException = null; try { //是否是文件上传请求 processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 找到当前请求使用哪个Handler(Controller的方法)处理 mappedHandler = getHandler(processedRequest); //HandlerMapping:处理器映射。/xxx->>xxxx
-
有五个默认HandleMapper
-
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
所有的请求映射都在HandlerMapping中
-
SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
-
SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
-
请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
-
如果有就找到这个请求对应的handler
-
如果没有就是下一个 HandlerMapping
-
我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
普通参数与基本注解
注解:
-
@PathVariable(路径参数)
@RestController public class ParamController { @RequestMapping("/car/{id}/{username}") public Map<String , Object > map(@PathVariable("id") Integer id, @PathVariable("username") String name,//获取指定的路径参数 @PathVariable Map<String,String> kv){//获取所有的路径参数 HashMap<String , Object > map = new HashMap<>(); map.put("id",id); map.put("username",name); map.put("map",kv); return map; } }
-
@RequestHeader(请求头)
@RestController public class ParamController { @RequestMapping("/car/{id}/{username}") public Map<String , Object > map(@RequestHeader("请求头名")String header,//获取指定的请求头 @RequestHeader Map<String,String> headers)//获取所有的请求头
-
@RequestParam(请求参数)
<a href="car?age=18&inters=song&inters=game">
@RestController public class ParamController { @RequestMapping("/car") public Map<String , Object > map(@RequestParam("inters")List<String> param,//获取指定的请求参数,多个相同名字的参数名为一个集合 @RequestHeader Map<String,String> params)//获取所有的请求参数
-
@CookieValue(Cookie值)
-
@RequestBody(请求体)
-
@RequestAttribute(获取request域属性)
@MatrixVariable(矩阵变量)
在url路径后加 分号";" 即是矩阵变量,以kv方式存储
语法:/car;age=1
springboot默认禁用矩阵变量的功能,需要手动开启
禁用原理:WebMvcConfigurer中使用configurePathMatch方法解析UrlPathHelper进行解析路径处理,其中removeSemicolonContext(移除分号内容)控制矩阵变量默认为true
修改springboot禁用矩阵变量方法:
自定义WebMvcConfigurer,然后new一个UrlPathHelper解析器,修改removeSemicolonContext的值
-
方法一:在容器中添加一个WebMvcConfigurer Bean
@Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); //不移除分号后面的内容 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }; }
-
方法二:让容器继承WebMvcConfigurer
@RestController public class ParamController implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); //不移除分号后面的内容 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }
-
使用
矩阵变量必须有url路径变量才能被解析
//矩阵变量控制器 @RestController public class MatrixVariableController { @GetMapping("/car/{path}") public Map<String,Object> matrix(@MatrixVariable("id")String id){ Map<String,Object> map = new HashMap<>(); map.put("id",id); return map; } }
-
当有两个相同名字的矩阵变量时,使用参数pathVar解析路径
url=/car/shell;id=1/user;id=2
@RestController public class MatrixVariableController { @GetMapping("/car/{path1}/{path2}") public Map<String,Object> matrix2(@MatrixVariable(value = "id",pathVar = "path1")String id1, @MatrixVariable(value = "id",pathVar = "path2")String id2){ Map<String,Object> map = new HashMap<>(); map.put("id1",id1); map.put("id2",id2); 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(); 获取到值的
自定义对象参数
可以自动类型转换与格式化,可以级联封装
/**
* 姓名: <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;
}
/**
* 数据绑定:页面提交的请求数据都可以和对象属性进行绑定
* @param person
* @return
*/
@RequestMapping("/person")
public Person person(Person person){
return person;
}
自定义类封装过程
- ServletModelAttributeMethodProcessor使用这个解析器处理
参数处理原理
-
HandlerMapping中找到能处理请求的Handler(Controller.method())
-
为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
-
适配器执行目标方法并确定方法参数的每一个值
1、HandlerAdapter
有四个适配器
0 - 支持方法上标注@RequestMapping
1 - 支持函数式编程的
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目标方法能写多少种参数类型。取决于参数解析器。
有26个参数解析器
- 当前解析器是否支持解析这种参数
- 支持就调用 resolveArgument
4、返回值处理器
5、如何确定目标方法每一个参数的值
============InvocableHandlerMethod==========================
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//获取方法所有参数详细信息
MethodParameter[] parameters = getMethodParameters();
//判断参数列表是否为空
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
//最终返回的参数数组
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
//如果有值则直接下一个
if (args[i] != null) {
continue;
}
//判断当前解析器是否支持这个参数 -> 5.1
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//解析这个参数的值 -> 5.2
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) {
//一般判断是否支持
//先判断是否标注了当前解析器的注解
//如果是再判断是否是map类型
//如果是map则把参数的KV放入map里,如果不是则找到具体参数的值
if (resolver.supportsParameter(parameter)) {
result = resolver;
//如果支持,放入缓存里,方便之后解析
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
5.2、解析这个参数的值
-
拿到所有的参数解析器
-
找到支持这个参数的解析器
-
解析值
调用各自 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.
//web资源绑定器:将请求参数的值绑定到指定的javabean里
//利用Converters资源转换器将请求数据转成指定的数据类型。再次封装到JavaBean中
//一共有124个Converter
//GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean -- Integer)
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;
}
5.4、自定义 Converter
输入宠物: name,age
@RestController
public class ParamController implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
//创建一个ConVerter
Converter<String, Pet> converter = new Converter<String, Pet>() {
//编写转换逻辑
@Override
public Pet convert(String s) {
String[] split = s.split(",");
Pet pet = new Pet();
pet.setName(split[0]);
pet.setAge(split[1]);
return pet;
}
};
registry.addConverter(converter);
}
}
6、目标方法执行完成
将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据
7、处理派发结果
//暴露模型作为请求域属性
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
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);
}
});
}
响应数据与内容协商
响应JSON
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、返回值处理器判断是否支持这种类型返回值 supportsReturnType
-
2、返回值处理器调用 handleReturnValue 进行处理
-
3、RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
- .1. 利用 MessageConverters 进行处理 将数据写为json
- 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
- 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
- 3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
- 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
- 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
- .1. 利用 MessageConverters 进行处理 将数据写为json
SpringMVC支持哪些返回值
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
HTTPMessageConverter原理
1、MessageConverter规范
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person
2、默认的MessageConverter
0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的。
最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)
内容协商
根据客户端接收能力不同,返回不同媒体类型的数据
引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
开启浏览器参数方式内容协商功能
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
发请求: http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
确定客户端接收什么样的内容类型
1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
2、最终进行内容协商返回给客户端json即可。
内容协商原理
-
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()));
}
自定义 MessageConverter
实现多协议数据兼容。json、xml、x-guigu
0、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 返回值处理器处理
1、Processor 处理方法返回值。通过 MessageConverter 处理
2、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter;
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
package com.qin.boot.converter;
import com.qin.boot.bean.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
public class QinConverter implements HttpMessageConverter<Person> {
@Override
public List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return null;
}
@Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return aClass.isAssignableFrom(Person.class);
}
/**
* 服务器要统计所有MessageConverter都能写出哪些内容
*
* appcliation/qin
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
//设置能处理的媒体类型
return MediaType.parseMediaTypes("application/qin");
}
@Override
public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
//自定义协议数据的写出
String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth();
//将数据写出去
OutputStream body = httpOutputMessage.getBody();
body.write(data.getBytes());
}
}
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new QinConverter());
Object[] objects = converters.toArray();
for (Object object : objects) {
System.out.println(object);
}
}
}
}
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】
视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
视图解析
1、视图解析原理流程
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
2、方法的参数如果是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
3、任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)
4、processDispatchResult 处理派发结果(页面改如何响应)
- 1、render(mv, request, response); 进行页面渲染逻辑
- 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
- 1、所有的视图解析器尝试是否能根据当前返回值得到View对象
- 2、得到了 redirect:/main.html --> Thymeleaf new RedirectView()
- 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
- 4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
- RedirectView 如何渲染【重定向到一个页面】
- 1、获取目标url地址
- 2、response.sendRedirect(encodedURL);
- 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
视图解析:
-
返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);
-
返回值以 redirect: 开始: new RedirectView() --》 render就是重定向
-
返回值是普通字符串: new ThymeleafView()—>
模板引擎-Thymeleaf
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模板引擎
基本语法
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、特殊操作
无操作: _
设置属性值-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>
HTML
复制代码
<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>
属性优先级
thymeleaf使用
引入Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
html页面
在 resource/templates 下新建html页面
添加thymeleaf名称空间,有联想提示
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>hello</h1>
<p th:text="${msg}"></p>
<h2>
<a href="" th:href="${link}">去百度</a>
<a href="" th:href="@{link}">去百度2</a>
</h2>
</body>
</html>
拦截器
-
编写一个拦截器实现HandlerInterceptor接口
package com.qin.boot.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * 登录检查 * 1.配置好拦截器要拦截哪些内容 * 2.把这个配置放在容器中 */ public class LoginInterceptor implements HandlerInterceptor { //方法执行之前 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser"); if (loginUser!=null)return true; System.out.println(request.getRequestURI()+"被拦截了"); request.setAttribute("msg","请登录"); request.getRequestDispatcher("/").forward(request,response); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
-
拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
-
指定拦截规则(如果是拦截所有,静态资源也会被拦截)
package com.qin.boot.config; import com.qin.boot.interceptor.LoginInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class LoginConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/*")//指定要拦截的路径 .excludePathPatterns("/login","/");//指定哪些路径不被拦截 } }
拦截器原理
1、根据当前请求,找到**HandlerExecutionChain【**可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
- 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、所有拦截器都返回True。执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
文件上传
页面表单
<!--提交方法一定是post
一定要有 enctype="multipart/form-data"-->
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div >
<label >邮箱</label>
<input type="email" name="email" >
</div>
<div >
<label >密码</label>
<input type="password" name="password" >
</div>
<div >
<label >图片1</label>
<input type="file" name="photo">
</div>
<div >
<label for="exampleInputFile">File input</label>
<!-- myltiple 可选择多个文件-->
<input type="file" name="photos" multiple>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit">上传</button>
</form>
文件上传代码
package com.qin.boot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Controller
public class FormController {
@RequestMapping("/form_layouts")
public String form_layouts(){
return "/form/form_layouts";
}
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("password")String password,
@RequestPart("photo")MultipartFile photo,
@RequestPart("photos")MultipartFile[] photos) throws IOException {
System.out.println(email+password+photo.getSize()+photos.length);
if (!photo.isEmpty()){//判断文件是否存在
String originalFilename = photo.getOriginalFilename();//源文件名
//transferTo 将文件上传至
photo.transferTo(new File("C:\\Users\\qinjingzhuo\\Desktop\\MakeDown\\SpringBoot\\img\\"+originalFilename));
}
if (photos.length>0){
for (MultipartFile file : photos) {
if (!file.isEmpty()){
String originalFilename = file.getOriginalFilename();
file.transferTo(new File("C:\\Users\\qinjingzhuo\\Desktop\\MakeDown\\SpringBoot\\img\\"+originalFilename));
}
}
}
return "main";
}
}
修改默认配置
spring.servlet.multipart.max-file-size=10MB # 单个文件最大容量
spring.servlet.multipart.max-request-size=100MB # 所有文件总容量大小
自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
- 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
- 原理步骤
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成MultipartFile
- 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝
异常处理
默认规则
-
默认情况下,Spring Boot提供
/error
处理所有错误的映射 -
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
-
要对其进行自定义,添加View解析为error
-
要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制但替换其内容。 -
error/下的4xx,5xx页面会被自动解析;
异常处理自动配置原理
-
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
-
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
异常处理步骤流程
1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException
2、进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, 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
-
定制错误处理逻辑
-
自定义错误页
-
error/404.html error/500.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
-
-
@ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的(推荐使用)
- 将自定义的异常处理器放至ExceptionHandlerExceptionResolver里
package com.qin.boot.exception; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @Slf4j @ControllerAdvice public class GlobalExceptionHandler { //数学运算异常,空指针异常 @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) public String handlerArithmeticException(Exception e){ log.error("异常:{}",e); //最终会返回 ModelAndView //如果是String类型返回值 Model为空 return "login";//视图地址 } }
-
自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
package com.qin.boot.exception; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Order(value = Ordered.HIGHEST_PRECEDENCE)//设置解析器优先级,数字越小优先级越高 @Component//将这个异常解析器添加进容器 public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { response.sendError(511,"自定义异常"); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } }
-
@ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息组装成ModelAndView返回;底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error
- 自定义异常
package com.qin.boot.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; //这个异常会返回一个状态码信息 @ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户数量太多") public class UserException extends RuntimeException { public UserException() { } public UserException(String message) { super(message); } }
-
抛出异常
@GetMapping("/dynamic_table") public String dynamic_table(Model model){ List<User> users=new LinkedList<>(); users.add(new User("zhangsan","000000")); users.add(new User("lisi","000123")); users.add(new User("jack","021")); if (users.size()>2){ throw new UserException(); } model.addAttribute("users",users); return "table/dynamic_table"; }
-
Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage();
Web原生组件注入
使用Servlet API
@WebServlet(urlPatterns = “/servlet”)
package com.qin.boot.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(urlPatterns = "/servlet")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("123456");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
@WebFilter(urlPatterns = “/servlet”)
package com.qin.boot.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/servlet")
@Slf4j
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("过滤器生效");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
@WebListener
package com.qin.boot.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
@Slf4j
public class TestListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("项目监听到");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
@ServletComponentScan
package com.qin.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan //扫描这个包下的servlet组件
@SpringBootApplication
public class Springboot04Application {
public static void main(String[] args) {
SpringApplication.run(Springboot04Application.class, args);
}
}
DispatchServlet 如何注册进来
-
容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
-
通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。
-
默认映射的是 / 路径。
-
多个Servlet都能处理到同一层路径,精确优选原则
使用RegistrationBean
package com.qin.boot.servlet;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.logging.Filter;
//(proxyBeanMethods = true)保证依赖的组件始终是单例的
@Configuration(proxyBeanMethods = true)
public class ServletRegistrationConfig {
@Bean
public ServletRegistrationBean testServlet(){
TestServlet servlet = new TestServlet();
return new ServletRegistrationBean(servlet,"/servlet");//注意路径前一定要加 斜杠/
}
@Bean
public FilterRegistrationBean testFilter(){
TestFilter testFilter = new TestFilter();
//可以直接传入一个ServletRegistrationBean,但是最好保持是单例
//return new FilterRegistrationBean(testFilter,testServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(testFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/servlet"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean testListener(){
TestListener listener = new TestListener();
ServletListenerRegistrationBean listenerBean = new ServletListenerRegistrationBean(listener);
return listenerBean;
}
}
嵌入式Servlet容器
切换嵌入式Servlet容器
-
默认支持的webServer
Tomcat
,Jetty
, orUndertow
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
, orUndertowServletWebServerFactory
底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
定制Servlet容器
-
实现 WebServerFactoryCustomizer
- 把配置文件的值和
ServletWebServerFactory 进行绑定
- 把配置文件的值和
-
修改配置文件 server.xxx
-
直接自定义 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);
}
}