本文是《轻量级 Java Web 框架架构设计》的系列博文。
在上文中,我使用了 JAX-WS 发布并调用 Web 服务,虽然已经很简单了,但还是有些冗余的地方,比如:
能否简化一下呢?
我的方案是:只需在接口上定义一个 @WebService 注解,便可发布为 Web 服务。
不过需要借助一个工具,它就是 CXF,它是 Apache 的顶级项目之一。或许有些朋友没有听说过,但它的前身或许会对大家并不陌生,那就是 XFire,已经很老了,现在已停止维护,摇身一变,成为了 CXF。
CXF 与 Spring 集成得非常好,发布或调用 Web 服务只需使用 Java 注解 + Spring 配置即可实现,CXF 将复杂的技术细节给屏蔽了,对于开发人员而言,只需掌握一些基本用法即可。所以在 Java 业界里,尤其是使用了 Spring 作为解决方案的项目,一定都会优先考虑使用 CXF 发布或调用 Web 服务。
可能有朋友会问:必须集成 Spring,才能使用 CXF 吗?
答案是否定的。单独用 CXF 甚至会更加简单,从 API 层面上来讲,更加容易定制与扩展,更容易把玩它!
还是系好您的安全带吧,我们即将起飞!
第一步:在 Maven 中添加 CXF 依赖
CXF 的依赖包比较多,但对于发布 Web 服务而言,至于使用以下配置:
... <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxws</artifactId> <version>2.7.7</version> </dependency> ...
Maven 真不愧是 Java 世界里最伟大的发明之一,有了它,无需再配置 CXF 的依赖包了,其实它的依赖包很多,我们没必要一个个地去找,Maven 已经帮我们做好了依赖关联关系。
第二步:自定义一个 @WebService 注解
为什么要自定义一个呢?JDK 不是已经提供了一个吗?
其实用 JDK 的也行,只不过这样做是考虑到将来的扩展,比如给该注解添加其他属性等。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface WebService { String value() default ""; }
目前仅提供了唯一的属性 value,默认值为空字符串。它具体有何意义呢?请您继续往下看。
第三步:定制 CXF
该步骤是本文的精华!所以请您务必仔细阅读。
CXF 提供了一个名为 org.apache.cxf.transport.servlet.CXFServlet 的 Servlet,就是靠它来处理 Web 服务请求的,在它内部实现中也耦合了 Spring。看来 CXF 真不愧为 Spring 的好伴侣啊~
如果直接使用 CXFServlet,或许会让 Smart 依赖于 Spring 了,我并不想这样干。虽然 Spring 非常优秀,但不到万不得已,我是不会轻易使用 Sprnig 的。
看了一下 CXFServlet 的源码,发现它还有一个父类,名为 org.apache.cxf.transport.servlet.CXFNonSpringServlet。看到这个名字,我们可以猜想出,这个父类是不依赖于 Spring 的。猎物不费吹灰之力就得到了。
不妨写一个 WebServiceServlet 来扩展 CXFNonSpringServlet 吧:
@WebServlet(name = "ws", urlPatterns = "/ws/*", loadOnStartup = 0) public class WebServiceServlet extends CXFNonSpringServlet { @Override protected void loadBus(ServletConfig sc) { // 设置 Bus setBus(sc); // 发布 Web 服务 publishWebService(); } ... }
以上只是一部分代码。首先定义了该 Servlet 的名称、URL 映射、初始化时自动加载(为提高运行时性能,没必要处理请求时进行加载)。然后重写了父类的 loadBus 方法,在该方法中做两件事情:1. 设置 Bus,2. 发布 Web 服务。
Bus 是 CXF 的核心,说通俗一点就是“总线”,可不是“巴士”哦。类似于我们计算机里的 Bus,它把许多部件进行连接。其实 Bus 也是一种设计模式,SOA/ESB 中就是一个最佳实践。对于该技术,本文暂不涉及,有兴趣的朋友,可以去 CXF 官网了解一下,不过官网的文档看起来十分高深莫测,您要有耐心了。
先看如何设置 Bus 吧。其实也就这样了:
... private void setBus(ServletConfig sc) { super.loadBus(sc); Bus bus = getBus(); BusFactory.setDefaultBus(bus); } ...
首先加载 Bus,然后获取 Bus,最后将 Bus 设置为 BusFactory 的默认 Bus。
下面的才是重头戏,如何发布 Web 服务。
... private void publishWebService() { // 遍历所有标注了 @WebService 注解的接口 List<Class<?>> wsInterfaceClassList = ClassHelper.getClassListByAnnotation(WebService.class); if (CollectionUtil.isNotEmpty(wsInterfaceClassList)) { for (Class<?> wsInterfaceClass : wsInterfaceClassList) { // 对于接口才能执行以下代码 if (wsInterfaceClass.isInterface()) { // 获取 value 属性 String value = wsInterfaceClass.getAnnotation(WebService.class).value(); // 获取 Web 服务地址 String wsAddress = getAddress(value, wsInterfaceClass); // 获取 Web 服务实现类(找到唯一的实现类) Class<?> wsImplementClass = IOCHelper.findImplementClass(wsInterfaceClass); // 获取实现类的实例 Object wsImplementInstance = BeanHelper.getBean(wsImplementClass); // 发布 Web 服务 WebServiceHelper.publishWebService(wsAddress, wsInterfaceClass, wsImplementInstance); } } } } ...
首先获取带有 @WebService 注解的接口,然后分别获取 Web 服务的地址、接口、实现类实例,最后将这三个参数传递到 WebServiceHelper 的 publishWebService 方法中,从而发布 Web 服务。
在阅读 WebServiceHelper 代码之前,先看看 getAddress 这个方法是如何获取 Web 服务地址的。
... private String getAddress(String value, Class<?> wsInterfaceClass) { String address; if (StringUtil.isNotEmpty(value)) { // 若不为空,则为 value address = value; } else { // 若为空,则为类名 address = wsInterfaceClass.getSimpleName(); } // 确保最前面只有一个 / if (!address.startsWith("/")) { address = "/" + address; } address = address.replaceAll("\\/+", "/"); return address; } ...
该方法判断 @WebService 注解中的 value 属性,若为空(不填),则地址为“/接口名”,否则为用户指定的地址。为了返回正确格式的地址,需确保地址的第一个字符是“/”。
最后再看看 WebServiceHelper 吧,它其实也并不神秘。
import org.apache.cxf.frontend.ClientProxyFactoryBean; import org.apache.cxf.frontend.ServerFactoryBean; public class WebServiceHelper { // 发布 Web 服务 public static void publishWebService(String wsAddress, Class<?> wsInterfaceClass, Object wsImplementInstance) { ServerFactoryBean factory = new ServerFactoryBean(); factory.setAddress(wsAddress); // 地址 factory.setServiceClass(wsInterfaceClass); // 接口 factory.setServiceBean(wsImplementInstance); // 实现 factory.create(); } // 创建 Web 服务客户端 public static <T> T createWebClient(String wsAddress, Class<? extends T> interfaceClass) { ClientProxyFactoryBean factory = new ClientProxyFactoryBean(); factory.setAddress(wsAddress); // 地址 factory.setServiceClass(interfaceClass); // 接口 return factory.create(interfaceClass); } }
目前 WebServiceHelper 仅提供这两个方法,一般情况下应该是够用了,不排除后续进行扩展。通过封装 CXF 的 API 应该是一个不错的解决方案,在这里还可以添加更多的 CXF 特性,比如:日志监控、安全加密等。
注意:在创建 Web 服务客户端时,只需提供接口,无需提供实现。
第四步:启动 Tomcat
由于在 WebServiceServlet 的 @WebServlet 注解中定义了 loadOnStartup = 0,所以在启动 Tomcat 时就会自动发布 Web 服务。
启动完毕后,可通过 CXF 提供的控制台进行查看。
控制台地址:http://localhost:8080/smart-sample/ws
注意地址最后的 /ws,这就是在 WebServiceServlet 中配置的 URL 映射。
可见这里已经有一个基于 SOAP 的 Web 服务发布了,可以点击 WSDL 地址查看具体细节。
此外,需要说明的是,CXF 还可以发布基于 REST 的 Web 服务。非常好用,有机会我会和大家分享。
最后一步:调用 Web 服务
不妨使用 WebServiceHelper 来调用这个 Web 服务吧,代码非常精简:
import com.smart.plugin.ws.helper.WebServiceHelper; import com.smart.sample.service.GreetingService; public class GreetingServiceClient { public static void main(String[] args) throws Exception { String wsAddress = "http://localhost:8080/smart-sample/ws/GreetingService"; GreetingService greetingService = WebServiceHelper.createWebClient(wsAddress, GreetingService.class); greetingService.sayHello("Jack"); } }
只需提供 Web 服务地址与接口,即可创建客户端对象(greetingService),其实它是一个 Proxy,通过这个 Proxy 来调用目标方法。
运行后,就可以在 Tomcat 控制台下看到 Hello Jack 的输出文字了。
总结
在 Smart 中发布 Web 服务只需做两件事情:
1. 通过 Maven 引入 Smart WebService 插件
... <dependency> <groupId>com.smart</groupId> <artifactId>smart-plugin-ws</artifactId> <version>1.0</version> </dependency> ...
2. 使用 @WebService 注解定义需要发布的接口
import com.smart.plugin.ws.annotation.WebService; @WebService public interface GreetingService { void sayHello(String name); }
发布 Web 服务仅此两步而已,调用 Web 服务也十分方便。您是否觉得这样更加轻量级呢?