如何重构J2EE应用程序以使用 ring功能。我们将使用XDoclet的基于JavaDoc的元数据来生成home和bean接口,以及EJB部署描述符。可以在下面的“下载”部分中找到本文中所有示例类的源代码。
  重构EJB组件以使用 ring的EJB类
  想像一个简单的股票报价EJB组件,它返回当前的股票交易价格,并允许设置新的交易价格。这个例子用于说明同时使用 ring Framework与J2EE服务的各个集成方面和最佳实践,而不是要展示如何编写股票管理应用程序。按照我们的要求,TradeManager业务接口应该就是下面这个样子:

public interface TradeManager {
public static String ID = "tradeManager";
public BigDecimal getPrice(String name);
public void setPrice(String name, BigDecimal price);
}
  在设计J2EE应用程序的过程中,通常使用远程无状态会话bean作为持久层中的外观和实体bean。下面的TradeManager1Impl说明了无状态会话bean中TradeManager接口的可能实现。注意,它使用了ServiceLocator来为本地的实体bean查找home接口。XDoclet注释用于为EJB描述符声明参数以及定义EJB组件的已公开方法。

/**
* @ejb.bean
* name="org.javatx. ring.aop.TradeManager1"
* type="Statele "
* view-type="both"
* tra action-type="Container"
*
* @ejb.tra action type="NotSu orted"
*
* @ejb.home
* remote-pattern="{0}Home"
* local-pattern="{0}LocalHome"
*
* @ejb.interface
* remote-pattern="{0}"
* local-pattern="{0}Local"
*/
public cla  TradeManager1Impl implements Se io ean, TradeManager {
private Se ionContext ctx;
private TradeLocalHome tradeHome;
/**
* @ejb.interface-method view-type="both"
*/
public BigDecimal getPrice(String symbol) {
try {
return tradeHome.findByPrimaryKey(symbol).getPrice();
} catch(ObjectNotFoundException ex) {
return null;
} catch(FinderException ex) {
throw new EJBException("Unable to find symbol", ex);
}
}
/**
* @ejb.interface-method view-type="both"
*/
public void setPrice(String symbol, BigDecimal price) {
try {
try {
tradeHome.findByPrimaryKey(symbol).setPrice(price);
} catch(ObjectNotFoundException ex) {
tradeHome.create(symbol, price);
}
} catch(CreateException ex) {
throw new EJBException("Unable to create symbol", ex);
} catch(FinderException ex) {
throw new EJBException("Unable to find symbol", ex);
}
}

public void ejbCreate() throws EJBException {
tradeHome = ServiceLocator.getTradeLocalHome();
}
public void ejbActivate() throws EJBException, RemoteException {
}
public void ej a ivate() throws EJBException, RemoteException {
}
public void ejbRemove() throws EJBException, RemoteException {
}
public void setSe ionContext(Se ionContext ctx) throws EJBException, RemoteException {
this.ctx = ctx;
}
}
  如果要在进行代码更改之后测试这样一个组件,那么在运行任何测试(通常是基于专用的容器内测试框架,比如Cactus或MockEJB)之前,必须要经过构建、启动容器和部署应用程序这整个周期。虽然在简单的用例中类的热部署可以节省重新部署的时间,但是当类模式变动(例如,添加域或方法,或者修改方法名)之后它就不行了。这个问题本身就是把所有逻辑转移到无格式Java对象中的最好理由。正如您在TradeManager1Impl代码中所看到的那样,大量的粘和代码把EJB中的所有内容组合在一起,而且您无法从围绕JNDI访问和异常处理的复制工作中抽身。然而, ring提供抽象的便利类,可以使用定制的EJB bean对它进行扩展,而无需直接实现J2EE接口。这些抽象的超类允许移除定制bean中的大多数粘和代码,而且提供用于获取 ring应用程序上下文的实例的方法。
  首先,需要把TradeManager1Impl中的所有逻辑都转移到新的无格式Java类中,这个新的类还实现了一个TradeManager接口。我们将把实体bean作为一种持久性机制,这不仅因为它超出了本文的讨论范围,还因为WebLogic Server提供了大量用于调优CMP bean性能的选项。在特定的用例中,这些bean可以提供非常好的性能。我们还将使用 ring IoC容器把TradeImpl实体bean的home接口注入到TradeDao的构造函数中,您将从下面的代码中看到这一点:

public cla  TradeDao implements TradeManager {
private TradeLocalHome tradeHome;
public TradeDao(TradeLocalHome tradeHome) {
this.tradeHome = tradeHome;
}
public BigDecimal getPrice(String symbol) {
try {
return tradeHome.findByPrimaryKey(symbol).getPrice();
} catch(ObjectNotFoundException ex) {
return null;
} catch(FinderException ex) {
throw new EJBException("Unable to find symbol", ex);
}
}
public void setPrice(String symbol, BigDecimal price) {
try {
try {
tradeHome.findByPrimaryKey(symbol).setPrice(price);
} catch(ObjectNotFoundException ex) {
tradeHome.create(symbol, price);
}
} catch(CreateException ex) {
throw new EJBException("Unable to create symbol", ex);
} catch(FinderException ex) {
throw new EJBException("Unable to find symbol", ex);
}
}
}
  现在,可以使用 ring的A tractStatele e io ean抽象类重写TradeManager1Impl,该抽象类还可以帮助您获得上面所创建的TradeDao bean的一个 ring托管的实例:

/**
* @ejb.home
* remote-pattern="TradeManager2Home"
* local-pattern="TradeManager2LocalHome"
* extends="javax.ejb.EJBHome"
* local-extends="javax.ejb.EJBLocalHome"
*
* @ejb.tra action type="NotSu orted"
*
* @ejb.interface
* remote-pattern="TradeManager2"
* local-pattern="TradeManager2Local"
* extends="javax.ejb.Se io ean"
* local-extends="javax.ejb.Se io ean, org.javatx. ring.aop.TradeManager"
*
* @ejb.env-entry
* name="BeanFactoryPath"
* value="a licationContext.xml"
*/
public cla  TradeManager2Impl extends A tractStatele e io ean implements TradeManager {
private TradeManager tradeManager;
public void setSe ionContext(Se ionContext se ionContext) {
super.setSe ionContext(se ionContext);
// make sure there will be the only one  ring bean config
setBeanFactoryLocator(ContextSingleto eanFactoryLocator.getI tance());
}
public void onEjbCreate() throws CreateException {
tradeManager = (TradeManager) getBeanFactory().getBean(TradeManager.ID);
}
/**
* @ejb.interface-method view-type="both"
*/
public BigDecimal getPrice(String symbol) {
return tradeManager.getPrice(symbol);
}
/**
* @ejb.interface-method view-type="both"
*/
public void setPrice(String symbol, BigDecimal price) {
tradeManager.setPrice(symbol, price);
}
}
   现在,EJB把所有调用都委托给在onEjbCreate()方法中从 ring获得的TradeManager实例,这个方法是在A tractEnterpriseBean中实现的,它处理所有查找和创建 ring应用程序上下文所需的工作。但是,必须在EJB部署描述符中为EJB声明BeanFactoryPath env-entry,以便将配置文件和bean声明的位置告诉 ring。上面的例子使用了XDoclet注释来生成这些信息。
  此外还要注意,我们重写了setSe ionContext()方法,以便告诉A tractStatele e io ean跨所有EJB bean使用 ing应用程序上下文的单个实例。
  现在,可以在a licationContext.xml中声明一个tradeManager bean。基本上需要创建一个上面TradeDao的新实例,把从JNDI获得的TradeLocalHome实例传递给它的构造函数。下面给出了可能的定义:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE bea  PUBLIC "-// RING//DTD BEAN//EN" " ring-bea .dtd">
<bea >
<bean id="tradeManager" cla ="org.javatx. ring.aop.TradeDao">
<co tructor-arg index="0">
<bean cla ="org. ringframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<bean id="org.javatx. ring.aop.TradeLocalHome.JNDI_NAME"
cla ="org. ringframework.bea .factory.config.FieldRetrievingFactoryBean"/>
</property>
<property name="proxyInterface" value="org.javatx. ring.aop.TradeLocalHome"/>
</bean>
</co tructor-arg>
</bean>
</bea >
  在这里,我们使用了一个匿名定义的TradeLocalHome实例,这个实例是使用 ring的JndiObjectFactoryBean从JNDI获得的,然后把它作为一个构造函数参数注入到tradeManager中。我们还使用了一个FieldRetrievingFactoryBean来避免硬编码TradeLocalHome的实际JNDI名称,而是从静态的域(在这个例子中为TradeLocalHome.JNDI_NAME)获取它。通常,使用JndiObjectFactoryBean时声明proxyInterface属性是一个不错的主意,如上面的例子所示。
  还有另一种简单的方法可以访问会话bean。 ring提供一个LocalStatele e io roxyFactoryBean,它允许立刻获得一个会话bean而无需经过home接口。例如,下面的代码说明了如何使用通过 ring托管的另一个bean中的本地接口访问的MyComponentImpl会话bean:

<bean id="tradeManagerEjb"
cla ="org. ringframework.ejb.acce .LocalStatele e io roxyFactoryBean">
<property name="jndiName">
<bean id="org.javatx. ring.aop.TradeManager2LocalHome.JNDI_NAME"
cla ="org. ringframework.bea .factory.config.FieldRetrievingFactoryBean"/>
</property>
<property name="busine Interface" value="org.javatx. ring.aop.TradeManager"/>
</bean>
  这种方法的优点在于,可以很容易地从本地接口切换到远程接口,只要使用SimpleRemoteStatele e io roxyFactoryBean修改 ring上下文中的一处bean声明即可。例如:

<bean id="tradeManagerEjb"
cla ="org. ringframework.ejb.acce .SimpleRemoteStatele e io roxyFactoryBean">
<property name="jndiName">
<bean id="org.javatx. ring.aop.TradeManager2Home.JNDI_NAME"
cla ="org. ringframework.bea .factory.config.FieldRetrievingFactoryBean"/>
</property>
<property name="busine Interface" value="org.javatx. ring.aop.TradeManager"/>
<property name="lookupHomeO tartup" value="false"/>
</bean>
  注意,lookupHomeO tartup property被设置为false,以支持延迟初始化。
  下面,我总结一下到此为止所学习的内容:

上面的重构已经为使用高级的 ring功能(也就是依赖性注入和AOP)奠定了基础。
在没有修改客户端API的情况下,我把所有业务逻辑都移出外观会话bean,这就使得这个EJB不惧修改,而且易于测试。
业务逻辑现在位于一个无格式Java对象中,只要该Java对象的依赖性不需要JNDI中的资源,就可以在容器外部对其进行测试,或者可以使用存根或模仿(mock)来代替这些依赖性。
现在,可以代入不同的tradeManager实现,或者修改初始化参数和相关组件,而无需修改Java代码。
  至此,我们已经完成了所有准备步骤,可以开始解决对TradeManager服务的新需求了。
  通知由 ring托管的组件
  在前面的内容中,我们重构了服务入口点,以便使用 ring托管的bean。现在,我将向您说明这样做将如何帮助改进组件和实现新功能。
  首先,假定用户想看到某些符号的价格,而这些价格并非由您的TradeManager组件所托管。换句话说,您需要连接到一个外部服务,以便获得当前您不处理的所请求符号的当前市场价格。您可以使用雅虎门户中的一个基于HTTP的免费服务,但是实际的应用程序将连接到提供实时数据的供应商(比如Reuters、Thomson、Bloomberg、NAQ等等)的实时数据更新服务(data feed)。
  首先,需要创建一个新的YahooFeed组件,该组件实现了相同的TradeManager接口,然后从雅虎金融门户获得价格信息。自然的实现可以使用HttpURLCo ection发送一个HTTP请求,然后使用正则表达式解析响应。例如:

public cla  YahooFeed implements TradeManager {
private static final String SERVICE_URL = "[url]http://finance.yahoo.com/d/quotes.csv?f=k1&;am[/url] =";
private Pattern pattern = Pattern.compile("\"(.*) - (.*)\"");
public BigDecimal getPrice(String symbol) {
HttpURLCo ection co 
String re o eMe age;
int re o eCode;
try {
URL serviceUrl = new URL(SERVICE_URL symbol);
co  = (HttpURLCo ection) serviceUrl.openCo ection();
re o eCode = co .getRe o eCode();
re o eMe age = co .getRe o eMe age();
} catch(Exception ex) {
throw new RuntimeException("Co ection error", ex);
}
if(re o eCode!=HttpURLCo ection.HTTP_OK) {
throw new RuntimeException("Co ection error " re o eCode " " re o eMe age);
}
String re o e = readRe o e(co );
Matcher matcher = pattern.matcher(re o e);
if(!matcher.find()) {
throw new RuntimeException("Unable to parse re o e [" re o e "] 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 U u ortedOperationException("Can't set price of 3rd party trade");
}
private String readRe o e(HttpURLCo ection co ) {
// ...
return re o e;
}
}
  完成这种实现并测试(在容器外部!)之后,就可以把它与其他组件进行集成。传统的做法是向TradeManager2Impl添加一些代码,以便检查getPrice()方法返回的值。这会使测试的次数至少增加一倍,而且要求为每个测试用例设定附加的先决条件。然而,如果使用 ring AOP框架,就可以更漂亮地完成这项工作。您可以实现一条通知,如果初始的TradeManager没有返回所请求符号的值,该通知将使用YahooFeed组件来获取价格(在这种情况下,它的值是null,但是也可能会得到一个Unknow ymbol异常)。
  要把通知应用到具体的方法,需要在 ring的bean配置中声明一个Advisor。有一个方便的类叫做NameMatchMethodPointcutAdvisor,它允许通过名称选择方法,在本例中还需要一个getPrice方法:

<bean id="yahooFeed" cla ="org.javatx. ring.aop.YahooFeed"/>
<bean id="foreignTradeAdvisor"
cla ="org. ringframework.aop.su ort.NameMatchMethodPointcutAdvisor">
<property name="ma edName" value="getPrice"/>
<property name="advice">
<bean cla ="org.javatx. ring.aop.ForeignTradeAdvice">
<co tructor-arg index="0" ref="yahooFeed"/>
</bean>
</property>
</bean>
  正如您所看到的,上面的advisor指派了一个ForeignTradeAdvice给getPrice()方法。针对通知类, ring AOP框架使用了AOP Alliance API,这意味着环绕通知的ForeignTradeAdvice应该实现MethodInterceptor接口。例如:

public cla  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 re 
Object[] args = invocation.getArguments();
String symbol = (String) args[0];
return tradeManager.getPrice(symbol);
}
}
  上面的代码使用invocation.proceed()调用了一个原始的组件,而且如果它返回null,它将调用另一个在通知创建时作为构造函数参数注入的tradeManager。参见上面foreignTradeAdvisor bean的声明。
  现在可以把在 ring的bean配置中定义的tradeManager重新命名为baseTradeManager,然后使用ProxyFactoryBean把tradeManager声明为一个代理。新的baseTradeManager将成为一个目标,我们将使用上面定义的foreignTradeAdvisor通知它:

<bean id="baseTradeManager"
cla ="org.javatx. ring.aop.TradeDao">
... same as tradeManager definition in the above example
</bean>
<bean id="tradeManager" cla ="org. ringframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="org.javatx. ring.aop.TradeManager"/>
<property name="target" ref="baseTradeManager"/>
<property name="interceptorNames">
<list>
<idref local="foreignTradeAdvisor"/>
</list>
</property>
</bean>
  基本上,就是这样了。我们实现了附加的功能而没有修改原始的组件,而且仅使用 ring应用程序上下文来重新配置依赖性。要想不借助于 ring AOP框架在典型的EJB组件中实现类似的修改,要么必须为EJB添加附加的逻辑(这会使其难以测试),要么必须使用decorator模式(实际上增加了EJB的数量,同时也提高了测试的复杂性,延长了部署时间)。在上面的例子中,您可以看到,借助于 ring,可以轻松地不修改现有组件而向这些组件添加附加的逻辑。现在,您拥有的是几个轻量级组件,而不是紧密耦合的bean,您可以独立测试它们,使用 ring Framework组装它们。注意,使用这种方法,ForeignTradeAdvice就是一个自包含的组件,它实现了自己的功能片断,可以当作一个独立单元在应用服务器外部进行测试,下面我将对此进行说明。
  测试通知代码
  您可能注意到了,代码不依赖于TradeDao或YahooFeed。这样就可以使用模仿对象完全独立地测试这个组件。模仿对象测试方法允许在组件执行之前声明期望,然后验证这些期望在组件调用期间是否得到满足。要了解有关模仿测试的更多信息,请参见“参考资料”部分。下面我们将会使用jMock框架,该框架提供了一个灵活且功能强大的API来声明期望。
  测试和实际的应用程序使用相同的 ring bean配置是个不错的主意,但是对于特定组件的测试来说,不能使用实际的依赖性,因为这会破坏组件的孤立性。然而, ring允许在创建 ring的应用程序上下文时指定一个Bea ostProce or,从而置换选中的bean和依赖性。在这个例子中,可以使用模仿对象的一个Map,这些模仿对象是在测试代码中创建的,用于置换 ring配置中的bean:

public cla  Stu ostProce or implements Bea ostProce or {
private final Map stu 
public Stu ostProce or( Map stu ) {
this.stu  = stu 
}
public Object postProce eforeInitialization(Object bean, String bea ame) {
if(stu .contai Key(bea ame)) return stu .get(bea ame);
return bea 
}
public Object postProce AfterInitialization(Object bean, String bea ame) {
return bea 
}
}
   在测试用例类的setUp()方法中,我们将使用baseTradeManager和yahooFeed组件的模仿对象来初始化Stu ostProce or,而这两个组件是使用jMock API创建的。然后,我们就可以创建Cla athXmlA licationContext(配置其使用Bea ostProce or)来实例化一个tradeManager组件。产生的tradeManager组件将使用模仿后的依赖性。
  这种方法不仅允许孤立要测试的组件,还可以确保在 ring bean配置中正确定义通知。实际上,要在不模拟大量容器基础架构的情况下使用这样的方法来测试在EJB组件中实现的业务逻辑是不可能的:

public cla  ForeignTradeAdviceTest extends TestCase {
TradeManager tradeManager;
private Mock baseTradeManagerMock;
private Mock yahooFeedMock;
protected void setUp() throws Exception {
super.setUp();
baseTradeManagerMock = new Mock(TradeManager.cla , "baseTradeManager");
TradeManager baseTradeManager = (TradeManager) baseTradeManagerMock.proxy();
yahooFeedMock = new Mock(TradeManager.cla , "yahooFeed");
TradeManager yahooFeed = (TradeManager) yahooFeedMock.proxy(); v
Map stu  = new HashMap();
stu .put("yahooFeed", yahooFeed);
stu .put("baseTradeManager", baseTradeManager);
ConfigurableA licationContext ctx = new Cla athXmlA licationContext(CTX_NAME);
ctx.getBeanFactory().addBea ostProce or(new Stu ostProce or(stu ));
tradeManager = (TradeManager) proxyFactory.getProxy();
}
...
  在实际的testAdvice()方法中,可以为模仿对象指定期望并验证(例如)baseTradeManager上的getPrice()方法是否返回null,然后yahooFeed上的getPrice()方法也将被调用:

public void testAdvice() throws Throwable {
String symbol = "testSymbol";
BigDecimal expectedPrice = new BigDecimal("0.222");
baseTradeManagerMock.expects(new InvokeOnceMatcher()).method("getPrice")
.with(new IsEqual(symbol)).will(new Retur tub(null));
yahooFeedMock.expects(new InvokeOnceMatcher()).method("getPrice")
.with(new IsEqual(symbol)).will(new Retur tub(expectedPrice));
BigDecimal price = tradeManager.getPrice(symbol);
a ertEquals("Invalid price", expectedPrice, price);
baseTradeManagerMock.verify();
yahooFeedMock.verify();
}
  这段代码使用jMock约束来指定,baseTradeManagerMock期望只使用一个等于symbol的参数调用getPrice()方法一次,而且这次调用将返回null。类似地,yahooFeedMock也期望对同一方法只调用一次,但是返回expectedPrice。这允许在setUp()方法中运行所创建的tradeManager组件,并断言返回的结果。
  这个测试用例很容易参数化,从而涵盖所有可能的用例。注意,当组件抛出异常时,可以很容易地声明期望。
 
 
测试
baseTradeManager
yahooFeed
期望
调用
返回
抛出
调用
返回
抛出
结果t
异常
1
true
0.22
-
false
-
-
0.22
-
2
true
-
e1
false
-
-
-
e1
3
true
null
-
true
0.33
-
0.33
-
4
true
null
-
true
null
-
null
-
5
true
null
-
true
-
e2
-
e2
  可以使用这个表更新测试类,使其使用一个涵盖了所有可能场景的参数化序列:

... public static TestSuite suite() {
BigDecimal v1 = new BigDecimal("0.22");
BigDecimal v2 = new BigDecimal("0.33");
RuntimeException e1 = new RuntimeException("e1");
RuntimeException e2 = new RuntimeException("e2");
TestSuite suite = new TestSuite(ForeignTradeAdviceTest.cla .getName());
suite.addTest(new ForeignTradeAdviceTest(true, v1, null, false, null, null, v1, null));
suite.addTest(new ForeignTradeAdviceTest(true, null, e1, false, null, null, null, e1));
suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, v2, null, v2, null));
suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, null, null, null, null));
suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, null, e2, null, e2));
return suite;
}
public ForeignTradeAdviceTest(
boolean baseCall, BigDecimal baseValue, Throwable baseException,
boolean yahooCall, BigDecimal yahooValue, Throwable yahooException,
BigDecimal expectedValue, Throwable expectedException) {
super("test");
this.baseCall = baseCall;
this.baseWill = baseException==null ?
(Stub) new Retur tub(baseValue) : new ThrowStub(baseException);
this.yahooCall = yahooCall;
this.yahooWill = yahooException==null ?
(Stub) new Retur tub(yahooValue) : new ThrowStub(yahooException);
this.expectedValue = expectedValue;
this.expectedException = expectedExceptio 
}
public void test() throws Throwable {
String symbol = "testSymbol";
if(baseCall) {
baseTradeManagerMock.expects(new InvokeOnceMatcher())
.method("getPrice").with(new IsEqual(symbol)).will(baseWill);
}
if(yahooCall) {
yahooFeedMock.expects(new InvokeOnceMatcher())
.method("getPrice").with(new IsEqual(symbol)).will(yahooWill);
}
try {
BigDecimal price = tradeManager.getPrice(symbol);
a ertEquals("Invalid price", expectedValue, price);
} catch(Exception e) {
if(expectedException==null) {
throw e;
}
}
baseTradeManagerMock.verify();
yahooFeedMock.verify();
}
public String getName() {
return super.getName() " " 
baseCalled " " baseValue " " baseException " " 
yahooCalled " " yahooValue " " yahooException " " 
expectedValue " " expectedExceptio 
} ...
  在更复杂的情况下,上面的测试方法可以很容易地扩展为大得多的输入参数集合,而且它仍然会立刻运行且易于管理。此外,把所有参数移入一个外部配置文件或者甚至Excel电子表格是合理的做法,这些配置文件或电子表格可以由QA团队管理,或者直接根据需求生成。
  组合和链接通知
  我们已经使用了一个简单的拦截器通知来实现附加的逻辑,并且将其当作一个独立的组件进行了测试。当应该在不进行修改并且与其他组件没有附加耦合的情况下扩展公共执行流时,这种设计十分有效。例如,当价格已经发生变化时,如果需要使用JMS或JavaMail发送通知,我们可以在tradeManager bean的setPrice方法上注册另一个拦截器,并使用它来向相关组件通知有关这些变化的情况。在很多情况下,这些方面都适用于非功能性需求,比如许多AOP相关的文章和教程中经常用作“hello world”例子的跟踪、登录或监控。
  另一个传统的AOP应用程序是缓存。例如,一个基于CMP实体bean的TradeDao组件将从WebLogic Server提供的缓存功能中受益。然而对于YahooFeed组件来说却并非如此,因为它必须通过Internet连接到雅虎门户。这明显是一个应该应用缓存的位置,而且它还允许减少外部连接的次数,并最终降低整个系统的负载。注意,基于截至时间的缓存也会在刷新信息时带来一些延迟,但是在很多情况下,它仍然是可以接受的。要应用缓存功能,可以定义一个yahooFeedCachingAdvisor,它将把CachingAdvice附加到yahooFeed bean上的getPrice()方法。在“下载”部分中,您可以找到一个CachingAdvice实现的例子。

<bean id="getPriceAdvisor" a tract="true"
cla ="org. ringframework.aop.su ort.NameMatchMethodPointcutAdvisor">
<property name="ma edName" value="getPrice"/>
</bean>
<bean id="yahooFeedCachingAdvisor" parent="getPriceAdvisor">
<property name="advice">
<bean cla ="org.javatx. ring.aop.CachingAdvice">
<co tructor-arg index="0" ref="cache"/>
</bean>
</property>
</bean>
  因为getPrice()方法已经成为几种通知的公共联结点,所以声明一个抽象的getPriceAdvisor bean,然后在yahooFeedCachingAdvisor中对其进行扩展,指定具体的通知CachingAdvice。注意,也可以修改前面的foreignTradeAdvisor,使其使用同一个getPriceAdvisor父bean。
  现在可以更新yahooFeed bean的定义,并将它包装在一个ProxyFactoryBean中,然后使用yahooFeedCachingAdvisor通知它。例如:

<bean id="yahooFeed" cla ="org. ringframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="org.javatx. ring.aop.TradeManager"/>
<property name="target">
<bean cla ="org.javatx. ring.aop.YahooFeed">
</property>
<property name="interceptorNames">
<list>
<value>yahooFeedCachingAdvisor</value>
</list>
</property>
</bean>
  当请求命中已经保存在缓存中的数据时,上面的修改将极大地提高性能,但是如果传入多个针对同一个符号的请求,而该符号尚未进入缓存或者已经到期,我们将看到多个并发的请求到达服务提供者,请求同一个符号。对此,存在一种显而易见的优化,就是中断对同一个符号的所有请求,直到第一个请求完成为止,然后使用第一个请求获得的结果。EJB规范(参见“Programming Restrictio ”,2.1版本的25.1.2部分)一般不推荐使用这种方法,因为它对运行在多个JVM上的集群环境不奏效。然而,至少在单个的节点中这种优化可以改进性能。图2所示的图表对比说明了优化之前和优化之后的情况:
 

图2. 优化之前和优化之后
  该优化也可以实现为通知,并添加在yahooFeed bean中的拦截器链的末端:

...
<property name="interceptorNames">
<list>
<idref local="yahooFeedCachingAdvisor"/>
<idref local="syncPointAdvisor"/>
</list>
</property>
  实际的拦截器实现应该像下面这样:

public cla  SyncPointAdvice implements MethodInterceptor {
private long DEFAULT_TIMEOUT = 10000L;
private Map requests = Collectio .synchronizedMap(new HashMap());
public Object invoke(MethodInvocation invocation) throws Throwable {
String symbol = (String) invocation.getArguments()[0];
Object[] lock = (Object[]) requests.get(symbol);
if(lock==null) {
lock = new Object[1];
requests.put(symbol, lock);
try {
lock[0] = invocation.proceed();
return lock[0];
} finally {
requests.remove(symbol);
synchronized(lock) {
lock.notifyAll();
}
}
}
synchronized(lock) {
lock.wait(DEFAULT_TIMEOUT);
}
return lock[0];
}
}
  可以看出,通知代码相当简单,而且不依赖于其他的组件,这使得JUnit测试变得十分简单。在“参考资料”部分,您可以找到SyncPointAdvice的JUnit测试的完整源代码。对于复杂的并发场景来说,使用Java 5中java.util.concurrent包的同步机制或者针对老的JVM使用其backport是一种不错的做法。
  结束语
  本文介绍了一种把J2EE应用程序中的EJB转换为 ring托管组件的方法,以及转换之后可以采用的强大技术。它还给出了几个实际的例子,说明如何借助于 ring的AOP框架、应用面向方面的方法来扩展J2EE应用程序,并在不修改现有代码的情况下实现新的业务需求。
  在EJB中使用 ring Framework将减少代码间的耦合,并使许多强大的功能即时生效,从而提高可扩展性和灵活性。这还使得应用程序的单个组件变得更加易于测试,包括新引入的AOP通知和拦截器,它们用于实现业务功能或者处理非功能性的需求,比如跟踪、缓存、安全性和事务。
 
//////////////////////////////////////////////////////////////
JDBC学习

DBC基础(一)
    来,我们认识一下!
    JDBC,JAVA平台的DATABASE的连通性.白话一句,什么意思啊?
    就是JAVA平台上和数据库进行连结的\"工具\".
    还是先一起来回顾一下接口吧:从下向上,接口是对\"案例\"的抽象,由一个案例抽象出一些规则.
反过来,从上向下,被抽象出来的接口是对案例的一种承诺和约束.
    也就是说,只要你实现我规定的接口,你的类就已经具有了接口对外承诺的方法,只要\"客户\"会
操作接口,不需要重新学习就会操作实现了该接口的新类!
    好了,用行话来说:
1.通过接口可以实现不相关的类的相同行为.
2.通过接口可以指明多个类需要实现的方法.
3.通过接口可以了解对象的交互方法而不需要了解对象所对应的类蓝本.
    这几句话很明白吧?好象有一本什么模式的书把这段话用了30多页写出来,结果别人看了还不如
我这几句话明白,不过我明白了为什么有些人要写书了.
    搞懂了以上这东西,JDBC就好明白了.
    为了通用,JAVA中要求有一种机制,在操作不同厂商数据库时有相同的方法去操作,而不是每接
触一种数据库就要学习新的方法.完成这种机制的\"东西\"就叫\"JDBC\"了.
    简单地分,JDBC有两部分组成,JDBC API和JDBC Driver Interface.
    JDBC API就是提供给\"客户\"(就是象你我这种菜鸟级程序员来用的,如果是高手都自己写JDBC了,
哈哈)的一组独立于数据库的API,对任何数据库的操作,都可以用这组API来进行.那么要把这些通用的API
翻译成特定数据库能懂的\"指令\",就要由JDBC Driver Interface来实现了,所以这部份是面向JDBC驱动程
序开发商的编程接口,它会把我们通过JDBC API发给数据库的通用指令翻译给他们自己的数据库.

    还是通过实际操作来看看JDBC如何工作的吧.
    因为JDBC API是通用接口,那么程序是如何知道我要连结的是哪种数据库呢?所以在和数据库连
结时先要加载(或注册可用的Driver),其实就是JDBC签名.加载驱动程序和好多方法,最常用的就是先把驱
动程序类溶解到内存中,作为\"当前\"驱动程序.注意\"当前\"是说内存中可以有多个驱动程序,但只有现在加
载的这个作为首选连结的驱动程序.
    Class.forName(\"org.gjt.mm.mysql.Driver\");
    Class.forName方法是先在内存中溶解签名为\"org.gjt.mm.mysql.Driver\"的Driver类,Driver类
就会把相应的实现类对应到JDBC API的接口中.比如把org.gjt.mm.mysql.Connection的实例对象赋给
java.sql.Connection接口句柄,以便\"客户\"能通过操作java.sql.Connection句柄来调用实际的
org.gjt.mm.mysql.Connection中的方法.之于它们是如果映射的,这是厂商编程的,\"客户\"只要调用
Class.forName(\"org.gjt.mm.mysql.Driver\");方法就可以顺利地操作JDBC API了.
    一个普通数据库的连结过程为:
    1.加载驱动程序.
    2.通过DriverManager到得一个与数据库连结的句柄.
    3.通过连结句柄绑定要执行的语句.
    4.接收执行结果.
    5.可选的对结果的处理.
    6.必要的关闭和数据库的连结.
前面说是,注册驱动程序有多方法,Class.forName();是一种显式地加载.当一个驱
动程序类被Classloader装载后,在溶解的过程中,DriverManager会注册这个驱动类的实例.
这个调用是自动发生的,也就是说DriverManager.registerDriver()方法被自动调用了,当然
我们也可以直接调用DriverManager.registerDriver()来注册驱动程序,但是,以我的经验.
MS的浏览中APPLET在调用这个方法时不能成功,也就是说MS在浏览器中内置的JVM对该方法的
实现是无效的.
    另外我们还可以利用系统属性jdbc.drivers来加载多个驱动程序:
System.setProperty(\"jdbc.drivers\",\"driver1:driver2:.....:drivern\");多个驱动程序之
间用\":\"隔开,这样在连结时JDBC会按顺序搜索,直到找到第一个能成功连结指定的URL的驱动
程序.
    在基础篇里我们先不介绍DataSource这些高级特性.
    在成功注册驱动程序后,我们就可以用DriverManager的静态方法getConnection来得
到和数据库连结的引用:
    Connection conn = DriverManager.getConnection(url);
    如果连结是成功的,则返回Connection对象conn,如果为null或抛出异常,则说明没有
和数据库建立连结.
    对于getConnection()方法有三个重载的方法,一种是最简单的只给出数据源即:
getConnection(url),另一种是同时给出一些数据源信息即getConnection(url,Properties),
另外一种就是给出数据源,用户名和密码:getConnection(url,user,passwod),对于数据源信息.
如果我们想在连结时给出更多的信息可以把这些信息压入到一个Properties,当然可以直接压
入用户名密码,别外还可以压入指定字符集,编码方式或默认操作等一些其它信息.
   
    在得到一个连结后,也就是有了和数据库找交道的通道.我们就可以做我们想要的操
作了.
    还是先来介绍一些一般性的操作:
    如果我们要对数据库中的表进行操作,要先缘故绑定一个语句:
    Statement stmt = conn.createStatement();
    然后利用这个语句来执行操作.根本操作目的,可以有两种结果返回,如果执行的查询
操作,返回为结果集ResultSet,如果执行更新操作,则返回操作的记录数int.
    注意,SQL操作严格区分只有两个,一种就是读操作(查询操作),另一种就是写操作(更
新操作),所以,create,insert,update,drop,delete等对数据有改写行为的操作都是更新操作.
    ResultSet rs = stmt.executeQuery(\"select * from table where xxxxx\");
    int x = stmt.executeUpdate(\"delete from table where ......\");
    如果你硬要用executeQuery执行一个更新操作是可以的,但不要把它赋给一个句柄,
当然稍微有些经验的程序员是不会这么做的.
    至于对结果集的处理,我们放在下一节讨论,因为它是可操作的可选项,只有查询操作
才返回结果集,对于一次操作过程的完成,一个非常必要的步骤是关闭数据库连结,在你没有了
解更多的JDBC知识这前,你先把这一步骤作为JDBC操作中最最重要的一步,在以后的介绍中我会
不断地提醒你去关闭数据库连结!!!!!!!!!!!
    按上面介绍的步骤,一个完成的例子是这样的:(注意,为了按上面的步骤介绍,这个例
子不是最好的)
    try{
        Class.forName(\"org.gjt.mm.mysql.Driver\");
    }catch(Exception e){
        System.out.println(\"没有成功加载驱动程序:\"+e.toString());
        return;
    }//对于象我这样的经验,可以直接从e.toString()的简单的几个字判断出异常原因,
     //如果你是一个新手应该选捕获它的子类,如何知道要捕获哪几个异常呢?一个简单
     //的方法就是先不加try{},直接Class.forName(\"org.gjt.mm.mysql.Driver\");,编
     //译器就会告诉你要你捕获哪几个异常了,当然这是偷机取巧的方法,最好还是自己
     //去看JDK文档,它会告诉你每个方法有哪些异常要你捕获.
    Connection conn = null;
    try{
        conn = DriverManager.getConnection(
                        \"jdbc:mysql://host:3306/mysql\",
                        \"user\",
                        \"passwd\");
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(\"select * from table\");
        //rs 处理
        [rs.close();]
        [stmt.close();]
    }
    catch(Exception e){
        System.out.println(\"数据库操作出现异常:\"+e.toString());
    }
    finally{
        try{conn.close();}catch(Exception){}
    }//不管你以前是学习到的关于数据库流程是如何操作的,如果你相信我,从现在开始,
     //请你一定要把数据库关闭的代码写到finally块中,切切!

关于Statement对象:
    前面说过,Statement对象是用来绑定要执行的操作的,在它上面有三种执行方法:
即用来执行查询操作的executeQuery(),用来执行更新操作的executeUpdate()和用来执行
动态的未知的操作的execute().
    JDBC在编译时并不对要执行的SQL语句检测,只是把它看着一个String,只有在驱动
程序执行SQL语句时才知道正确与否.
    一个Statement对象同时只能有一个结果集在活动.这是宽容性的,就是说即使没有
调用ResultSet的close()方法,只要打开第二个结果集就隐含着对上一个结果集的关闭.所以
如果你想同时对多个结果集操作,就要创建多个Statement对象,如果不需要同时操作,那么可
以在一个Statement对象上须序操作多个结果集.
   
    这里我不得不特别说明一下,很多人会用一个Statement进行嵌套查询,然后就来问
我说为什么不能循环?道理上面已经说清楚了.我们来详细分析一下嵌套查询:
    Connection conn = null;
    Statement stmt = null;
    conn = .......;
    stmt = conm.createStatement(xxxxxx);
    ResultSet rs = stmt.executeQuery(sql1);
    while(rs.next()){
        str = rs.getString(xxxxx);
        ResultSet rs1 = stmt.executeQuery(\"select * from 表 where 字段=str\");
    }
当stmt.executeQuery(\"select * from 表 where 字段=str\");赋给rs1时,这时隐含的操作
是已经关闭了rs,你还能循环下去吗?
所以如果要同时操作多个结果集一定要让它他绑定到不同的Statement对象上.好在一个connection
对象可以创建任意多个Statement对象,而不需要你重新获取连结.
    关于获取和设置Statement的选项:只要看看它的getXXX方法和setXXX方法就明白了,这儿
作为基础知识只提一下以下几个:
    setQueryTimeout,设置一个SQL执行的超时限制.
    setMaxRows,设置结果集能容纳的行数.
    setEscapeProcessing,如果参数为true,则驱动程序在把SQL语句发给数据库前进行转义替
换,否则让数据库自己处理,当然这些默认值都可以通过get方法查询.
    Statement的两个子类:
    PreparedStatement:对于同一条语句的多次执行,Statement每次都要把SQL语句发送给数据
库,这样做效率明显不高,而如果数据库支持预编译,PreparedStatement可以先把要执行的语句一次发
给它,然后每次执行而不必发送相同的语句,效率当然提高,当然如果数据库不支持预编译,
PreparedStatement会象Statement一样工作,只是效率不高而不需要用户工手干预.
    另外PreparedStatement还支持接收参数.在预编译后只要传输不同的参数就可以执行,大大
提高了性能.
       
    PreparedStatement ps = conn.prepareStatement(\"select * from 表 where 字段=?\");
    ps.setString(1,参数);
    ResultSet rs = ps.executeQuery();
   
    CallableStatement:是PreparedStatement的子类,它只是用来执行存储过程的.
    CallableStatement sc = conn.prepareCall(\"{call query()}\");
    ResultSet rs = cs.executeQuery();
   
    关于更高级的知识我们在JDBC高级应用中介绍.
 
作为基础知识的最后部分,我们来说一说结果集的处理,当然是说对一般结果集的处理.
至于存储过程返回的多结果集,我们仍然放在高级应用中介绍.
    SQL语句如何执行的是查询操作,那就要返回一个ResultSet对象,要想把查询结果最后
明白地显示给用户,必须对ResultSet进行处理.ResultSet返回的是一个表中符合条件的记录,对
ResultSet的处理要逐行处理,而对于每一行的列的处理,则可以按任意顺序(注意,这只是JDBC规
范的要求,有些JDBC实现时对于列的处理仍然要求用户按顺序处理,但这是极少数的).事实上,虽
然你可以在处理列的时候可以按任意顺序,但如果你按从左到右的顺序则可以得到较高的性能.
    这儿从底层来讲解一下ResultSet对象,在任何介绍JDBC的书上你是不会获得这样的知
识的,因为那是数据库厂商的事.ResultSet对象实际维护的是一个二维指针,第一维是指向当前
行,最初它指向的是结果集的第一行之前,所以如果要访问第一行,就要先next(),以后每一行都
要先next()才能访问,然后第二维的指针指向列,只要当你去rs.getXXX(列)时,才通过
Connection再去数据库把真实的数据取出来,否则没有什么机器能真的把要取的数据都放在内
存中.
    所以,千万要记住,如果Connection已经关闭,那是不可能再从ResultSet中取到数据的.
有很多人问我,我可不可以取到一个ResultSet把它写到Session中然后关闭Connection,这样就
不要每次都连结了.我只能告诉你,你的想法非常好,但,是错误的!当然在javax.sql包中JDBC高
级应用中有CacheRow和WebCacheRow可以把结果集缓存下来,但那和我们自己开一个数据结构把
ResultSet的行集中所有值一次取出来保存起来没有什么两样.
    访问行中的列,可以按字段名或索引来访问.下面是一个简单的检索结果的程序:
    ResultSet rs = stmt.executeQuery(\"select a1,a2,a3 from table\");
    while(rs.next()){
        int i = rs.getInt(1);
        String a = rs.getString(\"a2\");
        ..............
    }
    对于用来显示的结果集,用while来进行next()是最普通的,如果next()返回false,则
说明已经没有可用的行了.但有时我们可能连一行都没有,而如果有记录又不知道是多少行,这时
如果要对有记录和没有记录进行不同的处理,应该用以下流程进行判断:
    if(rs.next()){
        //因为已经先next()了,所经对记录应该用do{}while();来处理
        do{
            int i = rs.getInt(1);
            String a = rs.getString(\"a2\");
        }while(rs.next());
    }
    esle{
        System.out.println(\"没有取得符合条件的记录!\");
    }
    类型转换:
    ResultSet的getXXX方法将努力把结果集中的SQL数据类型转换为JAVA的数据类型,事实
大多数类型是可以转换的,但仍然有不少糊弄是不能转换的,如你不能将一个SQL的float转换成
JAVA的DATE,你无法将 VARCHAR \"我们\"转换成JAVA的Int.
    较大的值:
    对于大于Statement中getMaxFieldSize返回值的值,用普通的getBytes()或getString()
是不能读取的,好在JAVA提供了读取输入浪的方法,对于大对象,我们可以通过rs.getXXXStream()
来得到一个InputStream,XXX的类型包括Ascii,Binay,Unicode.根据你存储的字段类型来使用不
同的流类型,一般来说,二进制文件用getBinayStream(),文本文件用getAsciiStyream(),对于
Unicode字符的文本文件用getUnicodeStream(),相对应的数据库字段类型应该为:Blob,Clob和
Nlob.
    获取结果集的信息:
    大多数情况下编程人员对数据库结构是了解的,可以知道结果集中各列的情况,但有时并
不知道结果集中有哪些列,是什么类型.这时可以通过getMetaData()来获取结果集的情况:
    ResulSetMetaData rsmd = rs.getMetaData();
    rsmd.getColumnCount()返回列的个数.
    getColumnLabel(int)返回该int所对应的列的显示标题
    getColumnName(int)返回该int所对应的列的在数据库中的名称.
    getColumnType(int)返回该int所对应的列的在数据库中的数据类型.
    getColumnTypeName(int)返回该int所对应的列的数据类型在数据源中的名称.
    isReadOnly(int)返回该int所对应的列是否只读.
    isNullable(int)返回该int所对应的列是否可以为空

DBC初级应用实例
JDBC初级应用实例(一)
    在了解JDBC基础知识以后,我们先来写一个数据库操作的类(Bean)以后我们会
在这个类的基础上,随着介绍的深入不断提供优化的方案.
    要把一个数据库操作独立到一个类(Bean)中,至少要考虑以下几个方面:
    1.对于不同层次的应用,应该有不同的得到连结的方法,如果得到连结的方法要随
着应用层次的不同而改变,我们就应该把他独立成一个专门的类中,而把在任何应用层次
中都通用的处理方法封装到一个(类)Bean中.
    2.既然考虑到既作为javaBean使用又可以用为一个普通类调用,要考虑到javaBean
的规范和普通类的灵活性.
    3.对于特定的数据库操作不应封装到共性的(类)Bean中,而应该成为它的扩展类.
    以上几点是充分考虑JAVA的面象对象的思想,经过深入的抽象形成的层次,下面我
们就按这个思想来设计:
    一:定义一个用于连结的Bean,以后如果要在不同的应用中,如可以在J2EE中从
DataSource中得到连结,或从普通的连结池中得到连结,以及直接从DriverManager中得到
连结,只需修改本类中的得到连结的实现方法.
    package com.imnamg.axman.beans;
    import java.sql.*;
    import ..................
    public class ConnectionFactory{
        protected Connection conn;
        ConnectionFactory() throws SQLException
        {    //构造方法中生成连结
            //无论是从DataSource还是直接从DriverManager中取得连结.
            //先初始化环境,然后取得连结,本例作为初级应用,从
            //DriverManager中取得连结,因为是封装类,所以要把异常抛
            //给调用它的程序处理而不要用try{}catch(){}块自选处理了.
            //因为要给业务方法的类继承,而又不能给调用都访问,所以
            //conn声明为protected
            conn =     DriverManager.getConnection(url,user,passwd);
        }
       
        /**
            在多线程编程中,很多时候有可能在多个线程体中得到同一连
            结的引用,但如果在一个线程中关闭了连结,则另一个得到相同
            引用的线程就无法操作了,所以我们应该加一个重新建立连结
            的辅助方法,有人问为什么既然有这个辅助方法不直接调用这个
            辅助而要在构造方法中生成连结?因为这样可以增加效率,如果
            在构造时不能生成连结则就不能生成这个对象了,没有必要在
            对象生成后再测试能不能生成连结.
        */
        public void makeConnection(){
            //此处的代码同构造方法,无论以后如果实现连结,都将构造方
            //法的代码复制到此处.
            conn =     DriverManager.getConnection(url,user,passwd);
        }
    }
    这个类就封装到这里,当然你可以在这儿增加业务方法,但如果要修改连结的实现,
整个类都要重新编译,因为业务方法和应用层次无关,代码一经生成不易变动,所以独立封装.
以下我们实现业务方法:
    package com.imnamg.axman.beans;
    import java.sql.*;
    import ..................
    public class DBOperater extends ConnectionFactory{
        //private Statement stmt;
        //private ResultSet rs;
        //为什么要注释成员变量stmt和rs,基础部分已经说过,如果声明为成员变量,
        //在关闭conn时可以显示地先关闭rs和stmt,别的没有任何好处,而显示关
        //闭只是说明你编程风格好,但综合考虑,我们要生成多个stmt或不是类型的
        //stmt就不能声明为成员方法,否则引用同一对象,所以我们要业务方法中生
        //成stmt对象.不仅可以同时处理多个结果集,还可以提高性能和灵活性.
        public ResultSet executeQuery(String sql) throws SQLException{
            if(conn==null || conn.isClosed())
                makeConnection();
            Statement stmt = con.createStatement(
                    ResultSet.TYPE_SCROLL_INSENSITIVE,
                                         ResultSet.CONCUR_READ_ONLY);
            //对于一般的查询操作,我们只要生成一个可流动的结果集就行了.
            //而对于在查询时要更新记录,我们用另一个业务方法来处理,这样,
            //这样可以在普通查询时节省回滚空间.
            ResultSet rs = stmt.executeQuery(sql);
            return rs;
        }
       
        public ResultSet executeUpdatabledQuery(String sql) throws SQLException{
            if (con == null || con.isClosed())
                makeConnection();
            Statement stmt = con.createStatement(
                    ResultSet.TYPE_SCROLL_INSENSITIVE,
                    ResultSet.CONCUR_UPDATABLED);
            //可更新的结果结要更大的回滚空间,普通查询时不要调用这个方法
            ResultSet rs = stmt.executeQuery(sql);
            return rs;
        }
        /**
            基于同上的原因,在执行更新操作是我们根本不要任何回滚空间,所以建立
            一个基本类型的stmt,实现如下
        */
        public int executeUpdate(String sql) throws SQLException{
            if (con == null || con.isClosed())
                makeConnection();
            Statement stmt = con.createStatement();
            //这个stmt在执行更新操作时更加节省内存,永远记住,能节省的时候要节省
            //每一个字节的内存,虽然硬件设备可能会有很大的物理内存,但内存是给用
            //户用的而不是给程序员用的(!!!!!!!!!!!!!!!!!!)
            int s = stmt.executeUpdate(sql);
            return s;
        }
        //以上实现了常用功能,还有两个通用的功能也是\"共性\"的,我们一起在这个封装类
        //中实现:
        public PreparedStatement getPreparedStmt(String sql) throws SQLException{
            if (con == null || con.isClosed())
                makeConnection();
            PreparedStatement ps = con.prepareStatement(sql);
            return ps;
        }
        public CallableStatement getCallableStmt(String sql) throws SQLException{
            if (con == null || con.isClosed())
                makeConnection();
            PreparedStatement ps = con.prepareCall(sql);
            return ps;
        }
        //记住:对于封装类而言预编译语句和存储过程调用应该从连结中返PreparedStatement
        //和CallableStatement供调用者处理而不是返回它们的处理结果.也就是说封装类只封
        //装了它们的连结过程.最后再次声明,一定要有一个close()方法供调用者调用,而且告
        //诉调用者无论如果要调用这个方法:
        public void close() throws SQLException{
            if(conn != null && !conn.isClosed())
                conn.close();
        }
        //这个方法最好放在ConnectionFactory中,这样可以直接调用来只测试连结.而不用再
        调用子类来关闭
    }
    OK,我们已经实现了数据库常用操作的封装,注意这些业务方法都是把异常抛给调用者而没有用
try...catch来处理,你如果在这里处理了那么调用者则无法调试了.对于特定的数据库的特殊操作,不要封
装到此类中,可以再从这个类继承,或直接从ConnectionFactory类继承,当然最好是从这个业务类中继承,
这样不仅可以调用特殊方法也可以调用共性的业务方法,兴一个例子,我在应用Oracle时要把XML文件直接
存到数据数和把数据直接读取为XML文件,那么这两个方法只对Oracle才用到,所以:
    package com.inmsg.axman.beans;
    import java.sql.*;
    import oracle.xml.sql.query.OracleXMLQuery;
    import oracle.xml.sql.dml.OracleXMLSave;
   
    public class OracleDBOperater extends DBOperater{
         public OracleXMLQuery getOXQuery(String sql,String table) throws Exception
                 {
                         OracleXMLQuery qry = new OracleXMLQuery(con,sql);
                         qry.setRowsetTag(table);
                         qry.setRowTag(\"RECORD\");
                         return qry;
                 }
                 public int insertXML(String path,String table) throws Exception
                 {
                         OracleXMLSave sav = new OracleXMLSave(con,table);
                         URL url = sav.createURL(path);
                         sav.setRowTag(\"RECORD\");
                         int x = sav.insertXML(url);
                         sav.close();
                         return x;
                 }
    }
    现在,有了这样的几个\"东西\"在手里,你还有什么觉得不方便的呢?
    虽然本处作为初级应用,但设计思想已经是JAVA高手的套路了,是不是有些自吹自擂了啊?
好的,休息一下吧.
上面有一位朋友问了,如果在已经连结的情况下,知道当前连结的库的表的情况呢?
其实只你已经连结了,你就能知道这个库中所以情况而不仅仅上表的情况:
    有时(我到目前只见到过一次),我们对一种新的数据库根本不知道它的结构或者是
其中的内容,好坏么我们如何来获取数据库的情况呢?
    真实的例子是这样的,我的朋友的公司接到了一个单子,对方使用的数据库是叫什么
\"titanium\"的,说实话由于本人的孤陋寡闻,在此之前从来不知道还有这种数据库,更别说如何
访问了,现在朋友要看里面有什么\"东西\",当然是一筹莫展.所以只好找我.
    接到电话后,我先问他是什么平台上跑的,如果连结的,他说是在windows下可以建立
ODBC数据源,哈哈,就是说可以用java建立Connection了,OK
    只能建立一下Connection,那么就可以得到这个数据库的所有元信息:
    DatabaseMetadata dbmd = conn.getMetadata();然后你可以从这个对象获取以下信
息:
    getUrl();      //返回与这个数据库的连结的URL,当然是已知的,要不你怎么连上去
    getUserName(); //返回与这个数据库的连结的用户,同上
    isReadOnly();数据库是否为只读
    getDatabaseProduceName();//数据库产品名称
    getDatabaseProduceVersion();//版本号
    getDriverName();//驱动程序
    getDriverVersion();//驱动程序版本
    以上内容没有什么意义
    ResultSet getTables(String catalog,
                String schemaPattern,
                String tableNamePattern,
                String[] types)
    可以得到该库中\"表\"的所有情况,这里的表包括表,视图,系统表,临时空间,别名,同义词
    对于各参数:
    String catalog,表的目录,可能为null,\"null\"匹配所有
    String schemaPattern,表的大纲,同上
    String tableNamePattern,表名,同上
    String[] types,表的类型,\"null\"匹配所有,可用的类型为:
    TABLE,VIEW,SYSEM TABLE,GLOBAL TEMPORARY,LOCAL  TEMPORARY,ALIAS,SYNONYM
    例如:
        DatabaseMetaData dbmd = conn.getMetaData();
        ResultSet rs = dbmd.getTables(null,null,null,null);
        ResultSetMetaData rsmd = rs.getMetaData();
        int j = rsmd.getColumnCount();
        for(int i=1;i<=j;i++){
            out.print(rsmd.getColumnLabel(i)+\"\\t\");
        }
        out.println();
        while(rs.next()){
            for(int i=1;i<=j;i++){
                out.print(rs.getString(i)+\"\\t\");
            }
            out.println();
        }
    对于更详细的表中的列的信息,可以用dbmd(不是rsmd).getColumns(
            String catalog,
            String schemaPattern,
            String tableNamePattern,
            String columnNamePattern
        )
    不仅可以获得rsmd中的信息,还可以获得列的大小,小数位数,精度,缺省值,列在表中
的位置等相关信息.
    还有两个方法,调用和获取表信息一样,可以获得存储过程和索引的信息:
    ResultSet getProcedures(
            String catalog,
            String schemaPattern,
            String procedurePattern
        );
    ResultSet getIndexINFO(
            String catalog,
            String schemaPattern,
            String table,
            boolean unique,boolean approximate
 
/////////////////////////////////

示例 5-1 IBatisRentABike.java
 
public class IBatisRentABike extends SqlMapDaoSupport                 implements RentABike {        private String storeName ="";        public void setStoreName(String storeName) {                this.storeName= storeName;        }        public String getStoreName( ) {                return this.storeName;        }        public List getBikes() {                return getSqlMapTemplate().executeQueryForList("getBikes", null);        }        public Bike getBike(String serialNo) {                return (Bike) getSqlMapTemplate().                        executeQueryForObject("getBikeBySerialNo", serialNo);        }        public Bike getBike(int bikeId) {                return (Bike) getSqlMapTemplate().                        executeQueryForObject("getBikeByID", new Integer(bikeId));        }        public void saveBike(Bike bike) {                getSqlMapTemplate().executeUpdate("saveBike", bike);        }        public void deleteBike(Bike bike) {                getSqlMapTemplate().executeUpdate("deleteBike", bike);        }        public List getCustomers() {                return getSqlMapTemplate().executeQueryForList("getCustomers", null);        }        public Customer getCustomer(int custId) {                return (Customer) getSqlMapTemplate().                    executeQueryForObject("getCustomer", new Integer(custId));        }                public List getReservations() {                return getSqlMaptemplate().                    executeQueryForList("getReservations", null);        }        public List getReservations(Customer customer) {                return getSqlMaptemplate().                    executeQueryForList("getReservationsForCustomer", customer);        }        public List getReservations(Bike bike) {                return getSqlMaptemplate().                    executeQueryForList("getReservationsForBike",bike);        }        public List getReservations(Date date) {                return getSqlMaptemplate().                    executeQueryForList("getReservationsForDate", date);        }        public Reservation getReservation(int resId) {                return getSqlMaptemplate().                    executeQueryForObject("getReservation", new Integer(resId));        }}

这些就是命名式查询。iBATIS将每一查询分成一个独立的映射,那样你就可以用名字来执行查询。
SqlMapTemplate由Spring的SqlMapDaoSupport 类提供,我们的RentABike实现必须来继承这个类。SqlMapTemplate负责建立和管理底层数据存储的连接,同时也解释了你所提供的映射文件。你可以把template 看成是你对于iBATIS命名式查询所做那些事情的默认实现。
你也需要创建SQL语句,可以给每条SQL语句取一个名字。然后,把结果映射给Java Bean。在这里你有两种选择,你可以在SQL中把每个Bean属性作为别名来引用,或在查询和Bean 之间建立显式映射,就象示例5-2那样。在此例中我们也建立了Customer 与Reservation的映射。
示例5-2.  Bike.xml(iBATIS SQL 映射文件)
<?xml version="1.0" encoding="UTF-8" ?><sql-map name="Bike" >    <result-map name="result" class="com.springbook.Bike" >       <property name="bikeId" column="bikeId" columnIndex="1" />       <property name="manufacturer" column="manufacturer" columnIndex="2" />       <property name="model" column="model" columnIndex="3" />       <property name="frame" column="frame" columnIndex="4" />       <property name="serialNo" column="serialNo" columnIndex="5" />       <property name="weight" column="weight" columnIndex="6" />       <property name="status" column="status" columnIndex="7" />    </result-map>        <mapped-statement name="getBikes" result-map="result">        select bikeId, manufacturer, model, frame, serialNo, status        from bikes    </mapped-statement>    <mapped-statement name="getBikeBySerialNo" result-map="result">        select bikeId, manufacturer, model, frame, serialNo, status        from bikes       where serialNo=#value#    </mapped-statement>    <mapped-statement name="getBikeByID" result-map="result">        select bikeId, manufacturer, model, frame, serialNo, weight, status        from bikes        where bikeId=#value#    </mapped-statement>    <mapped-statement name="saveBike" >        insert into bikes        (bikeId, manufacturer, model, frame, serialNo, weight, status)        values(#bikeId#, #manufacturer#, #model#, #frame#, #serialNo#,         #weight#, #status#)    </mapped-statement>    <mapped-statement name="deleteBike" >        delete from bikes         where bikeId = #bikeId#    </mapped-statement></sql-map>

示例5-3. Customer.xml
<?xml version="1.0" encoding="UTF-8" ?><sql-map name="Customer" >    <result-map name="result" class="com.springbook.Customer" >       <property name="custId" column="custId" columnIndex="1" />       <property name="firstName" column="firstName" columnIndex="2" />       <property name="lastName" column="lastName" columnIndex="3" />    </result-map>        <mapped-statement name="getCustomers" result-map="result">            select custId,                     firstName,                    lastName             from customers    </mapped-statement>    <mapped-statement name="getCustomer" result-map="result">            select custId,                     firstName,                    lastName            from customers            where custId = #value#    </mapped-statement></sql-map>

示例 5-4. Reservation.xml
<?xml version="1.0" encoding="UTF-8" ?><sql-map name="Reservation" >    <result-map name="result" class="com.springbook.Customer" >       <property name="reservationId" column="resId" columnIndex="1" />       <property name="bike" column="bikeId" columnIndex="2" />       <property name="customer" column="custId" columnIndex="3" />       <property name="reservationDate" column="resDate" columnIndex="4" />    </result-map>        <mapped-statement name="getReservations" result-map="result">            select resId,                    bikeId,                              custId,                              resDate                        from reservations    </mapped-statement>    <mapped-statement name="getReservationsForCustomer" result-map="result">            select resId,                    bikeId,                              custId,                              resDate                        from reservations             where custId = #value#    </mapped-statement>    <mapped-statement name="getReservationsForBike" result-map="result">            select resId,                    bikeId,                              custId,                              resDate                        from reservations             where bikeId = #value#    </mapped-statement>    <mapped-statement name="getReservationsForDate" result-map="result">            select resId,                    bikeId,                              custId,                              resDate                        from reservations             where resDate = #value#    </mapped-statement>    <mapped-statement name="getReservation" result-map="result">            select resId,                    bikeId,                              custId,                              resDate                        from reservations             where resId = #value#    </mapped-statement></sql-map>

The <result-map> portion provides an explicit map between columns in the database and properties of a persistent class.  The <mapped-statement> can then simply define the SQL queries necessary to execute the needed functionality, and the map handles creation of the resultant Java object.  In addition to the Bike version above, your application currently also requires a map for Customer and Reservation.
<result-map>部分提供了数据库字段与持久化类属性之间的一显式映射。<mapped-statement>接着可以简单定义运行所需功能的必要的SQL查询,而映射负责创建合成的Java 对象。除了上述的Bike 版本之外,你的应用程序目前也需要一个关于Customer 和Reservation的映射。
你将不得不做一些OR框架通常为我们所做的活,如创建标识符。在此例中,你将使用MySQL生成的序列(就是数据库表中的那些AUTO_INCREMENT字段)。你只需在数据库表定义时简单地把bikeId标为AUTO_INCREMENT,这样当增加一条新记录时,你就可以在SQL语句中略过bikeId字段。我们映射中SaveBike语句就成了示例5-5。
示例5-5. Bike.xml
    <mapped-statement name="saveBike" >        insert into bikes        (manufacturer, model, frame, serialNo, weight, status)        values(#manufacturer#, #model#, #frame#, #serialNo#, #weight#,        #status#)    </mapped-statement>

若你是在使用Oracle, Spring和iBATIS也支持Oracle生成的序列。
下一步,你可以更新应用上下文,且需要列出我们的新fa&ccedil;ade,而fa&ccedil;ade要有SQL映射。把SQL 引入PROPERTIES 文件,就象我们对待JDBC 的参数一样。 此外,你还需要配置事务策略。( 示例5-6)
示例5-6. .RentABikeApp-servlet.xml
<beans><bean id="dataSource"class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property><property name="url"><value>jdbc:mysql://localhost/bikestore</value></property><property name="username"><value>bikestore</value></property></bean><bean id="rentaBike" class="com.springbook.IBatisRentABike"><property name="storeName"><value>Bruce's Bikes</value></property><property name="dataSource"><ref local="dataSource"/></property><property name="sqlMap"><ref local="sqlMap"/></property></bean><bean id="sqlMap"class="org.springframework.orm.ibatis.SqlMapFactoryBean"><property name="configLocation"><value>/WEB-INF/ibatis.config</value></property></bean><bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource"><ref local="dataSource"/></property></bean>

这个事务策略就在应用上下文中,你不必来管理提交适宜,因为Spring 会为你完成。你将在第六章中更完整地探讨事务策略。                                               
示例5-7. . ibatis.config
<?xml version="1.0" encoding="UTF-8"?><sql-map-config><sql-map resource="Bike.xml" /><sql-map resource="Customer.xml" /><sql-map resource="Reservation.xml" /></sql-map-config>

稍后我们会谈到事务策略。
发生了什么事?
你没看到OR映射。OR框架会有意地将一个数据库表与一个类或多个类关连起来。在此例中,iBATIS会将查询结果赋予一个类。这意味着iBATIS并未试图对你隐藏SQL细节。实际上,它是在拥抱SQL。
Spring打算通过模板来简化iBATIS的使用。iBATIS模板会给出类似JDBC模板的使用模型,你只需指定数据源和iBATIS映射的SQL语句。
当执行SQL语句时,Spring与iBATIS一起为你管理资源,并按要求来创建和关闭连接。Spring会把映射的SQL语句传给iBATIS, 由iBATIS来运行映射的语句,且若需要,iBATIS会把结果集传给给你映射SQL语句时所指定的Bean。如果你已有了任何参数,那就可将那些结果集放入hash map中并把结果集传给带有映射的SQL语句的模板。
然而,从内部所表现的来看,iBATIS或许并不象是一个OR框架,但使用模型的确有OR的趋向。 构建带有操作数据存储的数据访问对象(data access object),你不会在代码中看到SQL语句,因为它已被保存在配置文件中了,且iBATIS使用对象集合而不是结果集。简而言之,这是在全部OR与JDBC之间的一个幽雅的折衷。
关于…
一切都适合iBATIS吗? 由于这种使用模型与OR模型是那么的相似,且许多应用对他们可能生成的SQL需要有更多的控制,或许你倾向于普遍地使用iBATIS。尽管如此,象JDO和Hibernate的OR框架仍拥有着属于自己的舞台。OR框架给了你更多的灵活性和更强大的功能:
·一些高级的对象模型导致了非常复杂的SQL,把这些复杂的SQL交由ORM来完成是最好的处理方式。 例如,继承通常使得原始的JDBC API操作变得更为复杂。
·一些高级的性能特征,象延迟加载和数据抓取群组,要求对有效地的自动控制需要一个更正式的模型。然而,完美地优化后的JDBC至少要和ORM一样快速。相对于使用原始的JDBC而言,由ORM提供的性能优化选项对某些类型的问题更获得较佳性能。
·ORM使某些问题变得更加有趣。操作对象比创建一套的SQL查询更加简易。ORM很适合带有简单的查找,创建,更新,根据主键读取数据库中的数据以及删除记录的应用。
如若你的应用有快速的数据模型和对象模型的变更,iBATIS就会很适合。如果你有了CRUD类型的应用,那么,使用iBATIS可能就会有点乏味。相反地,如果你正寻找一种好的SQL访问方式及在ORM和原始的JDBC间有效的折衷,那么,iBATIS可能就会给了你所需的一切。