遠端服務HttpInvoker原始碼解析(五)客戶端實現
客戶端實現
分析了服務端的解析以及處理後,我們接下來分析客戶端的呼叫過程,在服務端呼叫的分析中我們反覆提到需要從HttpServletRequest中提取從客戶端傳來的RemoteInvocation例項,然後進行相應解析。所以客戶端,一個比較重要的任務就是構建RemoteInvocation例項,並傳送到伺服器。根據配置檔案中的資訊,我們還是首先確定HttpInvokerProxyFactoryBean類,並檢視其層次結構,客戶端配置檔案如下:
<?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.2.xsd"> <bean id="remoteService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"> <property name="serviceUrl" value="http://localhost:8080/httpinvokertest/remoting/hit"/> <property name="serviceInterface" value="org.tarena.note.service.HttpInvokerTest1"/> </bean> </beans>
層次結構:
public class HttpInvokerProxyFactoryBean extends HttpInvokerClientInterceptor
implements FactoryBean
httpInvokerProxyFactoryBean的父類的父類的父類實現了InitializingBean介面,同時又實現了FactoryBean以及其父類又實現了MethodInterceptor。這已經是老生常談的問題了,實現這幾個介面以及這幾個介面在Spring中會有什麼作用就不再贅述了,我們還是根據事先的InitializingBean介面分析初始化過程中的邏輯。
在afterPropertiesSet中主要建立了一個代理,該代理封裝了配置的服務介面,並使用當前類也就i是HttpInvokerProxyFactoryBean作為增強。因為HttpInvokerProxyFactoryBean實現了MethodPInterceptor方法,所以可以作為增強攔截器。public void afterPropertiesSet() { super.afterPropertiesSet(); if(getServiceInterface() == null) { throw new IllegalArgumentException("Property 'serviceInterface' is required"); } else { //建立代理並使用當前方法為攔截器增強 serviceProxy = (new ProxyFactory(getServiceInterface(), this)).getProxy(getBeanClassLoader()); return; } }
同樣,又由於HttpInvorkerProxyFactoryBean實現了FactoryBean介面,所以通過Spring中普通方式呼叫該bean時呼叫的並不是該bean本身,而是getObject方法中返回的例項,也就是例項化過程中所建立的代理。
那麼,綜合之前的使用示例,我們再次回顧一下,HttpInvokerProxyFactoryBean型別bean在初始化過程中建立了封裝服務介面的代理,並使用自身作為增強攔截器,然後又因為實現了FactoryBean介面,所以獲取Bean的時候返回的就i是建立的代理。那麼,彙總上面的邏輯,當呼叫如下程式碼時,其實是呼叫代理類中的服務方法,而在呼叫代理類中的服務方法時又會使用代理類中加入的增強器進行增強。public Object getObject() { return serviceProxy; }
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:client.xml");
HttpInvokerTest1 httpInvokerTest1 = (HttpInvokerTest1)context.getBean("remoteService");
System.out.println(httpInvokerTest1.getTestPo("dddd"));
因為HttpInvokerProxyFactoryBean實現了methodIntercepter介面,所有的邏輯分析其實已經轉向了對於增強器也就是HttpInvokerProxyFactoryBean類本身的invoke方法的分析。
在分析invoke方法之前,其實我們已經猜出了該方法所提供的主要功能就是將呼叫資訊封裝在RemoteInvocation中,傳送給服務端並等待返回結果。
public Object invoke(MethodInvocation methodInvocation)
throws Throwable
{
if(AopUtils.isToStringMethod(methodInvocation.getMethod()))
return (new StringBuilder()).append("HTTP invoker proxy for service URL [").append(getServiceUrl()).append("]").toString();
//將要呼叫的方法封裝RemoteInvocation
RemoteInvocation invocation = createRemoteInvocation(methodInvocation);
RemoteInvocationResult result = null;
try
{
result = executeRequest(invocation, methodInvocation);
}
catch(Throwable ex)
{
throw convertHttpInvokerAccessException(ex);
}
try
{
return recreateRemoteInvocationResult(result);
}
catch(Throwable ex)
{
if(result.hasInvocationTargetException())
throw ex;
else
throw new RemoteInvocationFailureException((new StringBuilder()).append("Invocation of method [").append(methodInvocation.getMethod()).append("] failed in HTTP invoker remote service at [").append(getServiceUrl()).append("]").toString(), ex);
}
}
函式主要有三個步驟。
(1)構建RemoteInvocation例項。
因為是代理中增強方法的呼叫,呼叫的方法及引數資訊會在代理中封裝至MethodInvocation例項中,並在增強器中進行傳遞。也就意味著當程式進入invoke方法時其實是已經包含了呼叫的相關資訊的,那麼,首先要做的就是將MethodInvocation中的資訊提取並構建RemoteInvocation例項。
(2)遠端執行方法。
(3)提取結果。
考慮到序列化的問題,在Spring中約定使用HttpInvoker方式進行遠端方法呼叫時,結果使用RemoteInvocationResult進行封裝,那麼在提取結果後還需要從封裝的結果中提取對應的結果。
而在三個步驟中最為關鍵的就是遠端方法的執行。執行遠端呼叫的首要步驟就是將呼叫方法的例項寫入輸出流中。
protected RemoteInvocationResult executeRequest(RemoteInvocation invocation, MethodInvocation originalInvocation)
throws Exception
{
return executeRequest(invocation);
}
protected RemoteInvocationResult executeRequest(RemoteInvocation invocation)
throws Exception
{
return getHttpInvokerRequestExecutor().executeRequest(this, invocation);
}
public final RemoteInvocationResult executeRequest(HttpInvokerClientConfiguration config, RemoteInvocation invocation)
throws Exception
{
//獲取輸出流
ByteArrayOutputStream baos = getByteArrayOutputStream(invocation);
if(logger.isDebugEnabled())
logger.debug((new StringBuilder()).append("Sending HTTP invoker request for service at [").append(config.getServiceUrl()).append("], with size ").append(baos.size()).toString());
return doExecuteRequest(config, baos);
}
在doExecuteRequest方法中真正實現了對遠端方法的構造與通訊,與遠端方法的連線功能實現中,Spring引入了第三方JAR:HttpClient。HttpClient是Apache Jakarta Common下的子專案,可以用來提供高效的,最新的,功能豐富的支援HTTP協議的客戶端程式設計工具包,並且它支援HTTP協議最新的版本和建議。對HttpClient的具體使用方法有興趣的讀者可以參考更多的資料和文件。<pre name="code" class="java"> protected RemoteInvocationResult doExecuteRequest(HttpInvokerClientConfiguration config, ByteArrayOutputStream baos)
throws IOException, ClassNotFoundException
{
//建立httpPost
PostMethod postMethod = createPostMethod(config);
RemoteInvocationResult remoteinvocationresult;
//設定含有方法的輸出流到post中
setRequestBody(config, postMethod, baos);
//執行方法
executePostMethod(config, getHttpClient(), postMethod);
//驗證
validateResponse(config, postMethod);
//提取返回的輸入流
InputStream responseBody = getResponseBody(config, postMethod);
//從輸入流中提取結果
remoteinvocationresult = readRemoteInvocationResult(responseBody, config.getCodebaseUrl());
postMethod.releaseConnection();
return remoteinvocationresult;
}
1.建立HttpPost
由於對於服務端方法的呼叫是通過Post方式進行的,那麼首先要做的就是構建HttpPost。構建HttpPost過程中可以設定一些必要的引數。
protected PostMethod createPostMethod(HttpInvokerClientConfiguration config)
throws IOException
{
//設定需要訪問的url
PostMethod postMethod = new PostMethod(config.getServiceUrl());
LocaleContext locale = LocaleContextHolder.getLocaleContext();
if(locale != null)
//加入Accept-Language屬性
postMethod.addRequestHeader("Accept-Language", StringUtils.toLanguageTag(locale.getLocale()));
if(isAcceptGzipEncoding())
//加入Accept-Encoding屬性
postMethod.addRequestHeader("Accept-Encoding", "gzip");
return postMethod;
}
2.設定RequestBody
構建好PostMethod例項後便可以將儲存RemoteInvocation例項的序列化形象的輸出流設定進去,當然這裡需要注意的是傳入的ContentType型別,一定要傳入application/x-java-serialized-object以保證服務端解析時會按照序列化物件的解析方式進行解析。
protected void setRequestBody(HttpInvokerClientConfiguration config, PostMethod postMethod, ByteArrayOutputStream baos)
throws IOException
{
//將序列化流加入到postMethod中並宣告ContentType型別為appliction、x-java-serialized-object
postMethod.setRequestEntity(new ByteArrayRequestEntity(baos.toByteArray(), getContentType()));
}
3.執行遠端方法
通過HttpClient所提供的方法來直接執行遠端方法。
protected void executePostMethod(HttpInvokerClientConfiguration config, HttpClient httpClient, PostMethod postMethod)
throws IOException
{
httpClient.executeMethod(postMethod);
}
4.遠端相應驗證
對於HTTP呼叫的響應碼處理,大於300則是非正常呼叫的響應碼。
protected void validateResponse(HttpInvokerClientConfiguration config, PostMethod postMethod)
throws IOException
{
if(postMethod.getStatusCode() >= 300)
throw new HttpException((new StringBuilder()).append("Did not receive successful HTTP response: status code = ").append(postMethod.getStatusCode()).append(", status message = [").append(postMethod.getStatusText()).append("]").toString());
else
return;
}
5.提取響應資訊
從伺服器返回的輸入流可能是經過壓縮的,不同的方式採用不同的辦法進行提取
protected InputStream getResponseBody(HttpInvokerClientConfiguration config, PostMethod postMethod)
throws IOException
{
if(isGzipResponse(postMethod))
return new GZIPInputStream(postMethod.getResponseBodyAsStream());
else
return postMethod.getResponseBodyAsStream();
}
6.提取返回結果
提取結果的流程主要是從輸入流中提取響應的虛擬惡化資訊。
protected RemoteInvocationResult readRemoteInvocationResult(InputStream is, String codebaseUrl)
throws IOException, ClassNotFoundException
{
ObjectInputStream ois = createObjectInputStream(decorateInputStream(is), codebaseUrl);
RemoteInvocationResult remoteinvocationresult = doReadRemoteInvocationResult(ois);
許多公司的分散式框架中都用到了遠端服務呼叫,無論是alibaba的dubbo,還是別的,瞭解遠端呼叫的原理都是大同小異的。都是通過http請求,封裝序列化的物件,通過動態代理的方式進行資訊獲取。只不過網際網路公司的遠端呼叫是布在分散式上罷了。