远程处理和Web服务
Spring使用各种技术为远程处理提供支持。远程处理支持简化了远程支持服务的开发,通过Java接口和对象作为输入和输出实现。目前,Spring支持以下远程处理技术:
- 远程方法调用(RMI):通过使用RmiProxyFactoryBean和RmiServiceExporter,Spring支持传统的RMI(使用java.rmi.Remote接口和java.rmi.RemoteException)以及通过RMI调用器(使用任何Java接口)进行透明远程处理。
- Spring HTTP Invoker:Spring提供了一种特殊的远程处理策略,允许通过HTTP进行Java序列化,支持任何Java接口(就像RMI调用器那样)。相应的支持类是HttpInvokerProxyFactoryBean和HttpInvokerServiceExporter。
- Hessian:通过使用Spring的HessianProxyFactoryBean和HessianServiceExporter,你可以通过Caucho提供的基于HTTP的轻量级二进制协议透明地公开你的服务。
- Java Web Services:Spring通过JAX-WS为Web服务提供远程处理支持。
- JMS:spring-jms模块中的JmsInvokerServiceExporter和JmsInvokerProxyFactoryBean类支持通过JMS作为底层协议进行远程处理。
- AMQP:通过AMQP作为底层协议的远程处理由单独的 Spring AMQP项目支持。
在讨论Spring的远程处理功能时,我们使用以下域模型和相应的服务:
public class Account implements Serializable{ private String name; public String getName(){ return name; } public void setName(String name) { this.name = name; } }
public interface AccountService { public void insertAccount(Account account); public List<Account> getAccounts(String name); }
// the implementation doing nothing at the moment public class AccountServiceImpl implements AccountService { public void insertAccount(Account acc) { // do something... } public List<Account> getAccounts(String name) { // do something... } }
本节从使用RMI向远程客户机公开服务开始,并讨论使用RMI的缺点。然后以一个使用Hessian作为协议的示例继续。
一、RMI(远程方法调用)
通过使用Spring对RMI的支持,你可以通过RMI基础设施透明地公开你的服务。设置完成后,你基本上拥有一个类似于远程EJB的配置,除了没有对安全上下文传播或远程事务传播的标准支持。当你使用RMI调用器时,Spring确实为这种额外的调用上下文提供了钩子,因此你可以(例如)插入安全框架或自定义安全凭证。
使用RmiServiceExporter导出服务
使用RmiServiceExporter,我们可以将AccountService对象的接口公开为RMI对象。接口可以通过使用RmiProxyFactoryBean访问,或者在传统RMI服务的情况下通过普通RMI访问。RmiServiceExporter明确支持通过RMI调用器公开任何非RMI服务。
我们首先要在Spring容器中设置服务。下面的示例演示如何执行此操作:
<bean id="accountService" class="example.AccountServiceImpl"> <!-- any additional properties, maybe a DAO? --> </bean>
接下来,我们必须使用RmiServiceExporter公开我们的服务。下面的示例演示如何执行此操作:
<bean class="org.springframework.remoting.rmi.RmiServiceExporter"> <!-- does not necessarily have to be the same name as the bean to be exported --> <property name="serviceName" value="AccountService"/> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> <!-- defaults to 1099 --> <property name="registryPort" value="1199"/> </bean>
servicePort属性已被省略(默认为0)。这意味着使用匿名端口与服务通信。
在客户端连接服务
我们的客户机是一个简单的对象,它使用AccountService来管理帐户,如下例所示:
public class SimpleObject { private AccountService accountService; public void setAccountService(AccountService accountService) { this.accountService = accountService; } // additional methods using the accountService }
为了在客户机上连接服务,我们创建一个单独的Spring容器,以包含以下简单对象和服务链接配置:
<bean class="example.SimpleObject"> <property name="accountService" ref="accountService"/> </bean> <bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl" value="rmi://HOST:1199/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
这就是我们需要做的,以支持客户端上的远程帐户服务。Spring透明地创建一个调用器,并通过RmiServiceExporter远程启用帐户服务。在客户端,我们使用RmiProxyFactoryBean将其链接起来。
二、使用Hessian通过HTTP远程调用服务
Hessian提供了一个基于HTTP的二进制远程处理协议。它是由Caucho开发的,你可以访问https://www.caucho.com/ 。
Hessian
Hessian通过HTTP进行通信,并使用一个定制的servlet进行通信。通过使用Spring的DispatcherServlet规则,我们可以连接这样一个servlet来公开你的服务。首先,我们必须在应用程序中创建一个新的servlet,如下面的web.xml文件:
<servlet> <servlet-name>remoting</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--springmvc.xml 是自己创建的SpringMVC全局配置文件,用contextConfigLocation作为参数名来加载 如果不配置 contextConfigLocation,那么默认加载的是/WEB-INF/servlet名称-servlet.xml,在这里也就是 springmvc-servlet.xml 参数多个值使用逗号隔开,如:a.xml,b.xml <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:config/springmvc.xml</param-value> </init-param> --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>remoting</servlet-name> <url-pattern>/remoting/*</url-pattern> </servlet-mapping>
如果你熟悉Spring的DispatcherServlet规则,那么你可能知道现在必须在WEB-INF目录创建一个名为remoting-servlet.xml的Spring容器配置。下一节将使用应用程序上下文。
或者,考虑使用Spring更简单的HttpRequestHandlerServlet。这样可以将远程导出器定义嵌入根应用程序上下文(默认情况下,在WEB-INF/applicationContext.xml),单个servlet定义指向特定的导出器bean。在本例中,每个servlet名称都需要与其目标导出器的bean名称相匹配。
使用HessianServiceExporter暴露你的bean
在这个新建的名为remoting-servlet.xml的应用程序上下文中,我们创建一个HessianServiceExporter来导出我们的服务,如下例所示:
<bean id="accountService" class="example.AccountServiceImpl"> <!-- any additional properties, maybe a DAO? --> </bean> <bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
现在我们已经准备好在客户端连接服务了。没有指定显式的处理程序映射(将请求url映射到服务上),所以我们使用BeanNameUrlHandlerMapping。因此,服务在包含DispatcherServlet实例的映射(如前所定义)中的bean名称所指示的URL处导出:http://主机:8080/远程处理/AccountService。
或者,你可以在根应用程序上下文中(例如,在WEB-INF中)创建HessianServiceExporter/applicationContext.xml),如下例所示:
<bean name="accountExporter" class="org.springframework.remoting.caucho.HessianServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
在后一种情况下,你应该在web.xml文件为这个导出器定义对应的Servlet,导出器被映射到位于/remoting/AccountService的请求路径。请注意,servlet名称需要与目标导出器的bean名称匹配。下面的示例演示如何执行此操作:
<servlet> <servlet-name>accountExporter</servlet-name> <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>accountExporter</servlet-name> <url-pattern>/remoting/AccountService</url-pattern> </servlet-mapping>
在客户端连接服务
通过使用HessianProxyFactoryBean,我们可以在客户端连接服务。同样的原则适用于RMI。我们创建一个单独的bean工厂或应用程序上下文,并在SimpleObject通过使用AccountService来管理帐户,如下例所示:
<bean class="example.SimpleObject"> <property name="accountService" ref="accountService"/> </bean> <bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean"> <property name="serviceUrl" value="http://remotehost:8080/remoting/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
对通过Hessian公开的服务应用进行HTTP基本身份验证
Hessian的一个优点是我们可以很容易地应用HTTP基本身份验证,因为这两个协议都是基于HTTP的。你的常规HTTP服务器安全机制可以通过使用web.xml文件,例如,安全功能。通常,你不需要在这里对每个使用用户进行安全凭据验证。相反,你可以使用在HessianProxyFactoryBean级别定义的共享凭据(类似于JDBC数据源),如下例所示:
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"> <property name="interceptors" ref="authorizationInterceptor"/> </bean> <bean id="authorizationInterceptor" class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor"> <property name="authorizedRoles" value="administrator,operator"/> </bean>
在上面的示例中,我们显式地提到了BeanNameUrlHandlerMapping 映射,并设置了一个拦截器,以便仅允许管理员和操作员调用此应用程序上下文中提到的bean。
三、Spring HTTP调用程序
与Hessian相反,Spring HTTP invokers都是轻量级协议,它们使用自己的slim序列化机制,并使用标准Java序列化机制通过HTTP公开服务。如果参数和返回类型是无法使用Hessian使用的序列化机制进行序列化的复杂类型,则这有很大的优势
在幕后,Spring使用JDK或apachehttpcomponents提供的标准工具来执行HTTP调用。
请注意由于不安全的Java反序列化导致的漏洞:在反序列化步骤中,被操纵的输入流可能导致服务器上执行不需要的代码。因此,不要向不受信任的客户端公开HTTP调用程序端点。相反,只在你自己的服务之间公开它们。一般来说,我们强烈建议使用其他任何消息格式(如JSON)。
如果你担心Java序列化导致的安全漏洞,请考虑核心JVM级别的通用序列化过滤器机制,该机制最初是为JDK 9开发的,但同时后移植到JDK 8、7和6。
https://blogs.oracle.com/java-platform-group/entry/incoming_filter_serialization_data_a 和 https://openjdk.java.net/jeps/290
公开服务对象
为服务对象设置HTTP invoker 基础设施与使用Hessian进行相同操作的方式非常相似。Hessian支持提供HessianServiceExporter,Spring的HttpInvoker支持提供org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.
要在Spring Web MVC DispatcherServlet中公开AccountService,需要在调度器的应用程序上下文中设置以下配置,如下例所示:
<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
或者,你可以在根应用程序上下文(例如,在WEB-INF/applicationContext.xml中)中创建HttpInvokerServiceExporter,如下例所示:
<bean name="accountExporter" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
此外,你可以在中为此导出器定义相应的servlet web.xml文件,servlet名称与目标导出器的bean名称匹配,如下例所示:
<servlet> <servlet-name>accountExporter</servlet-name> <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>accountExporter</servlet-name> <url-pattern>/remoting/AccountService</url-pattern> </servlet-mapping>
在客户端连接服务
同样,从客户端连接服务与使用Hessian时的方式非常相似。通过使用代理,Spring可以将你对http post请求的调用转换为指向导出服务的URL。以下示例显示如何配置:
<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"> <property name="serviceUrl" value="http://remotehost:8080/remoting/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
如前所述,你可以选择要使用的HTTP客户端。默认情况下,HttpInvokerProxy使用JDK的HTTP功能,但是你也可以通过设置httpInvokerRequestExecutor属性来使用apache HttpComponents客户机。下面的示例演示如何执行此操作:
<property name="httpInvokerRequestExecutor"> <bean class="org.springframework.remoting.httpinvoker.HttpComponentsHttpInvokerRequestExecutor"/> </property>
四、Java WebService
Spring完全支持标准Java web services API:
- 使用JAX-WS公开web服务
- 使用JAX-WS访问web服务
除了Spring Core中对JAX-WS的支持外,Spring Portfolio还提供了SpringWeb服务,它是契约优先、文档驱动的Web服务的解决方案,强烈推荐用于构建现代的、面向未来的Web服务。
使用JAX-WS公开基于Servlet的Web服务
Spring为JAX-WS Servlet端点实现提供了一个方便的基类:SpringBeanAutowiringSupport。
为了公开AccountService,我们扩展了Spring的SpringBeanAutowiringSupport类,并在这里实现业务逻辑,通常将调用委托给业务层。我们使用Spring的@Autowired注释来表达对Spring托管bean的依赖性。下面的示例显示了扩展SpringBeanAutowiringSupport的类:
import org.springframework.web.context.support.SpringBeanAutowiringSupport; @WebService(serviceName="AccountService") public class AccountServiceEndpoint extends SpringBeanAutowiringSupport { @Autowired private AccountService biz; @WebMethod public void insertAccount(Account acc) { biz.insertAccount(acc); } @WebMethod public Account[] getAccounts(String name) { return biz.getAccounts(name); } }
我们的AccountServiceEndpoint需要在与Spring上下文相同的web应用程序中运行,以允许访问Spring的设施。在 Java EE环境中,默认情况下,使用 JAX-WS Servlet端点部署的标准契约。
使用JAX-WS导出独立的Web服务
Oracle JDK附带的内置JAX-WS提供程序通过使用JDK中包含的内置HTTP服务器来支持web服务的公开。Spring的SimpleJaxWsServiceExporter检测Spring应用程序上下文中所有带@WebService注释的bean,并通过默认的JAX-WS服务器(JDK HTTP服务器)导出它们。
在这个场景中,端点实例被定义和管理为spring bean。它们在JAX-WS引擎中注册,但它们的生命周期取决于Spring应用程序上下文。这意味着你可以将Spring功能(例如显式依赖注入)应用于端点实例。Autowired injection通过@Autowired注解驱动。下面的示例演示如何定义这些bean:
<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter"> <property name="baseAddress" value="http://localhost:8080/"/> </bean> <bean id="accountServiceEndpoint" class="example.AccountServiceEndpoint"> ... </bean>
AccountServiceEndpoint可以但不必从Spring的SpringBeanauthoringSupport派生,因为本例中的端点是一个完全由Spring管理的bean。这意味着端点实现可以如下所示(没有声明任何超类 - ,Spring的@Autowired配置注释仍然适用):
@WebService(serviceName="AccountService") public class AccountServiceEndpoint { @Autowired private AccountService biz; @WebMethod public void insertAccount(Account acc) { biz.insertAccount(acc); } @WebMethod public List<Account> getAccounts(String name) { return biz.getAccounts(name); } }
使用JAX-WS RI的Spring支持导出Web服务
作为GlassFish项目的一部分开发的Oracle的JAX-WS RI将Spring支持作为其JAX-WS commons项目的一部分提供。这允许将JAX-WS端点定义为Spring托管bean,类似于上一节讨论的独立模式,但这次是在Servlet环境中。
这在Java EE环境中是不可移植的。它主要用于非EE环境,如Tomcat,这些环境将JAX-WS RI作为web应用程序的一部分嵌入其中。
与导出基于servlet的端点的标准样式的不同之处在于,端点实例本身的生命周期由Spring管理,并且只有一个JAX-WS servletweb.xml文件中定义。 使用标准Java EE样式,每个服务端点都有一个servlet定义,每个端点通常委托给spring bean(通过使用@Autowired)。
使用JAX-WS访问Web服务
Spring提供了两个工厂bean来创建JAX-WS web服务代理,LocalJaxWsServiceFactoryBean 和JaxWsPortProxyFactoryBean。前者只能返回一个JAX-WS服务类供我们使用。后者是完整的版本,可以返回实现业务服务接口的代理。在下面的示例中,我们使用JaxWsPortProxyFactoryBean为AccountService端点创建一个代理:
<bean id="accountWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean"> <property name="serviceInterface" value="example.AccountService"/> <property name="wsdlDocumentUrl" value="http://localhost:8888/AccountServiceEndpoint?WSDL"/> <property name="namespaceUri" value="http://example/"/> <property name="serviceName" value="AccountService"/> <property name="portName" value="AccountServiceEndpointPort"/> </bean>
- serviceInterface是客户端使用的业务接口。
- wsdlDocumentUrl是WSDL文件的URL。Spring需要在启动时使用它来创建JAX-WS服务。
- namespaceUri对应于.wsdl文件中的targetNamespace。
- serviceName与.wsdl文件中的服务名称相对应。
- portName对应于.wsdl文件中的端口名。
访问web服务很容易,因为我们为它提供了一个bean工厂,它将它公开为一个名为AccountService的接口。下面的示例显示了如何在Spring将其连接起来:
<bean id="client" class="example.AccountClientImpl"> ... <property name="service" ref="accountWebService"/> </bean>
从客户端代码中,我们可以像访问普通类一样访问web服务,如下例所示:
public class AccountClientImpl { private AccountService service; public void setService(AccountService service) { this.service = service; } public void foo() { service.insertAccount(...); } }
上面的内容稍微简化了,因为JAX-WS要求端点接口和实现类用@WebService、@SOAPBinding等注释进行注释。这意味着你不能(轻松地)将纯Java接口和实现类用作JAX-WS端点构件;你需要首先对它们进行相应的注释。
五、JMS(Java消息服务)
你还可以通过使用JMS作为底层通信协议来透明地公开服务。Spring框架中的JMS远程处理支持非常基本。它在同一个线程和同一个非事务性会话中发送和接收。因此,吞吐量取决于实现。注意,这些单线程和非事务性约束仅适用于Spring的JMS远程处理支持。
服务器端和客户端都使用以下接口:
package com.foo; public interface CheckingAccountService { public void cancelAccount(Long accountId); }
在服务器端使用上述接口的以下简单实现:
package com.foo; public class SimpleCheckingAccountService implements CheckingAccountService { public void cancelAccount(Long accountId) { System.out.println("Cancelling account [" + accountId + "]"); } }
以下配置文件包含在客户端和服务器上共享的JMS基础结构bean:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://ep-t43:61616"/> </bean> <bean id="queue" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg value="mmm"/> </bean> </beans>
服务器端配置
在服务器上,你需要公开使用JmsInvokerServiceExporter的服务对象,如下例所示:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="checkingAccountService" class="org.springframework.jms.remoting.JmsInvokerServiceExporter"> <property name="serviceInterface" value="com.foo.CheckingAccountService"/> <property name="service"> <bean class="com.foo.SimpleCheckingAccountService"/> </property> </bean> <bean class="org.springframework.jms.listener.SimpleMessageListenerContainer"> <property name="connectionFactory" ref="connectionFactory"/> <property name="destination" ref="queue"/> <property name="concurrentConsumers" value="3"/> <property name="messageListener" ref="checkingAccountService"/> </bean> </beans>
package com.foo; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Server { public static void main(String[] args) throws Exception { new ClassPathXmlApplicationContext("com/foo/server.xml", "com/foo/jms.xml"); } }
客户端配置
客户机只需要创建一个实现约定接口(CheckingAccountService)的客户端代理。
下面的示例定义了可以注入到其他客户端对象中的bean(代理负责通过JMS将调用转发到服务器端对象):
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="checkingAccountService" class="org.springframework.jms.remoting.JmsInvokerProxyFactoryBean"> <property name="serviceInterface" value="com.foo.CheckingAccountService"/> <property name="connectionFactory" ref="connectionFactory"/> <property name="queue" ref="queue"/> </bean> </beans>
package com.foo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Client { public static void main(String[] args) throws Exception { ApplicationContext ctx = new ClassPathXmlApplicationContext("com/foo/client.xml", "com/foo/jms.xml"); CheckingAccountService service = (CheckingAccountService) ctx.getBean("checkingAccountService"); service.cancelAccount(new Long(10)); } }
六、AMQP
Spring AMQP项目支持通过AMQP作为底层协议进行远程处理。
♣ 远程接口没有实现自动检测
对于远程接口,不会自动检测已实现接口的主要原因是避免向远程调用方打开太多的门。目标对象可以实现内部回调接口,例如InitializegBean或DisposableBean,这些接口不想向调用方公开。
在本地情况下,提供包含目标实现的所有接口的代理通常并不重要。但是,当你导出远程服务时,你应该公开一个特定的服务接口,其中包含用于远程使用的特定操作。除了内部回调接口外,目标可能实现多个业务接口,其中只有一个用于远程公开。由于这些原因,我们需要指定这样的服务接口。
这是配置便利性和内部方法意外暴露风险之间的权衡。总是指定一个服务接口并不需要太多的努力,它会使你在特定方法的受控公开方面处于安全的一面。
七、选择技术时的注意事项
这里介绍的每一种技术都有其缺点。在选择一种技术时,你应该仔细考虑你的需求、你公开的服务以及你通过网络发送的对象。
当使用RMI时,你不能通过HTTP协议访问对象,除非你对RMI通信进行隧道化。RMI是一个相当重的协议,因为它支持完整的对象序列化,当你使用需要通过网络进行序列化的复杂数据模型时,这一点非常重要。但是,RMI-JRMP与Java客户端绑定。它是一个Java到Java的远程处理解决方案。
如果你需要基于HTTP的远程处理,但也依赖于Java序列化,那么Spring的HTTP调用器是一个不错的选择。它与RMI调用程序共享基本的基础设施,但使用HTTP作为传输。请注意,HTTP调用程序不仅限于Java到Java的远程处理,而且还包括客户端和服务器端的Spring。(后者也适用于非RMI接口的Spring RMI调用器。)
当在异构环境中操作时,Hessian可能会提供重要的价值,因为它们明确地允许非Java客户机。但是,非Java支持仍然是有限的。已知的问题包括Hibernate对象与延迟初始化集合的序列化。如果你有这样的数据模型,请考虑使用RMI或HTTP调用程序,而不是Hessian。
JMS对于提供服务集群和让JMS代理处理负载平衡、发现和自动故障转移非常有用。默认情况下,Java序列化用于JMS远程处理,但是JMS提供程序可以使用不同的机制来进行有线格式化,例如XStream,以便在其他技术中实现服务器。
最后但并非最不重要的是,EJB比RMI有一个优势,它支持标准的基于角色的身份验证和授权以及远程事务传播。也可以让RMI调用器或HTTP调用器来支持安全上下文传播,尽管核心Spring没有提供这一功能。Spring只为插入第三方或定制解决方案提供了合适的钩子。
八、REST服务
Spring框架为调用REST服务提供了两种选择:
- RestTemplate:原始的spring rest客户机,具有同步的模板方法API。
- WebClient:一种无阻塞、响应式的替代方案,支持同步和异步以及流式场景。
RestTemplate
RestTemplate是一个执行HTTP请求的同步客户端。它是原始的spring rest客户端,在底层HTTP客户端库上公开了一个简单的模板方法API。
从5.0开始,Restemplate处于维护模式,以后只接受少量的更改和bug请求。请考虑使用WebClient,它提供了一个更现代的API,并支持同步、异步和流式处理方案。
RestTemplate 通过HTTP客户端库提供了更高级别的API。这使得在一行中调用REST端点变得很容易。它公开了以下重载方法组:
- getForObject:使用GET返回一个对象。
- getForEntity:使用GET返回ResponseEntity(即状态、头和正文)。
- headForHeaders:使用HEAD检索资源的所有标头。
- postForLocation:使用POST创建新资源,并从响应返回位置标头。
- postForObject:使用POST返回一个对象。
- postForEntity:使用POST返回ResponseEntity(即状态、头和正文)。
- put:使用PUT创建或更新资源。
- patchForObject:使用PATCH更新资源并从响应返回表示形式。请注意,JDK HttpURLConnection不支持该修补程序,但 Apache HttpComponents 和其他组件支持。
- optionsForAllow:使用ALLOW检索资源允许的HTTP方法。
- delete:使用DELETE删除指定URI处的资源。
- exchange:它接受RequestEntity(包括HTTP方法、URL、headers和body作为输入)并返回ResponseEntity。这些方法允许使用ParameterizedTypeReference而不是类来指定带有泛型的响应类型。
- execute:执行请求的最通用的方法,通过回调接口完全控制请求准备和响应提取。
初始化
默认构造函数使用java.net.HttpURLConnection执行请求。你可以使用ClientHttpPrequestFactory的实现切换到其他HTTP库。内置支持以下功能:
- Apache HttpComponents
- Netty
- OkHttp
例如,要切换到Apache HttpComponents,可以使用以下命令:
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
每个ClientHttpPrequestFactory都公开特定于底层HTTP客户机库的配置选项,例如凭据、连接池和其他详细信息。
URIs
许多RestTemplate 方法接受URI模板和URI template变量,要么作为字符串变量参数,要么作为Map<String,String>。
以下示例使用字符串变量参数:
String result = restTemplate.getForObject( "https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");
以下示例使用Map<String,String>:
Map<String, String> vars = Collections.singletonMap("hotel", "42"); String result = restTemplate.getForObject( "https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
请记住,URI模板是自动编码的,如下例所示:
restTemplate.getForObject("https://example.com/hotel list", String.class); // Results in request to "https://example.com/hotel%20list"
可以使用RestTemplate 的uriTemplateHandler属性自定义uri的编码方式。或者,你可以准备java.net.URI并将其传递到接受URI的RestTemplate 方法之一。
Headers
可以使用exchange()方法指定请求头,如下例所示:
String uriTemplate = "https://example.com/hotels/{hotel}"; URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity<Void> requestEntity = RequestEntity.get(uri) .header(("MyRequestHeader", "MyValue") .build(); ResponseEntity<String> response = template.exchange(requestEntity, String.class); String responseHeader = response.getHeaders().getFirst("MyResponseHeader"); String body = response.getBody();
你可以通过返回ResponseEntity的许多RestTemplate 方法变量获取响应头。
Body
在HttpMessageConverter的帮助下,传入RestTemplate 方法和从RestTemplate 方法返回的对象在原始内容之间进行转换。
在POST中,输入对象被序列化到请求正文,如下例所示:
URI location = template.postForLocation("https://example.com/people", person);
你不需要显式地设置请求的内容类型头。在大多数情况下,你可以找到基于源对象类型的兼容消息转换器,并且所选的消息转换器会相应地设置内容类型。如果需要,你可以使用exchange方法显式地提供Content-Type请求头,而这反过来又会影响所选择的消息转换器。
在GET中,响应的主体被反序列化为输出对象,如下例所示:
Person person = restTemplate.getForObject("https://example.com/people/{id}", Person.class, 42);
不需要显式设置请求的Accept头。在大多数情况下,可以根据预期的响应类型找到兼容的消息转换器,这有助于填充Accept报头。如果需要,可以使用exchange方法显式地提供Accept头。
默认情况下,RestTemplate 注册所有内置的消息转换器,这取决于类路径检查,这些检查有助于确定存在哪些可选的转换库。你还可以将消息转换器设置为显式使用。
消息转换
Spring Web模块包含HttpMessageConverter契约,用于通过InputStream和OutputStream读写HTTP请求和响应的主体。HttpMessageConverter实例用于客户端(例如,在restemplate中)和服务器端(例如,在springmvc REST控制器中)。
框架中提供了主媒体(MIME)类型的具体实现,默认情况下,在客户端向RestTemplate注册,在服务器端向RequestMethodHandlerAdapter注册。
HttpMessageConverter的实现将在以下部分中描述。对于所有转换器,都使用默认的媒体类型,但是可以通过设置SupportedMediaTypesBean属性来覆盖它。下表描述了每个实现:
- StringHttpMessageConverter:HttpMessageConverter实现,可以从HTTP请求和响应读取和写入字符串实例。默认情况下,此转换器支持所有文本媒体类型(text/*),并使用text/plain的内容类型进行写入。
- FormHttpMessageConverter:HttpMessageConverter实现,可以从HTTP请求和响应中读取和写入表单数据。默认情况下,此转换器读写application/x-www-form-urlencoded媒体类型。表单数据被读取并写入多值映射<String,String>。
- ByteArrayHttpMessageConverter:HttpMessageConverter实现,可以从HTTP请求和响应读取和写入字节数组。默认情况下,此转换器支持所有媒体类型(*/*),并且写入内容类型为application/octet stream。你可以通过设置supportedMediaTypes属性并重写getContentType(byte[])来覆盖它。
- MarshallingHttpMessageConverter:一个HttpMessageConverter实现,它可以使用Spring的Marshaller和Unmarshaller抽象来读写XMLorg.springframework.oxm包裹。此转换器需要封送器和解组器才能使用。你可以通过构造函数或bean属性注入这些内容。默认情况下,此转换器支持text/xml和application/xml。
- MappingJackson2HttpMessageConverter:一个HttpMessageConverter实现,可以使用Jackson的ObjectMapper读写JSON。你可以根据需要通过使用Jackson提供的注释定制JSON映射。当需要进一步控制时(对于需要为特定类型提供自定义JSON序列化程序/反序列化程序的情况),可以通过ObjectMapper属性注入自定义ObjectMapper。默认情况下,此转换器支持application/json。
- MappingJackson2XmlHttpMessageConverter:一个HttpMessageConverter实现,可以使用Jackson XML扩展的XmlMapper读写XML。你可以根据需要通过使用JAXB或Jackson提供的注释来定制XML映射。当需要进一步控制时(对于需要为特定类型提供自定义XML序列化程序/反序列化程序的情况),可以通过ObjectMapper属性注入自定义XmlMapper。默认情况下,此转换器支持application/xml。
- SourceHttpMessageConverter:一个可以读写的HttpMessageConverter实现javax.xml.transform。来自HTTP请求和响应的源。仅支持DOMSource、SAXSource和StreamSource。默认情况下,此转换器支持text/xml和application/xml。
- BufferedImageHttpMessageConverter:一个可以读写的HttpMessageConverter实现java.awt.image.BufferedImage来自HTTP请求和响应。此转换器读取和写入javai/oapi支持的媒体类型。
Jackson JSON视图
可以指定Jackson JSON视图来仅序列化对象属性的一个子集,如下例所示:
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); value.setSerializationView(User.WithoutPasswordView.class); RequestEntity<MappingJacksonValue> requestEntity = RequestEntity.post(new URI("https://example.com/user")).body(value); ResponseEntity<String> response = template.exchange(requestEntity, String.class);
Multipart
要发送multipart数据,你需要提供一个MultiValueMap<String,Object>,其值可以是部分内容的对象、文件部分的资源或带有头的部分内容的HttpEntity。例如:
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); parts.add("fieldPart", "fieldValue"); parts.add("filePart", new FileSystemResource("...logo.png")); parts.add("jsonPart", new Person("Jason")); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_XML); parts.add("xmlPart", new HttpEntity<>(myBean, headers));
在大多数情况下,不不必为每个部分指定内容类型。内容类型是根据选择序列化它的HttpMessageConverter自动确定的,或者对于基于文件扩展名的资源。如果需要,可以显式地为MediaType提供HttpEntity包装器。
一旦MultiValueMap准备就绪,就可以将其传递给RestTemplate ,如下所示:
MultiValueMap<String, Object> parts = ...; template.postForObject("https://example.com/upload", parts, Void.class);
如果MultiValueMap至少包含一个非字符串值,则FormHttpMessageConverter将内容类型设置为multipart/form data。如果MultiValueMap有字符串值,则内容类型默认为application/x-www-form-urlencoded。如果需要,还可以显式设置内容类型。
WebClient
WebClient是从Spring WebFlux 5.0版本开始提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具。它的响应式编程的基于Reactor的。WebClient中提供了标准Http请求方式对应的get、post、put、delete等方法,可以用来发起相应的请求。
创建WebClient的最简单方法是通过静态工厂方法之一:
- WebClient.create()
- WebClient.create(String baseUrl)
你也可以使用WebClient.builder()有更多选项:
- uriBuilderFactory:自定义的uriBuilderFactory用作基URL。
- defaultHeader:每个请求的头。
- defaultCookie:每个请求的Cookies。
- defaultRequest:消费者定制每个请求。
- filter:每个请求的客户端过滤器。
- exchangeStategies:HTTP消息读取器/编写器自定义。
- clientConnector:HTTP客户端库设置。
以下示例配置HTTP编解码器:
WebClient client = WebClient.builder() .exchangeStrategies(builder -> { return builder.codecs(codecConfigurer -> { //... }); }) .build();
一旦构建,WebClient实例是不可变的。但是,可以在不影响原始实例的情况下克隆它并生成修改后的副本,如下例所示:
WebClient client1 = WebClient.builder() .filter(filterA).filter(filterB).build(); WebClient client2 = client1.mutate() .filter(filterC).filter(filterD).build(); // client1 has filterA, filterB // client2 has filterA, filterB, filterC, filterD
最大内存大小
spring webflux配置了在编解码器中缓冲内存数据的限制,以避免应用程序内存问题。默认情况下,它被配置为256KB,如果这对于你的用例来说还不够,你将看到以下内容:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
你可以使用以下代码示例在所有默认编解码器上配置此限制:
Java:
WebClient webClient = WebClient.builder() .exchangeStrategies(builder -> builder.codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) ) ) .build();
Reactor Netty
要自定义Reactor Netty设置,只需提供预配置的HttpClient:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...); WebClient webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .build();
资源
默认情况下,HttpClient参与持有的全球Reactor Netty resourcesreactor.netty.http.HttpResources,包括事件循环线程和连接池。这是推荐的模式,因为固定的共享资源是事件循环并发的首选。在此模式下,全局资源将保持活动状态,直到进程退出。
如果服务器与进程同步,则通常不需要显式关闭。但是,如果服务器可以在进程内启动或停止(例如,部署为WAR的Spring MVC应用程序),则可以使用globalResources=true(默认值)声明一个Spring管理的bean,类型为Reactor Netty global resources,以确保在Spring ApplicationContext关闭时Reactor Netty全局资源,如下例所示:
@Bean public ReactorResourceFactory reactorResourceFactory() { return new ReactorResourceFactory(); }
你也可以选择不参与全球Reactor Netty 网络资源。但是,在这种模式下,确保所有Reactor Netty客户机和服务器实例使用共享资源的负担就在您身上,如下例所示:
@Bean public ReactorResourceFactory resourceFactory() { ReactorResourceFactory factory = new ReactorResourceFactory(); factory.setGlobalResources(false); ① return factory; } @Bean public WebClient webClient() { Function<HttpClient, HttpClient> mapper = client -> { // Further customizations... }; ClientHttpConnector connector = new ReactorClientHttpConnector(resourceFactory(), mapper); ② return WebClient.builder().clientConnector(connector).build(); ③ }
① 创建独立于全球资源的资源。
② 将ReactorClientHttpConnector构造函数与资源工厂一起使用。
③ 将Connector插入WebClient.Builder.
超时
配置连接超时
import io.netty.channel.ChannelOption; HttpClient httpClient = HttpClient.create() .tcpConfiguration(client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));
配置读和/或写超时值:
import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.WriteTimeoutHandler; HttpClient httpClient = HttpClient.create() .tcpConfiguration(client -> client.doOnConnected(conn -> conn .addHandlerLast(new ReadTimeoutHandler(10)) .addHandlerLast(new WriteTimeoutHandler(10))));
Jetty
以下示例显示如何自定义Jetty HttpClient设置:
HttpClient httpClient = new HttpClient(); httpClient.setCookieStore(...); ClientHttpConnector connector = new JettyClientHttpConnector(httpClient); WebClient webClient = WebClient.builder().clientConnector(connector).build();
默认情况下,HttpClient创建自己的资源(Executor、ByteBufferPool、Scheduler),这些资源在进程退出或调用stop()之前保持活动状态。
您可以在Jetty客户机(和服务器)的多个实例之间共享资源,并通过声明JettyResourceFactory类型的Spring托管bean来确保在Spring ApplicationContext关闭时关闭资源,如下例所示:
@Bean public JettyResourceFactory resourceFactory() { return new JettyResourceFactory(); } @Bean public WebClient webClient() { Consumer<HttpClient> customizer = client -> { // Further customizations... }; ClientHttpConnector connector = new JettyClientHttpConnector(resourceFactory(), customizer); ① return WebClient.builder().clientConnector(connector).build(); ② }
retrieve()
retrieve()方法是获取响应体并对其进行解码的最简单方法。下面的示例演示如何执行此操作:
WebClient client = WebClient.create("https://example.org"); Mono<Person> result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToMono(Person.class);
您还可以从响应中获得解码的对象流,如下例所示:
Flux<Quote> result = client.get() .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) .retrieve() .bodyToFlux(Quote.class);
默认情况下,带有4xx或5xx状态代码的响应将导致WebClientResponseException或其HTTP状态特定的子类之一,如WebClientResponseException.BadRequest,WebCli公司entResponseException.NotFound,以及其他。也可以使用onStatus方法自定义生成的异常,如下例所示:
Mono<Person> result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .retrieve() .onStatus(HttpStatus::is4xxServerError, response -> ...) .onStatus(HttpStatus::is5xxServerError, response -> ...) .bodyToMono(Person.class);
使用onStatus时,如果预期响应包含内容,则onStatus回调应使用它。否则,内容将被自动清空,以确保资源被释放。
exchange()
exchange()方法比retrieve方法提供更多的控制。以下示例相当于retrieve(),但也提供了对ClientResponse的访问:
Mono<Person> result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .exchange() .flatMap(response -> response.bodyToMono(Person.class));
在此级别,您还可以创建完全响应:
Mono<ResponseEntity<Person>> result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .exchange() .flatMap(response -> response.toEntity(Person.class));
请注意(与retrieve())不同,对于exchange(),4xx和5xx响应没有自动错误信号。您必须检查状态代码并决定如何继续。
使用exchange()时,必须始终使用ClientResponse的任何body或toEntity方法,以确保释放资源并避免HTTP连接池的潜在问题。如果不需要响应内容,你可以用bodytomo(Void.class)。但是,如果响应确实包含内容,则连接将关闭,并且不会放回池中。
Request Body
可以从对象对请求正文进行编码,如下例所示:
Mono<Person> personMono = ... ; Mono<Void> result = client.post() .uri("/persons/{id}", id) .contentType(MediaType.APPLICATION_JSON) .body(personMono, Person.class) .retrieve() .bodyToMono(Void.class);
未完待续......