SSM(十三) 将dubbo暴露出HTTP服务

dubbo暴露为http服务.jpg

前言

通常来说一个dubbo服务都是对内给内部调用的,但也有可能一个服务就是需要提供给外部使用,并且还不能有使用语言的局限性。

比较标准的做法是对外的服务我们统一提供一个openAPI,这样的调用方需要按照标准提供相应的appID以及密钥来进行验签才能使用。这样固然是比较规范和安全,但复杂度也不亚于开发一个单独的系统了。

这里所讲到的没有那么复杂,就只是把一个不需要各种权限检验的dubbo服务对外提供为HTTP服务。

调用示例:
dubbo-http封面.jpg

准备工作

以下是本文所涉及到的一些知识点:

  • Spring相关知识。
  • Java反射相关知识。
  • SpringMVC相关知识。

其实思路很简单,就是利用SpringMVC提供一个HTTP接口。
在该接口中通过入参进行反射找到具体的dubbo服务实现进行调用。

HttpProviderConf配置类

首先需要定义一个HttpProviderConf类用于保存声明需要对外提供服务的包名,毕竟我们反射时需要用到一个类的全限定名:

1
2
3
4
5
6
7
8
public class HttpProviderConf {

/**
* 提供http访问的包
*/
private List<String> usePackage ;
//省略getter setter方法
}

就只有一个usePackage成员变量,用于存放需要包名。
至于用List的原因是允许有多个。

请求响应入参、出参

HttpRequest入参

1
2
3
4
5
6
public class HttpRequest {
private String param ;//入参
private String service ;//请求service
private String method ;//请求方法
//省略getter setter方法
}

其中param是用于存放真正调用dubbo服务时的入参,传入json在调用的时候解析成具体的参数对象。

service存放dubbo服务声明的interface API的包名。

method则是真正调用的方法名称。

HttpResponse 响应

1
2
3
4
5
6
7
8
9
10
11
public class HttpResponse implements Serializable{

private static final long serialVersionUID = -552828440320737814L;

private boolean success;//成功标志

private String code;//信息码

private String description;//描述
//省略getter setter方法
}

这里只是封装了常用的HTTP服务的响应数据。

暴露服务controller

最重要的则是controller里的实现代码了。

先贴代码:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
@Controller
@RequestMapping("/dubboAPI")
public class DubboController implements ApplicationContextAware{

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

@Autowired
private HttpProviderConf httpProviderConf;

//缓存作用的map
private final Map<String, Class<?>> cacheMap = new HashMap<String, Class<?>>();

protected ApplicationContext applicationContext;


@ResponseBody
@RequestMapping(value = "/{service}/{method}",method = RequestMethod.POST)
public String api(HttpRequest httpRequest, HttpServletRequest request,
@PathVariable String service,
@PathVariable String method) {
logger.debug("ip:{}-httpRequest:{}",getIP(request), JSON.toJSONString(httpRequest));

String invoke = invoke(httpRequest, service, method);
logger.debug("callback :"+invoke) ;
return invoke ;

}


private String invoke(HttpRequest httpRequest,String service,String method){
httpRequest.setService(service);
httpRequest.setMethod(method);

HttpResponse response = new HttpResponse() ;

logger.debug("input param:"+JSON.toJSONString(httpRequest));

if (!CollectionUtils.isEmpty(httpProviderConf.getUsePackage())){
boolean isPac = false ;
for (String pac : httpProviderConf.getUsePackage()) {
if (service.startsWith(pac)){
isPac = true ;
break ;
}
}
if (!isPac){
//调用的是未经配置的包
logger.error("service is not correct,service="+service);
response.setCode("2");
response.setSuccess(false);
response.setDescription("service is not correct,service="+service);
}

}
try {
Class<?> serviceCla = cacheMap.get(service);
if (serviceCla == null){
serviceCla = Class.forName(service) ;
logger.debug("serviceCla:"+JSON.toJSONString(serviceCla));

//设置缓存
cacheMap.put(service,serviceCla) ;
}
Method[] methods = serviceCla.getMethods();
Method targetMethod = null ;
for (Method m : methods) {
if (m.getName().equals(method)){
targetMethod = m ;
break ;
}
}

if (method == null){
logger.error("method is not correct,method="+method);
response.setCode("2");
response.setSuccess(false);
response.setDescription("method is not correct,method="+method);
}

Object bean = this.applicationContext.getBean(serviceCla);
Object result = null ;
Class<?>[] parameterTypes = targetMethod.getParameterTypes();
if (parameterTypes.length == 0){
//没有参数
result = targetMethod.invoke(bean);
}else if (parameterTypes.length == 1){
Object json = JSON.parseObject(httpRequest.getParam(), parameterTypes[0]);
result = targetMethod.invoke(bean,json) ;
}else {
logger.error("Can only have one parameter");
response.setSuccess(false);
response.setCode("2");
response.setDescription("Can only have one parameter");
}
return JSON.toJSONString(result) ;

}catch (ClassNotFoundException e){
logger.error("class not found",e);
response.setSuccess(false);
response.setCode("2");
response.setDescription("class not found");
} catch (InvocationTargetException e) {
logger.error("InvocationTargetException",e);
response.setSuccess(false);
response.setCode("2");
response.setDescription("InvocationTargetException");
} catch (IllegalAccessException e) {
logger.error("IllegalAccessException",e);
response.setSuccess(false);
response.setCode("2");
response.setDescription("IllegalAccessException");
}
return JSON.toJSONString(response) ;
}

/**
* 获取IP
* @param request
* @return
*/
private String getIP(HttpServletRequest request) {
if (request == null)
return null;
String s = request.getHeader("X-Forwarded-For");
if (s == null || s.length() == 0 || "unknown".equalsIgnoreCase(s)) {

s = request.getHeader("Proxy-Client-IP");
}
if (s == null || s.length() == 0 || "unknown".equalsIgnoreCase(s)) {

s = request.getHeader("WL-Proxy-Client-IP");
}
if (s == null || s.length() == 0 || "unknown".equalsIgnoreCase(s)) {
s = request.getHeader("HTTP_CLIENT_IP");
}
if (s == null || s.length() == 0 || "unknown".equalsIgnoreCase(s)) {

s = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (s == null || s.length() == 0 || "unknown".equalsIgnoreCase(s)) {

s = request.getRemoteAddr();
}
if ("127.0.0.1".equals(s) || "0:0:0:0:0:0:0:1".equals(s))
try {
s = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException unknownhostexception) {
return "";
}
return s;
}

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

先一步一步的看:

  • 首先是定义了一个DubboController,并使用了SpringMVC的注解对外暴露HTTP服务。

  • 实现了org.springframework.context.ApplicationContextAware类,
    实现了setApplicationContext()方法用于初始化Spring上下文对象,在之后可以获取到容器里的相应对象。

  • 核心的invoke()方法。

  • 调用时:http://127.0.0.1:8080/SSM-SERVICE/dubboAPI/com.crossoverJie.api.UserInfoApi/getUserInfo

  • 具体如上文的调用实例。先将com.crossoverJie.api.UserInfoApigetUserInfo赋值到httpRequest入参中。

  • 判断传入的包是否是对外提供的。如下配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!--dubbo服务暴露为http服务-->
    <bean class="com.crossoverJie.dubbo.http.conf.HttpProviderConf">
    <property name="usePackage">
    <list>
    <!--需要暴露服务的接口包名,可多个-->
    <value>com.crossoverJie.api</value>
    </list>
    </property>
    </bean>
    <!--扫描暴露包-->
    <context:component-scan base-package="com.crossoverJie.dubbo.http"/>

    其中的com.crossoverJie.api就是自己需要暴露的包名,可以多个。

  • 接着在缓存map中取出反射获取到的接口类类型,如果获取不到则通过反射获取,并将值设置到缓存map中,这样不用每次都反射获取,可以节省系统开销(反射很耗系统资源)。

  • 接着也是判断该接口中是否有传入的getUserInfo方法。

  • 取出该方法的参数列表,如果没有参数则直接调用。

  • 如果有参数,判断个数。这里最多只运行一个参数。也就是说在真正的dubbo调用的时候只能传递一个BO类型,具体的参数列表可以写到BO中。因为如果有多个在进行json解析的时候是无法赋值到两个参数对象中去的。

  • 之后进行调用,将调用返回的数据进行返回即可。

总结

通常来说这样提供的HTTP接口再实际中用的不多,但是很方便调试。

比如写了一个dubbo的查询接口,在测试环境或者是预发布环境中就可以直接通过HTTP请求的方式进行简单的测试,或者就是查询数据。比在Java中写单测来测试或查询快的很多。

安装

1
git clone https://github.com/crossoverJie/SSM-DUBBO-HTTP.git
1
cd SSM-DUBBO-HTTP
1
mvn clean
1
mvn install

使用

1
2
3
4
5
<dependency>
<groupId>com.crossoverJie</groupId>
<artifactId>SSM-HTTP-PROVIDER</artifactId>
<version>1.0.0</version>
</dependency>

spring配置

1
2
3
4
5
6
7
8
9
10
11
<!--dubbo服务暴露为http服务-->
<bean class="com.crossoverJie.dubbo.http.conf.HttpProviderConf">
<property name="usePackage">
<list>
<!--需要暴露服务的接口包名,可多个-->
<value>com.crossoverJie.api</value>
</list>
</property>
</bean>
<!--扫描暴露包-->
<context:component-scan base-package="com.crossoverJie.dubbo.http"/>

插件地址:https://github.com/crossoverJie/SSM-DUBBO-HTTP

项目地址:https://github.com/crossoverJie/SSM.git

个人博客地址:http://crossoverjie.top

GitHub地址:https://github.com/crossoverJie