NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

logger(SLF4j和log4j)

| Comments

SLF4j

Simple Logging Facade for Java (SLF4J)

SLF4J,简单日志门面(Facade),是各个不同日志框架的抽象,它不是具体的日志解决方案,只服务于各种各样的日志系统,例如java.util.logging, log4j等,它允许最终用户在部署其应用时使用其所希望的日志系统。

什么意思?SLF4J就像一个接口,是一个门面,在写代码时,我们只需要根据这个接口提供的方法去记录日志。这个接口可以和不同的日志框架绑定,比如Java自带的java.util.logging,或者大家常听到的log4j。这样,你可以在不修改代码的情况下,去替换不同的日志框架。

基本概念明白之后,看下面这个例子:

1
2
3
4
5
6
7
8
9
import org.slf4j.logger;
import org.slf4j.loggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    logger logger = loggerFactory.getlogger(HelloWorld.class);
    logger.info("Hello World");
  }
}
1
2
3
4
5
6
7
8
9
10
apply plugin: 'idea'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.7'
}

输出的结果是:

SLF4J: Failed to load class “org.slf4j.impl.StaticloggerBinder”.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticloggerBinder for further details.

原因是你没有将SLF4j和某一个具体的日志框架绑定。怎么绑定呢?不需要你做任何事情,简单的加一个依赖就可以了。

1
2
3
4
5
6
7
8
9
10
11
apply plugin: 'idea'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.7'
    compile 'org.slf4j:slf4j-simple:1.7.7'
}

输出是:[main] INFO log.HelloWorld - Hello World

SLF4J支持对很多不同框架的绑定:

slf4j-log4j12-1.7.7.jar
slf4j-jdk14-1.7.7.jar
slf4j-nop-1.7.7.jar
slf4j-simple-1.7.7.jar
slf4j-jcl-1.7.7.jar

那么有个问题,SLF4J支不支持对多个日志框架的绑定呢?答案是不支持,也感觉一般这是不必要需求。

1
2
3
4
5
6
7
8
9
10
11
12
apply plugin: 'idea'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.7'
    compile 'org.slf4j:slf4j-simple:1.7.7'
    compile 'org.slf4j:slf4j-log4j12:1.7.7'
}

输出:

SLF4J: Class path contains multiple SLF4J bindings.

在SLF4J的官方文档上也说明了这点:

“SLF4J does not rely on any special class loader machinery. In fact, each SLF4J binding is hardwired at compile time to use one and only one specific logging framework. For example, the slf4j-log4j12-1.7.7.jar binding is bound at compile time to use log4j. In your code, in addition to slf4j-api-1.7.7.jar, you simply drop one and only one binding of your choice onto the appropriate class path location. Do not place more than one binding on your class path.”

OK,关于SLF4J的内容就到这里,该进入重头戏–log4J。

log4j

log4j相对于SLF4j就复杂许多了,毕竟SLF4j只是接口,log4j则是具体的实现了。

log4j由三个重要的组件组成:

  1. 类型和级别(logger)
  2. 输出目的地(Appender)
  3. 输出格式(Layout)

logger

logger hierarchy(logger的层次级别)

Named Hierarchy(以名字分层)

logger的名字是大小写敏感的,并且根据名字的不同,logger之间存在父子层次关系。举例来说,com.foo是com.foo.bar的父logger,com是com.foo.bar的祖先logger。

在这些logger之间,存在一个root logger,位于这个层次的顶层,它是永久存在,并且不能够通过名字获取,要获取它,只能通过logger.getRootlogger()方法获取。而这样一个层次关系,也决定了logger和logger之间存在着一些继承关系。

Level Inheritance(级别继承)

子类继承父类的级别:如果子类级别没有显示的指定,子类的级别等于第一个级别非空的父类logger的级别,直到追溯到根logger。

前面提到logger的级别,它们分别是TRACE, DEBUG, INFO, WARN, ERROR和FATAL。

这些级别存在高低关系:DEBUG < INFO < WARN < ERROR < FATAL。

如果你以前有用过一点点log4j,这些级别映射到你的记忆中,一定是反映成logger的各个方法。没错,在logger这个对象中,你会看到很多的打印方法,debug, info, warn, error, fatal和log。

回过头来想想,如果你给一个logger指定了级别,但是你打印日志时,又不是调用对应级别的方法,会发生什么事情呢?

答案是:对这些方法的调用,不光是打印日志信息,也是对logger对象级别改变的请求。

但是,并不是你调用它,级别就一定会改变,对级别改变的请求还是取决于logger对象本身的级别(无论是被显示的指定,还是继承)。如果请求的级别低于该logger当前的级别,那么改变就不会成功。

Appender

Appender的作用是指出日志信息的输出位置,比如控制台,文件。而且好处是,你可以给一个logger指定多个appender,比如同时在控制台和文件中输出日志。

Appender Additivity(附加性)

每一个Appender都有一个属性,叫做additivity flag,默认是true。什么意思呢?

子类会继承它直接父类的appender(包括该父类自己继承的appender),如果设置为false,则不继承其父类的appender。

这句话应该好理解,那下面这种情况呢?

Logger Appender Additivity
root A1 null
appenderFather A2 false
appenderFather.appenderChild A3 true

appenderFather的flag设置为了false,所以他拥有的appender的结果应该比较明显,就是A2。

appenderFather.appenderChild的结果应该是什么?答案是A2和A3。因为它只是继承它直接父类的appender。

log4j提供的Appender有哪些呢?

org.apache.log4j.ConsoleAppender(控制台)

org.apache.log4j.FileAppender(文件)

org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)

org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生新文件,用的较多,限制文件大小)

org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

Layout

日志信息布局(格式)是log4j提供的另外一个非常有用的特性。

log4j提供四种Layout方式:

org.apache.log4j.HTMLLayout(以HTML表格形式布局),

org.apache.log4j.PatternLayout(可以灵活地指定布局模式,比较常用),

org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),

org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)

log4j采用类似C语言中的printf函数的打印格式格式化日志信息,打印参数如下:

%m 输出代码中指定的消息  

%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL   

%r 输出自应用启动到输出该log信息耗费的毫秒数   

%c 输出所属的类目,通常就是所在类的全名   

%t 输出产生该日志事件的线程名   

%n 输出一个回车换行符,Windows平台为“rn”,Unix平台为“n”   

%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921   

%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)

%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。

%%: 输出一个”%”字符 %F: 输出日志消息产生时所在的文件名称

%L: 输出代码中的行号

%m: 输出代码中指定的消息,产生的日志具体信息

%n: 输出一个回车换行符,Windows平台为”\r\n”,Unix平台为”\n”输出日志信息换行 可以在%与模式字符之间加上修饰符来控制其最小宽度、最大宽度、和文本的对齐方式。

Ok,基本知识就到这里,看一个例子。

Example

1
2
3
4
5
6
7
8
9
10
11
apply plugin: 'idea'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.7'
    compile 'org.slf4j:slf4j-log4j12:1.7.7'
}

在配置依赖时,将SLF4J和log4j绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {

  private final Logger logger = LoggerFactory.getLogger(HelloWorld.class);

  public void say() {
      logger.info("start time");
      System.out.println("Hello, world!!!");
      System.out.println(logger.getName());
      logger.info("end time");
  }

  public static void main(String args[]) {
      HelloWorld helloWorld = new HelloWorld();
      helloWorld.say();
  }
}

通过SLF4J的LoggerFactory.getLogger()方法得到一个logger的实力。logger的名字就是类名(全限定的类名)。

1
2
3
4
5
6
7
8
9
10
log4j.rootLogger=info, console, file

log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%p %l %d{yyy MMM dd HH:mm:ss,SSS} %m %n

log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File=logger.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%p %l %d{yyy MMM dd HH:mm:ss,SSS} %m %n

在log4j的properties文件中,指定了根logger的级别,以及它的两个appender,分别是console和file。然后定义这两个appender,并指定它们的layout使用PatternLayout,让它们输出:级别,日志输出的位置,时间,指定的信息和换行。

输出结果如下:

INFO log.HelloWorld.say(HelloWorld.java:11) 2014 Jun 01 16:57:54,807 start time
Hello, world!!!
INFO log.HelloWorld.say(HelloWorld.java:13) 2014 Jun 01 16:57:54,809 end time
log.HelloWorld

总结,log不算是功能代码,但是他对于开发人员和维护人员,非常有用了,往往是解决问题的唯一线索,特别是对于后台系统,往往只能通过log来分析问题。

参考资料:

1.http://logging.apache.org/log4j/1.2/manual.html
2.http://www.slf4j.org/manual.html
3.http://www.cnblogs.com/dennisit/archive/2013/01/01/2841603.html

Spring Web Service by Example

| Comments

Web Service是一种独立的、低耦合的Web服务,它由几个核心技术组成:XML、XSD、WSDL、SOAP、UDDI。我们根据这些知识点,来一个个的学习。

XML

呵呵,这个太简单了,这个没什么好谈的,跳过。

XSD

XSD(XML Schema Definition)是DTD(Document Type Definition)的替代品,作用是定义XML文档的合法构建模块。

它能够做的事情有很多:

  1. 定义可出现在文档中的元素
  2. 定义可出现在文档中的属性
  3. 定义哪个元素是子元素
  4. 定义子元素的次序
  5. 定义子元素的数目
  6. 定义元素是否为空,或者是否可包含文本
  7. 定义元素和属性的数据类型
  8. 定义元素和属性的默认值以及固定值

….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           targetNamespace="http://benweizhu.github.io"
           elementFormDefault="qualified">
    <xs:element name="SearchBookRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="name" type="xs:string"/>
                <xs:element name="author" type="xs:string" minOccurs="0"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="SearchBookResponse">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="id" type="xs:int"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

</xs:schema>

我们来分析一下这个schema的含义:

(1)xmlns:xs= “http://www.w3.org/2001/XMLSchema“,它表示在这个schema中使用的元素,例如element,complexType,sequence等,都来自于命名空间”http://www.w3.org/2001/XMLSchema“,并且在使用这些元素时,要以xs最为前缀。

(2)targetNamespace= “http://benweizhu.github.io“,它表示在这个schema中定义的元素,例如SearchBookRequest,name,author等,都是来自于命名空间”http://benweizhu.github.io“。它表示在基于该schema构建XML时,XML中使用的元素来自于该命名空间。

1
2
3
4
<z:SearchBookRequest xmlns:z="http://benweizhu.github.io">
    <z:name>SpringWebService</z:name>
    <z:author>Spring</z:author>
</z:SearchBookRequest>

(3)xmlns=“http://benweizhu.github.io“,指定了一个默认的命名空间,什么意思呢?就是说如果schema中使用的元素,没有xs这样前缀,则都是来自于这个默认的命名空间的。

(4)elementFormDefault=“qualified”,指出任何在XML实例文档所使用的元素,只要在此schema中声明过,就必须被命名空间限定。

(5)<xs:element name=“xxx” type=“yyy”/>,此处 xxx 指元素的名称,yyy 指元素的数据类型。XML Schema 拥有很多内建的数据类型。最常用的类型有:

  • xs:string
  • xs:decimal
  • xs:integer
  • xs:boolean
  • xs:date
  • xs:time

(6)复杂元素,定义复杂元素的方式有两种,例子已经在上面给出,一种是直接在元素内部定义,一种是通过type引用该复杂类型。

(7)属性,element元素包含有很多的属性,例如,minOccurs=“0”,它表示出现最少次数为0,就是说这个元素是optional的,可选的。

SOAP

SOAP(Simple Object Access Protocol,简单对象访问协议),是交换数据的一种协议规范,它是一种通信协议,基于XML,用于发送和接受消息,它是真正在WebService的服务端和客户端传送的信息。

SOAP使用应用层协议作为其传输协议。SMTP以及HTTP协议都可以用来传输SOAP消息,但是由于HTTP在如今的因特网结构中工作得很好,特别是在网络防火墙下仍然正常工作,所以被广泛采纳。

SOAP基于XML,所以归根到底,SOAP发送的消息就是一个XML格式的文档消息。

一条SOAP消息的基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
<soap:Header>
  ...
</soap:Header>
<soap:Body>
  ...
  <soap:Fault>
    ...
  </soap:Fault>
</soap:Body>
</soap:Envelope>

一条SOAP消息包含下列元素:

  1. 必需的Envelope元素,它是用来将一个XML文档标识为一条SOAP消息。
  2. 可选的Header元素,包含头部信息
  3. 必需的Body元素,包含所有的调用和响应信息
  4. 可选的Fault元素,提供有关在处理此消息所发生错误的信息

所有以上的元素均被声明为,命名空间 http://www.w3.org/2001/12/soap-envelope 中的元素,以及针对SOAP编码和数据类型的命名空间: http://www.w3.org/2001/12/soap-encoding

关于 xmlns:soap=“http://www.w3.org/2001/12/soap-envelope” 应该比较容易理解,在上面的XSD介绍中已经解释过了,用于说明该XML的命名空间,相当于该SOAP的XSD中的targetNamespace。

可选的SOAP Header元素可包含有关SOAP消息的应用程序专用信息(比如认证、支付等)。如果Header元素被提供,则它必须是Envelope元素的第一个子元素。

1
2
3
<soap:Header>
<m:Trans xmlns:m="http://www.w3school.com.cn/transaction/" soap:mustUnderstand="1">234</m:Trans>
</soap:Header>

必需的SOAP Body元素包含打算传送到消息最终端点的实际SOAP消息。

1
2
3
4
5
6
<soap:Body>
  <z:SearchBookRequest xmlns:z="http://benweizhu.github.io">
    <z:name>SpringWebService</z:name>
    <z:author>Spring</z:author>
  </z:SearchBookRequest>
</soap:Body>

而可选的SOAP Fault元素用于指示错误消息。

WSDL

WSDL指网络服务描述语言(Web Services Description Language),也就是WebService的描述语言。

WSDL是一种使用XML编写的文档。这种文档用于描述某个web service,规定服务的位置,以及此服务提供的操作(或方法)。

一个WSDL文档是由如下几种元素组成: 元素定义:\, \, \, \

WSDL类型

\元素定义web service使用的数据类型。为了最大程度的平台中立性,WSDL使用XML Schema语法来定义数据类型。

WSDL消息

\元素定义一个操作的数据元素。每个消息均由一个或多个部件组成。可以把这些部件比作传统编程语言中一个函数调用的参数。

WSDL端口

\元素是最重要的WSDL元素。它可描述一个web service、可被执行的操作,以及相关的消息。可以把\元素比作传统编程语言中的一个函数库(或一个模块、或一个类)。

WSDL绑定协议

\ 元素为每个端口定义消息格式和协议细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<definitions>
    <types>
        definition of types........
    </types>
    <message>
        definition of a message....
    </message>
    <portType>
        definition of a port.......
    </portType>
    <binding>
        definition of a binding....
    </binding>
</definitions>

这里要说明一点,WSDL是WebService的描述语言,只是用来描述WebService,当客户端想要使用该WebService时,根据这个描述,可以知道该WebService的服务端口有哪些,服务内容有什么。但对于WebService本身的功能没有任何影响,换句话说,如果是你自己写的WebService,提供给自己使用,你根本不需要关心WSDL是什么,因为你知道自己提供的服务内容和端口。

关于这点,我会在后面的Spring WebService代码中证明。

Spring WebService

OK,到这里几个重要的概念都谈到了,应该开始正题,如果上面的内容没看懂,特别是WSDL,没关系,接下来,我们根据Spring WebService的例子,来一个个分析。

Contract First 契约优先

首先实现WebService的消息通信协议,也就是提供什么样的服务,接收什么消息,返回什么消息。

根据前面的知识:要根据XSD来约束发送和接收的XML长什么样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           targetNamespace="http://benweizhu.github.io"
           elementFormDefault="qualified">
    <xs:element name="SearchBookRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="name" type="xs:string"/>
                <xs:element name="author" type="xs:string" minOccurs="0"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="SearchBookResponse">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="id" type="xs:int"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

在这个例子中,我提供了一个图书搜索的服务,传给我书名name和作者author,我返回对应书的id。

根据这个XSD文件,发送出去的请求的XML的应该是长类似下面这个样子:

1
2
3
4
<z:SearchBookRequest xmlns:z="http://benweizhu.github.io">
    <z:name>SpringWebService</z:name>
    <z:author>Spring</z:author>
</z:SearchBookRequest>

JAXB

根据这个定义的XSD文件,可以生成对应的Java类文件,这一步很简单,在Eclipse或者IntellIj中都有提供XSD到Java文件的转换插件。你只需要转换一下,就会生成的两个类SearchBookRequest和SearchBookResponse,它们会在下面的Endpoint中被使用。

Endpoint

实现Endpoint,也就是,这个请求消息给服务端后,有谁来处理,并返回响应结果。

Spring提供了注解的方式来配置一个Endpoint。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package me.zeph.spring.ws.example.endpoint;

import jaxbgen.SearchBookRequest;
import jaxbgen.SearchBookResponse;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

@Endpoint
public class BookEndpoint {

  @PayloadRoot(localPart = "SearchBookRequest", namespace = "http://benweizhu.github.io")
  public @ResponsePayload SearchBookResponse searchBook(@RequestPayload SearchBookRequest searchBookRequest) {
      SearchBookResponse searchBookResponse = new SearchBookResponse();
      searchBookResponse.setId(1);
      return searchBookResponse;
  }
}

第一个注解@Endpoint不用我解释了,就是说明这个类是一个WebService的Endpoint。 第二个注解@PayloadRoot里面有两个参数,localPart和namespace,它们分别匹配到XML中的根元素名字和命名空间,也就是说,如果发送过来消息的满足这两个条件,就交给该函数处理。这样就形成了消息到Endpoint的映射。 第三和第四个注解@RequestPayload和@ResponsePayload就简单了,分别用来指明请求和响应的对象。

Spring的XML配置

配置比较简单,提供组件扫描和对Endpoint注解的支持即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:sws="http://www.springframework.org/schema/web-services"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/web-services
      http://www.springframework.org/schema/web-services/web-services-2.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context.xsd">

    <sws:annotation-driven/>

    <context:component-scan base-package="me.zeph.spring.ws.example"/>

</beans>

web.xml

最后再来看web.xml怎么配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
         http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <servlet>
        <servlet-name>spring-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-ws</servlet-name>
        <url-pattern>/services/*</url-pattern>
    </servlet-mapping>
</web-app>

和SpringMVC一样,WebService也需要一个前端的请求分发的Servlet,MessageDispatcherServlet。你可能会想为什么是Servlet?

其实,这点,我在前面已经提到过,WebService的协议是绑定在Soap上面的,而Soap又是以http协议作为应用层协议的载体,发出去的Request固然是http请求。不配置对应Servlet,不配置对应的URL,那应该怎么做呢?

到此为止,一个Spring Web Service的服务端代码就写完了。你肯定会想,说好的WSDL呢?它去哪了?我这里刻意没有配置WSDL,就是要证明WSDL本身对于WebService没有影响,所以WSDL可以不用配。

我们先把WSDL放在一边,先把客户端代码完成。

Spring WebService Template 客户端

这里的客户端代码使用Spring提供的WebServiceTemplate完成。

在applicationContext中配置WebServiceTemplate,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
        <property name="defaultUri" value="http://localhost:8080/spring-ws/services/"/>
        <property name="marshaller" ref="marshaller"/>
        <property name="unmarshaller" ref="marshaller"/>
    </bean>

    <bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="packagesToScan">
            <list>
                <value>jaxbgen</value>
            </list>
        </property>
    </bean>
</beans>

defaultUri用来说明该Template会向哪个URL发出请求。关于这个URL,因为在web.xml配置时,该MessageServlet是接受spring-ws/services/*的任何请求,而Spring WebService的Endpoint映射并不是根据URL来判断,所以这里你可以写任何满足该模式的URL,例如: http://localhost:8080/spring-ws/services/ssss

Marshaller用于实现XML到Java和Java到XML的转换。在Template需要配置marshaller和unmarshaller,这里它们用的同一个Marshaller对象。

客户端Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package integration;

import jaxbgen.SearchBookRequest;
import jaxbgen.SearchBookResponse;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.ws.client.core.WebServiceTemplate;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class BookClientIntegrationTest {

  @Autowired
  private WebServiceTemplate webServiceTemplate;

  @Test
  public void shouldGetResponseFromWebService() {
      //given
      SearchBookRequest searchBookRequest = new SearchBookRequest();

      //when
      SearchBookResponse searchBookResponse = (SearchBookResponse) webServiceTemplate.marshalSendAndReceive(searchBookRequest);

      //then
      assertThat(searchBookResponse.getId(), is(1));
  }
}

OK,到此为止,Spring WebService的服务端和客户端代码完成。

所以,现在我们再回过头来,看看WSDL。

WSDL

Automatic WSDL exposure 自动生成WSDL

在刚才的Spring XML配置中(/WEB-INF/[servlet-name]-servlet.xml)中加一段XML配置:

1
2
3
4
<sws:dynamic-wsdl id="book" portTypeName="SearchBookRequest"
                      locationUri="http://tobereplacedbyws:8080/spring-ws/services/">
        <sws:xsd location="classpath:book.xsd"/>
</sws:dynamic-wsdl>

要注意位置有两点:

  1. 属性id,除了唯一指定一个元素之外,它还有个更重要的作用,是说明该wsdl的名字。当你要访问该wsdl时,访问的url是 http://localhost:8080/spring-ws/services/book.wsdl
  2. 属性locationUri,前面已经看到WSDL跟WebService的功能性服务没有半毛钱关系,所以这里的locationUri属性配置也不会影响到WebService的功能,它只是在你在wsdl中看到的一个字段,用来描述服务端的服务端口(URL)是什么。如果你看上面例子中的locationUri,你会发现不是写的localhost,而是tobereplacedbyws。为什么呢?因为MessageDispatcherServlet提供了一个特性,可以在部署时,根据主机的名字,替换这个位置的内容。如何开启这个特性呢?

在web.xml中配置一个initparam即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
         http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <servlet>
        <servlet-name>spring-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
        <init-param>
            <param-name>transformWsdlLocations</param-name>
            <param-value>true</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-ws</servlet-name>
        <url-pattern>/services/*</url-pattern>
    </servlet-mapping>

</web-app>

这个时候,你访问 http://localhost:8080/spring-ws/services/book.wsdl ,就可以看到对应的WSDL信息,然后你就可以用SoapUI等类似软件去测试你的Webservice。

参考资料:

(1) spring webservice reference

(2) http://www.w3school.com.cn/ws.asp