⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.lqdev.cn/2018/11/09/springboot/chapter-thirty-three 「謝謝同学」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

前言

最近有个单位内网系统需要对接统一门户,进行单点登录和待办事项对接功能。一般上政府系统都会要求做统一登录功能,这个没啥问题,反正业务系统都是做单点登录的,改下shiro相关类就好了。看了接入方案,做坑爹的是需要业务系统提供一个webService服务,供统一平台调用。对于ws服务,是真的除了大学期间要去写个调用天气预报的作业后,就再也没有接触过了。查阅了SpringBoot文档后,发现确实有一章节是将webService的,所以,今天就来简单介绍下Spring Web Service的集成和使用吧。

一点知识

何为WebService

Web Service技术,能使得运行在不同机器上的不同应用无须借助附加的、专门的第三方软件或硬件, 就可相互交换数据或集成。依据Web Service规范实施的应用之间,无论它们所使用的语言、平台或内部协议是什么,都可以相互交换数据。

简单的说,WebService就是一种跨编程语言和跨操作系统平台的远程调用技术。

WebServcie技术支持

以下内容摘自百度百科:[Web Service](https://baike.baidu.com/item/Web Service/1215039)

Web Service平台需要一套协议来实现分布式应用程序的创建。任何平台都有它的数据表示方法和类型系统。要实现互操作性,Web Service平台必须提供一套标准的类型系统,用于沟通不同平台、编程语言和组件模型中的不同类型系统。这些协议有:

XML和XSD

可扩展的标记语言Web Service平台中表示数据的基本格式。除了易于建立和易于分析外,XML主要的优点在于它既与平台无关,又与厂商无关。XML是由万维网协会(W3C)创建,W3C制定的XML SchemaXSD定义了一套标准的数据类型,并给出了一种语言来扩展这套数据类型。 Web Service平台是用XSD来作为数据类型系统的。当你用某种语言如VB. NETC#来构造一个Web Service时,为了符合Web Service标准,所有你使用的数据类型都必须被转换为XSD类型。如想让它使用在不同平台和不同软件的不同组织间传递,还需要用某种东西将它包装起来。这种东西就是一种协议,如 SOAP

XSD全称为XML Schemas Definition,即:XML结构定义。是描述xml的,同时遵循xml规范。

SOAP

SOAP即简单对象访问协议(Simple Object Access Protocol),它是用于交换XML(标准通用标记语言下的一个子集)编码信息的轻量级协议。它有三个主要方面:XML-envelope为描述信息内容和如何处理内容定义了框架,将程序对象编码成为XML对象的规则,执行远程过程调用(RPC)的约定。SOAP可以运行在任何其他传输协议上。例如,你可以使用 SMTP,即因特网电子邮件协议来传递SOAP消息,这可是很有诱惑力的。在传输层之间的头是不同的,但XML有效负载保持相同。 Web Service 希望实现不同的系统之间能够用“软件-软件对话”的方式相互调用,打破了软件应用、网站和各种设备之间的格格不入的状态,实现“基于Web无缝集成”的目标。

WSDL

Web Service描述语言WSDL就是用机器能阅读的方式提供的一个正式描述文档而基于XML的语言,用于描述Web Service及其函数、参数和返回值。因为是基于XML的,所以WSDL既是机器可阅读的,又是人可阅读的。

UDDI

UDDI的目的是为电子商务建立标准;UDDI是一套基于Web的、分布式的、为Web Service提供的、信息注册中心的实现标准规范,同时也包含一组使企业能将自身提供的Web Service注册,以使别的企业能够发现的访问协议的实现标准。

何为Spring-Web-Services

Spring Web servicesSpring推出的一款构建webservice服务的框架。其主要侧重点是创建文档驱动的Web服务。Spring Web Services项目促进了契约优先的SOAP服务开发,提供了多种方式来创建灵活的Web服务,这些服务可以通过多种方式处理XML负载。可无缝地使用Spring依赖注入和配置等概念。

Spring-WS项目由由以下几个项目组成:

  • Spring-WS Core(spring-ws-core.jar) - 它是主要模块,提供WebServiceMessage和SoapMessage等中央接口,服务器端框架,强大的消息分发功能和支持类来实现Web服务端点。 它还提供Web Service消费者客户端作为:WebServiceTemplate。
  • Spring-WS Support(spring-ws-support.jar) − 该模块为JMS,电子邮件等提供支持。
  • Spring-WS Security(spring-ws-security.jar) - 该模块负责提供与核心Web服务模块集成的WS-Security实现。 使用这个模块,可以添加主体令牌,签名,加密和解密SOAP消息。该模块允许使用现有的Spring Security实现进行认证和授权。
  • Spring XML(spring-xml.jar) − 该模块为Spring Web Services提供XML支持类。 该模块由Spring-WS框架内部使用。
  • Spring OXM - 该模块提供了XML与对象映射的支持类。

之间的依赖关系,如下图所示:

简单来说,看了官网文档后,一切遵循契约优先原则,请求和响应的参数都应遵循约定,不然wsdl文件生成是错误的,这里踩了坑。。

Spring-WS服务端发布

spring-wsspring-mvc一样,在集成到web项目时,前端有个servlet分发请求消息的概念。 这个servlet接受soap消息,通过映射转发到后端的服务实现类方法中(Endpiont) 在请求进来处理过程中,可以添加,拦截器(Interceptor),异常处理器(ExceptionResolver)。 通过拦截器可以做一些额外的定制功能,比如安全。通过异常处理器定制异常信息显示,处理等。

这个servlet就是MessageDispatcher,来看看官网给出的处理流程图:

处理流程图

所以在需要对请求参数或者响应参数做处理时,可以编写对应的拦截器进行处理的。

现在,以一个简单示例来发布一个webService服务。创建工程:spring-boot-webservice-server

0.引入POM依赖。

<!-- spirng ws 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<!-- 生成wsdl文件 -->
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
</dependency>

1.创建一个xsd文件,用来描述请求和响应的各实体信息。这里简单以一个获取作者信息为例子。 author.xsd

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.lqdev.cn/webservice"
targetNamespace="http://www.lqdev.cn/webservice" elementFormDefault="qualified">

<!-- 定义请求实体 -->
<xs:element name="authorRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>

<!-- 定义响应实体 -->
<xs:element name="authorResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="author" type="tns:author"/>
</xs:sequence>
</xs:complexType>
</xs:element>

<!-- 定义请求实体 -->
<xs:element name="authorListRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="nonce" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>

<!-- 定义响应实体 -->
<xs:element name="authorListResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="author" type="tns:author" minOccurs="1" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>

<!-- 定义作者 信息 -->
<xs:complexType name="author">
<xs:sequence>
<xs:element name="name" type="xs:string" />
<!-- 爱好 列表形式 nillable=true 可为空 ,maxOccurs=unbouned 无限 -->
<xs:element name="hobby" type="xs:string" nillable="true" maxOccurs="unbounded" />
<!-- 性别 枚举类型 限定 -->
<xs:element name="sex" type="tns:sex" />
<!-- 生日 -->
<xs:element name="birthday" type="xs:string" />
<!-- 描述 -->
<xs:element name="description" type="xs:string" />
</xs:sequence>
</xs:complexType>

<!-- 枚举类型 性别:男 女 -->
<xs:simpleType name="sex">
<xs:restriction base="xs:string">
<xs:enumeration value="male"/>
<xs:enumeration value="female"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

这里需要注意,请求和返回的名字是有要求的,两个名字前面要一样,后缀分别是固定的配置,默认为Request和Response; 当然可以通过requestSuffixresponseSuffix属性来修改默认值的,在配置小节会说到。

关于xsd规则,可以查看:http://www.w3school.com.cn/schema/index.asp。

2.根据XDS文件创建实体对象。这里直接使用maven创建自动生成。pom中加入插件:jaxb2-maven-plugin

       <plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<schemaDirectory>${project.basedir}/src/main/resources/</schemaDirectory>
<outputDirectory>${project.basedir}/src/main/java</outputDirectory>
<clearOutputDir>false</clearOutputDir>
<!-- 包名路径 --> <packageName>cn.lqdev.learning.springboot.ws.webservice</packageName>
</configuration>
</plugin>

然后运行下命令:mvn install就会自动创建了。正常情况下,添加后,xsd文件有变动,都会实时创建对应实体对象的。此时,生成的对象如下:

实体对象

3.创建Endpoint服务,有点类似Controller,请求服务的入口。

/**
* 创建endpoint 类似于创建controller了。
* @author oKong
*
*/
@Endpoint
public class AuthorEndpoint {

@PayloadRoot(namespace = WsConst.NAMESPACE_URI, localPart = "authorRequest")
@ResponsePayload
public AuthorResponse getAuthor(@RequestPayload AuthorRequest authorReq){
AuthorResponse resp = new AuthorResponse();
Author author = new Author();
author.setBirthday("1990-01-23");
author.setName("姓名:" + authorReq.getName());
author.setSex(Sex.FEMALE);
author.getHobby().addAll(Arrays.asList("电影","旅游"));
author.setDescription("描述:一枚趔趄的猿。现在时间:" + new Date().getTime());
resp.setAuthor(author);
return resp;
}

@PayloadRoot(namespace = WsConst.NAMESPACE_URI, localPart = "authorListRequest")
@ResponsePayload
public AuthorListResponse getAuthorList(@RequestPayload AuthorListRequest request){
AuthorListResponse resp = new AuthorListResponse();
Author author = new Author();
author.setBirthday("1990-01-23");
author.setName("姓名:oKong");
author.setSex(Sex.FEMALE);
author.getHobby().addAll(Arrays.asList("电影","旅游"));
author.setDescription("描述:一枚趔趄的猿。现在时间:" + new Date().getTime());
resp.getAuthor().add(author);
resp.getAuthor().add(author);
return resp;
}
}

示例代码,只是为了演示,大部分信息都固定写死了。实际开发中,可以加入各自的业务逻辑,引入相应的service类的。

而且,这里需要注意:

  1. 方法声明上的 @PayloadRoot标注中的namespacelocalPart分别就是wsdl中的targetNamespacesoap方法名称。
  2. @ResponsePayload@RequestPayload 这两个标注的用法,以及它们对应的数据类型就是此前通过maven插件对wsdl定义生成的java类。

关于请求参数的类型,是否需要加@RequestPayload说明:

Handling method parameters

一般上,都是使用JAXB2对象了,也就是先前生成的实体对象。当然,有兴趣的同学可以试试,其他的对象参数,可以获取到不同的参数值的。

比如:

public void handle(@RequestPayload Element element)

一个org.w3c.dom.Element对象。

public void handle(@RequestPayload DOMSource domSource, SoapHeader header)

这样,能获取到SOAP的头部信息。

其他相关用法,可以查看此地址:https://docs.spring.io/spring-ws/docs/2.4.2.RELEASE/reference/#server-atEndpoint-methods

关于响应的参数,是否需要加@ResponsePayload,一下是官网给出的说明信息:

Handling method return types

这个有个坑:尝试无参数请求时,使用postman发送xml数据可以正常请求,但使用spirng-ws调用就调用不到了,嗯,我想应该是我调用方法不多。。⊙﹏⊙‖∣

4.创建配置类,生效webservice服务。

/**
* ws-配置
* @author oKong
*
*/
@EnableWs //开启webService
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter{

@Bean
public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);//true 地址会进行转换,不然都是本地地址
//这里可以设置 请求的工厂类,实现有两个:SaajSoapMessageFactory 和 AxiomSoapMessageFactory
//默认是 SaajSoapMessageFactory
// servlet.setMessageFactoryBeanName(messageFactoryBeanName);
return new ServletRegistrationBean(servlet, "/ws/*");
}

//name 就是对应 wsdl名如 :/ws/author.wsdl
@Bean(name = "author")
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema authorSchema) {
DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
wsdl11Definition.setPortTypeName("AuthorPort");
wsdl11Definition.setLocationUri("/ws");
wsdl11Definition.setSchema(authorSchema);
wsdl11Definition.setTargetNamespace(WsConst.NAMESPACE_URI);
return wsdl11Definition;
}

//可自定义SaajSoapMessageFactory 然后指定其SOAP版本
@Bean
public SaajSoapMessageFactory messageFactory() {
SaajSoapMessageFactory messageFactory = new SaajSoapMessageFactory();
//指定版本
messageFactory.setSoapVersion(SoapVersion.SOAP_11);//SoapVersion.SOAP_12
return messageFactory;
}

@Bean
public XsdSchema authorSchema() {
return new SimpleXsdSchema(new ClassPathResource("author.xsd"));
}

@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
//可以自定义拦截器
}
}

常量类:WsConst.java

/**
* 常量类
* @author oKong
*
*/
public class WsConst {
public static final String NAMESPACE_URI = "http://www.lqdev.cn/webservice";
}

5.编写启动类。

/**
* web-service 简单示例
*
* @author oKong
*
*/
@SpringBootApplication
@Slf4j
public class WebServiceApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(WebServiceApplication.class, args);
log.info("spring-boot-webservice-server-chapter33启动!");
}
}

6.启动应用,访问下:http://127.0.0.1:8090/ws/author.wsdl ,可以看见wsdl文件内容了。

wsdl文件

接着,我们使用postman调用下:POST http://127.0.0.1:8090/ws

说明已经正常发布了。接下来,我们使用spring-ws直接调用。

Spirng-WS客户端调用

创建一个新工程:spring-boot-webservice-client

0.引入POM依赖。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
</dependency>

1.获取wsdl文件,放入src\main\resources\schemas\文件夹中,同时加入maven插件:maven-jaxb2-plugin使其生成对应实体列。

<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<version>0.13.3</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<schemaLanguage>WSDL</schemaLanguage>
<generatePackage>cn.lqdev.webservice</generatePackage>
<generateDirectory>${basedir}/src/main/java</generateDirectory>
<schemas>
<schema>
<fileset>
<!-- Defaults to schemaDirectory. -->
<directory>${basedir}/src/main/resources/schemas</directory>
<!-- Defaults to schemaIncludes. -->
<includes>
<include>*.wsdl</include>
</includes>
</fileset>
</schema>
</schemas>
</configuration>
</plugin>

wsdl文件就不贴了。目录为:

生成后对应实体类为:

注意:写此文章前,尝试过使用cxf进行调用,而调用过程中,发现请求的实体需要在包cn.lqdev.webservice路径下,不然校验不通过。所以为了兼容,我直接写成此路径了。对于spring ws而言,包名可以自定义的。不知道cxf是不是可以修改,跟踪了下源码也没有找到具体这个规则是怎么来的,不知道是不是和targetNamespace的值http://www.lqdev.cn/webservice有关,有待测试。

2.创建客户端调用类。

/**
* 编写客户端 继承WebServiceGatewaySupport 类 方便调用
* @author oKong
*
*/
public class WsAuthorClient extends WebServiceGatewaySupport{

/**
* 获取作者信息
* @author oKong
*/
public AuthorResponse getAuthor(String name) {
AuthorRequest req = new AuthorRequest();
req.setName(name);
//使用 marshalSendAndReceive 进行调用
return (AuthorResponse) getWebServiceTemplate().marshalSendAndReceive(req);
}

/**
* 获取作者列表信息
* @author oKong
*/
public AuthorListResponse getAuthorList(){
AuthorListRequest request = new AuthorListRequest();
request.setNonce(UUID.randomUUID().toString());
return (AuthorListResponse) getWebServiceTemplate().marshalSendAndReceive(request);
}
}

此类,就是调用webservice服务的。

4.创建配置类.

/**
* 客户端调用配置
* @author oKong
*
*/
@Configuration
public class WsClientConfig {

@Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
//会扫描此类下面的对应的 jaxb2实体类 因为是使用 Marshaller和 unmarshaller来进行xml和bean直接转换的
//具体是判断此路径下是否包含 ObjectFactory.class 文件
//设置 JAXBContext 对象
marshaller.setContextPath("cn.lqdev.webservice");
//或者使用 以下方式设置
// marshaller.setPackagesToScan(packagesToScan);
// marshaller.setClassesToBeBound(classesToBeBound);
return marshaller;
}

/*
* 创建bean
*/
@Bean
public WsAuthorClient wsClient(Jaxb2Marshaller marshaller) {
WsAuthorClient client = new WsAuthorClient();
//默认对应的ws服务地址 client请求中还能动态修改的
client.setDefaultUri("http://127.0.0.1:8090/ws");
client.setMarshaller(marshaller);//指定转换类
client.setUnmarshaller(marshaller);
return client;
}
}

关于marshallerunmarshaller解析xml和读取xml相关知识,没有过多了解,感兴趣的可以自行搜索相关资料下。

5.创建示例控制层,调用各服务接口。

/**
* 简单调用示例
* @author oKong
*
*/
@RestController
@RequestMapping("/author")
public class DemoController {

@Autowired
WsAuthorClient authorClient;

@GetMapping("/get")
public AuthorResponse getAuthor(String name) {
return authorClient.getAuthor(name);
}

@GetMapping("/list")
public AuthorListResponse getAuthorList() {
return authorClient.getAuthorList();
}
}

6.修改端口号为:8096,同时启动应用。

server.port=8096

使用Postman,访问:http://127.0.0.1:8096/author/get?name=程序员

正常情况下可以看见以下返回内容:

get

说明调用成功了。

访问下列表:http://127.0.0.1:8096/author/list

list

简单使用CXF调用webService

接下类,尝试下使用cxf来访问下服务。

0.引入cxf相关依赖。

<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.1.11</version>
</dependency>

1.controller类新增一个方法,使用cxf方式调用服务。

@GetMapping("/cxf/{method}")
public Object cxf(@PathVariable String method,String name) throws Exception{
//获取客户端工厂类
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
//创建client对象
Client client = dcf.createClient("http://127.0.0.1:8090/ws/author.wsdl");

AuthorListRequest listReq = new AuthorListRequest();
listReq.setNonce(UUID.randomUUID().toString());

AuthorRequest req = new AuthorRequest();
req.setName(name);
//调用 第一个方法是operation 值,即调用方法,
//其后是调用参数。
Object[] objects = new Object[0];
//相关 operation值 可以根据 client.getEndpoint().getBinding().getBindingInfo().getOperations(); 获取
//有兴趣可以看下 client.getEndpoint().getBinding().getBindingInfo()提供的一些方法。

//这里就简单的判断了
if("authorList".equalsIgnoreCase(method)) {
objects = client.invoke("authorList", listReq);
} else {
objects = client.invoke("author", req);
}
//返回的对象objects[0]即为返回的值
return objects[0];
}

这里需要注意:对应的实体类,需要符合wsdl文件中解析出来的type Classes 对应上,本例子为:cn.lqdev.webservice.AuthorListRequest,猜猜应该和targetNamespace值有关,不然会出现以下类似提示:

Part {http://www.lqdev.cn/webservice}authorListRequest should be of type cn.lqdev.webservice.AuthorListRequest, not cn.lqdev.learning.webservice.AuthorListRequest

最后发现使用cxf也很简单呀,下次试试。是利用JAX-WS规范的。

2.重启应用,访问下:http://127.0.0.1:8096/author/cxf/author?name=趔趄的猿 最后效果是一样的。

访问:http://127.0.0.1:8096/author/cxf/authorList

有待补充

以上只是基于官方文档,简单的示例了一遍,具体一些高级用法以及相关安全校验、过滤器等等,没有过多涉及的。之后有时间再填坑吧,毕竟这个用的真的不多呀。

参考资料

  1. https://docs.spring.io/spring-ws/docs/2.4.2.RELEASE/reference/
  2. https://spring.io/guides/gs/producing-web-service/
  3. https://spring.io/guides/gs/consuming-web-service/

总结

本章节主要简单介绍了spring-ws的使用。原本是没有打算写关于WebService相关的。只是机缘巧合下刚好有个对接系统需要用上,就临时尝试一下了。还有很多深入的功能,就没有过多涉及了。等到时候真正开始对接时,有碰到一些问题或者有些知识点补充的,再来补充吧。毕竟,我想现在除了旧系统和政府部门的系统,应该很少再去开发webservice服务了吧。官网文档大致看了下,也确实觉得有点复杂呀,不知道是不是理解能力问题,⊙﹏⊙‖∣。理论上,按着规则走,问题应该也不是很大。就是一些比如无参数如何调用,或者返回参数节点自定义问题,这些理论上都可以使用提供的拦截器来完成的。有问题,还是建议查看官网吧,真的比较详细。最后看了cxf,也比较简单。下一篇就来写写使用cxf来发布webservice,多尝试几种方式~

文章目录
  1. 1. 前言
  2. 2. 一点知识
    1. 2.1. 何为WebService
    2. 2.2. WebServcie技术支持
      1. 2.2.1. XML和XSD
      2. 2.2.2. SOAP
      3. 2.2.3. WSDL
      4. 2.2.4. UDDI
    3. 2.3. 何为Spring-Web-Services
  3. 3. Spring-WS服务端发布
  4. 4. Spirng-WS客户端调用
  5. 5. 简单使用CXF调用webService
  6. 6. 有待补充
  7. 7. 参考资料
  8. 8. 总结