前言
通常來說一個dubbo
服務都是對內給內部呼叫的,但也有可能一個服務就是需要提供給外部使用,並且還不能有使用語言的侷限性。
比較標準的做法是對外的服務我們統一提供一個openAPI
,這樣的呼叫方需要按照標準提供相應的appID
以及金鑰來進行驗籤才能使用。這樣固然是比較規範和安全,但複雜度也不亞於開發一個單獨的系統了。
這裡所講到的沒有那麼複雜,就只是把一個不需要各種許可權檢驗的dubbo
服務對外提供為HTTP
服務。
呼叫示例:
準備工作
以下是本文所涉及到的一些知識點:
- Spring相關知識。
- Java反射相關知識。
- SpringMVC相關知識。
其實思路很簡單,就是利用
SpringMVC
提供一個HTTP
介面。
在該介面中通過入參進行反射找到具體的dubbo
服務實現進行呼叫。
HttpProviderConf配置類
首先需要定義一個HttpProviderConf
類用於儲存宣告需要對外提供服務的包名,畢竟我們反射時需要用到一個類的全限定名:
public class HttpProviderConf {
/**
* 提供http訪問的包
*/
private List<String> usePackage ;
//省略getter setter方法
}複製程式碼
就只有一個usePackage
成員變數,用於存放需要包名。
至於用List
的原因是允許有多個。
請求響應入參、出參
HttpRequest入參
public class HttpRequest {
private String param ;//入參
private String service ;//請求service
private String method ;//請求方法
//省略getter setter方法
}複製程式碼
其中param
是用於存放真正呼叫dubbo
服務時的入參,傳入json
在呼叫的時候解析成具體的引數物件。
service
存放dubbo
服務宣告的interface API
的包名。
method
則是真正呼叫的方法名稱。
HttpResponse 響應
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裡的實現程式碼了。
先貼程式碼:
@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.UserInfoApi
、getUserInfo
賦值到httpRequest
入參中。 判斷傳入的包是否是對外提供的。如下配置:
<!--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
中寫單測來測試或查詢快的很多。
安裝
git clone https://github.com/crossoverJie/SSM-DUBBO-HTTP.git複製程式碼
cd SSM-DUBBO-HTTP複製程式碼
mvn clean複製程式碼
mvn install複製程式碼
使用
<dependency>
<groupId>com.crossoverJie</groupId>
<artifactId>SSM-HTTP-PROVIDER</artifactId>
<version>1.0.0</version>
</dependency>複製程式碼
spring配置
<!--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"/>複製程式碼
個人部落格地址:crossoverjie.top。
GitHub地址:github.com/crossoverJi…。