通知由Spring托管的组件
在前面的内容中,我们重构了服务入口点,以便使用Spring托管的bean。现在,我将向您说明这样做将如何帮助改进组件和实现新功能。
首先,假定用户想看到某些符号的价格,而这些价格并非由您的TradeManager组件所托管。换句话说,您需要连接到一个外部服务,以便获得当前您不处理的所请求符号的当前市场价格。您可以使用雅虎门户中的一个基于HTTP的免费服务,但是实际的应用程序将连接到提供实时数据的供应商(比如 Reuters、Thomson、Bloomberg、NAQ等等)的实时数据更新服务(data feed)。
首先,需要创建一个新的YahooFeed组件,该组件实现了相同的TradeManager接口,然后从雅虎金融门户获得价格信息。自然的实现可以使用HttpURLConnection发送一个HTTP请求,然后使用正则表达式解析响应。例如:
public class YahooFeed implements TradeManager {
private static final String SERVICE_URL = http://finance.yahoo.com/d/quotes.csv?f=k1&s=;
private Pattern pattern = Pattern.compile("\"(.*) - (.*)\"");
public BigDecimal getPrice(String symbol) {
HttpURLConnection conn;
String responseMessage;
int responseCode;
try {
URL serviceUrl = new URL(SERVICE_URL+symbol);
conn = (HttpURLConnection) serviceUrl.openConnection();
responseCode = conn.getResponseCode();
responseMessage = conn.getResponseMessage();
} catch(Exception ex) {
throw new RuntimeException("Connection error", ex);
}
if(responseCode!=HttpURLConnection.HTTP_OK) {
throw new RuntimeException("Connection error "+responseCode+" "+responseMessage);
}
String response = readResponse(conn);
Matcher matcher = pattern.matcher(response);
if(!matcher.find()) {
throw new RuntimeException("Unable to parse response ["+response+"] for symbol "+symbol);
}
String time = matcher.group(1);
if("N/A".equals(time)) {
return null; // unknown symbol
}
String price = matcher.group(2);
return new BigDecimal(price);
}
public void setPrice(String symbol, BigDecimal price) {
throw new UnsupportedOperationException("Can't set price of 3rd party trade");
}
private String readResponse(HttpURLConnection conn) {
// ...
return response;
}
}
完成这种实现并测试(在容器外部!)之后,就可以把它与其他组件进行集成。传统的做法是向TradeManager2Impl添加一些代码,以便检查getPrice()方法返回的值。这会使测试的次数至少增加一倍,而且要求为每个测试用例设定附加的先决条件。然而,如果使用Spring AOP框架,就可以更漂亮地完成这项工作。您可以实现一条通知,如果初始的TradeManager没有返回所请求符号的值,该通知将使用 YahooFeed组件来获取价格(在这种情况下,它的值是null,但是也可能会得到一个UnknownSymbol异常)。
要把通知应用到具体的方法,需要在Spring的bean配置中声明一个Advisor。有一个方便的类叫做NameMatchMethodPointcutAdvisor,它允许通过名称选择方法,在本例中还需要一个getPrice方法:
<bean id="yahooFeed" class="org.javatx.spring.aop.YahooFeed"/>
<bean id="foreignTradeAdvisor"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="mappedName" value="getPrice"/>
<property name="advice">
<bean class="org.javatx.spring.aop.ForeignTradeAdvice">
<constructor-arg index="0" ref="yahooFeed"/>
</bean>
</property>
</bean>
正如您所看到的,上面的advisor指派了一个 ForeignTradeAdvice给getPrice()方法。针对通知类,Spring AOP框架使用了AOP Alliance API,这意味着环绕通知的ForeignTradeAdvice应该实现MethodInterceptor接口。例如:
public class ForeignTradeAdvice implements MethodInterceptor {
private TradeManager tradeManager;
public ForeignTradeAdvice(TradeManager manager) {
this.tradeManager = manager;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
Object res = invocation.proceed();
if(res!=null) return res;
Object[] args = invocation.getArguments();
String symbol = (String) args[0];
return tradeManager.getPrice(symbol);
}
}
上面的代码使用invocation.proceed()调用了一个原始的组件,而且如果它返回null,它将调用另一个在通知创建时作为构造函数参数注入的tradeManager。参见上面foreignTradeAdvisor bean的声明。
现在可以把在Spring的bean配置中定义的tradeManager重新命名为baseTradeManager,然后使用 ProxyFactoryBean把tradeManager声明为一个代理。新的baseTradeManager将成为一个目标,我们将使用上面定义的foreignTradeAdvisor通知它:
<bean id="baseTradeManager" class="org.javatx.spring.aop.TradeDao">
... same as tradeManager definition in the above example
</bean>
<bean id="tradeManager" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="org.javatx.spring.aop.TradeManager"/>
<property name="target" ref="baseTradeManager"/>
<property name="interceptorNames">
<list>
<idref local="foreignTradeAdvisor"/>
</list>
</property>
</bean>
基本上,就是这样了。我们实现了附加的功能而没有修改原始的组件,而且仅使用Spring应用程序上下文来重新配置依赖性。要想不借助于Spring AOP框架在典型的EJB组件中实现类似的修改,要么必须为EJB添加附加的逻辑(这会使其难以测试),要么必须使用decorator模式(实际上增加了EJB的数量,同时也提高了测试的复杂性,延长了部署时间)。在上面的例子中,您可以看到,借助于Spring,可以轻松地不修改现有组件而向这些组件添加附加的逻辑。现在,您拥有的是几个轻量级组件,而不是紧密耦合的bean,您可以独立测试它们,使用Spring Framework组装它们。注意,使用这种方法,ForeignTradeAdvice就是一个自包含的组件,它实现了自己的功能片断,可以当作一个独立单元在应用服务器外部进行测试,下面我将对此进行说明。
