主要的配置文件
<bean id="httpInvokerUserService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"> <property name="serviceUrl" value="http://localhost:8080/jws/httpInvokerUserService.service"/> <property name="serviceInterface" value="com.gosun.jws.httpinvoker.UserService" /> </bean>
在服務端調用的分析中我們反復提到需要從HttpServletRequest中提取從客戶端傳來的RemoteInvocation實例,然后進行相應解析。所以客戶端,一個比較重要的任務就是構建RemoteInvocation實例,並傳送到服務器。根據配置文件中的信息,我們還是首先確定HttpInvokerProxyFactoryBean類,並查看其層次結構。
public class HttpInvokerProxyFactoryBean extends HttpInvokerClientInterceptor implements FactoryBean
httpInvokerProxyFactoryBean的父類的父類的父類實現了InitializingBean接口,同時又實現了FactoryBean以及其父類又實現了MethodInterceptor。我們還是根據實現的InitializingBean接口分析初始化過程中的邏輯。
public void afterPropertiesSet() { super.afterPropertiesSet(); if(getServiceInterface() == null) { throw new IllegalArgumentException("Property 'serviceInterface' is required"); } else { //創建代理並使用當前方法為攔截器增強 serviceProxy = (new ProxyFactory(getServiceInterface(), this)).getProxy(getBeanClassLoader()); return; } }
在afterPropertiesSet中主要創建了一個代理,該代理封裝了配置的服務接口,並使用當前類也就是HttpInvokerProxyFactoryBean作為增強。因為HttpInvokerProxyFactoryBean實現了MethodPInterceptor方法,所以可以作為增強攔截器。同樣,又由於HttpInvorkerProxyFactoryBean實現了FactoryBean接口,所以通過spring中普通方式調用該bean時調用的並不是該bean本身,而是getObject方法中返回的實例,也就是實例化過程中所創建的代理。
public Object getObject(){ return serviceProxy; }
HttpInvokerProxyFactoryBean類型bean在初始化過程中創建了封裝服務接口的代理,並使用自身作為增強攔截器,然后又因為實現了FactoryBean接口,所以獲取Bean的時候返回的就是創建的代理。那么,當調用如下代碼時,其實是調用代理類中的服務方法,而在調用代理類中的服務方法時又會使用代理類中加入的增強器進行增強。
ApplicationContext ac = new ClassPathXmlApplicationContext("client-application/applicationContext-httpInvoker.xml"); UserService us = (UserService) ac.getBean("httpInvokerUserService"); Users user = us.getUser("a001");
因為HttpInvokerProxyFactoryBean實現了methodIntercepter接口,所有的邏輯分析其實已經轉向了對於增強器也就是HttpInvokerProxyFactoryBean類本身的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); } }
函數主要有三個步驟。
- 構建RemoteInvocation實例。因為是代理中增強方法的調用,調用的方法及參數信息會在代理中封裝至MethodInvocation實例中,並在增強器中進行傳遞。也就意味着當程序進入invoke方法時其實是已經包含了調用的相關信息的,那么,首先要做的就是將MethodInvocation中的信息提取並構建RemoteInvocation實例。
- 遠程執行方法。
- 提取結果。
在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協議最新的版本和建議。
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); try { return doReadRemoteInvocationResult(ois); } finally { ois.close(); } } protected RemoteInvocationResult doReadRemoteInvocationResult(ObjectInputStream ois) throws IOException, ClassNotFoundException { Object obj = ois.readObject(); if (!(obj instanceof RemoteInvocationResult)) { throw new RemoteException("Deserialized object needs to be assignable to type [" + RemoteInvocationResult.class.getName() + "]: " + obj); } return (RemoteInvocationResult) obj; }
許多公司的分布式框架中都用到了遠程服務調用,無論是dubbo,還是別的,了解遠程調用的原理都是大同小異的。都是通過http請求,封裝序列化的對象,通過動態代理的方式進行信息獲取。只不過互聯網公司的遠程調用是布在分布式上罷了。