Apache CXF应该都知道,是一个开源的 Services 框架,可以构建基于SOAP或RESTful 的WebServices ,并且可以和 Spring 天然地进行无缝集成。
不过这里不是关于CXF的系统介绍,而是在开发WebServices 时遇到一些问题,最终使用CXF拦截器完成需求。所以侧重记录使用CXF的拦截器。
先说问题,其实也很简单,简言之就是在请求时需要对请求做一些预先处理。
项目框架做成了所谓的前后端分离,即前台app使用第三方工具生成,调用后台通过WebServices。可以理解为前台就是静态的Html与js,后台就是WebServices。现在问题是要求是每次请求时需要做下验证,保证当前登录设备的用户没有在其他设备登录,如果有就到跳到登录界面。
具体做法就是,在每次登录时会在系统记录一个唯一编码,然后在每次请求时先比较登录的账号和这个唯一编码和系统中已存的是否一致,如果不一致就到登录也,否则就可以继续请求。当然拦截会排除登录请求。因为前台app所有请求都是通过WebServices ,这样可以保证如果同一账号同时只能在一台设备上操作。
CXF的拦截器是CXF功能最主要的扩展点。通过自定义的Interceptor,可以改变请求和响应的一些消息处理,其中最基本的原理还是一个动态代理。所以对于上面的需求应该可以是满足的.
查看cxf的API了解到,cxf是有拦截器接口定义的。
package org.apache.cxf.interceptor;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Message;
public interface Interceptor<T extends Message> {
void handleMessage(T var1) throws Fault;
void handleFault(T var1);
}
所以对于上面的问题就想到可以使用拦截器实现了。现在知道要做什么,也知道实现方向,就看下怎么做了。
通过上面的借口定义可知,Interceptor只有两个方法,一个处理消息 handleMessage, 一个是处理错误 handleFault。但这里需要提醒注意的是,在实行具体的Interceptor的这两个方法中,千万别调用Interceptor内部的成员变量。这是由于Interceptor是面向消息来进行处理的,每个Interceptor都有可能运行在不同的线程中,如果调用了Interceptor中的内部成员变量,就有在Interceptor中造成临界资源的访问的情况,而这时的Interceptor也就不是线程安全的Interceptor了。我在实现过程中,由于没有注意这个问题,花了不少时间处理异常。
通常cxf不允许我们直接实现Interceptor接口,Interceptor有一个子接口PhaseInterceptor,顾名思义就是分阶段的拦截器。cxf将消息的发送、接收的过程分为很多个阶段,如send、receive、read、write等,具体可以参考类org.apache.cxf.phase.Phase。cxf要求我们在定义一个拦截器的时候必须指定当前拦截器需要作用的阶段。
cxf已经为我们提供了一个实现了PhaseInterceptor接口的抽象实现类org.apache.cxf.phase.AbstractPhaseInterceptor,而且建议我们通过继承AbstractPhaseInterceptor来实现自定义拦截器。AbstractPhaseInterceptor中没有定义默认的空构造方法,所以子类在实现的时候必须在构造方法中调用父类的某一个构造方法。以下是一个非常简单的继承自AbstractPhaseInterceptor的自定义拦截器的实现。
到这里,技术基本上确定了,剩下的就是实现的一些细节问题。
首先,因为我需要比较请求参数和目前的后台保存的数据,所以首要解决怎么取得请求穿过来的参数。
通过上面的Interceptor接口定义可知,拦截器在执行时会将请求的参数放在Message中。所以只要知道如何解析Message应该就可解决问题了。
因cxf和spring天然的关系,自然会联想到通过spring的方式配置定义拦截器。
通过上面的分析思路基本清楚了,下面就开始具体编码实现.
先定义一个拦截器.完成后代码如下:
package com.mungo.aop;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.XMLMessage;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 系统全局拦截器(排除登录服务调用)
* 用于校验登录的账号是否已在别处登录
* 已在别处登录这抛出异常
*
*/
public class AuthInterceptor extends AbstractPhaseInterceptor<Message> {
private Logger logger = Logger.getLogger(this.getClass());
public AuthInterceptor() {
//定义拦截器阶段
super(Phase.RECEIVE);
}
/**
* @Description: 拦截器操作
* @param message 被拦截到的消息
* @throws Fault
*/
@Override
public void handleMessage(Message message) {
logger.info("=====自定义拦截器=======");
String methodName= getMethod(message);
//排除拦截的方法
List list = new ArrayList();
list.add("login");
list.add("forgotPassword");
if(!list.contains(methodName)) {
Map reqParamsMap = new HashMap();
//根据请求方式区分取得GET和POST的参数
if (message.get(message.HTTP_REQUEST_METHOD).equals("GET")) {
//get参数,=&格式
String reqParams = (String) message.get(message.QUERY_STRING);
logger.info("请求的参数:" + reqParams);
//注:toMap为自定义方法,实现将String转成map
reqParamsMap = StringUtils.toMap(reqParams, "&");
} else if (message.get(message.HTTP_REQUEST_METHOD).equals("POST")) {
InputStream is = message.getContent(InputStream.class);
BufferedReader in = new BufferedReader(new InputStreamReader(is));
StringBuffer buffer = new StringBuffer();
String line = "";
try {
while ((line = in.readLine()) != null) {
//post参数,json格式
buffer.append(line);
}
logger.info("请求的参数:" + buffer);
JSONObject jasonObject = JSONObject.parseObject(buffer.toString());
reqParamsMap = (Map) jasonObject;
} catch (IOException e) {
e.printStackTrace();
}
if (is != null) {
//这里一定要加,post参数流读取结束如果不加这个操作,会报io异常
message.setContent(InputStream.class, new ByteArrayInputStream(buffer.toString().getBytes()));
}
}
if (reqParamsMap != null) {
String agentCode = String.valueOf(reqParamsMap.get("agentCode"));
//注:这里是系统用户登陆唯一标志,使用时自己定义
String cid = getCid();
if (StringUtils.isNotEmpty(cid)&&
!StringUtils.equals(cid, String.valueOf(reqParamsMap.get("cid")))) {
throw new Fault(new Exception("已在别处已登录"));
}
}
}
}
/**
* @Description:handleMessage异常后执行
* @param message
*/
@Override
public void handleFault(Message message) {
super.handleFault(message);
logger.info("=================================:"+"handleFault");
}
/**
* @Description:取得请求服务的具体方法
* @param message
* @return
*/
private String getMethod(Message message) {
//通过分析webservice的uri取得实际执行的方法,该webservice使用cxf的RESTFul形式发布
String requestUri = (String) message.get(XMLMessage.REQUEST_URI);
String[] methods = StringUtils.split(requestUri,"/");
logger.debug("********method name:" + requestUri);
return methods!=null && methods.length>0?methods[methods.length-1]:"";
}
}
配置拦截器很简单,尤其是结合spring.
这里只有简单的说明,如果要配置成全局的拦截器,只需要在spring的配置文件中增加节点.
需要注意的是,在使用时候,一定要引入 命名空间xmlns:cxf=http://cxf.apache.org/core,及其对应的模式http://cxf.apache.org/schemas/core.xsd.
<cxf:bus>
<cxf:inInterceptors>
<bean class="com.meyacom.crm.aop.AuthInterceptor">bean>
cxf:inInterceptors>
cxf:bus>
如果拦截器只对某个webservice进行拦截,可以使用 或节点配置.
<jaxrs:server id="fileServiceContainer" address="/fileService">
<jaxrs:serviceBeans>
<ref bean="fileService" />
jaxrs:serviceBeans>
<jaxrs:extensionMappings>
<entry key="octet-stream" value="application/json" />
jaxrs:extensionMappings>
<jaxrs:providers>
<ref bean="jsonProvider" />
jaxrs:providers>
<jaxrs:inInterceptors>
<bean class="com.meyacom.crm.aop.AuthInterceptor">bean>
jaxrs:inInterceptors>
<jaxws:outInterceptors>
<bean class="org.apache.cxf.interceptor.LoggingOutInterceptor"/>
jaxws:outInterceptors>
jaxrs:server>
如上所示,可以同时为in和out两个拦截器链 配置 拦截器,其中in为cxf本身自带的日志拦截器;这里额外为out拦截器链配置打印日志显示。
当然配置除了使用配置文件,也可是使用代码实现,但这里只是为了解决实际问题不是为了系统介绍拦截器,所以不在赘述.
至此,遇到的问题基本解决了.总共花了大约一天半的时间,cxf之前没有系统的了解过,这次也第一次使用.也是靠着经验一边摸索一边实现,中间的确是很花时间.以后得抽个时间把cxf好好学习一次.