一般Web应用服务器默认的Servlet名称是"default",因此DefaultServletHttpRequestHandler可以找到它。如果你所有的Web应用服务器的默认Servlet名称不是"default",则需要通过default-servlet-name属性显示指定:<mvc:default-servlet-handler default-servlet-name="xxx" />
如果你是基于注解驱动的呢?这么做即可:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { // 注册 一个默认的servlet处理器 让它处理静态资源 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { DefaultServletHandlerConfigurer handlerConfigurer = new DefaultServletHandlerConfigurer(servletContext); // 这个Configurer只有一个enable方法,内部会 new DefaultServletHttpRequestHandler(); handlerConfigurer.enable("default"); //默认值的servlet名字就是default super.configureDefaultServletHandling(configurer); } }
这样就会把这个DefaultServletHttpRequestHandler这样子:
protected SimpleUrlHandlerMapping buildHandlerMapping() { if (this.handler == null) { return null; } SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping(); handlerMapping.setUrlMap(Collections.singletonMap("/**", this.handler)); handlerMapping.setOrder(Integer.MAX_VALUE); return handlerMapping; }
使用的SimpleUrlHandlerMapping把handler和url映射上。并且order的数学怒是最后一名。它最终使用的适配器是:HttpRequestHandlerAdapter去做适配~
// @since 3.0.4 可见它是一个HttpRequestHandler public class DefaultServletHttpRequestHandler implements HttpRequestHandler, ServletContextAware { }
方法2.采用<mvc:resources /> @since 3.0.4
<mvc:default-servlet-handler />将静态资源的处理经由Spring MVC框架交回Web应用服务器处理。
而<mvc:resources />更进一步,由Spring MVC框架自己处理静态资源,并添加一些有用的附加值功能。
首先,<mvc:resources />允许静态资源放在任何地方,如WEB-INF目录下、类路径下等,你甚至可以将JavaScript等静态文件打到JAR包中(为后续的webjar做好了充分的支持~)。通过location属性指定静态资源的位置,由于location属性是Resources类型,因此可以使用诸如"classpath:"等的资源前缀指定资源位置。
传统Web容器的静态资源只能放在Web容器的根路径下,<mvc:resources />完全打破了这个限制。
其次,<mvc:resources />依据当前著名的Page Speed、YSlow等浏览器优化原则对静态资源提供优化。你可以通过cacheSeconds属性指定静态资源在浏览器端的缓存时间,一般可将该时间设置为一年,以充分利用浏览器端的缓存。在输出静态资源时,会根据配置设置好响应报文头的Expires 和 Cache-Control值
这样在接收到静态资源的获取请求时,会检查请求头的Last-Modified值,如果静态资源没有发生变化,则直接返回303/304响应状态码,提示客户端使用浏览器缓存的数据,而非将静态资源的内容输出到客户端,以充分节省带宽,提高程序性能。
基于XML的使用示例:
<!-- 一次性可以写多个路径,里面都可议放置静态资源文件~ location表示资源文件的位置 mapping:表示请求URL--> <mvc:resources location="/,classpath:/META-INF/publicResources/" mapping="/resources/**"/> // 当然我们也可以写成多个,类似这样子 <mvc:resources location="/img/" mapping="/img/**"/> <mvc:resources location="/js/" mapping="/js/**"/> <mvc:resources location="/css/" mapping="/css/**"/>
以上配置将Web根路径"/"及类路径下 /META-INF/publicResources/ 的目录映射为/resources路径
它的核心处理类为:ResourceHttpRequestHandler,也是个HttpRequestHandler。ResourcesBeanDefinitionParser是用于解析XML里的这个标签的~~~~
如果你是基于注解驱动的呢?这么做即可:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resource/**").addResourceLocations("/WEB-INF/static/"); // 支持Ant风格的路径匹配 } }
最终每个Mapping都是对应着一个HttpRequestHandler处理器的
Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>(); // 这个HttpRequestHandler是这么生成的 // 使用的ResourceHttpRequestHandler 因为我们无法指定locationValues 所以这里是[] 空集合 // 个人感觉这是Spring的一个问题,后面有时间回去社区里反馈此问题~~~(我使用的Spring版本为5.1.x) protected ResourceHttpRequestHandler getRequestHandler() { ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); // 给设置好ResourceResolvers if (this.resourceChainRegistration != null) { handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers()); handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers()); } // location们 可以发现每个mapping相当于对应着所有的location的 handler.setLocationValues(this.locationValues); // 设置浏览器cache相关内容 if (this.cacheControl != null) { handler.setCacheControl(this.cacheControl); } else if (this.cachePeriod != null) { handler.setCacheSeconds(this.cachePeriod); } return handler; }
看看它的源码
// @since 3.0.4 可以看到它实现的接口很多 是非常多的强大的一个资源处理器 // EmbeddedValueResolverAware:它能处理占位符~~~~ // 它的afterPropertiesSet()方法里做了非常多的事~~~~ public class ResourceHttpRequestHandler extends WebContentGenerator implements HttpRequestHandler, EmbeddedValueResolverAware, InitializingBean, CorsConfigurationSource { ... }
说明:HttpRequestHandler也是一个Handler,它能够处理请求。由HttpRequestHandlerAdapter进行调用HttpRequestHandler的handleRequest(request, response)方法
以上是Spring MVC中最为重要的两个HttpRequestHandler,下面来看看关于远程调用的两个HttpRequestHandler
HttpInvokerServiceExporter
HttpInvoker是过去常用的Java同构系统之间方法调用实现方案。它通过HTTP通信即可实现两个Java系统之间的远程方法调用,使得系统之间的通信如同调用本地方法一般。
HttpInvoker和RMI同样使用JDK自带的序列化方式,但是HttpInvoker采用HTTP方式通信,这更容易配合防火墙、网闸的工作。
RMI:使用JRMP协议(基于TCP/IP),不允许穿透防火墙,使用JAVA系列化方式,使用于任何JAVA应用之间相互调用
Hessian:使用HTTP协议,允许穿透防火墙,使用自己的系列化方式,支持JAVA、C++、.Net等跨语言使用
Spring HTTP Invoker: 使用HTTP协议,允许穿透防火墙,使用JAVA系列化方式,但仅限于Spring应用之间使用,即调用者与被调用者都必须是使用Spring框架的应用。
服务端书写:
配置你需要暴露出来的服务端的接口:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { // 服务端:把HelloService暴露出去 服务端对接口必须有具体的实现~ @Bean("/helloService") // 注意这里的BeanName请使用/路径的形式 (推荐使用这种方式,简单明了~~~) public HttpInvokerServiceExporter helloServiceExporter(HelloService helloService) { HttpInvokerServiceExporter exporter = new HttpInvokerServiceExporter(); exporter.setService(helloService); // 真正的实现类 这个对服务端来说是必须的 exporter.setServiceInterface(HelloService.class); // 必须设置一个接口~~~~ return exporter; } }
显然我们访问:http://localhost:8080/demo_war_war/helloService反馈如下:
不是404说明咱们的请求是通的,服务端存在这个URL的映射了。
另外一种方式:就是配置一个HttpRequestHandlerServlet
如下:
//@Autowired //private ServletContext servletContext; // 需要注意的是:在Spring的config里面,是不允许通过servletContext再向web容器内添加servlet的~~~~ // 注册一个HttpRequestHandlerServlet 来专门处理client端的请求 // 要求:此servletName必须是上面对应的Bean的BeanName~~~~ //servletContext.addServlet("helloServiceExporter", new HttpRequestHandlerServlet()).addMapping("/helloService"); // 这里在SPI里完成Servlet的注册~~~ public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected void registerDispatcherServlet(ServletContext servletContext) { super.registerDispatcherServlet(servletContext); // 注册我们自己的Servlet 因为这个servlet在jar包里面 所以这里只能通过编程的方式注册~~~~ // 请保证这个servlet的名字和BeanName相同~~ helloServiceExporter:为Bean的名字~~~ servletContext.addServlet("helloServiceExporter", new HttpRequestHandlerServlet()).addMapping("/helloService"); servletContext.addServlet("helloServiceExporter", new HttpRequestHandlerServlet()).addMapping("/helloService"); } }
这样就不要求BeanName必须是/开头咯~~~~
因为服务端需要提供HTTP请求服务,而且是基于Servlet的,所以
服务端
需要跑在如Tomcat这样的Servlet Web容器上客户端书写:
写一个和服务端一样的接口(不需要实现类)配置上:
@Configuration public class RootConfig { @Bean("helloServiceFactoryBean") public HttpInvokerProxyFactoryBean helloServiceFactoryBean() { HttpInvokerProxyFactoryBean factoryBean = new HttpInvokerProxyFactoryBean(); factoryBean.setServiceInterface(HelloService.class); factoryBean.setServiceUrl("http://localhost:8080/demo_war_war/helloService"); return factoryBean; } }
单元测试:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class, AsyncConfig.class}) public class TestSpringBean { @Autowired private HelloService helloService; @Test public void test1() { System.out.println(helloService); // HTTP invoker proxy for service URL [http://localhost:8080/demo_war_war/helloService] System.out.println(helloService.getClass()); //class com.sun.proxy.$Proxy30 System.out.println(helloService.hello()); //service hello System.out.println("结束~"); } }
由此可见,我们的Client就这样就访问到服务端得接口。不用再自己去构造Http请求了,就像本地方法调用一样,使用起来确实非常方便。
Tips:
该种方式暴露请求,和请求方式。建议把Bean的注册都放在跟容器里面,而不是web子容器里。(若放在web子容器里,使用HttpRequestHandlerServlet这种方式的时候可能会找到Bean,亲测~)
HessianServiceExporter
使用方式同上。只是使用的是HessianServiceExporter和HessianProxyFactoryBean。它也可以使用HttpRequestHandlerServlet做servlet化处理。当然还是推荐BeanName使用"/"的方式去表示~
另外,若你想手动注册web组件,还可以自己实现一个SCI的方式:
@HandlesTypes(value={XXX.class}) public class MyServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set<Class<?>> arg0, ServletContext sc) throws ServletException { // 在这里里面完成所有的注册工作~~~~ } }
目前,Spring支持三种远程技术:
- 远程方法调用(RMI):RmiProxyFactoryBean 和 RmiServiceExporter
- Spring的HTTP调用器: HttpInvokerProxyFactoryBean和HttpInvokerServiceExporter
- Hessian:HessianProxyFactoryBean 和 HessianServiceExporter(使用Caucho提供的基于HTTP的轻量级二进制协议)
附:URL与Servlet的路径匹配规则:
当一个url与多个servlet的匹配规则可以匹配时,则按照:“ 精确路径 > 最长路径>扩展名”这样的优先级匹配到对应的servlet。 所以一般来说我们的DispatcherServlet一般都是最后被匹配上的(若有多个Servlet匹配的情况下)
自定义一个HttpRequestHandler的实现
它的使用方式和Servlet的使用方式特别的像:
@Controller("/demoHttpRequestHandler") public class DemoHttpRequestHandler implements HttpRequestHandler { @Override public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("this my DemoHttpRequestHandler"); response.getWriter().write("this my DemoHttpRequestHandler"); } }
就这样访问http://localhost:8080/demo_war_war/demoHttpRequestHandler
,控制台和浏览器都会有对应的输出。看看Serevlet的使用:
@WebServlet("/myServlet") public class MyServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.service(req, resp); } }
使用方式几乎如出一辙。所以上面才会说:其类似于一个简单的Servlet