我们知道Spring框架可以帮助开发者实现面向接口编程,基于dependency injection (DI)可以为POJO注入依赖,这样起码实现了编译时解耦(运行时依然是实际对象之间紧耦合);Spring Integration更进一步:SI中POJOs可以作为处理节点,节点之间使用消息通道Channel串接起来形成完整的消息处理管道Pipe,由于Channel是通用组件,因此节点之间完全无耦合(开发编译时和运行时都没有耦合)。这样的应用程序是通过通道组装各种细粒度、可重用的节点构成功能管道(Such an application is built by assembling fine-grained reusable components to form a higher level of functionality.),每一条组装好的管道甚至还可以再次组合,si与springBatch及spring ws等可以结合。
除了内部的这些消息通道,对外SI提供有通道适配器(channel adapters) 和网关(gateways) 来导入外部系统的消息:
1、通道适配器是单向的one-way、只能发或者收;
2、gateway是r-r双向request/reply、可以同时做应答;
比如inbound-channel-adapter只接收不响应可用于文件、inbound-gateway可以响应。 Spring Cloud Stream项目使用SI作为消息驱动的微服务运行引擎。官网示例Examples展示一个gateway接口(而且只有接口)可用于两种不同的服务实现:一个是最简单的请求响应一个是webservice,这需要spring-integration-ws 和spring-integration-xml模块。Spring3位spring-ws扩展了restful支持,si2.0使用了Spring3的REST支持,用spring-ws入栈网关暴露POX/soap接口。
SI的gateway可以作为抽象的调用stub:<int:gateway service-interface="" />. 之后的处理逻辑可以是同样写在xml配置文件里的表达式而不需要代码,和各种DSL结合的轻量xml处理也算是si的目标吧。
si里有用的一个东西是HTTP,因为借助它我们可以绕过soap,soap坑爹很久了,但对于遗留的soap格式交互无法改用rest的,可以基于si HTTP直接处理HTTP soap + 手撸(un)mashall xml,si术语叫做POX(plain old xml)、这样我们终于可以抛开WS-*和soap.
soap属于文本协议,随着SOA盛极一时,甚至有二进制协议已死一说,但是当下二进制协议开始回归,如典型二进制协议RSocket、AMQP,和二进制协议SDK Protobuf 以及HTTP2. 微服务间交互的service Mesh抛弃文本协议重归二进制协议只是时间问题。SOA、soap的兴盛和衰退都有其原因,文本确实有安全、可见、跨平台、方便调试和扩展的优点,SOA时代对于多厂家的异构系统集成来说,soap确实是最省事儿的一站式交互协议,但是期望的系统大集成时代没有到来,没有市场和应用,这些技术自然持续衰败,最终技术圈失去耐心,推到重来,转向推微服务。
Protocol交互协议分为应用层schema/encoding(消息格式如json/xml、AMQP、HTTP)和传输层transport(如TCP/UDP)两方面。一个协议是文本协议还是二进制协议看它在应用层面向的是文本还是数据结构:
1、rest:transport是HTTP;schema随意一般用json(属于schemaless半格式化数据、json不须要类似xml的xsd这种schema),在简单场景下,可以不做序列化直接用官方参考实现JSON-java处理字符串KV映射,也就是说文本协议本质上没有结构,没有结构也就对交互双方没有约束,调用服务之间算是松耦合。
2、soap:transport随意一般是HTTP、只要是跨平台协议都可以如SMTP、MIME;schema固定是xml,典型的文本协议,但采用的文本格式是严格的xml,所以调用服务之间是紧耦合。
3、gRPC:transport是HTTP2;schema固定是Protobuf,属于开放二进制协议。AMQP与之类似,都是vender neutral开放的,目标是天然跨平台:AMQP is the Internet Protocol for Business Messaging.
4、POX:transport是HTTP;schema是xml —— 可以替代soap;
gRPC在非纯文本的大数据量场景可以代替rest、HTTP2+protoBuf代替HTTP1.1+json,单纯的HTTP协议性能并不比TCP差太多,主要是文本的解析性能损耗。跨平台要求Protocol交互协议整体是open Internet Protocol 开放的,包括:
1、transport是开放的传输协议(TCP是一种ad-hoc的开放协议,HTTP当然是全面开放的协议)
2、schema消息格式只要求开放即可,可以是专有实现只要公开,最好为各个异构平台提供SDK.
ws-support:SI与Spring ws联合使用
如果接收到的是soap消息,那么要令Message headers 包含SOAP Action header. 就得使用<int-ws:outbound-gateway...也就是说SI要处理soap ws需要Spring WS的支持,SI在ws方面的feature包括:
1、POX- and SOAP-based gateways
2、Simple HTTP-based integration;经测普通http入栈会抛弃soap消息的SOAP Action头部,不过没什么影响;
至于Spring ws,需要注意的是它只支持“从上到下”的ws开发,也即Contract First. 这很务实,看看ws的历史明白一下为什么soap坑爹:
由于TCP是无处不在的,而HTTP是TCP之上考虑相对来说最全面的、应用最广的“应用层”协议,所以用HTTP进行跨系统的ws交互就成了最通用的一种消息交互形式:
但是实际上并没有对ws的“完全限定”定义,虽然W3C组织定义了soap和wsdl,但是在定义中也有免(甩)责(锅)声明,大意是我们只是定义个意思、工业界具体怎么用我们不管、对于其他定义我们也不置可否...尼玛soap可以看做只是个参考实现而已。在REST和soap之间的争论也表明soap和wsdl绝非ws的唯一选择,而且即使是soap这一边,soap也不是只能以HTTP作为传输协议的。也就是说ws和soap并没有必然联系,作为通用跨系统交互ws形式确实是首选,至于消息格式那你可以自由决定,和ws没关系,soap可以不是ws/HTTP形式的、ws的消息格式也可以不是soap的。oasis的陈述是这样的:A Web Service is a component that is described via WSDL and is capable of being accessed via standard software network protocols such as but not limited to SOAP over HTTP. 但是即使说wsdl自己,它也绝不是一个通用的东西。所以出现了gRPC,依然使用跨平台的Http但是抛弃了xml,固定用proto.
对标准化ws的尝试在1990年前后十分盛行,大多数大型软件商乐于提供一揽子的工具赚钱、还有在WS-*规范里加上自己的一笔,使得WS-*规范几乎无所不包,这种情况可称之为:add-ons 重在掺和...这是囚徒困境,导致soap ws臃肿不堪,复杂且无用,使得很多人开始寻找可替代的、轻量级方案,其中之一就是REST。综上所述:SOAP迅速演变成了一场商业闹剧。
起初ws被认为是一种RPC,之后人们发现ws和RPC的很多差异,ws更像消息,而xml则是数据的平台中立表达,是SOA构想的“世界语”。
实际上传输协议和消息格式本来就是无耦合的,基于Spring WS的SI继续遵循了这个最佳实践,其实在SI之前已经有了基于Spring WS和JMS协议、邮件协议以及XMPP(Extensible Messaging and Presence Protocol)的ws.
SI首推POX,soap也支持。SI的纯HTTP的通道适配器一般是用于非xml的或者是REST的消息。无论是POX还是soap,对xml消息的处理都少不了,这方面建议用Spring OXM module.
OXM模块起初是作为Spring WS子项目的一部分而开发,到spring3.0,OXM成为了spring core框架的一个模块,主要的俩接口:
package org.springframework.oxm;
public interface Marshaller {
boolean supports(Class<?> clazz);
void marshal(Object graph, Result result) throws IOException, XmlMappingException;
}
public interface Unmarshaller {
boolean supports(Class<?> clazz);
Object unmarshal(Source source) throws IOException, XmlMappingException;
}
如果你的场景仅仅是想做JavaBean和XML节点元素的互相转换,而不涉及动态XML的处理——说人话就是直接手撸(un)mashall,那么JAXB绝对是一个不错的选择。在比较新的jdk版本中,JAXB都是jdk的扩展包javax中自带的类库,不需要你引入第三方jar包。如果是已经有了wsdl+xsd的从上到下方式,可以用JDK的对象生成工具:
jdk1.8.0_151\bin>xjc -d C:\...\javaO -p ch.iec -readOnly -wsdl C:\...\*.wsdl -encoding UTF-8
从wsdl生成代码包ch.iec到javaO目录下。JAXB可以与OXM结合使用,其他场景比如是先有代码的从下至上方式,可以对业务对象类加JAXB注释,注意从下至上方式是不好的。@XmlJavaTypeAdapter可以做一些非JDK标准类型到标准类型的转换:
@XmlElement
@XmlJavaTypeAdapter(JodaDateTimeAdapter.class)
private DateTime startOfLegDate;
javax.xml.bind.annotation.adapters.XmlAdapter实现:
public class JodaDateTimeAdapter extends XmlAdapter<Calendar, DateTime> {
@Override
public DateTime unmarshal(Calendar cal) throws Exception {
return new DateTime(cal.getTime(), ISOChronology.getInstanceUTC());
}
@Override
public Calendar marshal(DateTime dt) throws Exception {
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(dt.getMillis());
cal.setTimeZone(TimeZone.getTimeZone("UTC"));
return cal;
}
}
SI基于OXM做了一个很薄的适配器层封装,开箱即用,配置一个Jaxb2Marshaller:
<beans:bean id="legMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<beans:property name="classesToBeBound" value="siia.booking.domain.trip.LegQuoteCommand"/>
</beans:bean>
配置中使用引入si-xml namespace:
http://www.springframework.org/schema/integration/xml
http://www.springframework.org/schema/integration/xml/spring-integration-xml.xsd
在两个通道之间做转换:
<channel id="javaLegQuoteCommands"/> //输入通道
<si-xml:marshalling-transformer input-channel="javaLegQuoteCommands" output-channel="xmlLegQuotes" marshaller="legMarshaller" //si的转换器需要一个marshaller
result-transformer="resultToDocumentTransformer"/>
<beans:bean id="resultToDocumentTransformer" class="org.springframework.integration.xml.transformer.ResultToDocumentTransformer"/>
<channel id="xmlLegQuotes"/> //输出通道、输出payLoad为Result的消息。对javax.xml.transform.Result能做的只是get/set system ID,用于错误消息或者relate to a file on the local filesystem.
POX
POX也可以称之为XML web service with Spring ws. 从Spring WS开始就提倡并支持:可以解耦应用代码和复杂的soap处理,因为soap里头的xm和命名空间l处理很烦人。Spring WS已经做了编程模型上的简化:把spring配置与简单的基于接口/注释的端点相结合,SI继承了这种简单模型,并添加了简化的消息模型、做了EIP的实现。
按照EIP实现ws,可以做诸如对消息的split和聚合,你可以只用Spring WS做到,但再加上SI可以省略不少工作,这就是为什么SI要基于Spring WS提供ws支持。要对外暴露纯正的ws服务,可以用Spring WS的入栈网关,支持POX及soap,配合使用SI的int-ws:inbound-gateway可以将ws请求导入SI消息通道。也可以只用纯HTTP组件,对soap自己做marshal。用Spring WS得配置MessageDispatcherServlet(也可以不配的,它可以将ws请求引入si消息管道),它带有getWsdlDefinition、getXsdSchema方法。该servlet专用于简化ws消息的分发,是对springMVC DispatcherServlet的替代。在Spring-WS中可以基于xsd和一些转换动态生成wsdl。也可以不使用DispatcherServlet,直接配置UriEndpointMapping,不过url path都固定为services.
集成处理一般不需要理解消息内容,那是所集成的业务系统的事,但有时需要对内容做特定处理比如基于内容的路由、消息内容增强之类,这类简单处理可以用XPath或者Extensible Stylesheet Language Transformation (XSLT). 如果是更复杂的处理须要将xml转换为业务对象的,可以用object-to-XML mapping (OXM) 做marshalling and unmarshalling.
spring OXM做了一层技术无关的java与xml映射的通用抽象、通过这个通用抽象层将代码与实现细节解耦,并提供了一致的异常处理层,这种解耦是通过封装Marshaller的对象到xml映射以及Unmarshaller映射实现的:
package org.springframework.oxm;
public interface Marshaller {
boolean supports(Class<?> clazz);
void marshal(Object graph, Result result) throws IOException, XmlMappingException;
}
package org.springframework.oxm;
public interface Unmarshaller {
boolean supports(Class<?> clazz);
Object unmarshal(Source source) throws IOException, XmlMappingException;
}
可以结合使用Java Architecture for XML Binding (JAXB) v2 与 spring OXM. 你可以使用JAXB注释去注释业务类而不是从xsd生成业务类,比如使用javax.xml.bind.annotation.XmlRootElement注释声明文档根,像这样:
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "legQuote")
public class LegQuoteCommand implements Command {
private Leg leg;
private HotelCriteria hotelCriteria;
private FlightCriteria flightCriteria; .......
还可以使用XmlJavaTypeAdapter注释去适配一些非JDK类:
@XmlRootElement
public class Leg {
@XmlElement
@XmlJavaTypeAdapter(JodaDateTimeAdapter.class)
private DateTime startOfLegDate; .....
JodaDateTimeAdapter 是这样的:
public class JodaDateTimeAdapter extends XmlAdapter<Calendar, DateTime> {
@Override
public DateTime unmarshal(Calendar cal) throws Exception {
return new DateTime(cal.getTime(), ISOChronology.getInstanceUTC());
}
@Override
public Calendar marshal(DateTime dt) throws Exception {
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(dt.getMillis());
cal.setTimeZone(TimeZone.getTimeZone("UTC"));
return cal;
}
}