4、响应处理
1.响应JSON
假设给前端自动返回json数据 ,需要引入相关的依赖
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-json</artifactId > <version > 2.3.4.RELEASE</version > <scope > compile</scope > </dependency >
控制层代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public class ResponseTestController { @ResponseBody @GetMapping(value = "/test/person") public Person getPerson () { Person person = new Person (); person.setAge(28 ); person.setBirth(new Date ()); person.setUserName("zhangsan" ); return person; } }
spring支持的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ModelAndView Model View ResponseEntity ResponseBodyEmitter StreamingResponseBody HttpEntity HttpHeaders Callable DeferredResult ListenableFuture CompletionStage WebAsyncTask 有 @ModelAttribute 且为对象类型的 @ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
ReturnValueHandler
[32、请求处理-[源码分析]-各种类型参数解析原理](## 32、请求处理-[源码分析]-各种类型参数解析原理)有讨论ReturnValueHandler 。现在直接看看重点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware , InitializingBean { ... @Nullable protected ModelAndView invokeHandlerMethod (HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest (request, response); try { ... ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this .argumentResolvers != null ) { invocableMethod.setHandlerMethodArgumentResolvers(this .argumentResolvers); } if (this .returnValueHandlers != null ) { invocableMethod.setHandlerMethodReturnValueHandlers(this .returnValueHandlers); } ... invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null ; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public void invokeAndHandle (ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); ... try { this .returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { ... } } public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { ... @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); } @Nullable private HandlerMethodReturnValueHandler selectHandler (@Nullable Object value, MethodParameter returnType) { boolean isAsyncValue = isAsyncReturnValue(value, returnType); for (HandlerMethodReturnValueHandler handler : this .returnValueHandlers) { if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { continue ; } if (handler.supportsReturnType(returnType)) { return handler; } } return null ; }
@ResponseBody
注解,即RequestResponseBodyMethodProcessor
,它实现HandlerMethodReturnValueHandler
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { ... @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); writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } }
2.HTTPMessageConverter原理
返回值处理器ReturnValueHandler
原理:
返回值处理器判断是否支持这种类型返回值 supportsReturnType
返回值处理器调用 handleReturnValue
进行处理
RequestResponseBodyMethodProcessor
可以处理返回值标了@ResponseBody
注解的。
利用MessageConverters
进行处理 将数据写为json
内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
SpringMVC会挨个遍历所有容器底层的HttpMessageConverter
,看谁能处理?
得到MappingJackson2HttpMessageConverter
可以将对象写为json
利用MappingJackson2HttpMessageConverter
将对象转为json再写出去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... protected <T> void writeWithMessageConverters (@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class<?> valueType; Type targetType; if (value instanceof CharSequence) { body = value.toString(); valueType = String.class; targetType = String.class; } else { body = value; valueType = getReturnValueType(body, returnType); targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); } ... MediaType selectedMediaType = null ; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null && contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response" ); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); if (body != null && producibleTypes.isEmpty()) { throw new HttpMessageNotWritableException ( "No converter found for return value of type: " + valueType); } List<MediaType> mediaTypesToUse = new ArrayList <>(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (mediaTypesToUse.isEmpty()) { if (body != null ) { throw new HttpMediaTypeNotAcceptableException (producibleTypes); } if (logger.isDebugEnabled()) { logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes); } return ; } MediaType.sortBySpecificityAndQuality(mediaTypesToUse); for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break ; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break ; } } if (logger.isDebugEnabled()) { logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes); } } if (selectedMediaType != null ) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> converter : this .messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null ); if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter <?>>) converter.getClass(), inputMessage, outputMessage); if (body != null ) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]" ); addContentDispositionHeader(inputMessage, outputMessage); if (genericConverter != null ) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body" ); } } return ; } } } ... }
HTTPMessageConverter
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public interface HttpMessageConverter <T> { boolean canRead (Class<?> clazz, @Nullable MediaType mediaType) ; boolean canWrite (Class<?> clazz, @Nullable MediaType mediaType) ; List<MediaType> getSupportedMediaTypes () ; T read (Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write (T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; }
HttpMessageConverter
: 看是否支持将 此 Class
类型的对象,转为MediaType
类型的数据。
例子:Person
对象转为JSON,或者 JSON转为Person
,这将用到MappingJackson2HttpMessageConverter
1 2 3 public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { ... }
关于MappingJackson2HttpMessageConverter
的实例化请看下节。
关于HttpMessageConverters的初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 DispatcherServlet`的初始化时会调用`initHandlerAdapters(ApplicationContext context) public class DispatcherServlet extends FrameworkServlet { ... private void initHandlerAdapters(ApplicationContext context) { this.handlerAdapters = null; if (this.detectAllHandlerAdapters) { // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts. Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerAdapters = new ArrayList<>(matchingBeans.values()); // We keep HandlerAdapters in sorted order. AnnotationAwareOrderComparator.sort(this.handlerAdapters); } } ...
上述代码会加载ApplicationContext
的所有HandlerAdapter
,用来处理@RequestMapping
的RequestMappingHandlerAdapter
实现HandlerAdapter
接口,RequestMappingHandlerAdapter
也被实例化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... private List<HttpMessageConverter<?>> messageConverters; ... public RequestMappingHandlerAdapter() { this.messageConverters = new ArrayList<>(4); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); if (!shouldIgnoreXml) { try { this.messageConverters.add(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } } this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); }
在构造器中看到一堆 HttpMessageConverter
。接着,重点查看AllEncompassingFormHttpMessageConverter
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter { /** * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to * ignore XML, i.e. to not initialize the XML-related infrastructure. * <p>The default is "false". */ private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); private static final boolean jaxb2Present; private static final boolean jackson2Present; private static final boolean jackson2XmlPresent; private static final boolean jackson2SmilePresent; private static final boolean gsonPresent; private static final boolean jsonbPresent; private static final boolean kotlinSerializationJsonPresent; static { ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader(); jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader); jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader); kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); } public AllEncompassingFormHttpMessageConverter() { if (!shouldIgnoreXml) { try { addPartConverter(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } if (jaxb2Present && !jackson2XmlPresent) { addPartConverter(new Jaxb2RootElementHttpMessageConverter()); } } if (jackson2Present) { addPartConverter(new MappingJackson2HttpMessageConverter());//<----重点看这里 } else if (gsonPresent) { addPartConverter(new GsonHttpMessageConverter()); } else if (jsonbPresent) { addPartConverter(new JsonbHttpMessageConverter()); } else if (kotlinSerializationJsonPresent) { addPartConverter(new KotlinSerializationJsonHttpMessageConverter()); } if (jackson2XmlPresent && !shouldIgnoreXml) { addPartConverter(new MappingJackson2XmlHttpMessageConverter()); } if (jackson2SmilePresent) { addPartConverter(new MappingJackson2SmileHttpMessageConverter()); } } } public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> { ... private List<HttpMessageConverter<?>> partConverters = new ArrayList<>(); ... public void addPartConverter(HttpMessageConverter<?> partConverter) { Assert.notNull(partConverter, "'partConverter' must not be null"); this.partConverters.add(partConverter); } ... }
在AllEncompassingFormHttpMessageConverter
类构造器看到MappingJackson2HttpMessageConverter
类的实例化,AllEncompassingFormHttpMessageConverter
包含 MappingJackson2HttpMessageConverter
。
ReturnValueHandler
是怎么与MappingJackson2HttpMessageConverter
关联起来?请看下节。
ReturnValueHandler与MappingJackson2HttpMessageConverter关联
再次回顾RequestMappingHandlerAdapter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... @Nullable private HandlerMethodReturnValueHandlerComposite returnValueHandlers;//我们关注的returnValueHandlers @Override @Nullable//本方法在AbstractHandlerMethodAdapter public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); } @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; ... mav = invokeHandlerMethod(request, response, handlerMethod); ... return mav; } @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) {//<---我们关注的returnValueHandlers invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ... invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } @Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans ... if (this.returnValueHandlers == null) {//赋值returnValueHandlers List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() { List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20); ... // Annotation-based return value types //这里就是 ReturnValueHandler与 MappingJackson2HttpMessageConverter关联 的关键点 handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),//<---MessageConverters也就传参传进来的 this.contentNegotiationManager, this.requestResponseBodyAdvice));// ... return handlers; } //------ public List<HttpMessageConverter<?>> getMessageConverters() { return this.messageConverters; } //RequestMappingHandlerAdapter构造器已初始化部分messageConverters public RequestMappingHandlerAdapter() { this.messageConverters = new ArrayList<>(4); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); if (!shouldIgnoreXml) { try { this.messageConverters.add(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } } this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); } ... }
应用中WebMvcAutoConfiguration
(底层是WebMvcConfigurationSupport
实现)传入更多messageConverters
,其中就包含MappingJackson2HttpMessageConverter
。
3.内容协商原理
根据客户端接收能力不同,返回不同媒体类型的数据。
引入XML依赖:
1 2 3 4 <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
可用Postman软件分别测试返回json和xml:只需要改变请求头中Accept字段(application/json、application/xml)。
Http协议中规定的,Accept字段告诉服务器本客户端可以接收的数据类型。
内容协商原理 :
判断当前响应头中是否已经有确定的媒体类型MediaType
。
获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)(这一步在下一节有详细介绍)
contentNegotiationManager
内容协商管理器 默认使用基于请求头的策略
HeaderContentNegotiationStrategy
确定客户端可以接收的内容类型
遍历循环所有当前系统的 MessageConverter
,看谁支持操作这个对象(Person)
找到支持操作Person的converter,把converter支持的媒体类型统计出来。
客户端需要application/xml,服务端有10种MediaType。
进行内容协商的最佳匹配媒体类型
用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 //RequestResponseBodyMethodProcessor继承这类 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... //跟上一节的代码一致 protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class<?> valueType; Type targetType; if (value instanceof CharSequence) { body = value.toString(); valueType = String.class; targetType = String.class; } else { body = value; valueType = getReturnValueType(body, returnType); targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); } ... //本节重点 //内容协商(浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型) MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null && contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response"); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); //服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据 List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); if (body != null && producibleTypes.isEmpty()) { throw new HttpMessageNotWritableException( "No converter found for return value of type: " + valueType); } List<MediaType> mediaTypesToUse = new ArrayList<>(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (mediaTypesToUse.isEmpty()) { if (body != null) { throw new HttpMediaTypeNotAcceptableException(producibleTypes); } if (logger.isDebugEnabled()) { logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes); } return; } MediaType.sortBySpecificityAndQuality(mediaTypesToUse); //选择一个MediaType for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } if (logger.isDebugEnabled()) { logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes); } } if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); //本节主角:HttpMessageConverter for (HttpMessageConverter<?> converter : this.messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); //判断是否可写 if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); addContentDispositionHeader(inputMessage, outputMessage); //开始写入 if (genericConverter != null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } return; } } } ... }
基于请求参数的内容协商原理
上一节内容协商原理的第二步:
获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)
contentNegotiationManager
内容协商管理器 默认使用基于请求头的策略
HeaderContentNegotiationStrategy
确定客户端可以接收的内容类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 //RequestResponseBodyMethodProcessor继承这类 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... //跟上一节的代码一致 protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class<?> valueType; Type targetType; ... //本节重点 //内容协商(浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型) MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null && contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response"); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); //服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据 List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); ... } //在AbstractMessageConverterMethodArgumentResolver类内 private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { //内容协商管理器 默认使用基于请求头的策略 return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); } } public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver { ... public ContentNegotiationManager() { this(new HeaderContentNegotiationStrategy());//内容协商管理器 默认使用基于请求头的策略 } @Override public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { for (ContentNegotiationStrategy strategy : this.strategies) { List<MediaType> mediaTypes = strategy.resolveMediaTypes(request); if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) { continue; } return mediaTypes; } return MEDIA_TYPE_ALL_LIST; } ... } //基于请求头的策略 public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy { /** * {@inheritDoc} * @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed */ @Override public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT); if (headerValueArray == null) { return MEDIA_TYPE_ALL_LIST; } List<String> headerValues = Arrays.asList(headerValueArray); try { List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues); MediaType.sortBySpecificityAndQuality(mediaTypes); return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST; } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotAcceptableException( "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage()); } } }
开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能。
1 2 3 4 spring: mvc: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式
内容协商管理器,就会多了一个ParameterContentNegotiationStrategy
(由Spring容器注入)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { private String parameterName = "format";// /** * Create an instance with the given map of file extensions and media types. */ public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) { super(mediaTypes); } /** * Set the name of the parameter to use to determine requested media types. * <p>By default this is set to {@code "format"}. */ public void setParameterName(String parameterName) { Assert.notNull(parameterName, "'parameterName' is required"); this.parameterName = parameterName; } public String getParameterName() { return this.parameterName; } @Override @Nullable protected String getMediaTypeKey(NativeWebRequest request) { return request.getParameter(getParameterName()); } //---以下方法在AbstractMappingContentNegotiationStrategy类 @Override public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest)); } /** * An alternative to {@link #resolveMediaTypes(NativeWebRequest)} that accepts * an already extracted key. * @since 3.2.16 */ public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException { if (StringUtils.hasText(key)) { MediaType mediaType = lookupMediaType(key); if (mediaType != null) { handleMatch(key, mediaType); return Collections.singletonList(mediaType); } mediaType = handleNoMatch(webRequest, key); if (mediaType != null) { addMapping(key, mediaType); return Collections.singletonList(mediaType); } } return MEDIA_TYPE_ALL_LIST; } }
然后,浏览器地址输入带format参数的URL:
1 2 3 http://localhost:8080/test/person?format=json 或 http://localhost:8080/test/person?format=xml
这样,后端会根据参数format的值,返回对应json或xml格式的数据。
自定义MessageConverter
实现多协议数据兼容。json、xml、x-guigu (这个是自创的)
@ResponseBody
响应数据出去 调用 RequestResponseBodyMethodProcessor
处理
Processor 处理方法返回值。通过 MessageConverter
处理
所有 MessageConverter
合起来可以支持各种媒体类型数据的操作(读、写)
内容协商找到最终的 messageConverter
SpringMVC的什么功能,一个入口给容器中添加一个 WebMvcConfigurer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 @Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new GuiguMessageConverter()); } } } } /** * 自定义的Converter */ public class GuiguMessageConverter implements HttpMessageConverter<Person> { @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return false; } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { return clazz.isAssignableFrom(Person.class); } /** * 服务器要统计所有MessageConverter都能写出哪些内容类型 * * application/x-guigu * @return */ @Override public List<MediaType> getSupportedMediaTypes() { return MediaType.parseMediaTypes("application/x-guigu"); } @Override public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } @Override public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { //自定义协议数据的写出 String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth(); //写出去 OutputStream body = outputMessage.getBody(); body.write(data.getBytes()); } } import java.util.Date; @Controller public class ResponseTestController { /** * 1、浏览器发请求直接返回 xml [application/xml] jacksonXmlConverter * 2、如果是ajax请求 返回 json [application/json] jacksonJsonConverter * 3、如果硅谷app发请求,返回自定义协议数据 [appliaction/x-guigu] xxxxConverter * 属性值1;属性值2; * * 步骤: * 1、添加自定义的MessageConverter进系统底层 * 2、系统底层就会统计出所有MessageConverter能操作哪些类型 * 3、客户端内容协商 [guigu--->guigu] * * 作业:如何以参数的方式进行内容协商 * @return */ @ResponseBody //利用返回值处理器里面的消息转换器进行处理 @GetMapping(value = "/test/person") public Person getPerson(){ Person person = new Person(); person.setAge(28); person.setBirth(new Date()); person.setUserName("zhangsan"); return person; } }
用Postman发送/test/person
(请求头Accept:application/x-guigu
),将返回自定义协议数据的写出。
浏览器与PostMan内容协商完全适配
假设你想基于自定义请求参数的自定义内容协商功能。
换句话,在地址栏输入http://localhost:8080/test/person?format=gg
返回数据,跟http://localhost:8080/test/person
且请求头参数Accept:application/x-guigu
的返回自定义协议数据的一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Configuration(proxyBeanMethods = false) public class WebConfig /*implements WebMvcConfigurer*/ { //1、WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { /** * 自定义内容协商策略 * @param configurer */ @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { //Map<String, MediaType> mediaTypes Map<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); //自定义媒体类型 mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu")); //指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); // parameterStrategy.setParameterName("ff"); //还需添加请求头处理策略,否则accept:application/json、application/xml则会失效 HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(parameterStrategy, headeStrategy)); } } } ... }
日后开发要注意,有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
5.视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
1、视图解析
自定义视图转发重定向视图处理方式
视图解析原理流程 :
目标方法处理的过程中(阅读DispatcherServlet
源码),所有数据都会被放在 ModelAndViewContainer
里面,其中包括数据和视图地址 。
方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
。
任何目标方法执行完成以后都会返回ModelAndView
(数据和视图地址)。
processDispatchResult()
处理派发结果(页面改如何响应)
视图解析的几个关键点 :
返回值以 forward:
开始: new InternalResourceView(forwardUrl);
--> 转发request.getRequestDispatcher(path).forward(request, response);
返回值以 redirect:
开始: new RedirectView()
--> render
就是重定向
返回值是普通字符串:new ThymeleafView()
—>
自定义视图解析器+自定义视图 ;
2、Thymeleaf
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
Thymeleaf’s main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.
With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.——Link
Thymeleaf官方文档
1️⃣thymeleaf使用
引入Starter
1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
自动配置好了thymeleaf
1 2 3 4 5 6 7 @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class }) @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration { ... }
自动配好的策略
所有thymeleaf的配置值都在 ThymeleafProperties
配置好了 SpringTemplateEngine
配好了 ThymeleafViewResolver
我们只需要直接开发页面
1 2 public static final String DEFAULT_PREFIX = "classpath:/templates/";//模板放置处 public static final String DEFAULT_SUFFIX = ".html";//文件的后缀名
编写一个控制层:
1 2 3 4 5 6 7 8 9 10 @Controller public class ViewTestController { @GetMapping("/hello") public String hello(Model model){ //model中的数据会被放在请求域中 request.setAttribute("a",aa) model.addAttribute("msg","一定要大力发展工业文化"); model.addAttribute("link","http://www.baidu.com"); return "success"; } }
/templates/success.html
:
注意需要写
<html lang="en" xmlns:th="http://www.thymeleaf.org">
thymeleaf的命名空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <!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}">nice</h1> <h2> <a href="www.baidu.com" th:href="${link}">去百度</a> <br/> <a href="www.google.com" th:href="@{/link}">去百度</a> </h2> </body> </html>
1 2 3 server: servlet: context-path: /app #设置应用名
这个设置后,URL要插入/app
, 如http://localhost:8080/app/hello.html
。
2️⃣基本语法
表达式
表达式名字
语法
用途
变量取值
${…}
获取请求域、session域、对象等值
选择变量
*{…}
获取上下文对象值
消息
#{…}
获取国际化等值
链接
@{…}
生成链接
片段表达式
~{…}
jsp:include 作用,引入公共页面片段
字面量
文本值: ‘one text’ , ‘Another one!’ ,…
数字: 0 , 34 , 3.0 , 12.3 ,…
布尔值: true , false
空值: null
变量: one,two,… 变量不能有空格
文本操作
字符串拼接: +
变量替换: |The name is ${name}|
在空白处插入数据:
数学运算
布尔运算
运算符: and , or
一元运算: ! , not
比较运算
比较: > , < , >= , <= ( gt , lt , ge , le )
等式: == , != ( eq , ne )
条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
特殊操作
行内写法
[[${loginUser.userName}]]
3️⃣设置属性值-th:attr
1 2 3 4 5 6 <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>
1 2 <img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
官方文档 - 5 Setting Attribute Values
4️⃣迭代
1 2 3 4 5 6 7 8 9 10 <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️⃣条件运算
1 2 3 4 5 6 7 8 <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️⃣属性优先级
Order
Feature
Attributes
1
Fragment inclusion
th:insert
th:replace
2
Fragment iteration
th:each
3
Conditional evaluation
th:if
th:unless
th:switch
th:case
4
Local variable definition
th:object
th:with
5
General attribute modification
th:attr
th:attrprepend
th:attrappend
6
Specific attribute modification
th:value
th:href
th:src
...
7
Text (tag body modification)
th:text
th:utext
8
Fragment specification
th:fragment
9
Fragment removal
th:remove
官方文档 - 10 Attribute Precedence
7️⃣ 引入公共片段
定义和引用片段
templates/footer.html
file containing this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <div th:fragment="layoutTest"> © 2011 The Good Thymes Virtual Grocery </div> </body> </html>
上面的代码定义了一个名为 copy 的片段,我们可以使用th:insert
或th:replace
属性之一轻松地将其包含在我们的主页中
还有th:include
,尽管自 Thymeleaf 3.0 起不再推荐使用它
测试代码
1 2 3 4 5 6 7 <body> ... <div th:insert="~{footer :: copy}"></div> </body>
注意,th:insert
需要一个片段表达式 (~{...})
,它是一个生成片段的表达式。
不过,在上面的例子中,它是一个非复杂的片段表达式,(~{,})
封闭是完全可选的,
所以上面的代码相当于:
1 2 3 4 5 6 <body> ... <div th:insert="footer :: copy"></div> </body>
片段规范语法
~{templatename::selector}
包括在名为 templatename 的模板上应用指定标记选择器产生的片段。
请注意,选择器可以只是一个片段名称,因此您可以像上面的 ~{footer :: copy}
一样指定像~{templatename::fragmentname}
这样简单的东西。标记选择器语法由底层 AttoParser 解析库定义,类似于 XPath 表达式或 CSS 选择器。有关详细信息,请参阅附录 C。
~{templatename}
包括名为 templatename 的完整模板。请注意,您在th:insert/th:replace
标记中使用的模板名称必须可由模板引擎当前使用的模板解析器解析。
~{::selector}
或 ~{this::selector}
插入来自相同模板的片段,匹配选择器。如果在出现表达式的模板上找不到,则遍历模板调用(插入)堆栈最初处理的模板(根),直到选择器在某个级别匹配。
以上示例中的模板名和选择器都可以是全功能表达式(甚至是条件表达式!)
比如:
1 <div th:insert="footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser})"></div>
使用不带 th:fragment
的片段
Difference between th:insert
and th:replace
(and th:include
)
th:insert
1 2 3 4 5 6 7 8 9 10 11 是最简单的:它会简单地将指定的片段 作为其宿主标签的主体插入 。 - 插入标签 - ``` th:replace
实际上用指定的片段替换它的主机标签。
- 替代当前的标签
th:include
th:insert
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 类似,但不是插入片段,而是 仅插入片段的内容 。 - 插入标签的内容 ![img](http://dhx-blog.oss-cn-beijing.aliyuncs.com/dhx/typoraimage-20220830233510721.png) ## 3、后台管理系统基本功能 ## 项目创建 使用IDEA的Spring Initializr。 - thymeleaf - web-starter - devtools - lombok ## 登陆页面 - `/static` 放置 css,js等静态资源 - `/templates/login.html` 登录页
1 2 3 4 5 6 7 - `/templates/main.html` 主页 `thymeleaf`内联写法: - 直接在空白处增加数据
Hello, [[${session.user.name}]] !
@Controller
public class IndexController {
/**
* 来登录页
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){
return "login";
}
@PostMapping("/login")
public String main(User user, HttpSession session, Model model){ //RedirectAttributes
if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}
/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session, Model model){
//最好用拦截器,过滤器
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "main";
}else {
//session过期,没有登陆过
//回到登录页面
model.addAttribute("msg","请重新登录");
return "login";
}
}
}
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private String userName;
private String password;
}
1 2 3 4 5 ## 遍历页面数据 **controller**
@GetMapping(“/dynamic_table”)
public String dynamic_table(Model model){
// 表格内容的遍历
List users = Arrays.asList(new User(“Vsa”, 12), new User(“Ads”, 21),
new User(“BSd”, 42), new User(“Arq”, 34));
model.addAttribute(“users”,users);
return “table/dynamic_table”;
}
</tr>
</tbody>
#
用户名
密码
Trident
Internet
[[${user.password}]]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 > **插播一条知识点** > > **model和session的区别** > > - `model`和`session`都是存放信息的地方,不同的地方就是他们的生命周期不同,model是request级别的,session是一次会话,在服务器与一个用户客户端交互过程中一直保留。 > - 即在用户登录一个网站后, > - `session`里存放的用户数据可以在该网站的其他页面使用。 > - model的数据只能在该页面使用。 # 6、拦截器 ## 1.登录检查与静态资源放行 1. 编写一个拦截器实现`HandlerInterceptor`接口 2. 拦截器注册到容器中(实现`WebMvcConfigurer`的`addInterceptors()`) 3. 指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截) ![img](http://dhx-blog.oss-cn-beijing.aliyuncs.com/dhx/image-20220831065533177.png) 编写一个实现`HandlerInterceptor`接口的拦截器: **LoginInterceptor.java**
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
*/
@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;
}
/**
* 目标方法执行完成以后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}
/**
* 页面渲染以后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}
1 2 3 4 5 拦截器**注册到容器中** && 指定拦截规则: **AdminWebConfig.java**
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())//拦截器注册到容器中
.addPathPatterns(“/“) //所有请求都被拦截包括静态资源
.excludePathPatterns(”/“,”/login","/css/ ”,“/fonts/“,”/images/ ”,
“/js/“,”/aa/ ”); //放行的请求
}
1 2 3 4 5 6 7 8 9 ## 2.拦截器的执行时机和原理 1. 根据当前请求,找到`HandlerExecutionChain`(可以处理请求的handler以及handler的所有 拦截器) 2. 先来顺序执行 所有拦截器的
preHandle()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 方法。 - 如果当前拦截器`preHandle()`返回为`true`。则执行下一个拦截器的`preHandle()` - 如果当前拦截器返回为`false`。直接倒序执行所有已经执行了的拦截器的 `afterCompletion();`。 3. 如果任何一个拦截器返回`false`,直接跳出不执行目标方法。 4. 所有拦截器都返回`true`,才执行目标方法。 5. 倒序执行所有拦截器的`postHandle()`方法。 6. 前面的步骤有任何异常都会直接倒序触发 `afterCompletion()`。 7. 页面成功渲染完成以后,也会倒序触发 `afterCompletion()`。 ![img](http://dhx-blog.oss-cn-beijing.aliyuncs.com/dhx/typoraimage-20220831074554614.png) `DispatcherServlet`中涉及到`HandlerInterceptor`的地方:
public class DispatcherServlet extends FrameworkServlet {
...
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;
...
//该方法内调用HandlerInterceptor的preHandle()
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
//该方法内调用HandlerInterceptor的postHandle()
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
...
}
}
private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {
if (mappedHandler != null) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
...
if (mappedHandler != null) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
}
public class HandlerExecutionChain {
...
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
//HandlerInterceptor的preHandle方法
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
//HandlerInterceptor接口的postHandle方法
interceptor.postHandle(request, response, this.handler, mv);
}
}
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
//HandlerInterceptor接口的afterCompletion方法
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}