今天有一個朋友問了我一個問題,他使用的是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.tkk7.com/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所提供的方便類,相信這些知識對您的開發都有所裨益。
|
|
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
---|
27 | 28 | 29 | 30 | 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 | 31 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|
常用鏈接
留言簿(5)
隨筆分類
隨筆檔案
好的blog
好的站點
搜索
最新評論

閱讀排行榜
評論排行榜
|
|