默认的 Spring 框架使用RequestMappingHandlerMapping
进行请求路径映射,它会根据@RequestMapping
注解进行路径映射,此外,Spring 框架还提供一些其它的路径映射方式。
使用 BeanNameUrlHandlerMapping
可以用 Bean 名称处理路径映射,在配置类中添加:
/**
* 处理器映射器,根据 bean 名称进行路径映射
* @return
*/
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping(){
return new BeanNameUrlHandlerMapping();
}
要搭配一个SimpleControllerHandlerAdapter
使用:
/**
* 处理器适配器,可以调用实现了 Controller 接口的控制器
* @return
*/
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter(){
return new SimpleControllerHandlerAdapter();
}
这个处理器适配器可以调用实现了Controller
接口的控制器。
因为在用 Bean 名称映射 url 请求的情况下,一个控制器 Bean 只能处理一个路径,换言之只应当有一个处理器方法,
Controller
接口就是定义此种情况的控制器类。
添加控制器类定义:
"/hello")
(static class HelloController implements Controller{
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("Hello World");
return null;
}
}
控制器类实现了Controller
接口,并且用 Bean 名称(/hello
)设定了可以处理的请求路径。
注意,此类 Bean 名称以
/
开头。
同样,也可以用 Bean 方法添加此类控制器 Bean:
"/bye")
(public Controller byeController(){
return new Controller() {
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("bye");
return null;
}
};
}
自定义 HandlerMapping
可以自己实现BeanNameUrlHandlerMapping
:
static class MyHandlerMapping implements HandlerMapping {
private ApplicationContext applicationContext;
private Map<String, Controller> urlControllerMap = new HashMap<>();
public void postConstruct() {
// 取容器中所有的路径名称控制器 bean,生成路径-控制器映射
// 获取所有实现了 Controller 接口的 bean
Map<String, Controller> controllerMap = applicationContext.getBeansOfType(Controller.class);
if (controllerMap.isEmpty()) {
return;
}
// 过滤掉名称不以 / 开头的 bean
urlControllerMap = controllerMap.entrySet().stream()
.filter(e -> e.getKey().startsWith("/"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
log.debug("收集到的 url-controller 映射:");
urlControllerMap.forEach((k, v) -> {
log.debug("{}->{}", k, v);
});
}
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
String url = request.getRequestURI();
Controller controller = urlControllerMap.get(url);
log.debug("路径{}匹配到{}", url, controller);
if (controller == null) {
return null;
}
return new HandlerExecutionChain(controller);
}
}
同样的,可以用自定义HandlerAdapter
取代SimpleControllerHandlerAdapter
:
static class MyHandlerAdapter implements HandlerAdapter {
/**
* 是否支持控制器方法调用
*
* @param handler the handler object to check
* @return
*/
public boolean supports(Object handler) {
// 只能调用实现了 Controller 接口的控制器 bean
return handler instanceof Controller;
}
/**
* 调用控制器方法
*
* @param request current HTTP request
* @param response current HTTP response
* @param handler the handler to use. This object must have previously been passed
* to the {@code supports} method of this interface, which must have
* returned {@code true}.
* @return
* @throws Exception
*/
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof Controller controller) {
return controller.handleRequest(request, response);
}
return null;
}
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
}
HandlerAdapter
接口的getLastModified
方法已经被废弃,简单返回-1
即可。
完整示例可以看。
RouterFunctionMapping
RouterFunctionMapping
使用RequestPredicate
作为路径映射依据,将路径映射到HandlerFunction
上。
在配置类在中添加:
public RouterFunctionMapping routerFunctionMapping(){
return new RouterFunctionMapping();
}
这个映射器对应的适配器是HandlerFunctionAdapter
:
public HandlerFunctionAdapter handlerFunctionAdapter(){
return new HandlerFunctionAdapter();
}
路径映射规则和控制器方法封装在RouterFunction
类型中:
@Bean
public RouterFunction<ServerResponse> helloRouterFunction(){
return RouterFunctions.route(RequestPredicates.GET("/hello"), new HandlerFunction<ServerResponse>() {
@Override
public ServerResponse handle(ServerRequest request) throws Exception {
return ServerResponse.ok().body("Hello World");
}
});
}
@Bean
public RouterFunction<ServerResponse> byeRouterFunction(){
return RouterFunctions.route(RequestPredicates.GET("/bye"), new HandlerFunction<ServerResponse>() {
@Override
public ServerResponse handle(ServerRequest request) throws Exception {
return ServerResponse.ok().body("bye");
}
});
}
RouterFunctions.route
可以创建一个RouterFunction
,第一个参数接收RequestPredicate
类型,代表一个路径映射规则,比如RequestPredicates.GET("/hello"
就表示按照请求方法是GET
,路径是/hello
进行映射。第二个参数表示映射规则匹配后需要执行的控制器方法,用HandlerFunction
类型表示,其返回值类型是ServerResponse
或其子类。可以通过ServerResponse.ok().body("bye")
这样的方式返回一个状态码是 200,响应体是bye
的ServerResponse
对象。
完整示例见。
SimpleUrlHandlerMapping
SimpleUrlHandlerMapping
可以对静态资源进行路径映射,对应的适配器是HttpRequestHandlerAdapter
:
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
Map<String, ResourceHttpRequestHandler> urlMap = context.getBeansOfType(ResourceHttpRequestHandler.class);
handlerMapping.setUrlMap(urlMap);
return handlerMapping;
}
@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
return new HttpRequestHandlerAdapter();
}
要注意的是,一般的HandlerMapping
会在生命周期方法afterPropertiesSet
中从容器收集路径映射,但SimpleUrlHandlerMapping
并没有这么做,因此需要手动为其完成路径映射的初始化工作(handlerMapping.setUrlMap
)。
静态资源的路径映射表示为ResourceHttpRequestHandler
类型:
@Bean("/**")
public ResourceHttpRequestHandler htmlResourceHttpRequestHandler() {
ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler();
resourceHttpRequestHandler.setLocations(List.of(new ClassPathResource("html/")));
return resourceHttpRequestHandler;
}
@Bean("/img/**")
public ResourceHttpRequestHandler imagesResourceHttpRequestHandler() {
ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler();
resourceHttpRequestHandler.setLocations(List.of(new ClassPathResource("images/")));
return resourceHttpRequestHandler;
}
处理的路径由 Bean 名称指定(可以使用通配符),比如/**
表示可以匹配任意的路径请求,/img/**
表示匹配以/img/
开头的请求。请求的目标资源目录由resourceHttpRequestHandler.setLocations
指定。这里通过ClassPathResource
设置类路径下的指定目录中的资源。
最终的效果是访问 http://localhost:8080/img/2.jpg 时,返回给浏览器的是 /images/2.jpg 文件。访问 http://localhost:8080/index.html 时,返回给浏览器的是 /html/index.html 文件。
完整示例看。
ResourceResolver
在ResourceHttpRequestHandler
的生命周期方法afterPropertiesSet
中,会初始化一个ResourceResolver
列表:
默认情况列表中只包含一个基本的实现PathResourceResolver
,这个资源解析器的效果是返回路径对应的资源文件。可以通过人为添加ResourceResolver
列表的方式扩展ResourceHttpRequestHandler
的功能:
@Bean("/**")
public ResourceHttpRequestHandler htmlResourceHttpRequestHandler() {
ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler();
resourceHttpRequestHandler.setResourceResolvers(List.of(
new CachingResourceResolver(new ConcurrentMapCache("resourceCache")), // 缓存
new EncodedResourceResolver(), // 压缩
new PathResourceResolver())); // 获取资源文件
resourceHttpRequestHandler.setLocations(List.of(new ClassPathResource("html/")));
return resourceHttpRequestHandler;
}
CachingResourceResolver
的作用是缓存请求资源,第二次请求会从缓存中获取,不再使用原始资源文件。创建时候需要指定一个缓存实现方式,这里使用的是ConcurrentMapCache
。
注意,这里三个 ResourceResolver 定义顺序是有意义的,请求先要查找有没有缓存,再查找有没有压缩后的资源,如果都没有,才通过路径获取原始资源文件。
这里依次调用 ResourceResolver 处理对静态资源的请求,实际上使用了职责链模式。
应用启动后,对同一个页面第二次请求,就会看到日志信息:
需要在 logback 中开启相应的日志,级别为 TRACE。
EncodedResourceResolver
的作用是使用压缩后的资源文件,这样可以缩小网络传输的数据体积。EncodedResourceResolver
并不会自动压缩,需要在请求进入服务器前就为资源文件准备好相应的压缩文件。
为配置类添加生命周期方法:
@PostConstruct
public void postConstruct() throws IOException {
// 为 html 文件生成压缩文件
// 获取 html 文件
ClassPathResource classPathResource = new ClassPathResource("html/");
File dir = classPathResource.getFile();
File[] files = dir.listFiles(ff -> ff.getName().endsWith(".html"));
if (files == null) {
return;
}
for (File file : files) {
// 生成压缩文件
FileInputStream fileInputStream = new FileInputStream(file);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(new FileOutputStream(file.getAbsolutePath() + ".gz"));
byte[] buffer = new byte[1024];
do{
int len = fileInputStream.read(buffer);
gzipOutputStream.write(buffer, 0, len);
}
while (fileInputStream.available() > 0);
fileInputStream.close();
gzipOutputStream.close();
}
}
应用启动后会在 target 目录生成压缩后的资源文件:
从浏览器调试信息中也能看到,返回的响应报文使用了 gzip 压缩:
完整示例看。
WelcomePageHandlerMapping
使用WelcomePageHandlerMapping
可以在访问根路径(http://localhost:8080/)的时候让页面返回指定的欢迎页。在配置类中添加:
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context){
Resource indexHtmlResource = context.getResource("classpath:html/index.html");
return new WelcomePageHandlerMapping(null, context, indexHtmlResource, "/**");
}
WelcomePageHandlerMapping
在创建的时候需要指定容器和欢迎页的资源对象(如果是静态资源页)。
注意,
WelcomePageHandlerMapping
类没有指定访问限定符,也就是说它是protected
,因此测试代码必须在包org.springframework.boot.autoconfigure.web.servlet
下才能使用这个类。
WelcomePageHandlerMapping
的构造器中实际上是为根路径添加了一个控制器,这个控制器实现了Controller
接口:
因此还需要像之前那样,添加一个能调用Controller
接口的控制器的适配器:
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter(){
return new SimpleControllerHandlerAdapter();
}
应用启动后能看到:
完整示例见。
总结
Spring 主要有这几种 HandlerMapping:
-
RequestMappingHandlerMapping
,最主要的 HandlerMapping,用于解析@RequestMapping
注解,生成路径和HandlerMethod
的映射。 -
WelcomePageHandlerMapping
,用于将根路径映射到欢迎页,会生成一个实现了Controller
接口的控制器用于处理请求。 -
BeanNameUrlHandlerMapping
,用于将 Bean 名称作为路径进行映射,映射到的目标控制器都实现了Controller
接口。 -
RouterFunctionMapping
,将路径映射到匿名函数(RouterFunction
) -
SimpleUrlHandlerMapping
,映射静态资源文件。
它们可以同时使用,优先级从上到下,RequestMappingHandlerMapping
的优先级最高,SimpleUrlHandlerMapping
的优先级最低。
主要有以下几种HandlerAdapter
:
-
RequestMappingHandlerAdapter
,用于处理和调用HandlerMethod
类型的 handler -
SimpleControllerHandlerAdapter
,用于处理和调用实现了Controller
接口的 handler -
HandlerFunctionAdapter
,用于处理和调用匿名函数(HandlerFunction
)类型的 handler -
HttpRequestHandlerAdapter
,用于处理静态资源(HttpRequestHandler
)类型的 handler
可以看到,HandlerMapping 的主要用途是将请求映射(Mapping)到不同的处理器(Handler),因此它叫处理器映射(Handler Mapping)。而由 HandlerMapping 生成的不同类型的处理器,由不同的 HandlerAdapter 处理和调用,因此它叫做处理器适配器(Handler Adapter)。
本文的完整示例可以从获取。
文章评论