今天有一个朋友问了我一个问题,他使用的是Hibernate/Spring/Struts架构,配置使用Spring的OpenSessionInView Filter,但是发现不生效,lazy的集合属性在页面访问的时候仍然报session已经关闭的错误。我和他一起检查了所有的配置和相关的代码,但是没有发现任何问题。经过调试发现,应用程序使用的Session和OpenSessionInView Filter打开的Session不是同一个,所以OpenSessionInView模式没有生效,但是为什么他们不使用同一个Session呢? 检查了一遍Spring的相关源代码,发现了问题的根源: 通常在Web应用中初始化Spring的配置,我们会在web.xml里面配置一个Listener,即:
xml代码: | <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> | 如果使用Struts,那么需要在Struts的配置文件struts-config.xml里面配置一个Spring的plugin:ContextLoaderPlugIn。
实际上ContextLoaderListener和ContextLoaderPlugIn的功能是重叠的,他们都是进行Spring配置的初始化工作的。因此,如果你不打算使用OpenSessionInView,那么你并不需要在web.xml里面配置ContextLoaderListener。
好了,但是你现在既需要Struts集成Spring,又需要OpenSessionInView模式,问题就来了!
由于ContextLoaderListener和ContextLoaderPlugIn功能重叠,都是初始化Spring,你不应该进行两次初始化,所以你不应该同时使用这两者,只能选择一个,因为你现在需要集成Struts,所以你只能使用ContextLoaderPlugIn。
但是令人困惑的是,ContextLoaderListener和ContextLoaderPlugIn有一个非常矛盾的地方!
ContextLoaderListener初始化spring配置,然后把它放在ServletContext对象里面保存:
java代码: | servletContext.setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
|
请注意,保存的对象的key是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE! 但是ContextLoaderPlugIn初始化spring配置,然后把它放在ServletContext对象里面保存:
java代码: |
String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac);
|
这个attrName和WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE名字是不一样的!
如果仅仅是名字不一样,问题还不大,你仍然可以放心使用ContextLoaderPlugIn,但是当你使用OpenSessionInView的时候,OpenSessionInViewFilter是使用哪个key取得spring配置的呢?
java代码: | WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
|
显然,OpenSessionInViewFilter是按照WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE这个key去拿spring配置的!
我们整理一下思路:
ContextLoaderPlugIn保存spring配置的名字叫做attrName; ,ContextLoaderListener保存spring配置的名字叫做WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE; 而OpenSessionInView是按照WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE这个名字去取得spring配置的! 而你的应用程序却是按照attrName去取得spring的配置的!
所以,OpenSessionInView模式失效!
解决办法: 修改ContextLoaderPlugIn代码,在getServletContext().setAttribute(attrName, wac);这个地方加上一行代码: getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac);
或者修改OpenSessionInViewFilter,让它按照attrName去取得spring配置。
我原来用struts/spring/hibernate的时候同样使用OpenSessionInView,但是似乎没有robbin所说的问题啊。而且我在使用的时候,是ContextLoaderListener和ContextLoaderPlugIn一起用的。整个配置如下: 1.首先是web.xml
java代码: |
<filter> <filter-name>OpenSessionInViewFilter</filter-name> <filter-class>org.springframework.orm.hibernate.support.OpenSessionInViewFilter</filter-class> </filter> <filter-mapping> <filter-name>OpenSessionInViewFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
......
|
2. 然后是struts-config.xml:
java代码: |
<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn"> <set-property property="contextConfigLocation" value="/WEB-INF/action-servlet.xml" /> </plug-in>
|
其余部分省略。
在上述配置下,使用OpenSessionInView似乎没有问题。
不知道robbin所说的ContextLoaderListener和ContextLoaderPlugIn不应该同时使用是不是做得是如下的配置:(struts-config.xml)
java代码: |
<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn"> <set-property property="contextConfigLocation" value="/WEB-INF/applicationContext.xml, /WEB-INF/action-servlet.xml"/> </plug-in>
|
我尝试了一下,用这种配置时,OpenSessionInView的确失效了。
我猜想,原因大概是这样:struts的这个plugIn,可能只是为了整合一个action-servlet.xml,将action-servlet.xml中的定义当作Spring的bean来使用,因此,在保存时,只要有action-servlet.xml的配置,就被保存到robbin所提到的那个attrName中,而不是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE中,所以,OpenSessionInView是取不到这个配置的。
那么这个配置什么时候被取到呢?直觉告诉我,可能是和Action的Proxy有关。于是,查看了org.springframework.web.struts.DelegatingActionProxy的源码,果然:
java代码: |
/** * Return the delegate Action for the given mapping. * <p>The default implementation determines a bean name from the * given ActionMapping and looks up the corresponding bean in the * WebApplicationContext. * @param mapping the Struts ActionMapping * @return the delegate Action * @throws BeansException if thrown by WebApplicationContext methods * @see #determineActionBeanName */ protectedAction getDelegateAction(ActionMapping mapping)throws BeansException { WebApplicationContext wac = getWebApplicationContext(getServlet(), mapping.getModuleConfig()); String beanName = determineActionBeanName(mapping); return(Action) wac.getBean(beanName, Action.class); }
/** * Fetch ContextLoaderPlugIn's WebApplicationContext from the * ServletContext, containing the Struts Action beans to delegate to. * @param actionServlet the associated ActionServlet * @param moduleConfig the associated ModuleConfig * @return the WebApplicationContext * @throws IllegalStateException if no WebApplicationContext could be found * @see DelegatingActionUtils#getRequiredWebApplicationContext * @see ContextLoaderPlugIn#SERVLET_CONTEXT_PREFIX */ protected WebApplicationContext getWebApplicationContext( ActionServlet actionServlet, ModuleConfig moduleConfig)throwsIllegalStateException{ return DelegatingActionUtils.getRequiredWebApplicationContext(actionServlet, moduleConfig); }
|
仔细看其中的取wac的代码,它并不是从WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE取的wac。
由此,我相信,除了robbin讲的修改源码以外,同时使用ContextLoaderListener和ContextLoaderPlugIn,但是不要在ContextLoaderPlugIn里面加入applicationContext.xml,只要加入你的action-servlet.xml,我相信,同样也可以非常流畅的使用OpenSessionInView 。
我也遇到了上面说的openSessionInView不起作用的问题(web.xml既定义了listener,也定义了struts plugin),我想问一下,上面提到的action-servlet.xml到底是什么内容? 在我的应用里spring的配置文件是application-context.xml,它本身是空的,引用spring-data.xml,sping-security.xml等等和存放对应struts action的spring 配置文件spring-struts-action.xml。 struts的配置文件是struts-config.xml,里面定义了所有的action,它们的class都是org.springframework.web.struts.DelegatingActionProxy。最后的plug-in是
java代码: |
<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn"> <set-property property="contextConfigLocation" value="/WEB-INF/applicationContext.xml"/> </plug-in>
|
结果也遇到了openSessionInView不起作用的问题 在我的应用里都没有出现过action-servlet.xml,我想问下它到底是什么?是对应于我的spring-struts-action.xml还是struts-config.xml引用的一部分?
通俗的说,这个action-servlet.xml到底是spring配置文件还是struts的配置文件?
我仔细想了一下,那个action-servlet.xml应该是spring配置的一部分,也就是说对应我的spring-struts-action.xml(明确的说,这个里面的xml语法是spring配置文件的),应该是这样的吧?不过按照这个理解下去,我又产生了问题。 我的理解时这样的,spring里面的listener会在web.xml里加载spring的容器,struts ActionServlet初始化时又会根据struts-config.xml里的spring plugin配置再初始化一个spring容器,所以原则上说只要一个就可以了,如果2处都配了,会初始化2个spring容器,在和struts结合的用法里,实际有效的是stuts配置里面那个plugin初始化的容器,因为用户操作的入口都是struts的action。那么二楼提供的方法其实就是所有的bean都由那个listener初始化的,存在于第一个spring容器中,然后stuts只初始化那些和struts action关联的action bean,存在第二个容器里(这两个容器的区分就在于robbin提到的他们的名字不同)但是问题就是: 为什么在二楼的的方法中,用户通过action访问spring bean,那么应该只是访问的第二个容器里的action bean,而service bean在第一个容器里,那第二个容器里的action bean是怎么会可以访问到第一个容器里的service bean和其他所有spring bean的呢?实在是费解 。
感谢搂主的分析,spring的struts plugin确实有上述描述的问题 如果根据原来的方法,context会初始化2次,看了plugin的源码以后我对它作了小小的修改,首先检查context是不是被初始化过,如果有则直接从attribute中获取,如果没有初始化,则根据plugin的配置初始化,同时保证了context只被初始化一次。 原来的意图是屏蔽web.xml中的context监听,直接用plugin初始化context,但启动失败,于是作了上述修改
java代码: |
//read the application context from the aplication attribute WebApplicationContext wac = null; String attrName = getServletContextAttributeName(); if(getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)!=null){ wac = (WebApplicationContext) getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); logger.info("Using the context listener's context "+wac); } else { logger.info("Load the context plugin application context "); WebApplicationContext parent = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
wac = createWebApplicationContext(parent); if (logger.isInfoEnabled()) { logger.info("Using context class '" + wac.getClass().getName() + "' for servlet '" + getServletName() + "'"); } //set to attribute to spring listener getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); } // Publish the context as a servlet context attribute. getServletContext().setAttribute(attrName, wac);
|
PS. 有个疑问,如果说spirng中的bean只有一个实例,应该说无论初始化多少次都应该获得的是同一个实例啊?
|
描述: | 根据spirng 1.2.7 重新编译的ContextLoaderPlugin |
|  下载 | 文件名: | strutspugin.rar | 文件大小: | 4.16 KB | 下载过的: | 文件被下载或查看 194 次 |
我觉得这个根本就是大家对Spring的理解的问题。
如果这真是一个严重的问题,以至于需要修改源码来修正,Spring的team不会到现在没有发现,到现在还没有修正。为什么Spring的context分成了多个文件?为什么用applicationContext.xml了,还有xxx-servlet.xml?
如果大家监听ContextRefreshedEvent的话,会发现一个web app至少会有两个这样的event,下面是我的现在的应用打印出的context及其所包含的beans: org.springframework.web.context.support.XmlWebApplicationContext: display name [Root WebApplicationContext]; startup date [Wed May 10 17:30:13 CST 2006]; child of [org.springframework.context.support.ClassPathXmlApplicationContext: display name [org.springframework.context.support.ClassPathXmlApplicationContext;hashCode=3736840]; startup date [Wed May 10 17:30:09 CST 2006]; root of context hierarchy]; config locations [/WEB-INF/webApplicationContext.xml,/WEB-INF/webApplicationContext-*.xml,/WEB-INF/standardJspApplicationContext.xml,classpath *:config/spring/app-context-base.xml,classpath*:config/spring/app-context-hibernate.xml,classpath*:config/spring/app-context- ibatis.xml,classpath*:config/spring/app-context-integration.xml,classpath*:config/spring/app-context-biz.xml]
[messageSource, localeResolver, exposeSpringBeanDefinition, dataListOfTerminalInfoForm, dataListOfPointsSpecialOfferForm, dataListOfSearchTerminalForm, pointsSpecialOfferForm1, terminalInfoForm1, searchTerminalForm1, dataListOfSearchCardAccountDetailForm, dataListOfSearchPhysicalCardInfoForm, dataListOfSearchCardApplicationInfoForm, dataListOfSearchTransactionForm, searchCardAccountDetailForm1, searchCardAccountDetailForm2, searchCardAccountDetailForm3, searchPhysicalCardInfoForm1, searchPhysicalCardInfoForm2, searchPhysicalCardInfoForm3, searchTransactionForm1, searchTransactionForm2, searchTransactionForm3, displayTransactionFormForTest, dataListOfReplaceCardForm, dataListOfSearchCardInfoForm, cardInfoFormForTest, cardAccountForm1, cardAccountForm2, cardAccountForm3, searchCardInfoForm1, searchCardInfoForm2, searchCardInfoForm3, replaceCardForm1, replaceCardForm2, replaceCardForm3, searchCardApplicationInfoForm1, searchCardApplicationInfoForm2, searchCardApplicationInfoForm3, displayCardApplicationInfoFormForTest, displayCardApplicationInfoFormForTest1, csvDisplayProvider, excelDisplayProvider, classicLook, simpleLook, microsoftLook, dacHandler, integer0, integer1, integer2, integer3, integer4, integer5, integer6, integer7, integer8, integer9, sessionFactory, transactionManager, hibernateTemplate, abstractHibernateDao, abstractDacHolderHibernateDao, ageLevelDefinitionDao, auditLogDao, bankDao, bankBranchDao, binRangeDao, cardDao, cardAccountDao, cardAccountDetailDao, cardApplicationDao, cardSalesAgentDao, cardTypeDefinitionDao, centerDao, centerAccountDao, centerAccountDetailDao, corporationDao, corporationTypeDefinitionDao, csaAccountDao, csaAccountDetailDao, csaBillsDao, csaTypeDefinitionDao, educationLevelDefinitionDao, generalLedgerDao, generalLedgerDetailDao, generalLedgerTypeDefinitionDao, identificationTypeDefinitionDao, incomeLevelDefinitionDao, industryDao, journalDao, journalBackupDao, maritalStatusDefinitionDao, merchantDao, merchantAccountDao, merchantAccountDetailDao, merchantApplicationDao, merchantBillsDao, merchantTypeDefinitionDao, occupationTypeDefinitionDao, outboundEmailDao, permissionDao, physicalCardDao, pointsSpecialOfferDao, residentialTypeDefinitionDao, roleDefinitionDao, rolePermissionDao, systemConfigDao, terminalDao, terminalConfigurationDao, terminalModelDefinitionDao, transactionSummaryDao, transactionTypeDefinitionDao, userDao, userCreditRatingDao, userLevelDefinitionDao, userRoleDao, sqlMapClient, abstractIbatisValueListAdapter, valueListHandler, propertyConfigurer, dataSource, voidTransactionTemplate, inquiryBalanceTransactionTemplate, definitionBizFacade, facadeHolder, pointsTransactionTemplate, emailBizObject, clsSpringEventListener, balanceBizFacadeTarget, kernelBizObject, printBizFacadeTarget, pointsSpecialOfferBizFacadeTarget, settlementBizObject, addPointsTransactionTemplate, inquiryMerchantAccountInfoTransactionTemplate, addMerchantPointsTransactionTemplate, emailBizFacadeTarget, centerBizFacadeTarget, monitorBizFacadeTarget, endOfDayReportTransactionTemplate, cardBizFacadeTarget, pointsCalculator, balanceBizObject, pointsSpecialOfferBizObject, auditLogBizFacadeTarget, terminalBizFacadeTarget, terminalBizObject, templateHolder, settlementBizFacadeTarget, merchantBizObject, userBizObject, changePinTransactionTemplate, centerPurchasePointsBackTransactionTemplate, definitionBizObject, monitorBizObject, auditLogBizObject, merchantBizFacadeTarget, userBizFacadeTarget, responseMessageDataFactoryBean, tradingBizObject, printBizObject, csaBizObject, csaBizFacadeTarget, kernelBizFacadeTarget, cardBizObject, centerBizObject, tradingBizFacadeTarget, downloadParametersTransactionTemplate, baseTransactionProxy, abstractDataFacade, balanceBizFacade, printBizFacade, settlementBizFacade, emailBizFacade, kernelBizFacade, auditLogBizFacade, pointsSpecialOfferBizFacade, terminalBizFacade, userBizFacade, merchantBizFacade, csaBizFacade, monitorBizFacade, cardBizFacade, centerBizFacade, tradingBizFacade, pointsConverter, transactionTypeHelperBean, integer100, integer106, integer110, integer111, integer120, integer180, integer181, integer182, integer200, integer220, integer221]
---------------------------------------------------------------context2 org.springframework.web.context.support.XmlWebApplicationContext: display name [WebApplicationContext for namespace 'action-servlet']; startup date [Wed May 10 17:31:01 CST 2006]; child of [org.springframework.web.context.support.XmlWebApplicationContext: display name [Root WebApplicationContext]; startup date [Wed May 10 17:30:13 CST 2006]; child of [org.springframework.context.support.ClassPathXmlApplicationContext: display name [org.springframework.context.support.ClassPathXmlApplicationContext;hashCode=3736840]; startup date [Wed May 10 17:30:09 CST 2006]; root of context hierarchy]; config locations [/WEB-INF/webApplicationContext.xml,/WEB-INF/webApplicationContext-*.xml,/WEB-INF/standardJspApplicationContext.xml,classpath *:config/spring/app-context-base.xml,classpath*:config/spring/app-context-hibernate.xml,classpath*:config/spring/app-context- ibatis.xml,classpath*:config/spring/app-context-integration.xml,classpath*:config/spring/app-context-biz.xml]]; config locations [/WEB-INF/action-servlet.xml]
[/EditCurrentUserInfoAction, /FakeLoginAction, /SaveCardInfoAction, /LoginAction, /EditOperatorPswAction, /SavePointsSpecialOfferAction, /DisplayCurrentUserInfoAction, /DisplayOperatorApplicationAction, /EditEmailAction, /RegisterCardAction, /IssueCardsAction, /SearchEmailAction, /RegisterCsaAction, /EditTerminalInfoAction, /ReleaseTerminalAction, /ReleaseCsaAction, /SaveTerminalInfoAction, /SearchTransactionAction, /SearchOperatorInfoAction, /ProcessCardApplicationAction, /EditTerminalConfigurationAction, /EditGenericUserByIdAction, /SearchMerchantInfoAction, /SearchTerminalAction, /SaveCsaPswAction, /SaveCsaInfoAction, /SaveCardTypeAction, /RegisterPointsSpecialOfferAction, /SearchCardInfoAction, /EditMerchantInfoAction, /SearchCardAccountDetailAction, /SearchCardApplicationInfoAction, /DisplayTransactionStatisticsAction, /DisplayRegisterCardInfoAction, /SaveEmailAction, /EditCardInfoByIdAction, /MoniterSystemLogAction, /ReleasePointsSpecialOfferAction, /SearchMerchantAccountDetailAction, /EditMerchantPswAction, /ReleaseMerchantAction, /ListCardTypeAction, /StockCardsAction, /ProcessOperatorApplicationAction, /SearchPhysicalCardInfoAction, /SearchCsaInfoAction, /SearchOperatorApplicationAction, /ReleaseOperatorAction, /DisplayCardApplicationInfoAction, searchEmailValueListBuilder, /SearchCsaApplicationAction, /RegisterCardTypeAction, /MonitorTerminalStatusAction, /SearchCsaAccountDetailAction, /SearchUserAction, /ReleaseCardTypeAction, /ReleaseUserAction, /ReleaseEmailAction, /CreateBlankCardsAction, /RegisterBulkCardsAction, /SaveMerchantPswAction, /SearchPointsSpecialOfferAction, /EditPswAction, /SearchMerchantApplicationAction, /DisplayCsaApplicationAction, /EndOfDayAction, /EditPointsSpecialOfferAction, /DisplayMerchantApplicationAction, /RegisterEmailAction, /EditCsaPswAction, /ProcessMerchantApplicationAction, /EditCardTypeDefinitionAction, /SaveOperatorInfoAction, /SaveMerchantInfoAction, /SaveOperatorPswAction, /EditOperatorInfoAction, /RegisterMerchantAction, /EditCsaInfoAction, /ChangeSystemStatusAction, /ProcessSystemLogAction, /RegisterOperatorAction, /RegisterTerminalAction, /DisplayTransanctionAction, /ProcessCsaApplicationAction, /MerchantPointsRedeemTransactionReportAction, /SaveTerminalConfigurationAction, autoProxyCreator, profilingAdvice, profilingAdvisor, strutsActionAdvice, baseSearchAction, userTypeBasedSelector, valueListBuilder]
------------------------------------------------------------------------context3 org.springframework.web.context.support.XmlWebApplicationContext: display name [WebApplicationContext for namespace 'trading-servlet']; startup date [Wed May 10 17:31:08 CST 2006]; child of [org.springframework.web.context.support.XmlWebApplicationContext: display name [Root WebApplicationContext]; startup date [Wed May 10 17:30:13 CST 2006]; child of [org.springframework.context.support.ClassPathXmlApplicationContext: display name [org.springframework.context.support.ClassPathXmlApplicationContext;hashCode=3736840]; startup date [Wed May 10 17:30:09 CST 2006]; root of context hierarchy]; config locations [/WEB-INF/webApplicationContext.xml,/WEB-INF/webApplicationContext-*.xml,/WEB-INF/standardJspApplicationContext.xml,classpath *:config/spring/app-context-base.xml,classpath*:config/spring/app-context-hibernate.xml,classpath*:config/spring/app-context- ibatis.xml,classpath*:config/spring/app-context-integration.xml,classpath*:config/spring/app-context-biz.xml]]; config locations [/WEB-INF/trading-servlet.xml] [clsRawTagElementParser, transactionProcessor, clsTradingServlet, rawTagElementParser]
如果照大家所说的方法去改源代码,那么后启动的servlet的context会覆盖前面一个启动的servlet的context,对于我的应用来说,那种方式会导致action-servlet丢失。开始robbin提出的错误,是因为你在strutsPlugin里多配置了appContext,导致实际上有2分appContext的beans存在,child在自己的context里就可以找到所需要的bean,也就不会去parent里找了。StrutsPlugin里的attrName是正确合理的。
当然你可以把所有所有的bean全部放到root context里,这也行的通,不过本人极力反对这种方式,bean的组织太乱。
Spring的context是分层次的:不要把在写contextConfigLocation的时候,把你的xxx-servlet.xml路径也加进去;不要在写xxx-servlet.xml的context的时候把applicationContext的路径也配进去;不要在parent的context里引用children里的bean,不要在你的appContext里引用xxx-servlet的bean。
总之,就是要求你合理的、有层次的组织你的bena,而不是一陀摆出来。applicationContext.xml如果不引用action-servlet.xml路径的话,那么action如何来引用bo;
java代码: |
<bean name="/test" class="com.xy.action.TestAction"> <property name="testBo"><ref bean="testBoProxy"/></property> </bean>
|
如果bo在applicationContext.xml中的话; 服务器会报错,找不到bo 。 okokok 写道: | applicationContext.xml如果不引用action-servlet.xml路径的话,那么action如何来引用bo;
java代码: |
<bean name="/test" class="com.xy.action.TestAction"> <property name="testBo"><ref bean="testBoProxy"/></property> </bean>
|
如果bo在applicationContext.xml中的话; 服务器会报错,找不到bo |
我不太清楚你的bean的组织,在我的系统里,BO是在applicationContext之类的基础context里定义,而且工作很正常。
另外你需要搞清楚的是:对于Spring的BeanFactory(ApplicationContext),如果它在自己的context里找不到bean,会去parent里找。
java代码: |
// Check if bean definition exists in this factory. if(getParentBeanFactory() != null && !containsBeanDefinition(beanName)){ // Not found -> check parent. if(getParentBeanFactory() instanceof AbstractBeanFactory){ // Delegation to parent with args only possible for AbstractBeanFactory. return((AbstractBeanFactory) getParentBeanFactory()).getBean(name, requiredType, args); } elseif(args == null){ // No args -> delegate to standard getBean method. return getParentBeanFactory().getBean(name, requiredType); } else{ throw new NoSuchBeanDefinitionException(beanName, "Cannot delegate to parent BeanFactory because it does not supported passed-in arguments"); } }
|
所以无论如何,只要你在applicationContext里定义了BO,那么webApp的context一定找得到这个bean,因为applicationContext是webApp的context的parent。奇了怪了,昨天一直报找不到bo的错,今天居然没报错;服务器有问题? 还有个问题,既然web.xml里可以用listener来加载applicationContext.xml,为什么还要在struts-config.xml里再用plug-in?我觉得在applicationContext.xml里按模块放置每个模块的action,bo,dao的xml文件的路径是个不错方法,比如:
java代码: |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <import resource="SpringConfig/module1.xml" /> <import resource="SpringConfig/module2.xml" /> <import resource="SpringConfig/module3.xml" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName"> <value>org.gjt.mm.mysql.Driver</value> </property> <property name="url"> <value>jdbc:mysql://localhost/airline</value> </property> <property name="username"> <value>root</value> </property> <property name="password"> <value>123456</value> </property> </bean> ... </beans>
|
而module1.xml的内容是:
java代码: |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <!-- 数据访问层 --> <bean id="testDao" class="com.xy.dao.TestDao"> <property name="sessionFactory"><ref bean="sessionFactory"/></property> </bean> <!-- 业务罗基层 --> <bean id="testBo" class="com.xy.bo.TestBo"> <property name="testDao"><ref bean="testDao"/></property> <!-- <property name="transactionManager"><ref bean="transactionManager"/></property> --> </bean> <!-- Action层 --> <bean name="/test" class="com.xy.action.TestAction"> <property name="testBo"><ref bean="testBoProxy"/></property> </bean> </beans>
|
这样的话不用在struts-config.xml里配置plug-in了吧 。 struts-config.xml里配置plug-in是要配的,关键在于你Spring的配置文件的合理分层,如果像你那样什么东西都放在一个applictionContext里,那么就有可能出现象这样OpenSessioInView失效的这样“意想不到”的问题。而且会导致你错误地理解Spring,比如Spring的ApplicationEvent,本身web层的context里的Listener是听不到root层的Event的,但是你这样的配置(也是这个topic的配置)会导致Event混淆,也违背了Spring本身的设计意图。
PS:关于Event的问题可以看我blog,http://spaces.msn.com/sweetriver/blog/cns!367370EB9A9B2807!129.entry
在我的配置里,各个层次都有属于自己的配置文件,messageSource同样应该有分层,为了图方便而简单吧所有bean罗列在一个配置里是不可取的,而且会导致某些设计与实现上的问题(开始我的messageSource是没有分层的,但是后来这样导致了一些非常痛苦的问题与抉择,结果还是改回分层的messageSource)。applicationContext是有层次的,那样的方式会让struts plugIn中配置的sessionFactory比root中的sessionFactory占优,这个struts plugIn中的bean都是访问这里配置的sessionFactory。opensession的最大问题还是长链接的时候无法释放session的问题,在对外系统中问题尤为突出。springMVC可以仗着先天优势用interecptor,但是webwork可就没有这样的优势了,尝试自己做一个适合webwork的,但是发现一旦有安全框架介入,session作用的切面就变得难以把握。到现在还无果~~~~
组合公式为 "((r1c1+r1c2)/r1c3)" 1. 组合的公式是一个字符串. 2. 字符串元素包括"rc0123456789+-*/()" 3. r1c1=7,r1c2=2,r1c3=4 4. 求组合成的公式的值是多少.
解决思路: 1.对公式进行字符合法性检查. 2.对公式中出现的变量进行取值替换. 3.检查语句的合法性.(组合规则) 4.检查运算的合法性.(除数为0的情况) 5.求值. 具体解决方法: str1 = "(r1c1+r1c2)/r1c3)" str1 = replace(str1," ","") '去除公式中间出现的空格 1. 对公式进行字符合法性检查.
bool1 = getDataOperationCharCheck(str1) if bool1 = false then Response.write "公式不合法.有异常字符出现.<br>" else Response.write "公式检查通过!<br>" end if 2.对公式中出现的变量进行取值替换 RCstr = getdataoperation(str1) 3.检查语句的合法性.(组合规则) bool2 = getDataOperationSyntaxCheck(RCstr) if bool2 = false then Response.write "运算公式语法不合法,有组合异常字符出现.<br>" else Response.Write "运算公式语法检查通过!<br>" end if 4.检查运算的合法性.(除数为0的情况) bool3 = getDataOperationRunCheck(RCstr) if bool3 = false then Response.write "运算公式运算不合法,有除数为0出现.<br>" else Response.write "运算公式运算合法性检查通过!<br>" end if 5.求值. intValue = getRunSearch(RCstr) 6.运算结果: (((7*1.0)+(2*1.0))/(4*1.0)) = 2.25 '1.============================================================= '对原始公式进行字符检查,是否有不合法的字符 function getDataOperationCharCheck(datastr) datastr = replace(datastr," ","") sumchar = "rc0123456789+-*/()" strlen = len(datastr) strreturn = true for i = 1 to strlen singlechar = mid(datastr,i,1) if instr(sumchar,singlechar) = 0 then strreturn = false exit for end if next getDataOperationCharCheck = strreturn end function '2.============================================================== '对原始计划公式进行取值替换. '实现方法:对原始字符串进行单个字符检查, '在出现 "+-*/()" 中的任何一个字符时,对已经组合生成的变量进行取值. '在取到值的同时对其进行 double 转换 ,实现方法是 (intvalue * 1.0) function getdataoperation(datastr) strlen = len(datastr) sunstr = "" strID = "" intvalue = 0 singlechar = "" operationstr="()+-*/" for i=1 to strlen 'Response.write mid(datastr,i,1) & "<br>" singlechar = mid(datastr,i,1) if instr(operationstr,singlechar) > 0 then if strID <> "" then intvalue = getValue(strID) sunstr = sunstr & "(" & intvalue & "*1.0)" '(1) intvalue = 0 strID = "" end if sunstr = sunstr & singlechar singlechar = "" else strID = strID & singlechar end if next getdataoperation = sunstr end function '变量取值函数. '下列数据是为测试使用. ' function getValue(strRC) select case strRC case "r1c1" getValue = 2 case "r1c2" getValue = 7 case "r1c3" getValue = 2 end select end function '3.============================================================== '对公式进行语法合法性检查. 'eg.检查 (),--,++,**,//,(/,*) 等 是否成对出现. '检查是否有 function getDataOperationSyntaxCheck(datastr) strreturn = true datastr = replace(datastr," ","") '去除所有的空格 strlen = len(datastr) num1 = 0 '记录 括号的 个数 采用 有 ( 加1, 有 ) 减1 upsinglechar = "" '相对本次的字符的上一个字符 singlechar = "" operationstr1="()+-*/" operationstr2="(+-*/" '相对 在 ( 左边出现的运的符号是正确的. for i = 1 to strlen singlechar = mid(datastr,i,1) select case singlechar case "(" num1 = num1 + 1 if upsinglechar <> "" then if instr(operationstr2,upsinglechar) = 0 then '在左括号的左边若不为空,必需出现 "(+-*/" 中的一个. strreturn = false exit for end if end if case ")" num1 = num1 - 1 if num1 < 0 then strreturn = false exit for end if if instr(operationstr2,upsinglechar) > 0 then '在右括号的左边若不为空,必需不能出现 "(+-*/" 中的一个 strreturn = false exit for end if case "+" if instr(operationstr2,upsinglechar) > 0 then '在加号的左边若不空,必需不能出现 "(+-*/" 中的一个 strreturn = false exit for end if case "-" if instr(operationstr2,upsinglechar) > 0 then '在减号的左边若不空,必需不能出现 "(+-*/" 中的一个 strreturn = false exit for end if case "*" if instr(operationstr2,upsinglechar) > 0 then '在乘号的左边若不空,必需不能出现 "(+-*/" 中的一个 strreturn = false exit for end if case "/" if instr(operationstr2,upsinglechar) > 0 then '在除号的左边若不空,必需不能出现 "(+-*/" 中的一个 strreturn = false exit for end if end select upsinglechar = singlechar singlechar = "" next getDataOperationSyntaxCheck = strreturn end function
'4.============================================================== '对组合公式进行运算合法性的检查 '首选查找有没有 "/0"出现. '其次查找类似 "/(****)" = /0 出现 function getDataOperationRunCheck(datastr) strreturn = true if instr(datastr,"/")>0 then if instr(datastr,"/0") > 0 then strreturn = false else '对/ 后面出现的()内的数据进行运算,取值是否会有0出现. '首先计算 "/" 出现的次数 '再次判断 "/(" 出现的次数 '若 "/(" 出现的次为0 则安全. intnum1 = getInstrNum(datastr,"/") intnum2 = getInstrNum(datastr,"/(") if intnum2 > 0 then for j = intnum2 to 1 step -1 intpoint1 = getInstrPoint(datastr,"/(",j) if intpoint1 > 0 then sumpoint = getRunCheck(datastr,intpoint1) if CDbl(sumpoint) = CDbl(0) then strreturn = false exit for end if end if next end if end if end if getDataOperationRunCheck= strreturn end function '检查字符运行的合法性. '主要是对/()出现的字公式进行计算是否会等于0
function getRunCheck(datastr,intpoint1) strlen = len(datastr) intpoint = intpoint1 + 1 intnum = 0 singlechar = "" rcsearch = "" intreturn = 0 for m = intpoint to strlen singlechar = mid(datastr,m,1) if singlechar = "(" then intnum = intnum + 1 end if if singlechar = ")" then intnum = intnum - 1 end if rcsearch = rcsearch & singlechar if intnum = 0 then intreturn = getRunSearch(rcsearch) exit for end if next getRunCheck = intreturn end function '5.============================================================== '求值. function getRunSearch(strrcsearch) sql = "select " & strrcsearch & " as rcvalue " Set rs = conn.execute(sql) 'Response.write "<br>" & strrcsearch & "=" & rs("rcvalue") & "<br>" getRunSearch = rs("rcvalue") end function '公共函数============================================================== '返回substr 在 str1 中出现的次数 function getInstrNum(str1,substr) strlen = len(str1) substrlen = len(substr) singlechar = "" intreturn = 0 for i = 1 to strlen singlechar = mid(str1,i,substrlen) if singlechar = substr then intreturn = intreturn + 1 end if next getInstrNum = intreturn end function '返回substr 在 str1 中 第 intnum 次出现的位置 'intnum 必需是大于0的正整数 function getInstrPoint(str1,substr,intnum) intreturn = 0 strlen = len(str1) substrlen = len(substr) singlechar = "" intcount = 0 for i = 1 to strlen singlechar = mid(str1,i,substrlen) if singlechar = substr then intcount = intcount + 1 end if if intcount = intnum then intreturn = i exit for end if next getInstrPoint = intreturn end function
Spring的轻量级的bean容器为业务对象(business objects)、DAO对象和资源(如:JDBC数据源或者Hibernate SessionFactorie等)对象提供了IoC类型的装配能力。Spring使用一个xml格式的应用配置文件为开发者提供了一种通过解析定制的属性文件来手动管理单实例对象或者工厂对象的选择性。由于Spring将非入侵性做为一个重要的目标,因此可以由Spring配置管理的bean对象均不需要依赖Spring自有的接口和类就可以通过它们的bean属性完成配置。这个概念可以被应用到任何环境中,无论你开发的是一个J2EE的web应用还是一个桌面应用甚至只是一个applet都可以。
在使用Hibernate的应用中, Spring的对DAO对象通常的事务管理特别应该引起关注。它的目的就是分离数据访问和事务处理,使事务性业务对象不与任何特殊的数据访问或者事务策略绑在一起,从而不影响业务对象的可复用性。这种划分既可以经由事务模板(TransactionTemplate)用编程的方式实现,也可以经由面向方面(AOP)事务拦截器(TransactionTemplate)用声明的方式实现。无论是本地的Hibernate / JDBC事务,还是JTA事务都支持对象外的事务策略,这对于本地的无状态会话Bean(Stateless Session Beans)是一个非常有用的选择。 Spring的HibernateTemplate类提供了一个简单的方式实现了Hibernate-based DAO对象而不必关心如何获得Hibernate的Session实例,也不必关心多方参与的事务处理。无需使用try-catch块,也无需进行事务检查。一个简单的Hibernate访问方法就完全解决了些麻烦! 无论是在多个DAO接口还是在多方事务的情况下,Spring使得多种DAO对象无缝地协同工作。例如:某些DAO对象可能是基于plain JDBC的实现,更适合于经由Spring的JdbcTemplate来避免手动的异常处理。 你可以单独地使用许多Spring特性,因为Spring的所有对象都是设计成可复用的JavaBean对象的集合。也不要因为Spring可以提供一个完整的应该框架而气馁!使用其他的Spring特性时,应用配置概念是一个附加的特性,并不是一个必须的特性。无论如何,当你要决定去构建一个象Spring这样的内在的基础架构的时候,在使用Spring的路途上没有什么范围上的限制。 1. 介绍: 资源管理 典型的业务应用系统常常由于重复的资源管理代码而导致混乱。许多项目试着用自己的方法来解决这个问题,有时要为此付出失败的代价,Spring针对适当的资源管理提倡了一种引人注目的简单方法:即经由模板来倒置控制(Inversion of control),例如:基础类使用回调接口,或者应用AOP拦截器。其基础核心是适当的资源处理和将特殊的API异常转换为一个unchecked的基础异常。 Spring引入了一个DAO异常层适用于任何数据访问策略。对于直接的JDBC,JdbcTemplate类关注于连接处理,并且关注于对SQLException转换为适当的DataAccessException,包括对特殊的数据库SQL错误转换为有意义的异常。 经由不同的事务管理对象,Spring支持JTA和JDBC事务。Spring 也提供对Hibernate和JDO的支持,它的这种支持由与JdbcTemplate类的作用相类似的HibernateTemplate类和JdoTemplate类, 以及HibernateInterceptor类、JdoInterceptor类,还有Hibernate、JDO 事务管理类组成。 最主要的目的是要使应用的层次分明,为此将数据访问和事务处理同应用对象分离开来。所有的业务对象都不再依赖数据访问或者事务策略。不再有硬编码的资源查找代码,不再有难以替换的单例对象,也不再需要定制服务注册。 所有的单独的数据访问特性均无需依赖于Spring,可以单独使用,无需让Spring知道,同时也可以通过Spring的应用配置(提供基于XML的配置和对普通JavaBean实例的交叉引用)来进行装配。在一个典型的Spring应用中,大部分重要的对象都是普通的JavaBean:数据访问模板对象(data access templates)、数据访问对象(使用数据访问模板对象的对象)、事务管理对象及业务对象(使用数据访问对象和事务对象的对象),web表示分解对象、web控制对象(使用业务对象的对象)等等。 2. 应用配置中的资源定义 为了避免应用对象将资源查找的代码硬编码,Spring允许在应用配置中将一个如JDBC DataSource或者Hibernate SessionFactory定义为一个Bean。应用对象如果需要访问资源只需要通过Bean引用(DAO定义在下一部分说明)接受先前定义的实例的引用。以下的内容引用自一个应用配置定义,显示了如何建立一个JDBC DataSource和一个Hibernate的SessionFactory: <beans>
<bean id="myDataSource" class="org.springframework.jndi .JndiObjectFactoryBean"> <property name="jndiName"> <value>jdbc/myds</value> </property> </bean>
<bean id="mySessionFactory" class="org.springframework.orm.hibernate .LocalSessionFactoryBean"> <property name="mappingResources"> <list> <value>product.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">net.sf.hibernate.dialect .MySQLDialect</prop> </props> </property> <property name="dataSource"> <ref bean="myDataSource"/> </property> </bean>
...
</beans> |
注意选择是用JNDI来定位数据源还是从一个象Jakarta Commons DBCP BasicDataSource这样的本地定义取得一个数据源,只是一个改变配置的事: <bean id="myDataSource" class="org.apache.commons .dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName"> <value>org.hsqldb.jdbcDriver</value> </property> <property name="url"> <value>jdbc:hsqldb:hsql://localhost:9001</value> </property> <property name="username"> <value>sa</value> </property> <property name="password"> <value></value> </property> </bean> |
你也可以使用一个JNDI查找SessionFactory,但是通常对于EJB环境之外的应用来说并不是需要的(参考"container resources vs local resources"部分的讨论)。 3. 倒置控制(Inversion of Control): 模板和回调 模板的基本编程模式就象你将在下面看到那样,至于方法就如同任何定制的数据访问对象或者业务的对象的方法一样。除了需要向其提供一个Hibernate的SessionFactory之外,再没有对周围执行对象的信赖的限制。虽然最好是从一个Spring的应用配置中经由一个简单setSessionFactory bean的属性设置使用Bean引用来获得它,但随后你可以从任何地方获得它。随后的引用片段包括一段在Spring应用配置中对DAO定义的配置,其中引用了在其前面定义的SessionFactory,和一段DAO方法的实现的例子。 <beans>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>
...
</beans> |
public class ProductDaoImpl implements ProductDao {
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public List loadProductsByCategory(final String category) {
HibernateTemplate hibernateTemplate =
new HibernateTemplate(this.sessionFactory);
return (List) hibernateTemplate.execute(
new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException {
List result = session.find(
"from test.Product product where product.category=?",
category, Hibernate.STRING);
// do some further stuff with the result list
return result;
}
}
);
}
}
|
一个回调的实现可以被有效地用在任何Hibernate数据访问中。在任何情况下都由HibernateTemplate来管理Session的开闭和自动的多方事务。模板实例是线程安全和可重用的,因此它们可以做为其他类的变量。 对于简单的单步的动作,象find, load, saveOrUpdate或者delete的调用,HibernateTemplate提供更为便利的选择以代替象一行的回调的执行。此外,Spring提供了一个方便的基本类,就是HibernateDaoSupport类,它提供了setSessionFactory方法来接受一个SessionFactory,同时提供了getSessionFactory和getHibernateTemplate方法供其继承类使用。将这些结合起来,允许对于典型的需求给出了非常简单的DAO实现:
public class ProductDaoImpl extends HibernateDaoSupport implements ProductDao {
public List loadProductsByCategory(String category) {
return getHibernateTemplate().find(
"from test.Product product where product.category=?", category,
Hibernate.STRING);
}
}
|
4. 应用一个AOP拦截器代替一个模板 除使用HibernateTemplate之外的另一个选择就是使用Spring的AOP HibernateInterceptor。用直接在一个委托的try/catch块中编写Hibernate代码,配合相应的在应用配置中分别的拦截器配置来代替执行回调。下面的片段显示了一个Spring应用配置中的DAO, interceptor和proxy的各自的定义,同时给出了一个DAO方法实现的例子: <beans>
...
<bean id="myHibernateInterceptor"
class="org.springframework.orm.hibernate .HibernateInterceptor">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>
<bean id="myProductDaoTarget" class="product.ProductDaoImpl">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>
<bean id="myProductDao" class="org.springframework.aop .framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>product.ProductDao</value>
</property>
<property name="interceptorNames">
<list>
<value>myHibernateInterceptor</value>
<value>myProductDaoTarget</value>
</list>
</property>
</bean>
...
</beans>
|
public class ProductDaoImpl extends HibernateDaoSupport implements ProductDao {
public List loadProductsByCategory(final String category) throws MyException {
Session session = SessionFactoryUtils .getSession(getSessionFactory(), false);
try {
List result = session.find(
"from test.Product product where product.category=?",
category, Hibernate.STRING);
if (result == null) {
throw new MyException("invalid search result");
}
return result;
}
catch (HibernateException ex) {
throw SessionFactoryUtils.convertHibernateAccessException(ex);
}
}
}
|
这个方法将只在有一个与它配合的HibernateInterceptor时才能正常工作,HibernateInterceptor为它负责在方法调用前线程绑定Session的开启和方法调用后的关闭。getSession方法调用中的"false"标志是要确认Session必须是已经存在的,如果没有发现任何一个Session,SessionFactoryUtils将会为其创建一个。如果已经有一个Session句柄绑定在本线程上,比如是由一个HibernateTransactionManager事务绑定的,在任何情况下SessionFactoryUtils会自动接入这个Session。HibernateTemplate在底层也使用SessionFactoryUtils,与以上说的方式基本是一样的。 HibernateInterceptor的主要益处是它允许在数据访问代码中抛出checked application exception,而HibernateTemplate由于受限于回调只能在其中抛出unchecked exceptions。注意到这点我们可以推迟各自的检验,同时在回调后抛出应用异常。拦截方式的主要缺点是它需要在配置中进行特殊的配置。HibernateTemplate在大多数情况下都是一种简单好用的方法。
5. 程序事务划分 在这种底层的数据访问服务之上,事务处理可以在更高的应用层被划分 ,形成一些操作。这里除了需要一个Spring的PlatformTransactionManager对象外,对于周围运行的业务对象也没有任何限制。同样的,其后你可以从任何地方获得它们,但是经由Bean引用的方式通过setTransactionManage方法获得更为适合,象productDAO要经由一个setProductDao方法获得一样。下面的引用片段显示了在一个Spring应用配置中的事务管理对象和业务对象的定义,并且还提供了一个业务方法实现的例子: <beans>
...
<bean id="myTransactionManager"
class="org.springframework.orm.hibernate .HibernateTransactionManager">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>
<bean id="myProductService" class="product.ProductServiceImpl">
<property name="transactionManager">
<ref bean="myTransactionManager"/>
</property>
<property name="productDao">
<ref bean="myProductDao"/>
</property>
</bean>
</beans>
|
public class ProductServiceImpl implements ProductService {
private PlatformTransactionManager transactionManager;
private ProductDao productDao;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
public void increasePriceOfAllProductsInCategory(final String category) {
TransactionTemplate transactionTemplate =
new TransactionTemplate(this.transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition .PROPAGATION_REQUIRED);
transactionTemplate.execute(
new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
List productsToChange = productDAO.loadProductsByCategory(category);
...
}
}
);
}
}
|
6. 声明性事务划分 我们还可以选择使用Spring的AOP TransactionInterceptor通过在应用配置中定义拦截器配置来代替事务划分代码的事务处理方式。这允许我们保持业务对象独立于每个业务对象中重复的事务划分代码。此外,事务行为和隔离层次的变化可以通过一个配置文件来改变而不需要对业务对象的实现造成影响。 <beans>
...
<bean id="myTransactionManager"
class="org.springframework.orm.hibernate .HibernateTransactionManager">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>
<bean id="myTransactionInterceptor"
class="org.springframework.transaction .interceptor.TransactionInterceptor">
<property name="transactionManager">
<ref bean="myTransactionManager"/>
</property>
<property name="transactionAttributeSource">
<value>
product.ProductService.increasePrice*=PROPAGATION_REQUIRED
product.ProductService.someOtherBusinessMethod= PROPAGATION_MANDATORY
</value>
</property>
</bean>
<bean id="myProductServiceTarget" class="product .ProductServiceImpl">
<property name="productDao">
<ref bean="myProductDao"/>
</property>
</bean>
<bean id="myProductService" class="org.springframework.aop .framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>product.ProductService</value>
</property>
<property name="interceptorNames">
<list>
<value>myTransactionInterceptor</value>
<value>myProductServiceTarget</value>
</list>
</property>
</bean>
</beans>
|
public class ProductServiceImpl implements ProductService {
private ProductDao productDao;
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
public void increasePriceOfAllProductsInCategory(final String category) {
List productsToChange = this.productDAO .loadProductsByCategory(category);
...
}
} |
如同使用HibernateInterceptor一样,TransactionInterceptor允许任何checked application exception从回调代码中抛出,而TransactionTemplate受回调限制在其内部抛出unchecked exceptions,在出现一个unchecked application exception的情况时,TransactionTemplate将引发一个回滚或者这个事务由应用(通过事务状态)标记为回滚。TransactionInterceptor默认情况也是同样的行为,但是允许为每一个方法制定回滚策略。 建立声明性事务的一个便利的方式是使用TransactionProxyFactoryBean,特别是如果没有其他AOP拦截器的话,TransactionProxyFactoryBean将联合定义为代理的自身与一个特殊的目标Bean的事务配置。这将减少一个代理Bean对应一个目标Bean的配置情况。此外,你不必指定哪个接口或者哪个类必须定义事务方法。 <beans>
...
<bean id="myTransactionManager"
class="org.springframework.orm.hibernate .HibernateTransactionManager">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>
<bean id="myProductServiceTarget" class="product .ProductServiceImpl">
<property name="productDao">
<ref bean="myProductDao"/>
</property>
</bean>
<bean id="myProductService"
class="org.springframework.transaction.interceptor .TransactionProxyFactoryBean">
<property name="transactionManager">
<ref bean="myTransactionManager"/>
</property>
<property name="target">
<ref bean="myProductServiceTarget"/>
</property>
<property name="transactionAttributes">
<props>
<prop key="increasePrice*">PROPAGATION_REQUIRED</prop>
<prop key="someOtherBusinessMethod">PROPAGATION_MANDATORY</prop>
</props>
</property>
</bean>
</beans>
|
7. 事务管理策略 对于Hibernate应用来说,无论是TransactionTemplate还是TransactionInterceptor都是委托验实际的事务处理给PlatformTransactionManager实例,可以是一个HibernateTransactionManager(由一个单一的Hibernate的SessionFactory,使用一个ThreadLocal Session)或者可以是一个JtaTransactionManager(代理容器的JTA子系统)。甚至你可以使用一个自定义的PlatformTransactionManager实现。 如果选择从本地Hibernate事务管理转为由JTA来进行事务管理,例如:当你的应用的部署面对分布的事务需求时,也仅仅是改变一下配置的事。只要简单地将Hibernate的事务管理换为JTA事务实现即可。所有的事务划分和数据访问无需做任何变动仍可以继续工作,因为他们使用的都是普通的事务管理API。 对于分布式的事务会跨越多个Hibernate的session factories,仅仅是联合JtaTransactionManager与多个LocalSessionFactoryBean定义作为事务策略。你的每一个DAO将通过它们各自的Bean属性得到一个特殊的SessionFactory的引用。如果这一切都是在下面的JDBC数据源是事务容器,一个业务对象可以划分事务跨越很多DAO和很多session factories而无需做特别的处理,对于使用JtaTransactionManager做为事务策略也是一样的。 <beans>
<bean id="myDataSource1" class="org.springframework.jndi .JndiObjectFactoryBean">
<property name="jndiName">
<value>jdbc/myds1</value>
</property>
</bean>
<bean id="myDataSource2" class="org.springframework.jndi. JndiObjectFactoryBean">
<property name="jndiName">
<value>jdbc/myds2</value>
</property>
</bean>
<bean id="mySessionFactory1"
class="org.springframework.orm.hibernate. LocalSessionFactoryBean">
<property name="mappingResources">
<list>
<value>product.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect. MySQLDialect</prop>
</props>
</property>
<property name="dataSource">
<ref bean="myDataSource1"/>
</property>
</bean>
<bean id="mySessionFactory2"
class="org.springframework.orm.hibernate. LocalSessionFactoryBean">
<property name="mappingResources">
<list>
<value>inventory.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate. dialect.OracleDialect</prop>
</props>
</property>
<property name="dataSource">
<ref bean="myDataSource2"/>
</property>
</bean>
<bean id="myTransactionManager"
class="org.springframework.transaction.jta. JtaTransactionManager"/>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="sessionFactory">
<ref bean="mySessionFactory1"/>
</property>
</bean>
<bean id="myInventoryDao" class="product.InventoryDaoImpl">
<property name="sessionFactory">
<ref bean="mySessionFactory2"/>
</property>
</bean>
<bean id="myProductServiceTarget" class="product. ProductServiceImpl">
<property name="productDao">
<ref bean="myProductDao"/>
</property>
<property name="inventoryDao">
<ref bean="myInventoryDao"/>
</property>
</bean>
<bean id="myProductService"
class="org.springframework.transaction.interceptor. TransactionProxyFactoryBean">
<property name="transactionManager">
<ref bean="myTransactionManager"/>
</property>
<property name="target">
<ref bean="myProductServiceTarget"/>
</property>
<property name="transactionAttributes">
<props>
<prop key="increasePrice*">PROPAGATION_REQUIRED</prop>
<prop key="someOtherBusinessMethod">PROPAGATION_MANDATORY</prop>
</props>
</property>
</bean>
</beans>
|
无论是HibernateTransactionManager还是JtaTransactionManager允许适当的对Hibernate的在JVM层次的缓存处理-不需要容器-提供特殊的事务查找或者JCA连接器(只要不使用EJB发起事务)。另外,HibernateTransactionManager能输出JDBC连接供通常的JDBC访问代码使用。这样就允许在高层次上的事务划分是混合了Hibernate与JDBC而不要JTA的,只要只是访问一个数据库就可以! 8. 使用Spring管理应用的Bean 一个Spring应用配置定义可以被多种配置实现所加载,从FileSystemXmlApplicationContext和ClassPathXmlApplicationContext到XmlWebApplicationContext。这就允许在各种环境下重用Spring管理的数据访问和业务对象。默认情况下,一个Web应用将有它自己的定义在“WEB-INF/applicationContext.xml”中的根配置。 在任何一个Spring应用中,一个应用配置定义在一个XML格式的文件中用来对应用的所有有关的Bean进行装配,从Hibernate的session factory到自定义的数据访问和业务对象(象上面所有的Bean那样)。他们中的大多数不需要Spring容器知道他们,甚至即使是与其他Bean合作时也一样,因为他们只是简单的JavaBean之间的协作。下面的Bean定义可能是一个Spring Web 的MVC配置中用来访问业务对象的配置的一部分。 <bean id="myProductList" class="product.ProductListController">
<property name="productService">
<ref bean="myProductService"/>
</property>
</bean>
|
Spring的Web控制器经由Bean引用拥有它们需要的所有的业务和数据访问对象,因此它们无需在应用配置中做任何手工的Bean查找。但是当使用Spring管理的Beans用于Struts或者是在EJB实现,或者一个applet中时常常是需要必须手工查找一个Bean的。因此Spring的Bean可以被用在任何地方。也许只是需要是一应用配置的引用,或者经由一个web容器的Servlet配置属性,或者从一个文件中或者类路径的资源中创建它。 ApplicationContext context =WebApplicationContextUtils. getWebApplicationContext(servletContext);
ProductService productService =
(ProductService) context.getBean("myProductService");
|
ApplicationContext context =
new FileSystemXmlApplicationContext("C:/myContext.xml");
ProductService productService =
(ProductService) context.getBean("myProductService");
|
ApplicationContext context =
new ClassPathXmlApplicationContext("myContext.xml");
ProductService productService =
(ProductService) context.getBean("myProductService"); |
9. 容器资源VS本地资源 Spring的资源管理允许简单地在一个JNDI SessionFactory和一个本地SessionFactory间做选择,同样允许在一个JNDI DataSource与本地DataSource间做选择,而无需改变应用的一行代码。在容器中保存资源定义还是在应用本地保存,主要是一个事务策略方面的事。比较一个Spring定义的本地SessionFactory与一个手工注册的JNDI SessionFactory没有任何益处。如果经由Hibernate的JCA连接器注册,才会有加入JTA事务的明显益处,特别是对EJB。 一个重要的Spring事务提供的好处是它不与任何容器绑定。定义包括JTA在内的策略,它都可以独立工作或者在一个试验环境中工作。特别是对典型的一个数据库的事务来说,对于JTA这是一个非常轻量的和强大的选择。当使用本地EJB SLSB的事务时,你将同时依赖EJB容器和JTA-即使你只是访问一个数据库,即使只是使用SLSBs经由CMT来声明事务。选择使用 JTA编程也需要一个J2EE环境。 就JTA自身和JNDI数据源来说JTA不只是包括容器依赖。对于不使用Spring的JTA驱动的Hibernate事务,你必须使用HibernateJCA连接器或者在合适的JVM缓冲层专门写Hibernate的事务代码配置JTA事务。在只访问一个数据库的情况下,Spring驱动的事务可以与一个本地定义的Hibernate的SessionFactory配合良好,就如同与一个本地JDBC数据源相配合一样。因此当面对分布的事务需求时,你只需要转换为Spring的JTA事务策略即可。 要注意一个JCA连接器需要特别的容器的部署步骤,并且显然首先得支持JCA。这比使用本地资源定义和Spring驱动事务来部署一个简单的Web应用有更多的争议。而且你常常需要企业版本的容器支持,象WebLogic Express就不提供JCA。一个只用一个数据库的使用本地资源和事务的Spring应用可以在任何J2EE的Web容器中工作,Web容器不必支持JTA, JCA和EJB,如:Tomcat, Resin甚至最小的Jetty。另外,这样一个中间层就可以很容易地在桌面应用或者在测试套件中被重用。 所有考虑过的事情包括:如果你不使用EJB,坚持使用本地SessionFactory,使用SpringHibernateTransactionManager或者JtaTransactionManager,你将获得包括适当处理的JVM层的缓存和分布事务的所有益处,而无需引起任何关于容器部署的争论。经由JCA连接器的一个Hibernate的SessionFactory的JNDI注册只是在使用EJB的情况中才会有明显的附加值。 10. Skeletons和例子
配置使用Spring和HIbernate的一个J2EE的Web应用的注释和细节最好去看看在Spring Framework的例子中的“典型的Web应用”Skeletons,它给出了适合于JDBC 和 Hibernate应用的多种数据源及事务管理的配置项,仔细看一下事务拦截器的配置,它也同样向你展示了如何配置AOP拦截器。 在Spring的1.0 M2版中,例子Petclinic提供了JDBC和Hibernate的DAO实现和应用配置的选择。Petclinic 可以作为一个可工作的简单应用说明如何在一个Spring web 应用中使用Hibernate,同样也包括根据不同的事务策略来声明事务划分。
LinksSpring Framework website Spring Framework documentation
1. Ant是什么?2. 安装Ant3. 运行Ant4. 编写build.xml5. 内置task(internet) 6. EAR task(internet) 7. WAR task(internet) 8. JUnit task(internet)
1. Ant是什么?
Ant是一种基于Java的build工具。理论上来说,它有些类似于(Unix)C中的make ,但没有make的缺陷。
既然我们已经有了make, gnumake, nmake, jam以及其他的build工具为什么还要要一种新的build工具呢?因为Ant的原作者在多种(硬件)平台上开发软件时,无法忍受这些工具的限制和不便。类似于make的工具本质上是基于shell(语言)的:他们计算依赖关系,然后执行命令(这些命令与你在命令行敲的命令没太大区别)。这就意味着你可以很容易地通过使用OS特有的或编写新的(命令)程序扩展该工具;然而,这也意味着你将自己限制在了特定的OS,或特定的OS类型上,如Unix。
Makefile也很可恶。任何使用过他们的人都碰到过可恶的tab问题。Ant的原作者经常这样问自己:“是否我的命令不执行只是因为在我的tab前有一个空格?!!”。类似于jam的工具很好地处理了这类问题,但是(用户)必须记住和使用一种新的格式。
Ant就不同了。与基于shell命令的扩展模式不同,Ant用Java的类来扩展。(用户)不必编写shell命令,配置文件是基于XML的,通过调用target树,就可执行各种task。每个task由实现了一个实现了特定Task接口的对象来运行。(如果你对Ant一点概念都没有的话,可能看不懂这一节,没有关系,后面会对target,task做详细的介绍。你如果没有太多的时间甚至可以略过这一节,然后再回来浏览一下这里的介绍,那时你就会看懂了。同样,如果你对make之类的工具不熟悉也没关系,下面的介绍根本不会用到make中的概念。)
必须承认,这样做,在构造shell命令时会失去一些特有的表达能力。如`find . -name foo -exec rm {}`,但却给了你跨平台的能力-你可以在任何地方工作。如果你真的需要执行一些shell命令,Ant有一个<exec> task,这个task允许执行特定OS上的命令。
返回
2. 安装Ant
由于Ant是一个Open Source的软件,所以有两种安装Ant的方式,一种是用已编译好的binary 文件安装Ant,另一种是用源代码自己build Ant。
binary 形式的Ant可以从http://jakarta.apache.org/builds/ant/release/v1.4.1/bin下载。如果你希望你能自己编译Ant,则可从 http://jakarta.apache.org/builds/ant/release/v1.4.1/src。注意所列出的连接都是最新发行版的Ant。如果你读到此文时,发现已经有了更新的版本,那么请用新版本。如果你是一个疯狂的技术追求者,你也可以从Ant CVS repository下载最新版本的Ant。
系统需求
要想自己build Ant。你需要一个JAXP兼容的XML解析器(parser)放在你的CLASSPATH系统变量中。
binary 形式的Ant包括最新版的Apache Crimson XML解析器。你可以从http://java.sun.com/xml/ 得到更多的关于JAXP的信息。如果你希望使用其他的JAXP兼容的解析器。你要从Ant的lib目录中删掉jaxp.jar以及crimson.jar。然后你可将你心爱的解析器的jar文件放到Ant的lib目录中或放在你的CLASSPATH系统变量中。
对于当前版本的Ant,需要你的系统中有JDK,1.1版或更高。未来的Ant版本会要求使用JDK 1.2或更高版本。
安装Ant
binary 版的Ant包括三个目录:bin, docs 和lib。只有bin和lib目录是运行Ant所需的。要想安装Ant,选择一个目录并将发行版的文件拷贝到该目录下。这个目录被称作ANT_HOME。
在你运行Ant之前需要做一些配置工作。
- 将bin目录加入PATH环境变量。
- 设定ANT_HOME环境变量,指向你安装Ant的目录。在一些OS上,Ant的脚本可以猜测ANT_HOME(Unix和Windos NT/2000)-但最好不要依赖这一特性。
- 可选地,设定JAVA_HOME环境变量(参考下面的高级小节),该变量应该指向你安装JDK的目录。
注意:不要将Ant的ant.jar文件放到JDK/JRE的lib/ext目录下。Ant是个应用程序,而lib/ext目录是为JDK扩展使用的(如JCE,JSSE扩展)。而且通过扩展装入的类会有安全方面的限制。
可选Task
Ant支持一些可选task。一个可选task一般需要额外的库才能工作。可选task与Ant的内置task分开,单独打包。这个可选包可以从你下载Ant的同一个地方下载。目前包含可选task的jar文件名叫jakarta-ant-1.4.1-optional.jar。这个jar文件应该放到Ant安装目录的lib目录下。
每个可选task所需的外部库可参看依赖库小节。这些外部库可以放到Ant的lib目录下,这样Ant就能自动装入,或者将其放入环境变量中。
Windows
假定Ant安装在c:\ant\目录下。下面是设定环境的命令:
set ANT_HOME=c:\ant set JAVA_HOME=c:\jdk1.2.2 set PATH=%PATH%;%ANT_HOME%\bin Unix (bash)
假定Ant安装在/usr/local/ant目录下。下面是设定环境的命令:
export ANT_HOME=/usr/local/ant export JAVA_HOME=/usr/local/jdk-1.2.2 export PATH=${PATH}:${ANT_HOME}/bin 高级
要想运行Ant必须使用很多的变量。你至少参考需要下面的内容:
- Ant的CLASSPATH必须包含ant.jar以及你所选的JAXP兼容的XML解析器的jar文件。
- 当你需要JDK的功能(如javac或rmic task)时,对于JDK 1.1,JDK的classes.zip文件必须放入CLASSPATH中;对于JDK 1.2或JDK 1.3,则必须加入tools.jar。如果设定了正确的JAVA_HOME环境变量,Ant所带的脚本,在bin目录下,会自动加入所需的JDK类。
- 当你执行特定平台的程序(如exec task或cvs task)时,必须设定ant.home属性指向Ant的安装目录。同样,Ant所带的脚本利用ANT_HOME环境变量自动设置该属性。
Building Ant
要想从源代码build Ant,你要先安装Ant源代码发行版或从CVS中checkout jakarta-ant模块。
安装好源代码后,进入安装目录。
设定JAVA_HOME环境变量指向JDK的安装目录。要想知道怎么做请参看安装Ant小节。
确保你已下载了任何辅助jar文件,以便build你所感兴趣的task。这些jar文件可以放在CLASSPATH中,也可以放在lib/optional目录下。参看依赖库小节可知不同的task需要那些jar文件。注意这些jar文件只是用作build Ant之用。要想运行Ant,你还要像安装Ant小节中所做的那样设定这些jar文件。
现在你可以build Ant了:
build -Ddist.dir=<directory_to_contain_Ant_distribution> dist (Windows) build.sh -Ddist.dir=<directory_to_contain_Ant_distribution> dist (Unix)
这样就可你指定的目录中创建一个binary版本。
上面的命令执行下面的动作:
- 如果有必要可以bootstrap Ant的代码。bootstrap 包括手工编辑一些Ant代码以便运行Ant。bootstrap 用于下面的build步骤。
- 向build脚本传递参数以调用bootstrap Ant。参数定义了Ant的属性值并指定了Ant自己的build.xml文件的"dist" target。
大多数情况下,你不必直接bootstrap Ant,因为build脚本为你完成这一切。运行bootstrap.bat (Windows) 或 bootstrap.sh (UNIX) 可以build一个新的bootstrap版Ant。
如果你希望将Ant安装到ANT_HOME目录下,你可以使用:
build install (Windows) build.sh install (Unix)
如果你希望跳过冗长的Javadoc步骤,可以用:
build install-lite (Windows) build.sh install-lite (Unix)
这样就只会安装bin和lib目录。
注意install和install-lite都会覆盖ANT_HOME中的当前Ant版本。
依赖库
如果你需要执行特定的task,你需要将对应的库放入CLASSPATH或放到Ant安装目录的lib目录下。注意使用mapper时只需要一个regexp库。同时,你也要安装Ant的可选jar包,它包含了task的定义。参考上面的安装Ant小节。
返回
3. 运行Ant
运行Ant非常简单,当你正确地安装Ant后,只要输入ant就可以了。
没有指定任何参数时,Ant会在当前目录下查询build.xml文件。如果找到了就用该文件作为buildfile。如果你用 -find 选项。Ant就会在上级目录中寻找buildfile,直至到达文件系统的根。要想让Ant使用其他的buildfile,可以用参数 -buildfile file,这里file指定了你想使用的buildfile。
你也可以设定一些属性,以覆盖buildfile中指定的属性值(参看property task)。可以用 -Dproperty=value 选项,这里property是指属性的名称,而value则是指属性的值。也可以用这种办法来指定一些环境变量的值。你也可以用property task来存取环境变量。只要将 -DMYVAR=%MYVAR% (Windows) 或 -DMYVAR=$MYVAR (Unix) 传递给Ant -你就可以在你的buildfile中用${MYVAR}来存取这些环境变量。
还有两个选项 -quite,告诉Ant运行时只输出少量的必要信息。而 -verbose,告诉Ant运行时要输出更多的信息。
可以指定执行一个或多个target。当省略target时,Ant使用标签<project>的default属性所指定的target。
如果有的话,-projecthelp 选项输出项目的描述信息和项目target的列表。先列出那些有描述的,然后是没有描述的target。
命令行选项总结:
ant [options] [target [target2 [target3] ...]] Options: -help print this message -projecthelp print project help information -version print the version information and exit -quiet be extra quiet -verbose be extra verbose -debug print debugging information -emacs produce logging information without adornments -logfile file use given file for log output -logger classname the class that is to perform logging -listener classname add an instance of class as a project listener -buildfile file use specified buildfile -find file search for buildfile towards the root of the filesystem and use the first one found -Dproperty=value set property to value 例子
ant
使用当前目录下的build.xml运行Ant,执行缺省的target。
ant -buildfile test.xml
使用当前目录下的test.xml运行Ant,执行缺省的target。
ant -buildfile test.xml dist
使用当前目录下的test.xml运行Ant,执行一个叫做dist的target。
ant -buildfile test.xml -Dbuild=build/classes dist
使用当前目录下的test.xml运行Ant,执行一个叫做dist的target,并设定build属性的值为build/classes。
文件
在Unix上,Ant的执行脚本在做任何事之前都会source(读并计算值)~/.antrc 文件;在Windows上,Ant的批处理文件会在开始时调用%HOME%\antrc_pre.bat,在结束时调用%HOME%\antrc_post.bat。你可以用这些文件配置或取消一些只有在运行Ant时才需要的环境变量。看下面的例子。
环境变量
包裹脚本(wrapper scripts)使用下面的环境变量(如果有的话):
- JAVACMD Java可执行文件的绝对路径。用这个值可以指定一个不同于JAVA_HOME/bin/java(.exe)的JVM。
- ANT_OPTS 传递给JVM的命令行变量-例如,你可以定义属性或设定Java堆的最大值
手工运行Ant
如果你自己动手安装(DIY)Ant,你可以用下面的命令启动Ant:
java -Dant.home=c:\ant org.apache.tools.ant.Main [options] [target]
这个命令与前面的ant命令一样。选项和target也和用ant命令时一样。这个例子假定你的CLASSPATH包含:
- ant.jar
- jars/classes for your XML parser
- the JDK's required jar/zip files
返回 4. 编写build.xml
Ant的buildfile是用XML写的。每个buildfile含有一个project。
buildfile中每个task元素可以有一个id属性,可以用这个id值引用指定的任务。这个值必须是唯一的。(详情请参考下面的Task小节)
Projects
project有下面的属性:
Attribute | Description | Required | name | 项目名称. | No | default | 当没有指定target时使用的缺省target | Yes | basedir | 用于计算所有其他路径的基路径。该属性可以被basedir property覆盖。当覆盖时,该属性被忽略。如果属性和basedir property都没有设定,就使用buildfile文件的父目录。 | No | 项目的描述以一个顶级的<description>元素的形式出现(参看description小节)。
一个项目可以定义一个或多个target。一个target是一系列你想要执行的。执行Ant时,你可以选择执行那个target。当没有给定target时,使用project的default属性所确定的target。
Targets
一个target可以依赖于其他的target。例如,你可能会有一个target用于编译程序,一个target用于生成可执行文件。你在生成可执行文件之前必须先编译通过,所以生成可执行文件的target依赖于编译target。Ant会处理这种依赖关系。
然而,应当注意到,Ant的depends属性只指定了target应该被执行的顺序-如果被依赖的target无法运行,这种depends对于指定了依赖关系的target就没有影响。
Ant会依照depends属性中target出现的顺序(从左到右)依次执行每个target。然而,要记住的是只要某个target依赖于一个target,后者就会被先执行。
<target name="A"/> <target name="B" depends="A"/> <target name="C" depends="B"/> <target name="D" depends="C,B,A"/>
假定我们要执行target D。从它的依赖属性来看,你可能认为先执行C,然后B,最后A被执行。错了,C依赖于B,B依赖于A,所以先执行A,然后B,然后C,最后D被执行。
一个target只能被执行一次,即时有多个target依赖于它(看上面的例子)。
如果(或如果不)某些属性被设定,才执行某个target。这样,允许根据系统的状态(java version, OS, 命令行属性定义等等)来更好地控制build的过程。要想让一个target这样做,你就应该在target元素中,加入if(或unless)属性,带上target因该有所判断的属性。例如:
<target name="build-module-A" if="module-A-present"/> <target name="build-own-fake-module-A" unless="module-A-present"/>
如果没有if或unless属性,target总会被执行。
可选的description属性可用来提供关于target的一行描述,这些描述可由-projecthelp命令行选项输出。
将你的tstamp task在一个所谓的初始化target是很好的做法,其他的target依赖这个初始化target。要确保初始化target是出现在其他target依赖表中的第一个target。在本手册中大多数的初始化target的名字是"init"。
target有下面的属性:
Attribute | Description | Required | name | target的名字 | Yes | depends | 用逗号分隔的target的名字列表,也就是依赖表。 | No | if | 执行target所需要设定的属性名。 | No | unless | 执行target需要清除设定的属性名。 | No | description | 关于target功能的简短描述。 | No |
Tasks
一个task是一段可执行的代码。
一个task可以有多个属性(如果你愿意的话,可以将其称之为变量)。属性只可能包含对property的引用。这些引用会在task执行前被解析。
下面是Task的一般构造形式:
<name attribute1="value1" attribute2="value2" ... />
这里name是task的名字,attributeN是属性名,valueN是属性值。
有一套内置的(built-in)task,以及一些可选task,但你也可以编写自己的task。
所有的task都有一个task名字属性。Ant用属性值来产生日志信息。
可以给task赋一个id属性:
<taskname id="taskID" ... />
这里taskname是task的名字,而taskID是这个task的唯一标识符。通过这个标识符,你可以在脚本中引用相应的task。例如,在脚本中你可以这样:
<script ... > task1.setFoo("bar"); </script>
设定某个task实例的foo属性。在另一个task中(用java编写),你可以利用下面的语句存取相应的实例。
project.getReference("task1").
注意1:如果task1还没有运行,就不会被生效(例如:不设定属性),如果你在随后配置它,你所作的一切都会被覆盖。
注意2:未来的Ant版本可能不会兼容这里所提的属性,因为很有可能根本没有task实例,只有proxies。
Properties
一个project可以有很多的properties。可以在buildfile中用property task来设定,或在Ant之外设定。一个property有一个名字和一个值。property可用于task的属性值。这是通过将属性名放在"${"和"}"之间并放在属性值的位置来实现的。例如如果有一个property builddir的值是"build",这个property就可用于属性值:${builddir}/classes。这个值就可被解析为build/classes。
内置属性
如果你使用了<property> task 定义了所有的系统属性,Ant允许你使用这些属性。例如,${os.name}对应操作系统的名字。
要想得到系统属性的列表可参考the Javadoc of System.getProperties。
除了Java的系统属性,Ant还定义了一些自己的内置属性: basedir project基目录的绝对路径 (与<project>的basedir属性一样)。
ant.file buildfile的绝对路径。
ant.version Ant的版本。
ant.project.name 当前执行的project的名字;由<project>的name属性设定.
ant.java.version Ant检测到的JVM的版本; 目前的值有"1.1", "1.2", "1.3" and "1.4".
例子
<project name="MyProject" default="dist" basedir=".">
<!-- set global properties for this build -->
<property name="src" value="."/>
<property name="build" value="build"/>
<property name="dist" value="dist"/>
<target name="init">
<!-- Create the time stamp -->
<tstamp/>
<!-- Create the build directory structure used by compile -->
<mkdir dir="${build}"/>
</target>
<target name="compile" depends="init">
<!-- Compile the java code from ${src} into ${build} -->
<javac srcdir="${src}" destdir="${build}"/>
</target>
<target name="dist" depends="compile">
<!-- Create the distribution directory -->
<mkdir dir="${dist}/lib"/>
<!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
<jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
</target>
<target name="clean">
<!-- Delete the ${build} and ${dist} directory trees -->
<delete dir="${build}"/>
<delete dir="${dist}"/>
</target>
</project>
Token Filters
一个project可以有很多tokens,这些tokens在文件拷贝时会被自动扩展,这要求在支持这一行为的task中选择过滤拷贝功能。这一功能可用filter task在buildfile中设定。
既然这很可能是一个有危害的行为,文件中的tokens必须采取@token@的形式,这里token是filter task中设定的token名。这种token语法与其他build系统执行类似filtering的语法相同,而且与大多数的编程和脚本语言以及文档系统并不冲突,
注意:如果在一个文件中发现了一个@token@形式的token,但没有filter与这个token关连,则不会发生任何事;因此,没有转义方法-但只要你为token选择合适的名字,就不会产生问题。
警告:如果你在拷贝binary文件时打开filtering功能,你有可能破坏文件。这个功能只针对文本文件。
Path-like Structures 你可以用":"和";"作为分隔符,指定类似PATH和CLASSPATH的引用。Ant会把分隔符转换为当前系统所用的分隔符。
当需要指定类似路径的值时,可以使用嵌套元素。一般的形式是
<classpath>
<pathelement path="${classpath}"/>
<pathelement location="lib/helper.jar"/>
</classpath> location属性指定了相对于project基目录的一个文件和目录,而path属性接受逗号或分号分隔的一个位置列表。path属性一般用作预定义的路径--其他情况下,应该用多个location属性。
为简洁起见,classpath标签支持自己的path和location属性。所以:
<classpath>
<pathelement path="${classpath}"/>
</classpath> 可以被简写作:
<classpath path="${classpath}"/> 也可通过<fileset>元素指定路径。构成一个fileset的多个文件加入path-like structure的顺序是未定的。
<classpath>
<pathelement path="${classpath}"/>
<fileset dir="lib">
<include name="**/*.jar"/>
</fileset>
<pathelement location="classes"/>
</classpath> 上面的例子构造了一个路径值包括:${classpath}的路径,跟着lib目录下的所有jar文件,接着是classes目录。
如果你想在多个task中使用相同的path-like structure,你可以用<path>元素定义他们(与target同级),然后通过id属性引用--参考Referencs例子。
path-like structure可能包括对另一个path-like structurede的引用(通过嵌套<path>元素):
<path id="base.path">
<pathelement path="${classpath}"/>
<fileset dir="lib">
<include name="**/*.jar"/>
</fileset>
<pathelement location="classes"/>
</path>
<path id="tests.path">
<path refid="base.path"/>
<pathelement location="testclasses"/>
</path>
前面所提的关于<classpath>的简洁写法对于<path>也是有效的,如:
<path id="tests.path">
<path refid="base.path"/>
<pathelement location="testclasses"/>
</path> 可写成:
<path id="base.path" path="${classpath}"/> 命令行变量
有些task可接受参数,并将其传递给另一个进程。为了能在变量中包含空格字符,可使用嵌套的arg元素。
Attribute | Description | Required | value | 一个命令行变量;可包含空格字符。 | 只能用一个 | line | 空格分隔的命令行变量列表。 | file | 作为命令行变量的文件名;会被文件的绝对名替代。 | path | 一个作为单个命令行变量的path-like的字符串;或作为分隔符,Ant会将其转变为特定平台的分隔符。 |
例子
<arg value="-l -a"/> 是一个含有空格的单个的命令行变量。
<arg line="-l -a"/> 是两个空格分隔的命令行变量。
<arg path="/dir;/dir2:\dir3"/> 是一个命令行变量,其值在DOS系统上为\dir;\dir2;\dir3;在Unix系统上为/dir:/dir2:/dir3 。
References
buildfile元素的id属性可用来引用这些元素。如果你需要一遍遍的复制相同的XML代码块,这一属性就很有用--如多次使用<classpath>结构。
下面的例子:
<project ... >
<target ... >
<rmic ...>
<classpath>
<pathelement location="lib/"/>
<pathelement path="${java.class.path}/"/>
<pathelement path="${additional.path}"/>
</classpath>
</rmic>
</target>
<target ... >
<javac ...>
<classpath>
<pathelement location="lib/"/>
<pathelement path="${java.class.path}/"/>
<pathelement path="${additional.path}"/>
</classpath>
</javac>
</target>
</project> 可以写成如下形式:
<project ... >
<path id="project.class.path">
<pathelement location="lib/"/>
<pathelement path="${java.class.path}/"/>
<pathelement path="${additional.path}"/>
</path>
<target ... >
<rmic ...>
<classpath refid="project.class.path"/>
</rmic>
</target>
<target ... >
<javac ...>
<classpath refid="project.class.path"/>
</javac>
</target>
</project> 所有使用PatternSets, FileSets 或 path-like structures嵌套元素的task也接受这种类型的引用。
在 struts+ hibernate 这种结构中,是不应该把Hibernate产生的PO直接传递给JSP的,不管他是Iterator,还是List,这是一个设计错误。
我来谈谈在J2EE架构中各层的数据表示方法:
Web层的数据表示是FormBean,数据来源于HTML Form POST 业务层的数据表示是VO 持久层的数据表示是PO,其数据来源于数据库,持久层的数据表示例如CMP
在一个规范的J2EE架构中,不同层的数据表示应该被限制在层内,而不应该扩散到其它层,这样可以降低层间的耦合性,提高J2EE架构整体的可维护性和可扩展性。比如说Web层的逻辑进行了修改,那么只需要修改FormBean的结构,而不需要触动业务层和持久层的代码修改。同样滴,当数据库表进行了小的调整,那么也只需要修改持久层数据表示,而不需要触动业务层代码和Web层代码。
不过由于Hibernate的强大功能,例如动态生成PO,PO的状态管理可以脱离Session,使得在应用了Hibernate的J2EE框架中,PO完全可以充当VO,因此我们下面把PO和VO合并,统称为PO。
先来谈谈ActionFormBean和持久层的PO之间的重大区别。
在简单的应用中,ActionFormBean和PO几乎是没有区别,所以很多人干脆就是用ActionFormBean来充当PO,于是ActionFormBean从JSP页面到Servlet控制层再到业务层,然后穿过持久层,最后一直映射到数据库表。真是一竿子捅到了底!
但是在复杂的应用中,ActionFormBean和PO是分离的,他们也不可能一样。ActionFormBean是和网页里面的Form表单一一对应的,Form里面有什么元素,Bean里面就有什么属性。而PO和数据库表对应,因此如果数据库表不修改,那么PO也不会修改,如果页面的流程和数据库表字段对应关系不一致,那么你又如何能够使用ActionFormBean来取代PO呢?
比如说吧,用户注册页面要求注册用户的基本信息,因此HTML Form里面包含了基本信息属性,于是你需要一个ActionFormBean来一一对应(注意:是一一对应),每个Bean属性对应一个文本框或者选择框什么的。
而用户这个持久对象呢?他的属性和ActionFormBean有什么明显不同呢?他会有一些ActionFormBean所没有的集合属性,比如说用户的权限属性,用户的组属性,用户的帖子等等。另外还有可能的是在ActionFormBean里面有3个属性,分别是用户的First Name, Middle Name, Last Name,而在我的User这个持久对象中就是一个 Name 对象属性。
假设我的注册页面原来只要你提供First Name,那么ActionFormBean就这一个属性,后来我要你提供全名,你要改ActionFormBean,加两个属性。但是这个时候PO是不应该修改滴,因为数据库没有改。
那么在一个完整的J2EE系统中应该如何进行合理的设计呢?
JSP(View) ---> ActionFormBean(Module) ---> Action(Control)
ActionFormBean是Web层的数据表示,它和HTML页面Form对应,只要Web页面的操作流程发生改变,它就要相应的进行修改,它不应该也不能被传递到业务层和持久层,否则一旦页面修改,会一直牵连到业务层和持久层的大面积的代码进行修改,对于软件的可维护性和可扩展性而言,是一个灾难,Actiont就是他的边界,到此为止!
Action(Web Control) ---> Business Bean ---> DAO ---> ORM --->DB
而PO则是业务层和持久层的数据表示,它在业务层和持久层之间进行流动,他不应该也不能被传递到Web层的View中去,而ActionServlet就是他的边界,到此为止!
然后来看一看整个架构的流程:
当用户通过浏览器访问网页,提交了一个页面。于是Action拿到了这个FormBean,他会把FormBean属性读出来,然后构造一个PO对象,再调用业务层的Bean类,完成了注册操作,重定向到成功页面。而业务层Bean收到这个PO对象之后,调用DAO接口方法,进行持久对象的持久化操作。
当用户查询某个会员的信息的时候,他用全名进行查询,于是Action得到一个UserNameFormBean包括了3个属性,分别是first name, middle name, last name,然后Action把UserNameFormBean的3个属性读出来,构造Name对象,再调用业务Bean,把Name对象传递给业务Bean,进行查询。
业务Bean取得Name(注意: Name对象只是User的一个属性)对象之后调用DAO接口,返回一个User的PO对象,注意这个User不同于在Web层使用的UserFormBean,他有很多集合属性滴。然后业务Bean把User对象返回给Action。
Action拿到User之后,把User的基本属性取出(集合属性如果不需要就免了),构造UserFormBean,然后把UserFormBean request.setAttribute(...),然后重定向到查询结果页面。
查询页面拿到request对象里面的ActionFormBean,自动调用tag显示之。
总结:
FormBean是Web层的数据表示,他不能被传递到业务层;PO是持久层的数据表示,在特定情况下,例如Hibernate中,他可以取代VO出现在业务层,但是不管PO还是VO都必须限制在业务层内使用,最多到达Web层的Control,绝不能被扩散到View去。
FormBean和PO之间的数据转化是在Action中进行滴。
BTW:
JDO1.x还不能像Hibernate功能这样强大,PO不能脱离持久层,所以必须在业务层使用VO,因此必须在业务层进行大量的VO和PO的转化操作,相对于Hibernate来说,编程比较烦琐。
当然咯,理论是一回事,实际操作也不一定非要这样干,你可以自行取舍,在实际项目中灵活一点,增加一点bad smell,提高开发效率。只不过在大型项目中最好还是严丝合缝,不然的话,改版的时候会痛苦的很滴。
摘要:这篇文章提供了一个对J2EE的简化,展示了如何消除应用服务器的消耗和限制。特别地,这篇文章提到了:许多应用程序实际上并不需要运行应用服务器。 尽管J2EE平台(应用程序服务器)及其编程模型(企业JAVA组件,简称EJB)拥有的众所周知的复杂性,但是基于J2EE的应用程序仍然在企业领域里变得非常成功.我们要感谢应用于轻量级容器的控制反转(IoC)和面向方面编程(AOP),比如Spring框架. 我们能够更简单地设计更大型的编程模型。然而,即使有了这些工具,应用服务器仍然是复杂度和消耗的一个重要瓶颈。这篇文章提供了一个对J2EE的简化,展示了如何消除应用服务器的消耗和限制。特别地,这篇文章提到了:许多应用程序实际上并不需要运行应用服务器。这样,J2EE应用组件将会变得: · 开发更容易:不再需要EJB运行代码; · 更简单: 继承不需要EJB类或接口; · 测试更容易:你的应用程序及测试能在你的开发环境(IDE)中直接运行; · 更少的资源消耗:你只需要你的对象,不需要应用服务器,更不需要应用服务器的对象; · 安装更容易:没有运行应用服务专门的安装软件, 没有加载额外的XML文件; · 维护更容易:所有的过程都更简单,因此维护也更容易。 J2EE不必要的复杂度已经成为一个阻碍。今天,这种复杂度能够通过在这篇文章中提到的方法来避免。另外,程序还能够保留事务和安全这些典型的服务。J2EE程序从来没有比这更有趣过。 版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接作者:Guy Pardon; chmei83(作者的blog: http://blog.matrix.org.cn/page/chmei83) 原文: http://www.onjava.com/pub/a/onjava/2006/02/08/j2ee-without-application-server.html译文: http://www.matrix.org.cn/resource/article/44/44250_J2ee+Application+Server.html关键字:J2ee;Application;Server 例子:消息驱动Bank为了阐述我们的观点,我们将开发和安装一个完整的样板应用程序:一个消息驱动的银行系统. 通过(幸亏有Spring)改进的基于POJOs的编程模型和保留相同的事务,我们可以不需要EJB或者一个应用服务器来实现这个系统。在下一个部分,我们将从消息驱动架构产生到另一个架构.就像基于WEB的架构一样.图1展示我们的样本应用程序的架构.  Figure 1. Architecture of the message-driven bank 在我们的例子中,我们将处理来自Java消息服务队列的银行定单.一张定单的处理包括通过JDBC来更新当前帐户的数据库.为了避免信息的丢失和重复,我们将使用JTA和JTA/XA事务来配合更新:处理信息和更新数据库将发生在一个原子事务里.资源部分可得到JTA/XA的更多信息. 编写应用程序代码该应用程序将由两个JAVA类组成: Bank(一个DAO)和MessageDrivenBank.如图2.  Figure 2. Classes for the message-driven bank Bank是一个数据访问对象,这个对象封装数据库访问。MessageDrivenBank是一个消息驱动façade并且是DAO的委托.与典型的J2EE方法不同,这个应用程序不包括EJB类. 第一步:编写Bank DAO如下, Bank源代码是很直接和简单的JDBC操作. package jdbc; import javax.sql.*; import java.sql.*; public class Bank { private DataSource dataSource; public Bank() {} public void setDataSource ( DataSource dataSource ) { this.dataSource = dataSource; }
private DataSource getDataSource() { return this.dataSource; }
private Connection getConnection() throws SQLException { Connection ret = null; if ( getDataSource() != null ) { ret = getDataSource(). getConnection(); } return ret; }
private void closeConnection ( Connection c ) throws SQLException { if ( c != null ) c.close(); } public void checkTables() throws SQLException { Connection conn = null; try { conn = getConnection(); Statement s = conn.createStatement(); try { s.executeQuery ( "select * from Accounts" ); } catch ( SQLException ex ) { //table not there => create it s.executeUpdate ( "create table Accounts ( " + "account VARCHAR ( 20 ), " + "owner VARCHAR(300), " + "balance DECIMAL (19,0) )" ); for ( int i = 0; i < 100 ; i++ ){ s.executeUpdate ( "insert into Accounts values ( " + "'account"+i +"' , 'owner"+i +"', 10000 )" ); } } s.close(); } finally { closeConnection ( conn );
}
//That concludes setup }
// //Business methods are below //
public long getBalance ( int account ) throws SQLException { long res = -1; Connection conn = null;
try { conn = getConnection(); Statement s = conn.createStatement(); String query = "select balance from Accounts where account='"+ "account" + account +"'"; ResultSet rs = s.executeQuery ( query ); if ( rs == null || !rs.next() ) throw new SQLException ( "Account not found: " + account ); res = rs.getLong ( 1 ); s.close(); } finally { closeConnection ( conn ); } return res; }
public void withdraw ( int account , int amount ) throws Exception { Connection conn = null;
try { conn = getConnection(); Statement s = conn.createStatement();
String sql = "update Accounts set balance = balance - "+ amount + " where account ='account"+ account+"'"; s.executeUpdate ( sql ); s.close(); } finally { closeConnection ( conn );
} } } 注意:代码并没有依赖EJB或任何专门的应用程序服务器.实际上,这是一个纯JAVA代码,这个JAVA代码是能在任何J2SE环境下运行的. 你同时应注意:我们使用了来自JDBC的DataSource接口.这意味着我们的类是独立于目前JDBC供应商提供的类. 你可能会疑惑,这怎么能与特定的数据管理系统(DBMS)提供商的JDBC实现紧密结合呢? 这里就是Spring框架帮你实现的. 这个技术被称为依赖注入:在我们的应用程序的启动期间,通过调用setDataSource方法,Spring为我们提供了相应的datasource对象.在后面几部分我们会更多地提到Spring.如果我们在以前使用应用程序服务器,我们将不得不借助于JAVA命名绑定接口(JNDI)查询. 除了直接使用JDBC,我们也可以使用Hibernate或者一个JDO工具来实现我们的持久层.这同样不需要任何的EJB代码. 第二步:配置BankDAO我们会将便用Spring框架来配置我们的应用程序.Spring不是必需的,但是使用Spring的好处是我们将可以简单的添加服务,如:我们JAVA对象的事务和安全.这类似于应用服务器为EJB提供的东西,只是在我们的例子中Spring将变得更容易. Spring也允许我们把我们的类从目前的JDBC驱动实现中分离出来:Spring能够配置Driver(基于我们的XML配置数据)并把它提供给BankDAO对象(依赖注入原理).这样可以保持我们的JAVA代码的清淅和集中.这步的Spring配置文件如下: <?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="datasource" class="com.atomikos.jdbc.nonxa.NonXADataSourceBean">
<property name="user"> <value>sa</value> </property> <property name="url"> <value>jdbc:hsqldb:SpringNonXADB </value> </property> <property name="driverClassName"> <value>org.hsqldb.jdbcDriver</value> </property> <property name="poolSize"> <value>1</value> </property> <property name="connectionTimeout"> <value>60</value> </property> </bean>
<bean id="bank" class="jdbc.Bank"> <property name="dataSource"> <ref bean="datasource"/> </property> </bean>
</beans> 这个XML文件包括两个对象的配置:访问数据库的DataSource和使用这个DataSource的Bank对象.下面是由Spring维护的一些基本任务. · 创建应用程序(例: Bank和DataSource)需要的对象(“beans”).在XML文件中给出了这些对象的类名,并且在我们的例子中,这些对象需要有一个公共的无参数constructor (Spring也允许参数,但是配置语法上有所不同).这些对象都被命名(XML中的id属性),所以我们后面能够引用这些对象. id也允许我们的应用程序找回它需要的已配置对象. · 这些对象的初始化是通过在XML文件中的properties的值实现. 在XML文件中这些properties名 应与对应的类中的setXXX方法相对应. · 将对象连接在一起 :一个property可能是另一个对象(例如:在我们例子中的数据源)的引用,引用可以通过id创建. 注意:在我们下一步中, 我们将选择配置一个JTA-enabled的数据源(由Atomikos Transactions提供,可用于企业和J2SE的JTA产品,我们将应用于我们的应用程序). 简单起见,我们将使用HypersonicSQLDB,这个DBMS不需要专门的安装步骤—它是在.jar文件里,就像JTA和Spring. 但是,考虑到渐增的可靠性需求,强列推荐你使用XA-capable的DBMS和JDBC驱动.没有XA的支持, 在crash或重启之后你的应用程序将不能恢复原有数据. 资源部分有链接到关于事务和XA的信息和一些例子. 作为一个练习,你可以试试从HypersonicSQLDB转换到FirstSQL,一个易安装XA-compliant的DBMS.换句话说,任何其他为企业准备的和XA-capable的DBMS也会做得很好. 第三步:测试BankDAO让我们来测试我们的代码,(使用极限编程的程序员会首先写测试,但因开始不是很清淅,所以我们直到现在才开始写测试.)下面是一个简单的单元测试.这个测试可在你的的应用程序里运行:它通过Spring获得一个BANK对象来进行测试(这在setUp方法中实现).注意:这个测试使用清楚的事务划分:每一个测试开始之前开始一个事务,每个测试结束时强制进行事务回滚.这是通过手工的方式来减少测试对数据库数据的影响. package jdbc; import com.atomikos.icatch.jta.UserTransactionImp; import junit.framework.TestCase; import java.io.FileInputStream; import java.io.InputStream; import org.springframework.beans.factory.xml.XmlBeanFactory;
public class BankTest extends TestCase {
private UserTransactionImp utx;
private Bank bank;
public BankTest ( String name ) { super ( name ); utx = new UserTransactionImp();
}
protected void setUp() throws Exception { //start a new transaction //so we can rollback the //effects of each test //in teardown! utx.begin(); //open bean XML file InputStream is = new FileInputStream("config.xml");
//the factory is Spring's entry point //for retrieving the configured //objects from the XML file
XmlBeanFactory factory = new XmlBeanFactory(is);
bank = ( Bank ) factory.getBean ( "bank" ); bank.checkTables(); }
protected void tearDown() throws Exception { //rollback all DBMS effects //of testing utx.rollback(); }
public void testBank() throws Exception { int accNo = 10; long initialBalance = bank.getBalance ( accNo ); bank.withdraw ( accNo , 100 ); long newBalance = bank.getBalance ( accNo ); if ( ( initialBalance - newBalance ) != 100 ) fail ( "Wrong balance after withdraw: " + newBalance ); } } 我们将需要JTA事务来确保JMS和JDBC都是原子操作.一般来说,当经常都是两个或多个连接的时候,你应考虑一下JTA/XA。例如,在我们例子中的JMS和JDBC. Spring本身不提供JTA事务;它需要一个JTA实现或者委派一个应用服务器来处理这个事务.在这里,我们使用了一个JTA实现,这个实现可以在任何J2SE平台上工作. 最终架构如下面图3.白色方框代表我们的应用程序代码.  Figure 3. Architecture for the test 如你所看到的,当我们执行我们的测试,将会发生下面的情况: 1. BankTest开始一个新事务. 2. 然后,这个test在Spring运行期间获得一个BANK对象.这步触发Sping的创建和初始化过程. 3. 这个test调用BANK的方法. 4. BANK调用datasource对象,通过它自己的setDataSource 方法从Spring 获取这个对像. 5. 这个数据源是JTA-enabled,并且与JTA实现交互来注册当前事务. 6. JDBC statements和帐户数据库交互. 7. 当方法返回时, test调用事务回滚. 8. JTA记得住datasource对象,会命令它进行回滚. 第四步:添加声明式事务管理Spring允许添加声明式事务管理来管理java对象.假设我们想确认bank总是和一个有效的事务上下文一起被调用.我们通过在实际对象的上部配置一个proxy对象. Proxy和实际对象有相同接口,所以客户通过完全相同的方式使用它. 配置Proxy wrap每个BankDAO方法到事务中.结果配置文件如下. 不要被XML的庞大吓倒—大多数内容能通过复制和粘贴到你自己的工程中再使用. <?xml version="1.0" encoding="UTF-8"?>
<beans> <!-- Use a JTA-aware DataSource to access the DB transactionally --> <bean id="datasource" class="com.atomikos.jdbc.nonxa.NonXADataSourceBean"> <property name="user"> <value>sa</value> </property> <property name="url"> <value>jdbc:hsqldb:SpringNonXADB</value> </property> <property name="driverClassName"> <value>org.hsqldb.jdbcDriver</value> </property> <property name="poolSize"> <value>1</value> </property> <property name="connectionTimeout"> <value>60</value> </property> </bean> <!-- Construct a TransactionManager, needed to configure Spring --> <bean id="jtaTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager"/> <!-- Also configure a UserTransaction, needed to configure Spring --> <bean id="jtaUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp"/> <!-- Configure the Spring framework to use JTA transactions from the JTA provider --> <bean id="springTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager"> <ref bean="jtaTransactionManager"/> </property> <property name="userTransaction"> <ref bean="jtaUserTransaction"/> </property> </bean> <!-- Configure the bank to use our datasource --> <bean id="bankTarget" class="jdbc.Bank"> <property name="dataSource"> <ref bean="datasource"/> </property> </bean> <!-- Configure Spring to insert JTA transaction logic for all methods --> <bean id="bank" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"> <ref bean="springTransactionManager"/> </property> <property name="target"> <ref bean="bankTarget"/> </property> <property name="transactionAttributes"> <props> <prop key="*"> PROPAGATION_REQUIRED, -Exception </prop> </props> </property> </bean> </beans> 这个XML文件告诉Spring去配置下面的对象: 1. 需要通过JDBC连接的datasource. 2. 添加jtaTransactionManager和jtaUserTransaction用于为JTA事务的Spring配置作准备. 3. springTransactionManager用于告诉Spring需要使用JTA. 4. BankDAO被重命名为bankTarget (因如下解释的原因). 5. bank对象被添加用于包装事务和bankTarget的所有方法.我们通过配置bank对象来使用 springTransactionManager,这意味着所有事务将是JTA事务. 每个事务都被设置为PROPAGATION_REQUIRED,这将在任何异常下出现强制回滚. 对这些对象包含的内容,你都可以很容易的复制和粘贴jtaTransactionManager, jtaUserTransaction, springTransactionManager到其他工程.其他的是应用程序相关的对象:datasource, bankTarget, bank. Bank对象很有趣:事实上对于bankTarget它是一个proxy;他们拥有相同的接口. Trick如下:当我们的应用程序请求Spring去配置和返回bank对象,Spring实际上将返回proxy(看起来和我们的应用程序完全相同),随后这个proxy将为我们开始/结束事务.这样,应用程序和Bank类本身都不需要知道JTA!图4阐述了在这步我们所得到的.  Figure 4. Architecture with declarative JTA transactions in Spring 现在的工作如下: 1. 应用程序调用bank对象.这将触发Spring的初始化处理和返回proxy对象. 对应用程序而言,这个proxy行为和我们的Bank是一样的. 2. 当bank的一个方法被调用, 这个调用将会通过proxy进行. 3. proxy使用springTransactionManager创建一个新事务. 4. springTransactionManager被配置为使用JTA,因些它委派到JTA. 5. 调用被forward到Bank的实际对象,bankTarget. 6. bankTarget使用从Spring中得到的datasource. 7. datasource对事务进行注册. 8. 通过规则的JDBC访问数据库. 9. 在返回时, proxy终止事务:如果在先前的序列中没有发生异常,那么将会提交终止指令.否则,它将会被回滚. 10. transaction 管理器与数据库配合进行提交和回滚. 在这步中的测试怎样进行?我们可重用BankTest 和它清晰的事务划分:因为PROPAGATION_REQUIRED, proxy将和在BankTest中创建的事务上下文一起执行. 第五步:编写PROPAGATION_REQUIRED在这步,我们将添加JMS处理逻辑.为了做到这样,我们主要需要实现JMS MessageListener接口.我们也会添加公共的setBank方法使Spring的依赖注入起作用.源代码如下: package jms; import jdbc.Bank; import javax.jms.Message; import javax.jms.MapMessage; import javax.jms.MessageListener;
public class MessageDrivenBank implements MessageListener { private Bank bank;
public void setBank ( Bank bank ) { this.bank = bank; }
//this method can be private //since it is only needed within //this class private Bank getBank() { return this.bank; }
public void onMessage ( Message msg ) { try { MapMessage m = ( MapMessage ) msg; int account = m.getIntProperty ( "account" ); int amount = m.getIntProperty ( "amount" ); bank.withdraw ( account , amount ); System.out.println ( "Withdraw of " + amount + " from account " + account ); } catch ( Exception e ) { e.printStackTrace(); //force rollback throw new RuntimeException ( e.getMessage() ); } } } 第六步:配置MessageDrivenBank这里我们配置MessageDrivenBank去监听事务的QueueReceiverSessionPool.这样给我们可以实现和EJB(没有丢失信息和冗余信息)类似的消息机制,但在这里我们是用简单的POJO对象实现.当向pool中插入一个MessageListener,这个会话池将确保用JTA/XA事务接收到消息.结合JTA/XA-capable 的JDBC数据源,我们可以实现可靠的消息机制. Spring的配置如下: <?xml version="1.0" encoding="UTF-8"?>
<!-- NOTE: no explicit transaction manager bean is necessary because the QueueReceiverSessionPool will start transactions by itself. --> <beans> <bean id="datasource" class="com.atomikos.jdbc.nonxa.NonXADataSourceBean"> <property name="user"> <value>sa</value> </property> <property name="url"> <value>jdbc:hsqldb:SpringNonXADB</value> </property> <property name="driverClassName"> <value>org.hsqldb.jdbcDriver</value> </property> <property name="poolSize"> <value>1</value> </property> <property name="connectionTimeout"> <value>60</value> </property> </bean> <bean id="xaFactory" class="org.activemq.ActiveMQXAConnectionFactory"> <property name="brokerURL"> <value>tcp://localhost:61616</value> </property> </bean> <bean id="queue" class="org.activemq.message.ActiveMQQueue"> <property name="physicalName"> <value>BANK_QUEUE</value> </property> </bean> <bean id="bank" class="jdbc.Bank"> <property name="dataSource"> <ref bean="datasource"/> </property> </bean> <bean id="messageDrivenBank" class="jms.MessageDrivenBank"> <property name="bank"> <ref bean="bank"/> </property> </bean> <bean id="queueConnectionFactoryBean" class="com.atomikos.jms.QueueConnectionFactoryBean"> <property name="resourceName"> <value>QUEUE_BROKER</value> </property> <property name="xaQueueConnectionFactory"> <ref bean="xaFactory"/> </property> </bean> <bean id="queueReceiverSessionPool" class="com.atomikos.jms.QueueReceiverSessionPool" init-method="start"> <property name="queueConnectionFactoryBean"> <ref bean="queueConnectionFactoryBean"/> </property> <property name="transactionTimeout"> <value>120</value> </property> <!-- default license allows only limited concurrency so keep pool small --> <property name="poolSize"> <value>1</value> </property> <property name="queue"> <ref bean="queue"/> </property> <property name="messageListener"> <ref bean="messageDrivenBank"/> </property> </bean> </beans> 因为这篇文章需要一个便于安装的JMS服务,所以这里我们使用ActiveMQ.如果你正在使用另一个JMS实现,那么你将仍然能使用这部分提出的技术.接下来除了datasource和bank对象,我们将增加下面的对象定义: · xaFactory: 为建立JMS连接的connection工厂. · queue: queue代表我们将使用的JMS队列, 这个队列被配置成ActiveMQ要求的形式. · queueConnectionFactoryBean:一个JTA-aware的JMS连接器. · A queueReceiverSessionPool for JTA-enabled message consumption:注意:我们同时指定了用来调用的初始化方法(例:start);这是Spring的另一个特性. Start方法在session pool类里定义,它是在Spring配置文件中进行配置的. · messageDrivenBank:负责处理消息. 你可以问问自己事务管理是在哪里进行的.事实上, 在先前部分被添加的对象已消失.为什么呢?因为我们现在使用QueueReceiverSessionPool来接收来自JMS的消息,并且这个类也为每次接收启动一个JTA事务.我们也可以保留JTA配置,另外添加JMS配置, 但是这样可能会使XML文件更长. 现在session pool类将担当事务管理角色.它和proxy方法的工作相似; 只是这个类需要JMS MessageListener 为之添加事务. 通过这样配置,在每个消息收接之前程序将启动一个新事务 无论何时, 当我们的消息实例正常返回时, 这个事务将提交. 如果出现RuntimeException, 那么这个事务将回滚. 结构如下面图5(可以清淅地看到一些JMS对象).  Figure 5. Architecture for message-driven applications in Spring 现在该架构工作如下: 1. 应用程序调用bank对象和初始化数据库表. 2. 应用程序queueReceiverSessionPool, 因此触发一个start方法的调用去监听到达的消息. 3. queueReceiverSessionPool在队列中侦察一个新消息. 4. queueReceiverSessionPool开始一个新事务,并且注册这个事务. 5. queueReceiverSessionPool调用已注册的MessageListener (messageDrivenBank). 6. 这将触发对bank 对象的调用. 7. bank 对象通过datasource访问数据库. 8. datasource注册事务. 9. 通过JDBC访问数据库. 10. 当处理完成时, queueReceiverSessionPool会终止这个事务。然后进行commint(除非发生RuntimeException). 11. transaction manager开始消息队列的两阶段提交. 12. transaction manager开始数据库的两阶段提交. 第七步:编写应用程序因为我们没有使用容器,我们仅仅提供一个Java应用程序就可以启动整个银行系统.我们的Java应用程序是非常简单: 它有能力找回配置的对象(Spring通过XML文件将他们放到一起). 这个应用程序能在任何兼容的JDK(Java Development Kit)上运行,并且不需要应用服务器. package jms; import java.io.FileInputStream; import java.io.InputStream; import org.springframework.beans.factory.xml.XmlBeanFactory; import com.atomikos.jms.QueueReceiverSessionPool; import jdbc.Bank;
public class StartBank { public static void main ( String[] args ) throws Exception { //open bean XML file InputStream is = new FileInputStream(args[0]); //the factory is Spring's entry point //for retrieving the configured //objects from the XML file XmlBeanFactory factory = new XmlBeanFactory(is); //retrieve the bank to initialize //alternatively, this could be done //in the XML configuration too Bank bank = ( Bank ) factory.getBean ( "bank" ); //initialize the bank if needed bank.checkTables();
//retrieve the pool; //this will also start the pool //as specified in the beans XML file //by the init-method attribute!
QueueReceiverSessionPool pool = ( QueueReceiverSessionPool ) factory.getBean ( "queueReceiverSessionPool" );
//Alternatively, start pool here //(if not done in XML) //pool.start();
System.out.println ( "Bank is listening for messages..." ); } } 这就是J2EE!是不是认为J2EE也很容易呢? 对通用性的考虑这部分里我们看看更多的概念,这些概念在许多J2EE应用程序中是很重要的.我们同样将看到对这些概念来说,一个应用服务器并不是必须的. 集群和可扩展性健壮的企业应用程序需要集群来分流负担. 在消息驱动应用程序的例子中,这很容易:我们自动地从JMS应用程序继承得来处理能力.如果我们需要更强大的处理能力,那么我们只需增加更多连接相同JMS服务器的进程.一个对服务性能有效的衡量标准是在队列中停留的消息的数量. 在其他情况下,如基于web的 架构(如下)我们能很容易地使用web环境下的集群能力. 方法级别的安全一个典型的观点是认为EJB能增加方法级的安全性.虽然并没有在这篇文章中提到,但是在Sping中配置方法级别的安全是可能的.这种配置类似于我们增加方法级的事务划分的方式. 对非消息驱动的应用程序的通用性在不改变源代码的情况下(除了主应用程序类),我们使用的平台能很容易地被整合到任何J2EE web 应用服务器. 换句话说 , 通过JMS进行后台处理;这使得web服务器在面对后台处理的延迟问题上更可靠和更独立.在任何情况下, 为了实现容器管理的事务或容器管理的安全性,我们都不再需要依靠EJB容器来实现. 关于容器管理持久化?存在并被很多开发者检验过的技术例如:JDO或者Hibernate 都不一定需要一个应用服务器. 另外,这些工具已经占据了持久化市场. 结论今天,不需要应用服务器的J2EE已经成为可能,也很容易. 有人或许会说,没有应用程序服务器,有一些应用程序仍不能实现:例如,如果你需要一般的JCA(Java Connectivity API) 功能性, 那么我们上面提供的平台是不够的. 但是,这可能会发生改变,因为不用使用一个应用程序服务器进行开发,测式和部署的好处实在是太大. 人们越来越相信: 将来得J2EE是一个模块化的”选择你所需要”架构. 这与我们之前的完全基于应用服务器的方法相反. 在这样的情况下,J2EE开发者将从应用服务器和EJB中解放出来. 资源: · 代码下载· Guy Pardon's presentation on Transactions in Spring published at TheServerSide · More information on Atomikos Transactions and message-driven functionality without EJB · The home page of Spring · More information on JUnit · FirstSQL is an easy-to-install, XA-compliant DBMS · More information on HSQLDB · More information on ActiveMQ
(转载自http://www.blogjava.net/ltc603/archive/2006/01/13/27966.html) 引言
文件的上传和下载在J2EE编程已经是一个非常古老的话题了,也许您马上就能掰着指头数出好几个著名的大件:如SmartUpload、Apache的FileUpload。但如果您的项目是构建在Struts+Spring+Hibernate(以下称SSH)框架上的,这些大件就显得笨重而沧桑了,SSH提供了一个简捷方便的文件上传下载的方案,我们只需要通过一些配置并辅以少量的代码就可以完好解决这个问题了。
本文将围绕SSH文件上传下载的主题,向您详细讲述如何开发基于SSH的Web程序。SSH各框架的均为当前最新版本:
·Struts 1.2
·Spring 1.2.5
·Hibernate 3.0
本文选用的数据库为Oracle 9i,当然你可以在不改动代码的情况下,通过配置文件的调整将其移植到任何具有Blob字段类型的数据库上,如MySQL,SQLServer等。
总体实现
上传文件保存到T_FILE表中,T_FILE表结构如下:
图 1 T_FILE表结构
|
其中: ·FILE_ID:文件ID,32个字符,用Hibernate的uuid.hex算法生成。 ·FILE_NAME:文件名。 ·FILE_CONTENT:文件内容,对应Oracle的Blob类型。 ·REMARK:文件备注。 文件数据存储在Blob类型的FILE_CONTENT表字段上,在Spring中采用OracleLobHandler来处理Lob字段(包括Clob和Blob),由于在程序中不需要引用到oracle数据驱动程序的具体类且屏蔽了不同数据库处理Lob字段方法上的差别,从而撤除程序在多数据库移植上的樊篱。 1.首先数据表中的Blob字段在Java领域对象中声明为byte[]类型,而非java.sql.Blob类型。 2.数据表Blob字段在Hibernate持久化映射文件中的type为org.springframework.orm.hibernate3.support.BlobByteArrayType,即Spring所提供的用户自定义的类型,而非java.sql.Blob。 3.在Spring中使用org.springframework.jdbc.support.lob.OracleLobHandler处理Oracle数据库的Blob类型字段。 通过这样的设置和配置,我们就可以象持久化表的一般字段类型一样处理Blob字段了。 以上是Spring+Hibernate将文件二进制数据持久化到数据库的解决方案,而Struts通过将表单中file类型的组件映射为ActionForm中类型为org.apache.struts.upload. FormFile的属性来获取表单提交的文件数据。 综上所述,我们可以通过图 2,描绘出SSH处理文件上传的方案:  图 2 SSH处理文件上传技术方案 |
文件上传的页面如图 3所示:  图 3 文件上传页面 |
文件下载的页面如图 4所示:  图 4 文件下载页面 |
该工程的资源结构如图 5所示:  图 5 工程资源结构 |
工程的类按SSH的层次结构划分为数据持久层、业务层和Web层;WEB-INF下的applicationContext.xml为Spring的配置文件,struts-config.xml为Struts的配置文件,file-upload.jsp为文件上传页面,file-list.jsp为文件列表页面。 本文后面的章节将从数据持久层->业务层->Web层的开发顺序,逐层讲解文件上传下载的开发过程。 数据持久层 1、领域对象及映射文件 您可以使用Hibernate Middlegen、HIbernate Tools、Hibernate Syhchronizer等工具或手工的方式,编写Hibernate的领域对象和映射文件。其中对应T_FILE表的领域对象Tfile.java为: 代码 1 领域对象Tfile 1. package sshfile.model; 2. public class Tfile 3.{ 4. private String fileId; 5. private String fileName; 6. private byte[] fileContent; 7. private String remark; 8. …//getter and setter 9. } |
特别需要注意的是:数据库表为Blob类型的字段在Tfile中的fileContent类型为byte[]。Tfile的Hibernate映射文件Tfile.hbm.xml放在Tfile .java类文件的相同目录下: 代码 2 领域对象映射文件 1. <?xml version="1.0"?> 2. <!DOCTYPE hibernate-mapping PUBLIC 3. "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 4. "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" > 5. <hibernate-mapping> 6. <class name="sshfile.model.Tfile" table="T_FILE"> 7. <id name="fileId" type="java.lang.String" column="FILE_ID"> 8. <generator class="uuid.hex"/> 9. </id> 10. <property name="fileContent" 11. type="org.springframework.orm.hibernate3.support.BlobByteArrayType" 12. column="FILE_CONTENT" lazy="true"/> 13. …//其它一般字段的映射 14. </class> 15. </hibernate-mapping> |
fileContent字段映射为Spring所提供的BlobByteArrayType类型,BlobByteArrayType是用户自定义的数据类型,它实现了Hibernate 的org.hibernate.usertype.UserType接口。BlobByteArrayType使用从sessionFactory获取的Lob操作句柄lobHandler将byte[]的数据保存到Blob数据库字段中。这样,我们就再没有必要通过硬编码的方式,先insert然后再update来完成Blob类型数据的持久化,这个原来难伺候的老爷终于被平民化了。关于lobHandler的配置请见本文后面的内容。 此外lazy="true"说明地返回整个Tfile对象时,并不返回fileContent这个字段的数据,只有在显式调用tfile.getFileContent()方法时才真正从数据库中获取fileContent的数据。这是Hibernate3引入的新特性,对于包含重量级大数据的表字段,这种抽取方式提高了对大字段操作的灵活性,否则加载Tfile对象的结果集时如果总是返回fileContent,这种批量的数据抽取将可以引起数据库的"洪泛效应"。 2、DAO编写和配置 Spring强调面向接口编程,所以我们将所有对Tfile的数据操作的方法定义在TfileDAO接口中,这些接口方法分别是: ·findByFildId(String fileId) ·save(Tfile tfile) ·List findAll() TfileDAOHibernate提供了对TfileDAO接口基于Hibernate的实现,如代码 3所示: 代码 3 基于Hibernate 的fileDAO实现类 1. package sshfile.dao; 2. 3. import sshfile.model.*; 4. import org.springframework.orm.hibernate3.support.HibernateDaoSupport; 5. import java.util.List; 6. 7. public class TfileDAOHibernate 8. extends HibernateDaoSupport implements TfileDAO 9. { 10. public Tfile findByFildId(String fileId) 11. { 12. return (Tfile) getHibernateTemplate().get(Tfile.class, fileId); 13. } 14. public void save(Tfile tfile) 15. { 16. getHibernateTemplate().save(tfile); 17. getHibernateTemplate().flush(); 18. } 19. public List findAll() 20. { 21. return getHibernateTemplate().loadAll(Tfile.class); 22. } 23. } |
TfileDAOHibernate通过扩展Spring提供的Hibernate支持类HibernateDaoSupport而建立,HibernateDaoSupport封装了HibernateTemplate,而HibernateTemplate封装了Hibernate所提供几乎所有的的数据操作方法,如execute(HibernateCallback action),load(Class entityClass, Serializable id),save(final Object entity)等等。 所以我们的DAO只需要简单地调用父类的HibernateTemplate就可以完成几乎所有的数据库操作了。 由于Spring通过代理Hibernate完成数据层的操作,所以原Hibernate的配置文件hibernate.cfg.xml的信息也转移到Spring的配置文件中: 代码 4 Spring中有关Hibernate的配置信息 1. <beans> 2. <!-- 数据源的配置 //--> 3. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 4. destroy-method="close"> 5. <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> 6. <property name="url" value="jdbc:oracle:thin:@localhost:1521:ora9i"/> 7. <property name="username" value="test"/> 8. <property name="password" value="test"/> 9. </bean> 10. <!-- Hibernate会话工厂配置 //--> 11. <bean id="sessionFactory" 12. class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> 13. <property name="dataSource" ref="dataSource"/> 14. <property name="mappingDirectoryLocations"> 15. <list> 16. <value>classpath:/sshfile/model</value> 17. </list> 18. </property> 19. <property name="hibernateProperties"> 20. <props> 21. <prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop> 22. <prop key="hibernate.cglib.use_reflection_optimizer">true</prop> 23. </props> 24. </property> 25. </bean> 26. <!-- Hibernate 模板//--> 27. <bean id="hibernateTemplate" 28. class="org.springframework.orm.hibernate3.HibernateTemplate"> 29. <property name="sessionFactory" ref="sessionFactory"/> 30. </bean> 31. <!--DAO配置 //--> 32. <bean id="tfileDAO" class="sshfile.dao.TfileDAOHibernate"> 33. <property name="hibernateTemplate" ref="hibernateTemplate" /> 34. </bean> 35. … 36. </beans> |
第3~9行定义了一个数据源,其实现类是apache的BasicDataSource,第11~25行定义了Hibernate的会话工厂,会话工厂类用Spring提供的LocalSessionFactoryBean维护,它注入了数据源和资源映射文件,此外还通过一些键值对设置了Hibernate所需的属性。 其中第16行通过类路径的映射方式,将sshfile.model类包目录下的所有领域对象的映射文件装载进来,在本文的例子里,它将装载进Tfile.hbm.xml映射文件。如果有多个映射文件需要声明,使用类路径映射方式显然比直接单独指定映射文件名的方式要简便。 第27~30行定义了Spring代理Hibernate数据操作的HibernateTemplate模板,而第32~34行将该模板注入到tfileDAO中。 需要指定的是Spring 1.2.5提供了两套Hibernate的支持包,其中Hibernate 2相关的封装类位于org.springframework.orm.hibernate2.*包中,而Hibernate 3.0的封装类位于org.springframework.orm.hibernate3.*包中,需要根据您所选用Hibernate版本进行正确选择。 3、Lob字段处理的配置 我们前面已经指出Oracle的Lob字段和一般类型的字段在操作上有一个明显的区别--那就是你必须首先通过Oracle的empty_blob()/empty_clob()初始化Lob字段,然后获取该字段的引用,通过这个引用更改其值。所以要完成对Lob字段的操作,Hibernate必须执行两步数据库访问操作,先Insert再Update。 使用BlobByteArrayType字段类型后,为什么我们就可以象一般的字段类型一样操作Blob字段呢?可以确定的一点是:BlobByteArrayType不可能逾越Blob天生的操作方式,原来是BlobByteArrayType数据类型本身具体数据访问的功能,它通过LobHandler将两次数据访问的动作隐藏起来,使Blob字段的操作在表现上和其他一般字段业类型无异,所以LobHandler即是那个"苦了我一个,幸福十亿人"的那位幕后英雄。 LobHandler必须注入到Hibernate会话工厂sessionFactory中,因为sessionFactory负责产生与数据库交互的Session。LobHandler的配置如代码 5所示: 代码 5 Lob字段的处理句柄配置 1. <beans> 2. … 3. <bean id="nativeJdbcExtractor" 4. class="org.springframework.jdbc.support.nativejdbc.CommonsDbcpNativeJdbcExtractor" 5. lazy-init="true"/> 6. <bean id="lobHandler" 7. class="org.springframework.jdbc.support.lob.OracleLobHandler" lazy-init="true"> 8. <property name="nativeJdbcExtractor"> 9. <ref local="nativeJdbcExtractor"/> 10. </property> 11. </bean> 12. … 13. </beans> |
首先,必须定义一个能够从连接池中抽取出本地数据库JDBC对象(如OracleConnection,OracleResultSet等)的抽取器:nativeJdbcExtractor,这样才可以执行一些特定数据库的操作。对于那些仅封装了Connection而未包括Statement的简单数据连接池,SimpleNativeJdbcExtractor是效率最高的抽取器实现类,但具体到apache的BasicDataSource连接池,它封装了所有JDBC的对象,这时就需要使用CommonsDbcpNativeJdbcExtractor了。Spring针对几个著名的Web服务器的数据源提供了相应的JDBC抽取器: ·WebLogic:WebLogicNativeJdbcExtractor ·WebSphere:WebSphereNativeJdbcExtractor ·JBoss:JBossNativeJdbcExtractor 在定义了JDBC抽取器后,再定义lobHandler。Spring 1.2.5提供了两个lobHandler: ·DefaultLobHandler:适用于大部分的数据库,如SqlServer,MySQL,对Oracle 10g也适用,但不适用于Oracle 9i(看来Oracle 9i确实是个怪胎,谁叫Oracle 公司自己都说Oracle 9i是一个过渡性的产品呢)。 ·OracleLobHandler:适用于Oracle 9i和Oracle 10g。 由于我们的数据库是Oracle9i,所以使用OracleLobHandler。 在配置完LobHandler后, 还需要将其注入到sessionFactory的Bean中,下面是调用后的sessionFactory Bean的配置: 代码 6 将lobHandler注入到sessionFactory中的配置 1. <beans> 2. … 3. <bean id="sessionFactory" 4. class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> 5. <property name="dataSource" ref="dataSource"/> 6. <!-- 为处理Blob类型字段的句柄声明 //--> 7. <property name="lobHandler" ref="lobHandler"/> 8. … 9. </bean> 10. … 11. </beans> |
如第7所示,通过sessionFactory的lobHandler属性进行注入。 业务层 1、业务层接口 "面向接口而非面向类编程"是Spring不遗余力所推荐的编程原则,这条原则也已经为大部开发者所接受;此外,JDK的动态代理只对接口有效,否则必须使用CGLIB生成目标类的子类。我们依从于Spring的倡导为业务类定义一个接口: 代码 7 业务层操作接口 1. public interface FileService 2. { 3. void save(FileActionForm fileForm);//将提交的上传文件保存到数据表中 4. List getAllFile();//得到T_FILE所示记录 5. void write(OutputStream os,String fileId);//将某个文件的文件数据写出到输出流中 6. String getFileName(String fileId);//获取文件名 7. } |
其中save(FileActionForm fileForm)方法,将封装在fileForm中的上传文件保存到数据库中,这里我们使用FileActionForm作为方法入参,FileActionForm是Web层的表单数据对象,它封装了提交表单的数据。将FileActionForm直接作为业务层的接口入参,相当于将Web层传播到业务层中去,即将业务层绑定在特定的Web层实现技术中,按照分层模型学院派的观点,这是一种反模块化的设计,但在"一般"的业务系统并无需提供多种UI界面,系统Web层将来切换到另一种实现技术的可能性也微乎其微,所以笔者觉得没有必要为了这个业务层完全独立于调用层的过高目标而去搞一个额外的隔离层,浪费了原材料不说,还将系统搞得过于复杂,相比于其它原则,"简单"始终是最大的一条原则。 getAllFile()负责获取T_FILE表所有记录,以便在网页上显示出来。 而getFileName(String fileId)和write(OutputStream os,String fileId)则用于下载某个特定的文件。具体的调用是将Web层将response.getOutputStream()传给write(OutputStream os,String fileId)接口,业务层直接将文件数据输出到这个响应流中。具体实现请参见错误!未找到引用源。节下载文件部分。 2、业务层接口实现类 FileService的实现类为FileServiceImpl,其中save(FileActionForm fileForm)的实现如下所示: 代码 8 业务接口实现类之save() 1. … 2. public class FileServiceImpl 3. implements FileService 4. { 5. private TfileDAO tfileDAO; 6. public void save(FileActionForm fileForm) 7. { 8. Tfile tfile = new Tfile(); 9. try 10. { 11. tfile.setFileContent(fileForm.getFileContent().getFileData()); 12. } 13. catch (FileNotFoundException ex) 14. { 15. throw new RuntimeException(ex); 16. } 17. catch (IOException ex) 18. { 19. throw new RuntimeException(ex); 20. } 21. tfile.setFileName(fileForm.getFileContent().getFileName()); 22. tfile.setRemark(fileForm.getRemark()); 23. tfileDAO.save(tfile); 24. } 25. … 26. } |
在save(FileActionForm fileForm)方法里,完成两个步骤: 其一,象在水桶间倒水一样,将FileActionForm对象中的数据倒入到Tfile对象中; 其二,调用TfileDAO保存数据。 需要特别注意的是代码的第11行,FileActionForm的fileContent属性为org.apache.struts.upload.FormFile类型,FormFile提供了一个方便的方法getFileData(),即可获取文件的二进制数据。通过解读FormFile接口实现类DiskFile的原码,我们可能知道FormFile本身并不缓存文件的数据,只有实际调用getFileData()时,才从磁盘文件输入流中获取数据。由于FormFile使用流读取方式获取数据,本身没有缓存文件的所有数据,所以对于上传超大体积的文件,也是没有问题的;但是,由于数据持久层的Tfile使用byte[]来缓存文件的数据,所以并不适合处理超大体积的文件(如100M),对于超大体积的文件,依然需要使用java.sql.Blob类型以常规流操作的方式来处理。 此外,通过FileForm的getFileName()方法就可以获得上传文件的文件名,如第21行代码所示。 write(OutputStream os,String fileId)方法的实现,如代码 9所示: 代码 9 业务接口实现类之write() 1. … 2. public class FileServiceImpl 3. implements FileService 4. { 5. 6. public void write(OutputStream os, String fileId) 7. { 8. Tfile tfile = tfileDAO.findByFildId(fileId); 9. try 10. { 11. os.write(tfile.getFileContent()); 12. os.flush(); 13. } 14. catch (IOException ex) 15. { 16. throw new RuntimeException(ex); 17. } 18. } 19. … 20. } |
write(OutputStream os,String fileId)也简单地分为两个操作步骤,首先,根据fileId加载表记录,然后将fileContent写入到输出流中。 3、Spring事务配置 下面,我们来看如何在Spring配置文件中为FileService配置声明性的事务 1. <beans> 2. … 3. <bean id="transactionManager" 4. class="org.springframework.orm.hibernate3.HibernateTransactionManager"> 5. <property name="sessionFactory" ref="sessionFactory"/> 6. </bean> 7. <!-- 事务处理的AOP配置 //--> 8. <bean id="txProxyTemplate" abstract="true" 9. class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> 10. <property name="transactionManager" ref="transactionManager"/> 11. <property name="transactionAttributes"> 12. <props> 13. <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop> 14. <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop> 15. <prop key="save">PROPAGATION_REQUIRED</prop> 16. <prop key="write">PROPAGATION_REQUIRED,readOnly</prop> 17. </props> 18. </property> 19. </bean> 20. <bean id="fileService" parent="txProxyTemplate"> 21. <property name="target"> 22. <bean class="sshfile.service.FileServiceImpl"> 23. <property name="tfileDAO" ref="tfileDAO"/> 24. </bean> 25. </property> 26. </bean> 27. </beans> |
Spring的事务配置包括两个部分: 其一,定义事务管理器transactionManager,使用HibernateTransactionManager实现事务管理; 其二,对各个业务接口进行定义,其实txProxyTemplate和fileService是父子节点的关系,本来可以将txProxyTemplate定义的内容合并到fileService中一起定义,由于我们的系统仅有一个业务接口需要定义,所以将其定义的一部分抽象到父节点txProxyTemplate中意义确实不大,但是对于真实的系统,往往拥有为数众多的业务接口需要定义,将这些业务接口定义内容的共同部分抽取到一个父节点中,然后在子节点中通过parent进行关联,就可以大大简化业务接口的配置了。 父节点txProxyTemplate注入了事务管理器,此外还定义了业务接口事务管理的方法(允许通过通配符的方式进行匹配声明,如前两个接口方法),有些接口方法仅对数据进行读操作,而另一些接口方法需要涉及到数据的更改。对于前者,可以通过readOnly标识出来,这样有利于操作性能的提高,需要注意的是由于父类节点定义的Bean仅是子节点配置信息的抽象,并不能具体实现化一个Bean对象,所以需要特别标注为abstract="true",如第8行所示。 fileService作为一个目标类被注入到事务代理器中,而fileService实现类所需要的tfileDAO实例,通过引用3.2节中定义的tfileDAO Bean注入。 Web层实现 1、Web层的构件和交互流程 Web层包括主要3个功能: ·上传文件。 ·列出所有已经上传的文件列表,以供点击下载。 ·下载文件。 Web层实现构件包括与2个JSP页面,1个ActionForm及一个Action: ·file-upload.jsp:上传文件的页面。 ·file-list.jsp:已经上传文件的列表页面。 ·FileActionForm:file-upload.jsp页面表单对应的ActionForm。 ·FileAction:继承org.apache.struts.actions.DispatchAction的Action,这样这个Action就可以通过一个URL参数区分中响应不同的请求。 Web层的这些构件的交互流程如图 6所示:  图 6 Web层Struts流程图 |
其中,在执行文件上传的请求时,FileAction在执行文件上传后,forward到loadAllFile出口中,loadAllFile加载数据库中所有已经上传的记录,然后forward到名为fileListPage的出口中,调用file-list.jsp页面显示已经上传的记录。 2、FileAction功能 Struts 1.0的Action有一个弱项:一个Action只能处理一种请求,Struts 1.1中引入了一个DispatchAction,允许通过URL参数指定调用Action中的某个方法,如http://yourwebsite/fileAction.do?method=upload即调用FileAction中的upload方法。通过这种方式,我们就可以将一些相关的请求集中到一个Action当中编写,而没有必要为某个请求操作编写一个Action类。但是参数名是要在struts-config.xml中配置的: 1. <struts-config> 2. <form-beans> 3. <form-bean name="fileActionForm" type="sshfile.web.FileActionForm" /> 4. </form-beans> 5. <action-mappings> 6. <action name="fileActionForm" parameter="method" path="/fileAction" 7. type="sshfile.web.FileAction"> 8. <forward name="fileListPage" path="/file-list.jsp" /> 9. <forward name="loadAllFile" path="/fileAction.do?method=listAllFile" /> 10. </action> 11. </action-mappings> 12. </struts-config> |
第6行的parameter="method"指定了承载方法名的参数,第9行中,我们还配置了一个调用FileAction不同方法的Action出口。 FileAction共有3个请求响应的方法,它们分别是: ·upload(…):处理上传文件的请求。 ·listAllFile(…):处理加载数据库表中所有记录的请求。 ·download(…):处理下载文件的请求。 下面我们分别对这3个请求处理方法进行讲解。 2.1 上传文件 上传文件的请求处理方法非常简单,简之言之,就是从Spring容器中获取业务层处理类FileService,调用其save(FileActionForm form)方法上传文件,如下所示: 1. public class FileAction 2. extends DispatchAction 3. { 4. //将上传文件保存到数据库中 5. public ActionForward upload(ActionMapping mapping, ActionForm form, 6. HttpServletRequest request, 7. HttpServletResponse response) 8. { 9. FileActionForm fileForm = (FileActionForm) form; 10. FileService fileService = getFileService(); 11. fileService.save(fileForm); 12. return mapping.findForward("loadAllFile"); 13. } 14. //从Spring容器中获取FileService对象 15. private FileService getFileService() 16. { 17. ApplicationContext appContext = WebApplicationContextUtils. 18. getWebApplicationContext(this.getServlet().getServletContext()); 19. return (FileService) appContext.getBean("fileService"); 20. } 21. … 22. } |
由于FileAction其它两个请求处理方法也需要从Spring容器中获取FileService实例,所以我们特别提供了一个getFileService()方法(第15~21行)。重构的一条原则就是:"发现代码中有重复的表达式,将其提取为一个变量;发现类中有重复的代码段,将其提取为一个方法;发现不同类中有相同的方法,将其提取为一个类"。在真实的系统中,往往拥有多个Action和多个Service类,这时一个比较好的设置思路是,提供一个获取所有Service实现对象的工具类,这样就可以将Spring 的Service配置信息屏蔽在一个类中,否则Service的配置名字散落在程序各处,维护性是很差的。 2.2 列出所有已经上传的文件 listAllFile方法调用Servie层方法加载T_FILE表中所有记录,并将其保存在Request域中,然后forward到列表页面中: 1. public class FileAction 2. extends DispatchAction 3. { 4. … 5. public ActionForward listAllFile(ActionMapping mapping, ActionForm form, 6. HttpServletRequest request, 7. HttpServletResponse response) 8. throws ModuleException 9. { 10. FileService fileService = getFileService(); 11. List fileList = fileService.getAllFile(); 12. request.setAttribute("fileList",fileList); 13. return mapping.findForward("fileListPage"); 14. } 15. } |
file-list.jsp页面使用Struts标签展示出保存在Request域中的记录: 1. <%@page contentType="text/html; charset=GBK"%> 2. <%@taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%> 3. <%@taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%> 4. <html> 5. <head> 6. <title>file-download</title> 7. </head> 8. <body bgcolor="#ffffff"> 9. <ol> 10. <logic:iterate id="item" name="fileList" scope="request"> 11. <li> 12. <a href='fileAction.do?method=download&fileId= 13. <bean:write name="item"property="fileId"/>'> 14. <bean:write name="item" property="fileName"/> 15. </a> 16. </li> 17. </logic:iterate> 18. </ol> 19. </body> 20. </html> |
展现页面的每条记录挂接着一个链接地址,形如:fileAction.do?method=download&fileId=xxx,method参数指定了这个请求由FileAction的download方法来响应,fileId指定了记录的主键。 由于在FileActionForm中,我们定义了fileId的属性,所以在download响应方法中,我们将可以从FileActionForm中取得fileId的值。这里涉及到一个处理多个请求Action所对应的ActionForm的设计问题,由于原来的Action只能对应一个请求,那么原来的ActionForm非常简单,它仅需要将这个请求的参数项作为其属性就可以了,但现在一个Action对应多个请求,每个请求所对应的参数项是不一样的,此时的ActionForm的属性就必须是多请求参数项的并集了。所以,除了文件上传请求所对应的fileContent和remark属性外还包括文件下载的fileId属性:  图 7 FileActionForm |
当然这样会造成属性的冗余,比如在文件上传的请求中,只会用到fileContent和remark属性,而在文件下载的请求时,只会使用到fileId属性。但这种冗余是会带来好处的--它使得一个Action可以处理多个请求。 2.3 下载文件 在列表页面中点击一个文件下载,其请求由FileAction的download方法来响应,download方法调用业务层的FileService方法,获取文件数据并写出到response的响应流中。通过合理设置HTTP响应头参数,将响应流在客户端表现为一个下载文件对话框,其代码如下所示: 代码 10 业务接口实现类之download 1. public class FileAction 2. extends DispatchAction 3. { 4. … 5. public ActionForward download(ActionMapping mapping, ActionForm form, 6. HttpServletRequest request, 7. HttpServletResponse response) 8. throws ModuleException 9. { 10. FileActionForm fileForm = (FileActionForm) form; 11. FileService fileService = getFileService(); 12. String fileName = fileService.getFileName(fileForm.getFileId()); 13. try 14. { 15. response.setContentType("application/x-msdownload"); 16. response.setHeader("Content-Disposition", 17. "attachment;" + " filename="+ 18. new String(fileName.getBytes(), "ISO-8859-1")); 19. fileService.write(response.getOutputStream(), fileForm.getFileId()); 20. } 21. catch (Exception e) 22. { 23. throw new ModuleException(e.getMessage()); 24. } 25. return null; 26. } 27. } |
第15~18行,设置HTTP响应头,将响应类型设置为application/x-msdownload MIME类型,则响应流在IE中将弹出一个文件下载的对话框,如图 4所示。IE所支持的MIME类型多达26种,您可以通过这个网址查看其他的MIME类型: http://msdn.microsoft.com/workshop/networking/moniker/overview/appendix_a.asp。 如果下载文件的文件名含有中文字符,如果不对其进行硬编码,如第18行所示,客户文件下载对话框中出现的文件名将会发生乱码。 第19行代码获得response的输出流,作为FileServie write(OutputStream os,String fileId)的入参,这样文件的内容将写到response的输出流中。 3、web.xml文件的配置 Spring容器在何时启动呢?我可以在Web容器初始化来执行启动Spring容器的操作,Spring提供了两种方式启动的方法: ·通过org.springframework.web.context .ContextLoaderListener容器监听器,在Web容器初始化时触发初始化Spring容器,在web.xml中通过<listener></listener>对其进行配置。 ·通过Servlet org.springframework.web.context.ContextLoaderServlet,将其配置为自动启动的Servlet,在Web容器初始化时,通过这个Servlet启动Spring容器。 在初始化Spring容器之前,必须先初始化log4J的引擎,Spring也提供了容器监听器和自动启动Servlet两种方式对log4J引擎进行初始化: ·org.springframework.web.util .Log4jConfigListener ·org.springframework.web.util.Log4jConfigServlet 下面我们来说明如何配置web.xml启动Spring容器: 代码 11 web.xml中对应Spring的配置内容 1. <web-app> 2. <context-param> 3. <param-name>contextConfigLocation</param-name> 4. <param-value>/WEB-INF/applicationContext.xml</param-value> 5. </context-param> 6. <context-param> 7. <param-name>log4jConfigLocation</param-name> 8. <param-value>/WEB-INF/log4j.properties</param-value> 9. </context-param> 10. <servlet> 11. <servlet-name>log4jInitServlet</servlet-name> 12. <servlet-class>org.springframework.web.util.Log4jConfigServlet</servlet-class> 13. <load-on-startup>1</load-on-startup> 14. </servlet> 15. <servlet> 16. <servlet-name>springInitServlet</servlet-name> 17. <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class> 18. <load-on-startup>2</load-on-startup> 19. </servlet> 20. … 21. </web-app> |
启动Spring容器时,需要得到两个信息:Spring配置文件的地址和Log4J属性文件,这两上信息分别通过contextConfigLocationWeb和log4jConfigLocation容器参数指定,如果有多个Spring配置文件,则用逗号隔开,如: /WEB-INF/applicationContext_1.xml, /WEB-INF/applicationContext_1.xm2 由于在启动ContextLoaderServlet之前,必须事先初始化Log4J的引擎,所以Log4jConfigServlet必须在ContextLoaderServlet之前启动,这通过<load-on-startup>来指定它们启动的先后顺序。 乱码是开发Web应用程序一个比较老套又常见问题,由于不同Web应用服务器的默认编码是不一样的,为了方便Web应用在不同的Web应用服务器上移植,最好的做法是Web程序自身来处理编码转换的工作。经典的作法是在web.xml中配置一个编码转换过滤器,Spring就提供了一个编码过滤器类CharacterEncodingFilter,下面,我们为应用配置上这个过滤器: 1. <web-app> 2. … 3. <filter> 4. <filter-name>encodingFilter</filter-name> 5. <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> 6. <init-param> 7. <param-name>encoding</param-name> 8. <param-value>GBK</param-value> 9. </init-param> 10. </filter> 11. <filter-mapping> 12. <filter-name>encodingFilter</filter-name> 13. <url-pattern>/*</url-pattern> 14. </filter-mapping> 15. … 16. </web-app> |
Spring的过滤器类是org.springframework.web.filter.CharacterEncodingFilter,通过encoding参数指定编码转换类型为GBK,<filter-mapping>的配置使该过滤器截获所有的请示。 Struts的框架也需要在web.xml中配置,想必读者朋友对Struts的配置都很熟悉,故在此不再提及,请参见本文所提供的源码。 总结 本文通过一个文件上传下载的Web应用,讲解了如何构建基于SSH的Web应用,通过Struts和FormFile,Spring的LobHandler以及Spring为HibernateBlob处理所提供的用户类BlobByteArrayType ,实现上传和下载文件的功能仅需要廖廖数行的代码即告完成。读者只需对程序作稍许的调整,即可处理Clob字段: ·领域对象对应Clob字段的属性声明为String类型; ·映射文件对应Clob字段的属性声明为org.springframework.orm.hibernate3.support.ClobStringType类型。 本文通过SSH对文件上传下载简捷完美的实现得以管中窥豹了解SSH强强联合构建Web应用的强大优势。在行文中,还穿插了一些分层的设计经验,配置技巧和Spring所提供的方便类,相信这些知识对您的开发都有所裨益。
|
|
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
---|
30 | 31 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|
常用链接
留言簿(5)
随笔分类
随笔档案
好的blog
好的站点
搜索
最新评论

阅读排行榜
评论排行榜
|
|