SpringMVC基础

Servlet工作原理

ApplicationContext

AppCtx

简介

ApplicationContext

ApplicationContext是一个很综合的类,其UML图如上图所示:

  • 它是一个BeanFactory,拥有管理bean的能力

    • 继承了ListableBeanFactory接口,拥有管理bean的能力,可以获得beanNames、判断某个beanName是否存在beanDefinition对象
    • 继承了HierarchicalBeanFactory接口,拥有层级化的关系,可以获得父BeanFactory等功能
  • 继承了ResourceLoader,拥有加载Resource的能力

    • 继承了ResourcePatternResolver了,间接继承了ResourceLoader。这里的Resource可以是图片、文件、URL资源等等
  • 继承了ApplicationEventPublisher,拥有发布事件的能力

    通过ApplicaitonContext可以轻松实现发布-订阅模式,将Event发布给响应的Listener

  • 继承了EnvironmentCapable,拥有获取系统环境变量的能力

  • 继承了MessageSource接口,通过它可以事件国际化功能。

Bean管理功能

常用功能

通过ApplicationContext可以实现对bean的操作,常用的如下:

/**

获取对象

@param name

@return Object 一个以所给名字注册的bean的实例

*/

public static Object getBean(String name) throws BeansException {

return applicationContext.getBean(name);

}


/**

获取类型为requiredType的对象

如果bean不能被类型转换,相应的异常将会被抛出(BeanNotOfRequiredTypeException)

@param name       bean注册名

@param requiredType 返回对象类型

@return Object 返回requiredType类型对象

*/

public static Object getBean(String name, Class requiredType) throws BeansException {

return applicationContext.getBean(name, requiredType);

}


/**

如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true

@param name

*/

public static boolean containsBean(String name) {

return applicationContext.containsBean(name);

}


/**

判断以给定名字注册的bean定义是一个singleton还是一个prototype。

如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)

@param name

@return boolean

*/

public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {

return applicationContext.isSingleton(name);

}


/**

@param name

@return Class 注册对象的类型

*/

public static Class getType(String name) throws NoSuchBeanDefinitionException {

return applicationContext.getType(name);

}


/**
如果给定的bean名字在bean定义中有别名,则返回这些别名

@param name

*/

public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {

return applicationContext.getAliases(name);

}

bean的加载过程

本节参考segmentfault,本节暂不深入

bean加载过程

实例化+填充,这块的过程

Resource加载功能

Spring的Resource接口旨在成为一个更有能力的接口,用于抽象对低级资源的访问。

Resource主要指的就是文件,而文件可以视作一个InputStream来看待。

文件存储可以是:

实现类说明
FileSystemResource通过文件系统获取资源
ClassPathResource通过类路径获取资源文件,打包之后的classes目录
URLResource通过URL地址获取资源
ServletContextResource相关Web应用程序根目录中的相对路径
ByteArrayResource字节数组的Resource实现类
InputStreamResource给定的输入流(InputStream)的Resource实现

Resource的接口不多


public interface Resource extends InputStreamSource {
    
    //判断资源是否存在,true表示存在
    boolean exists();

    //判断资源的内容是否可读
    default boolean isReadable() {
        return this.exists();
    }

    //判断当前Resource代表的底层资源是否已经打开,如果返回true,则只能被读取一次然后关闭以避免资源泄露;该方法主要针对于InputStreamResource,实现类中只有它的返回结果为true,其他都为false。
    default boolean isOpen() {
        return false;
    }

    default boolean isFile() {
        return false;
    }

    //返回当前资源对应的URL。如果当前资源不能解析为一个URL则会抛出异常。如ByteArrayResource就不能解析为一个URL。
    URL getURL() throws IOException;

    //返回当前资源对应的URI。如果当前资源不能解析为一个URI则会抛出异常。
    URI getURI() throws IOException;

    File getFile() throws IOException;

    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(this.getInputStream());
    }

    //返回当前资源内容的长度。
    long contentLength() throws IOException;

    //返回当前Resource代表的底层资源的最后修改时间。
    long lastModified() throws IOException;

    //根据资源的相对路径创建新资源。[默认不支持创建相对路径资源]
    Resource createRelative(String var1) throws IOException;

    //获取资源的文件名
    @Nullable
    String getFilename();

    //返回当前资源底层资源的描述符,通常就是资源的全路径(实际文件名或实际URL地址)
    String getDescription();
}

除此之外,继承了一个InputStreamSource的getInputStream方法。该方法获取当前资源代表的输入流,除了InputStreamResource实现类以外,其它Resource实现类每次调用getInputStream()方法都将返回一个全新的InputStream。

如何加载资源这里也应该加一下

参考

Event发布功能

包括3个元素: publisher、event、listener

使用方式

@ComponentScan("com.wxx.official.event")
@Configuration
@EnableAsync  
public class Test {
   public static void main(String[] args) {
      ApplicationEventPublisher publisher = new AnnotationConfigApplicationContext(Test.class);
      publisher.publishEvent(new Event("注解事件"));
   }


   static class Event {
      String name;


      Event(String name) {
         this.name = name;
      }


      @Override
      public String toString() {
         return name;
      }
   }


   @Component
   static class Listener {
      @EventListener
      @Async // 异步
      public void listen(Event event) {
         System.out.println("接收到事件:" + event);
         System.out.println("处理事件");
      }
}

listener可以通过@Order来实行顺序串联

参考

环境变量获取功能

它其实代表了当前Spring容器的运行环境,比如JDK环境,系统环境。每个环境都有自己的配置数据,如System.getProperties()可以拿到JDK环境数据,System.getenv()可以拿到系统变量,ServletContext.getInitParameter()可以拿到Servlet环境配置数据。Spring抽象了一个Environment来表示Spring应用程序环境配置,整合了各种各样的外部环境,并且提供统一访问的方法。

Environment接口继承了PropertyResolver,而Environment接口自身主要提供了对Profile的操作,PropertyResolver接口主要提供了对当前运行环境中属性的操作,如果我们去查看它的一些方法的实现可以发现,对属性的操作大都依赖于PropertySource。

参考

ApplicationContext的不同类型

参考

ServletContext

  • 定义

    这里加一段ServletContext的概念

    ServletContext 对象由服务器进行创建,一个项目只有一个对象。不管在项目的任意位置进行获取得到的都是同一个对象,那么不同用户发起的请求获取到的也就是同一个对象了,该对象由用户共同拥有。

    Request 解决了一次请求内的数据共享问题,

    session 解决了用户不同请求的数据共享问题,

    ServletContext解决了不同的用户的数据共享的问题

  • 使用

    通过 this.getServletContext(); 得到ServletContext对象。你可以把它想象成一张表,这个和Session非常相似:每一行就是一个属性,如下:

    • 添加属性:setAttribute(String name, Object obj);
    • 得到值:getAttribute(String name),这个方法返回Object
    • 删除属性:removeAttribute(String name)

SpringMVC流程

基本流程

springMVC原理

1、 用户发送请求至前端控制器DispatcherServlet。

2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。

3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

4、 DispatcherServlet调用HandlerAdapter处理器适配器。

5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

6、 Controller执行完成返回ModelAndView。

7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

9、 ViewReslover解析后返回具体View。

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、 DispatcherServlet响应用户。

组件

DispatcherServlet:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低组件之间的耦合性,提高每个组件的扩展性。

HandlerMapping:通过扩展处理器映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

HandlAdapter:通过扩展处理器适配器,支持更多类型的处理器。

ViewResolver:通过扩展视图解析器,支持更多类型的视图解析,例如:jsp、freemarker、pdf、excel等。

方法

映射关系

  • @Controller

    • RestController
  • @RequestMapping

    • GetMapping、PostMapping、PutMapping、DeleteMapping、PatchMapping

    RequestMapping中有几个属性,可以对映射进行定义

    属性描述
    value指定请求的实际地址
    method指定请求的method类型
    cosumes指定处理请求的Content-Type,例如application/json, text/html;
    produces指定返回的内容类型,仅当request请求头中的(Accept类型中包含该指定类型才返回
    params指定request中必须包含某些参数值是,才让该方法处理
    headers指定request中必须包含某些指定的header值,才能让该方法处理请求

    comsumes示例:

    @Controller  
    @RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")  
    public void addPet(@RequestBody Pet pet, Model model) {      
        // implementation omitted  
    }  
    

    方法仅处理request Content-Type为“application/json”类型的请求。

    produces示例:

    @Controller  
    @RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, produces="application/json")  
    @ResponseBody  
    public Pet getPet(@PathVariable String petId, Model model) {      
        // implementation omitted  
    }  
    

    方法仅处理request请求中Accept头中包含了"application/json"的请求,同时暗示了返回的内容类型为application/json;

    params示例:

    @Controller  
    @RequestMapping("/owners/{ownerId}")  
    public class RelativePathUriTemplateController {  
    
      @RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, params="myParam=myValue")  
      public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {      
        // implementation omitted  
      }  
    } 
    

    仅处理请求中包含了名为“myParam”,值为“myValue”的请求;

    headers示例:

    @Controller  
    @RequestMapping("/owners/{ownerId}")  
    public class RelativePathUriTemplateController {  
    
    @RequestMapping(value = "/pets", method = RequestMethod.GET, headers="Referer=http://www.ifeng.com/")  
      public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {      
        // implementation omitted  
      }  
    }  
    

    仅处理request的header中包含了指定“Refer”请求头和对应值为“http://www.ifeng.com/”的请求;

处理方法

注解描述
@RequestBody请求body体
@ResponseBody返回body体
@ResponseStatusHttp返回状态码
@PathVariableURL路径变量
@RequestParamquery参数
@RequestHeaderHTTP的请求头
HttpEntity请求头部和消息体
ResonponseEntityHttpEntity+返回状态吗
RequestEntityMethod + URI + HttpEntity +Type

参考

  • HttpEntity、RequestEntity、ResponseEntity

    spring Web使用类HttpEntity包装一个HTTP请求或者响应的以下内容 : 头部和消息体。概念上来讲,可以简单理解成这样 :
    HttpEntity = n headers + 1 body

    Spring Web针对请求和响应两种情况分别继承HttpEntity提供了实现类 :RequestEntity与ResponseEntity

    RequestEntity = HttpEntity + HttpMethod + URI + Type(消息体类型)

    ResponseEntity = HttpEntity + (HttpStatus/Intgeger类型) 状态码

  • 示例

    RestTemplate

    // 用于表示请求消息
     HttpHeaders headers = new HttpHeaders();
     headers.setContentType(MediaType.TEXT_PLAIN);
     HttpEntity<String> entity = new HttpEntity<String>(helloWorld, headers);
     URI location = template.postForLocation("https://example.com", entity);
    
     // 用于接收响应消息
      HttpEntity<String> entity = template.getForEntity("https://example.com", String.class);
     String body = entity.getBody();
     MediaType contentType = entity.getHeaders().getContentType();
    

    controller

    @RequestMapping("/handle")
     public HttpEntity<String> handle() {
       HttpHeaders responseHeaders = new HttpHeaders();
       responseHeaders.set("MyResponseHeader", "MyValue");
       return new HttpEntity<String>("Hello World", responseHeaders);
     }
    

类型转换

  • 概念

    Converter与Formatter都是做类型转换用的

    Converter接口只有一个convert,从S转成T

    public interface Converter<S, T> {
        T convert(S var1);
    }
    

    Formatter接口有2个,一个print,一个parse,都是对象与String之间的接口

    public interface Formatter<T> extends Printer<T>, Parser<T> {
    }
    public interface Printer<T> {
        String print(T var1, Locale var2);
    }
    public interface Parser<T> {
        T parse(String var1, Locale var2) throws ParseException;
    }
    

    Converter是一般工具,可以将一种类型转换成另一种类型。例如,将String转换成Date,或者将Long转换成Date。Converter既可以用在web层,也可以用在其它层中。
    Formatter只能将String转成成另一种java类型。例如,将String转换成Date,但它不能将Long转换成Date。所以,Formatter更适用于web层。

    参考

  • 使用

    Formatter与Converter的使用相同:

    1. 实现响应的Formatter与Converter接口
    2. 通过@Component自动将其添加到SpringBoot容器,这样就会自动生效。
  • 示例

    Formatter

    @Component
    public class DateFormatter implements Formatter<Date> {
    
        @Override
        public Date parse(String text, Locale locale) throws ParseException {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
            System.out.println("parse input text: " + text);
            System.out.println("parse date: " + dateFormat.parse(text));
            return dateFormat.parse(text);
        }
    
    
        @Override
        public String print(Date object, Locale locale) {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
            System.out.println("print method." + object);
            return dateFormat.format(object);
        }
    }
    

    Converter

    @Component
    public class StudentConvert implements Converter<String ,Student> {
    
        @Override
        public Student convert(String text) {
            if (NumberUtils.isParsable(text)) {
                Student s =  new Student();
                s.setAge(Integer.parseInt(text));
                return s;
            }
            return new Student();
        }
    }
    

    参考

定义校验

参考1

基本使用

  • 依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    

    注意这个依赖,有时有Jarkata的依赖,那个加了也没用

  • 验证规则注解

    public class UserDto{
        @NotNull(message="age不能为空")
        private Integer age;
    }
    

    注解主要有:

    注解校验功能
    @AssertFalse必须是false
    @AssertTrue必须是true
    @DecimalMax小于等于给定的值
    @DecimalMin大于等于给定的值
    @Digits可设定最大整数位数和最大小数位数
    @Email校验是否符合Email格式
    @Future必须是将来的时间
    @FutureOrPresent当前或将来时间
    @Max最大值
    @Min最小值
    @Negative负数(不包括0)
    @NegativeOrZero负数或0
    @NotBlank不为null并且包含至少一个非空白字符
    @NotEmpty不为null并且不为空
    @NotNull不为null
    @Null为null
    @Past必须是过去的时间
    @PastOrPresent必须是过去的时间,包含现在
    @Pattern必须满足正则表达式
    @PositiveOrZero正数或0
    @Size校验容器的元素个数

  • Controller上进行验证

    @Slf4j
    @RestController
    public class UserController {
    
        @PostMapping("/user")
        public String test1(@RequestBody @Validated UserDto dto){
            log.info("user is {}",dto);
            return "success";
        }
    }
    

@Valid与@Validated的差异

参考1参考2

  • @Valid

    所在包javax.validation

    级联验证时,可以增加@Valid

    public class CreateMembershipRemarkInDto {
    
        @NotBlank
        private String remark;
    
        @NotBlank
        private String fooStr;
    
        @NotNull
        @Valid  // 增加 @Valid 注解
        private CreateBarInDto bar;
    
        public static class CreateBarInDto {
            @NotBlank
            private String barStr;
        }
    }
    

    不支持分组

  • @Validated

    所在包org.springframework.validation.annotation,@Validated是@Valid 的一次封装,是Spring提供的校验机制使用。@Validated提供了分组验证的功能

    定义分组接口:

    public interface Create {  }
    public interface Update { }
    

    注解规则时,注明分组:

    
    public class UserDto {  
        **//在Update分组时,判断不能为空**  
        @NotEmpty(groups={Update.class})  
        private String id;  
    
    
        @NotNull(message="age不能为空")
        private Integer age;
    
        //name字段不为空,且长度在3-8之间  
        @NotEmpty  
        @Size(min=3,max=8)  
        private String name;  
    }
    

    对于age与name,不分配groups,默认每次都要进行验证。

    Controller上进行验证时,注明分组:

    @Controller  
    public class UserController {  
        @PostMapping("/user")  
        //不需验证ID  
        public String addUser(@Validated({Create.class}) UserDto userDto)  {  
           ...
        }
    }
    
    

    这样,验证时,采用@Validated会更好一些。

BindingResult

除了能直接跑出异常,也可以通过BindingResult,将验证的结果引导Controller内部去使用:

如下:

public String addUser(@Validated UserDto userDto, BindingResult result) {
    List<FieldError> fieldErrors = result.getFieldErrors();
    if(!fieldErrors.isEmpty()){
        return fieldErrors.get(0).getDefaultMessage();
    }
    return "OK";
}

这样在Controller内部可以对验证的结果进行自定义的输出

Exception

以上处理比较麻烦,使用全局异常处理会更好一些。

当我们写了@validated注解,不写BindingResult的时候,Spring 就会抛出异常。由此,可以写一个全局异常处理类来统一处理这种校验异常,从而免去重复组织异常信息的代码。

全局异常处理类只需要在类上标注@RestControllerAdvice,并在处理相应异常的方法上使用@ExceptionHandler注解,写明处理哪个异常即可。

@RestControllerAdvice
public class GlobalControllerAdvice {
    private static final String BAD_REQUEST_MSG = "客户端请求参数错误";
    
    // <1> 处理 form data方式调用接口校验失败抛出的异常 
    @ExceptionHandler(BindException.class)
    public ResultInfo bindExceptionHandler(BindException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
                .map(o -> o.getDefaultMessage())
                .collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
    
    // <2> 处理 json 请求体调用接口校验失败抛出的异常 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultInfo methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
                .map(o -> o.getDefaultMessage())
                .collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
    
    // <3> 处理单个参数校验失败抛出的异常
    @ExceptionHandler(ConstraintViolationException.class)
    public ResultInfo constraintViolationExceptionHandler(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
        List<String> collect = constraintViolations.stream()
                .map(o -> o.getMessage())
                .collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
    
}

视图渲染原理

源码在:org.springframework:spring-webmvc下

结构

参考

ViewResolver

viewResolver的接口只有一个函数:resolveViewName(),根据viewName解析得到View

public interface ViewResolver {

	/**
	 * Resolve the given view by name.
	 ...
	 * ViewResolvers that support internationalization should respect this.
	 * @return the View object, or {@code null} if not found
	 * (optional, to allow for ViewResolver chaining)
	 * @throws Exception if the view cannot be resolved
	 * (typically in case of problems creating an actual View object)
	 */
	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;
}

常见的ViewResolver:

视图解析器说明
AbstractCachingViewResolver一个抽象视图,继承该类可以让视图解析器具有缓存功能
ContentNegotiatingViewResolver用于解析基于Content-Type或Accept的视图
UrlBasedViewResolver一个简单的视图解析器,不做任何匹配,需要视图名和实际视图文件名相同
FreeMarkerViewResolver也是UrlBasedViewResolver的子类,用于FreeMarker视图技术
ResourceBundleViewResolver使用properties配置文件的视图解析器,默认配置文件是类路径下的views.properties
InternalResourceViewResolverUrlBasedViewResolver的一个子类,支持Servlet容器的内部类型(JSP、Servlet、以及JSTL等),可以使用setViewClass(..)指定具体的视图类型

View

view的接口有2个:getContentType与render

public interface View {
	...
	/**
	 * Return the content type of the view, if predetermined.
	 * Can be used to check the view's content type upfront,
	 */
	@Nullable
	default String getContentType() {
		return null;
	}

	/**
	 * Render the view given the specified model.
	 */
	void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception;

}

常见的View:

我们挑几个常用的了解下

视图说明
InternalResourceView将 JSP 或其他资源封装成一个视图,一般 JSP 页面用该视图类
JstlView继承自InternalResourceView,如果 JSP 页面使用了 JSTL 标签,则需要使用该视图类
AbstractPdfViewPDF视图的抽象超类
AbstractXlsView传统XLS格式的Excel文档视图的便捷超类,与Apache POI 3.5及更高版本兼容。
AbstractXlsxViewOffice 2007 XLSX格式的Excel文档视图的便捷超类,兼容Apache POI 3.5及更高版本。
MappingJackson2JsonView将模型数据 通过 Jackson 开源框架的 ObjectMapper 以 JSON 方式输出,继承自AbstractJackson2View
MappingJackson2XmlView将模型数据 通过 Jackson 开源框架的 ObjectMapper 以 xml 方式输出,集成自AbstractJackson2View

流程

流程都是在DispatchServlet中完成,过程包括:

  1. initStrategies

    通过initViewResolvers(context),将所有的ViewResolver加载到ApplicationContext中

  2. doDispatch

    • 请求来到之后,经过doService()进入到doDispatch中进行真正的处理:

      mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    • 处理完成之后,通过处理调到结果

      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    • 在其中进行render操作

      render中,通过view = resolveViewName(viewName, mv.getModelInternal(), locale, request);得到view。这里依次调用viewResolver,找到第一个view返回。

      然后调用view.render(mv.getModelInternal(), request, response)进行渲染

使用

  • Jackson2View的使用

    可以通过注入@JsonComponent注入给MappingJackson2JsonView使用的序列化与反序列化器,从而控制Model到View的渲染过程。

    序列化:

    @JsonComponent
    public class MoneySerializer extends StdSerializer<Money> {
        protected MoneySerializer() {
            super(Money.class);
        }
    
        @Override
        public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeNumber(money.getAmount());
        }
    }
    
    

    反序列化:

    @JsonComponent
    public class MoneyDeserializer extends StdDeserializer<Money> {
        protected MoneyDeserializer() {
            super(Money.class);
        }
    
        @Override
        public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
        }
    }
    

Interceptor

SpringBoot中Interceptor与Nest也类似,实现响应接口,然后通过WebConfigure进行配置即可,源码也是在DispatchServlet中完成的

实现接口

实现HandlerInterceptor接口,该接口有3个函数如下:

public interface HandlerInterceptor {
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		return true;
	}
	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}

preHandle在方法调用前,postHandle与afterCompletion在方法调用后(它们2者的区别在于一个是在view render前,一个在view render后)。

另外还有一个专门用于异步的接口AsyncHandlerInterceptor,它有专门的后处理接口:

public interface AsyncHandlerInterceptor extends HandlerInterceptor {
	default void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response,
			Object handler) throws Exception {
	}
}

专门的意思是postHandle与afterCompletion不会生效。

配置使用

SpringBoot中使用

  • 创建一个@Configuration的WebMvcConfigurer配置类

    这个配置类中,不能带@EnableWebMvc,除非自己想完全控制MVC的配置。

  • 通过addInterceptors(),将自定义的Interceptor加入

源码解析

源码也是在DispatcherServlet中的,在initStrategies中通过initHandlerAdapters,注册了所有的handler,保存在handlerAdapters中。

	protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}

请求来时,在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);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

                // 这里调用Pre
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                // 这里判断是异步,就退出。这里进入到后边的finally中
				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
                // 这里调用Post
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
            
            // 前面看到在这里进行了view的渲染,结束后调用了triggerAfterCompletion
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// 这里是异步调用的后处理,由此它不会调用Post与AfterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

示例

这里借用极客时间的示例:

public class PerformanceInteceptor implements HandlerInterceptor {
    private ThreadLocal<StopWatch> stopWatch = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        StopWatch sw = new StopWatch();
        stopWatch.set(sw);
        sw.start();
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        stopWatch.get().stop();
        stopWatch.get().start();
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        StopWatch sw = stopWatch.get();
        sw.stop();
        String method = handler.getClass().getSimpleName();
        if (handler instanceof HandlerMethod) {
            String beanType = ((HandlerMethod) handler).getBeanType().getName();
            String methodName = ((HandlerMethod) handler).getMethod().getName();
            method = beanType + "." + methodName;
        }
        log.info("{};{};{};{};{}ms;{}ms;{}ms", request.getRequestURI(), method,
                response.getStatus(), ex == null ? "-" : ex.getClass().getSimpleName(),
                sw.getTotalTimeMillis(), sw.getTotalTimeMillis() - sw.getLastTaskTimeMillis(),
                sw.getLastTaskTimeMillis());
        stopWatch.remove();
    }
}
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching
public class WaiterServiceApplication implements WebMvcConfigurer {

	public static void main(String[] args) {
		SpringApplication.run(WaiterServiceApplication.class, args);
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new PerformanceInteceptor())
				.addPathPatterns("/coffee/**").addPathPatterns("/order/**");
	}
    ...
}
# spring 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×