您的位置:首页 > 运维架构 > 网站架构

使用open source产品组装你的web应用架构

2006-02-12 10:02 369 查看
其实,就算用Java建造一个不是很烦琐的web应用,也不是件轻松的事情。在构架的一开始就有很多事情要考虑。从高处看,摆在开发者面前有很多问题:要考虑是怎样建立用户接口?在哪里处理业务逻辑?怎样持久化的数据。而这三层构架中,每一层都有他们要仔细考虑的。各个层该使用什么技术?怎样的设计能松散耦合还能灵活改变?怎样替换某个层而不影响整体构架?应用程序如何做各种级别的业务处理(比如事务处理)?
构架一个Web应用需要弄明白好多问题。幸运的是,已经有不少开发者已经遇到过这类问题,并且建立了处理这类问题的框架。一个好框架具备以下几点:减轻开发者处理复杂的问题的负担(“不重复发明轮子”);内部有良好的扩展;并且有一个支持它的强大的用户团体。好的构架一般有针对性的处理某一类问题,并且能将它做好(DoOneThingwell)。然而,你的程序中有几个层可能需要使用特定的框架,已经完成的UI(用户接口)并不代表你也可以把你的业务逻辑和持久逻辑偶合到你的UI部分。举个例子,你不该在一个Controller(控制器)里面写JDBC代码作为你的业务逻辑,这不是控制器应该提供的。一个UI控制器应该委派给其它给在UI范围之外的轻量级组件。好的框架应该能指导代码如何分布。更重要的是,框架能把开发者从编码中解放出来,使他们能专心于应用程序的逻辑(这对客户来说很重要)。
这篇文章将讨论怎样结合几种著名的框架来使得你的应用程序做到松弛耦合。
如何建立你的架构,并且怎样让你的各个应用层保持一致?如何整合框架以便让每个层在以一种松散偶合的方式彼此作用而不用管低层的技术细节?这对我们来说真是一种挑战。这里讨论一个整合框架的策略(使用3种受欢迎的开源框架):表示层我们用Struts;业务层我们用Spring;而持久层则用Hibernate。你也可以用其他FrameWork替换只要能得到同样的效果。见图1(框架组合示意图)

应用程序的分层

大部分的Web应用在职责上至少能被分成4层。这四层是:presentation(描述),persistence(持久),business(业务)和domainmodel(域模块)。每个层在处理程序上都应该有一项明确的责任,而不应该在功能上与其它层混合,并且每个层要与其它层分开的,但要给他们之间放一个通信接口。我们就从介绍各个层开始,讨论一下这些层应该提供什么,不应该提供什么。

表示层(ThePresentationLayer)

一般来讲,一个典型的Web应用的的末端应该是表示层。很多Java发者也理解Struts所提供的。象业务逻辑之类的被打包到org.apache.struts.Action.,因此,我们很赞成使用Struts这样的框架。
下面是Struts所负责的:
*管理用户的请求,做出相应的响应。
*提供一个Controller,委派调用业务逻辑和其它上层处理。
*处理异常,抛给StrutsAction
*为显示提供一个模型
*UI验证。
以下条款,不该在Struts显示层的编码中经常出现。它们与显示层无关的。
*直接的与数据库通信,例如JDBC调用。
*与你应用程序相关联的业务逻辑以及校验。
*事物管理。
在表示层引入这些代码,则会带来高偶合和麻烦的维护。

持久层(ThePersistenceLayer)

典型的Web应用的另一个末端是持久层。这里通常是程序最容易失控的地方。开发者总是低估构建他们自己的持久框架的挑战性。系统内部的持续层不但需要大量调试时间,而且还经常缺少功能使之变得难以控制,这是持久层的通病。还好有几个ORM开源框架很好的解决了这类问题。尤其是Hibernate。Hibernate为java提供了OR持久化机制和查询服务,它还给已经熟悉SQL和JDBCAPI的Java开发者一个学习桥梁,他们学习起来很方便。Hibernate的持久对象是基于POJO和Javacollections。此外,使用Hibernate并不妨碍你正在使用的IDE。
请看下面的条目,你在持久层编码中需要了解的。
*查询对象的相关信息的语句。Hibernate通过一个OO查询语言(HQL)或者正则表达的API来完成查询。HQL非常类似于SQL--只是把SQL里的table和columns用Object和它的fields代替。你需要学习一些新的HQL语言;不管怎样,他们容易理解而文档也做的很好。HQL是一种对象查询的自然语言,花很小的代价就能学习它。
*如何存储,更新,删除数据库记录。
*象Hibernate这类的高级ORM框架支持大部分主流数据库,并且他们支持Parent/child关系,事物处理,继承和多态。

业务层(TheBusinessLayer)

一个典型Web应用的中间部分是业务层或者服务层。从编码的视角来看,这层是最容易被忽视的一层。而我们却往往在UI层或持久层周围看到这些业务处理的代码,这其实是不正确的,因为它导致了程序代码的紧密偶合,这样一来,随着时间推移这些代码很难维护。幸好,针对这一问题有好几种Frameworks存在。最受欢迎的两个框架是Spring和PicoContainer。这些为也被称为microcontainers,他们能让你很好的把对象搭配起来。这两个框架都着手于‘依赖注射’(dependencyinjection)(还有我们知道的‘控制反转’InversionofControl=IoC)这样的简单概念。这篇文章将关注于Spring的注射(译注:通过一个给定参数的Setter方法来构造Bean,有所不同于Factory),Spring还提供了SetterInjection(type2),ConstructorInjection(type3)等方式供我们选择。Spring把程序中所涉及到包含业务逻辑和Dao的Objects——例如transactionmanagementhandler(事物管理控制)、ObjectFactoris(对象工厂)、serviceobjects(服务组件)——都通过XML来配置联系起来。
后面我们会举个例子来揭示一下Spring是怎样运用这些概念。
业务层所负责的如下:
*处理应用程序的业务逻辑和业务校验
*管理事物
*允许与其它层相互作用的接口
*管理业务层级别的对象的依赖。
*在显示层和持久层之间增加了一个灵活的机制,使得他们不直接的联系在一起。
*通过揭示从显示层到业务层之间的Context来得到businessservices。
*管理程序的执行(从业务层到持久层)。

域模块层(TheDomainModelLayer)

既然我们致力于的是一个不是很复杂的Web的应用,我们需要一个对象集合,让它在不同层之间移动的。域模块层由实际需求中的业务对象组成比如,OrderLineItem,Product等等。开发者在这层不用管那些DTOs,仅关注domainobject即可。例如,Hibernate允许你将数据库中的信息存放入对象(domainobjects),这样你可以在连接断开的情况下把这些数据显示到UI层。而那些对象也可以返回给持续层,从而在数据库里更新。而且,你不必把对象转化成DTOs(这可能似的它在不同层之间的在传输过程中丢失),这个模型使得Java开发者能很自然运用OO,而不需要附加的编码。

一个简单例子

既然我们已经从全局上理解这些组件。现在就让我们开始实践吧。我们还是用Struts,Spring和Hibernate。这三个框架已经被描述够多了,这里就不重复介绍了。这篇文章举例指导你如何使用这三个框架整合开发,并向你揭示一个请求是如何贯穿于各个层的。(从用户的加入一个Order到数据库,显示;进而更新、删除)。
从这里可以下载到程序程序原代码(download
既然每个层是互相作用的,我们就先来创建domainobjects。首先,我们要在这些Object中要确定那些是需要持久化的,哪些是提供给businesslogic,那些是显示接口的设计。下一步,我们将配置我们的持久层并且定义好Hibernate的ORmappings。然后定义好BusinessObjects。有了这些组成部分之后,我们将使用Spring把这些连接起来。最后,我们提供给Spring一个持久层,从这个持久层里我们可以知道它是如何与业务逻辑层(businessservicelayer)通信的,以及它是怎样处理其他层抛出的异常的.

域对象层(DomainObjectLayer)

这层是编码的着手点,我们的编码就从这层开始。例子中Order与OrderItem是一个One—To—Many的关系。下面就是DomainObjectLayer的两个对象:
com.meagle.bo.Order.java:包含了一个Order的概要信息
com.meagle.bo.OrderLineItem.java:包含了Order的详细信息
好好考虑怎你的package命名,这反应出了你是怎样分层的。例如domainobjects在程序中可能打包在
com.meagle.bo
内。更详细一点将打包在
com.meagle.bo
的子目录下面。businesslogic应该从
com.meagle.serice
开始打
包,而DAO对象应该位于
com.meagle.service.dao.hibernate
。反应
Forms
Actions
持久对象(
presentationclasses)应该分别放在
com.meagle.action
com.meagle.forms
包。准确的给包命名使得你的classes很好分割并且易于维护,并且在你添加新的classes时,能使得程序结构上保持上下一致。

持久层的配置(PersistenceLayerConfiguration)

建立Hibernate的持久层需要好几个步骤。第一步让我们把BO持久化。既然Hibernate是通过POJO工作的,因此Order和OrderLineItem对象需要给所有的fileds加上getter,setter方法。Hibernate通过XML文件来映射(OR)对象,以下两个xml文件分别映射了Order和OrderItem对象。(这里有个叫XDoclet工具可以自动生成你的XML影射文件)
Order.hbm.xml
OrderLineItem.hbm.xml
你可以在WebContent/WEB-INF/classes/com/meagle/bo目录下找到这些xml文件。Hibernate的SessionFactory是用来告诉程序应该与哪个数据库通信,该使用哪个连接池或使用了DataSource,应该加载哪些持久对象。而Session接口是用来完成Selecting,Saving,Delete和Updating这些操作。后面的我们将讲述SessionFactory和Session是怎样设置的。

业务层的配置(BusinessLayerConfiguration)

既然我们已经有了domainobjects,接下来我们就要businessserviceobjects了,用他们来执行程序的logic,调用持久层,得到UI层的requests,处理transactions,并且控制exceptions。为了将这些连接起来并且易于管理,我们将使用面向方面的SpringFramework。Spring提供了控制倒置(inversionofcontrol0==IoC)和注射依赖设置(setterdependencyinjection)这些方式(可供选择),用XML文件将对象连接起来。IoC是一个简单概念(它允许一个对象在上层接受其他对象的创建),用IoC这种方式让你的对象从创建中释放了出来,降低了偶合度。
这里是一个没有使用IoC的对象创建的例子,它有很高偶合度。

图2.没有使用IoC.A创建了B和C
而这里是一个使用IoC的例子,这种方式允许对象在高层可以创建并进入另外一个对象,所以这样可以直接被执行。

图3.对象使用了IoC。A包含了接受B,C的setter方法,这同样达到了由A创建B,C的目的。

建立我们的业务服务对象(BuildingOurBusinessServiceObjects)

BusinessObject中的Setter方法接受的是接口,这样我们可以很松散的定义对象实现,然后注入。在我们的案例中,我们将用一个businessserviceobject接收一个DAO,用它来控制domainobjects的持久化。由于在这个例子中使用了Hibernate,我们可以很方便的用其他持久框架实现同时通知Spring有新的DAO可以使用了。在面向接口的编程中,你会明白“注射依赖”模式是怎样松散耦合你的业务逻辑和持久机制的:)。
下面是一个接口businessserviceobject,DAO代码片段:
publicinterfaceIOrderService{
publicabstractOrdersaveNewOrder(Orderorder)
throwsOrderException,OrderMinimumAmountException;

publicabstractListfindOrderByUser(Stringuser)
throwsOrderException;

publicabstractOrderfindOrderById(intid)
throwsOrderException;

publicabstractvoidsetOrderDAO(IOrderDAOorderDAO);
}
注意到这段代码里有一个setOrderDao(),它就是一个DAOObject设置方法(注射器)。但这里并没有一个getOrderDao的方法,这不必要,因为你并不会在外部访问这个orderDao。这个DAOObjecte将被调用,和我们的persistencelayer通信。我们将用Spring把DAOObject和businessserviceobject搭配起来的。因为我们是面向接口编程的,所以并不需要将实现类紧密的耦合在一起。
接下去我们开始我们的DAO的实现类进行编码。既然Spring已经有对Hibernate的支持,那这个例子就直接继承
HibernateDaoSupport
类了,这个类很有用,我们可以参考
HibernateTemplate
(它主要是针对HibernateDaoSupport的一个用法,译注:具体可以查看Srping的API)。下面是这个DAO接口代码:
publicinterfaceIOrderDAO{

publicabstractOrderfindOrderById(finalintid);

publicabstractListfindOrdersPlaceByUser(finalStringplacedBy);

publicabstractOrdersaveOrder(finalOrderorder);

}

我们仍然要给我们持久层组装很多关联的对象,这里包含了
HibernateSessionFactory
TransactionManager
Spring提供了一个
HibernateTransactionManager
,他用线程捆绑了一个HibernateSession,用它来支持transactions(请查看
ThreadLocal
)。
下面是HibernateSessionFactory和HibernateTransactionManager的配置:
<beanid="mySessionFactory"
class="org.springframework.orm.hibernate.
LocalSessionFactoryBean">
<propertyname="mappingResources">
<list>
<value>
com/meagle/bo/Order.hbm.xml
</value>
<value>
com/meagle/bo/OrderLineItem.hbm.xml
</value>
</list>
</property>
<propertyname="hibernateProperties">
<props>
<propkey="hibernate.dialect">
net.sf.hibernate.dialect.MySQLDialect
</prop>
<propkey="hibernate.show_sql">
false
</prop>
<propkey="hibernate.proxool.xml">
C:/MyWebApps/.../WEB-INF/proxool.xml
</prop>
<propkey="hibernate.proxool.pool_alias">
spring
</prop>
</props>
</property>
</bean>

<!--TransactionmanagerforasingleHibernate
SessionFactory(alternativetoJTA)-->
<beanid="myTransactionManager"
class="org.springframework.orm.hibernate.
HibernateTransactionManager">
<propertyname="sessionFactory">
<reflocal="mySessionFactory"/>
</property>
</bean>

可以看出:每个对象都可以在Spring配置信息中用<bean>标签引用。在这里,mySessionFactory引用了HibernateSessionFactory,而myTransactionManager引用了HibernateTransactionManage。注意代码中myTransactionMangerBean有个sessionFactory属性。HibernateTransactionManager有个sessionFactorysetter和getter方法,这是用来在Spring启动的时候实现“依赖注入”(dependencyinjection)的。在sessionFactory属性里引用mySessionFactory。这两个对象在Spring容器初始化后就被组装了起来了。这样的搭配让你从单例(singletonobjects)和工厂(factories)中解放了出来,降低了代码的维护代价。mySessionFactory.的两个属性,分别是用来注入mappingResources和hibernatePropertes的。通常,如果你在Spring之外使用Hibernate,这样的设置应该放在hibernate.cfg.xml中的。不管怎样,Spring提供了一个便捷的方式-----在Spring内部配置中并入了Hibernate的配置。如果要得到更多的信息,可以查阅SpringAPI。
既然我们已经组装配置好了ServiceBeans,就需要把BusinessServiceObject和DAO也组装起来,并把这些对象配到一个事务管理器(transactionmanager)里。
在Spring中的配置信息:
<!--ORDERSERVICE-->
<beanid="orderService"
class="org.
springframework.
transaction.
interceptor.
TransactionProxyFactoryBean">
<propertyname="transactionManager">
<reflocal="myTransactionManager"/>
</property>
<propertyname="target">
<reflocal="orderTarget"/>
</property>
<propertyname="transactionAttributes">
<props>
<propkey="find*">
PROPAGATION_REQUIRED,readOnly,-OrderException
</prop>
<propkey="save*">
PROPAGATION_REQUIRED,-OrderException
</prop>
</props>
</property>
</bean>

<!--ORDERTARGETPRIMARYBUSINESSOBJECT:
Hibernateimplementation-->
<beanid="orderTarget"
class="com.
meagle.
service.
spring.
OrderServiceSpringImpl">
<propertyname="orderDAO">
<reflocal="orderDAO"/>
</property>
</bean>

<!--ORDERDAOOBJECT-->
<beanid="orderDAO"
class="com.
meagle.
service.
dao.
hibernate.
OrderHibernateDAO">
<propertyname="sessionFactory">
<reflocal="mySessionFactory"/>
</property>
</bean>
图4是我们对象搭建的一个提纲。从中可以看出,每个对象都联系着Spring,并且能通过Spring注入到其他对象。把它与Spring的配置文件比较,观察他们之间的关系

图4.Spring就是这样基于配置文件,将各个Bean搭建在一起。
这个例子使用一个TransactionProxyFactoryBean,它定义了一个setTransactionManager()。这对象很有用,他能很方便的处理你申明的事物还有ServiceObject。你可以通过transactionAttributes属性来定义怎样处理。想知道更多还是参考TransactionAttributeEditor吧。TransactionProxyFactoryBean还有个setter.这会被我们Businessserviceobject(orderTarget)引用,orderTarget定义了业务服务层,并且它还有个属性,由setOrderDAO()引用。这个属性Spring和Bean的还有一点要注意的:bean可以以用两种方式创造。这些都在单例模式(Sington)和原型模式(propotype)中定义了。默认的方式是singleton,这意味着共享的实例将被束缚。而原形模式是在Spring用到bean的时候允许新建实例的。当每个用户需要得到他们自己Bean的Copy时,你应该仅使用prototype模式。(更多的请参考设计模式中的单例模式和原形模式)

提供一个服务定位器(ProvidingaServiceLocator)

既然我们已经将我们的Serices和DAO搭配起来了。我们需要把我们的Service显示到其他层。这个通常是在Struts或者Swing这层里编码。一个简单方法就是用服务定位器返回给Springcontext。当然,可以通过直接调用Spring中的Bean来做。
下面是一个StrutsActin中的服务定位器的一个例子。
publicabstractclassBaseActionextendsAction{
privateIOrderServiceorderService;
publicvoidsetServlet(ActionServletactionServlet){
super.setServlet(actionServlet);
ServletContextservletContext=
actionServlet.getServletContext();

WebApplicationContextwac=
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);

this.orderService=(IOrderService)
wac.getBean("orderService");
}

protectedIOrderServicegetOrderService(){
returnorderService;
}
}

UI层配置(UILayerConfiguration)

这个例子里UI层使用了Strutsframework.这里我们要讲述一下在给程序分层的时候,哪些是和Struts部分的。我们就从一个Struts-config.xml文件中的Action的配置信息开始吧。
struts-config.xmlfile.
<actionpath="/SaveNewOrder"
type="com.meagle.action.SaveOrderAction"
name="OrderForm"
scope="request"
validate="true"
input="/NewOrder.jsp">
<display-name>SaveNewOrder</display-name>
<exceptionkey="error.order.save"
path="/NewOrder.jsp"
scope="request"
type="com.meagle.exception.OrderException"/>
<exceptionkey="error.order.not.enough.money"
path="/NewOrder.jsp"
scope="request"
type="com.
meagle.
exception.
OrderMinimumAmountException"/>
<forwardname="success"path="/ViewOrder.jsp"/>
<forwardname="failure"path="/NewOrder.jsp"/>
</action>
SaveNewOrder这个Action是用来持久化UI层里的表单提交过来Order的。这是Struts中一个很典型的Action;注意观察这个Action中exception配置,这些Exceptions也在Spring配置文件(applicationContext-hibernate.xml)中配置了(就在businessserviceobject的transactionAttributes属性里)。当异常在业务层被被抛出时,我们可以控制他们,并适当的显示给UI层。
第一个异常,OrderException,在持久层保存order对象失败的时候被触发。这将导致事物回滚并且通过BO把异常回传到Struts这一层。
第二个异常,OrderMinimumAmountException也同第一个一样。
搭配整和的最后一步通过是让你显示层和业务层相结合。这个已经被服务定位器(servicelocator)实现了(前面讨论过了),这里服务层作为一个接口提供给我们的业务逻辑和持久层。
SaveNewOrderAction在Struts中用一个服务定位器(servicelocator)来调用执行业务方法的。方法代码如下:
publicActionForwardexecute(
ActionMappingmapping,
ActionFormform,
javax.servlet.http.HttpServletRequestrequest,
javax.servlet.http.HttpServletResponseresponse)
throwsjava.lang.Exception{

OrderFormoForm=(OrderForm)form;

//UsetheformtobuildanOrderobjectthat
//canbesavedinthepersistencelayer.
//Seethefullsourcecodeinthesampleapp.

//Obtainthewiredbusinessserviceobject
//fromtheservicelocatorconfiguration
//inBaseAction.
//Delegatethesavetotheservicelayerand
//furtherupstreamtosavetheOrderobject.
getOrderService().saveNewOrder(order);

oForm.setOrder(order);

ActionMessagesmessages=newActionMessages();
messages.add(
ActionMessages.GLOBAL_MESSAGE,
newActionMessage(
"message.order.saved.successfully"));

saveMessages(request,messages);

returnmapping.findForward("success");
}

总结

这篇文章在技术和构架方面掩盖了很多低层的基础信息,文章的主要的意图在于让你意识到如何给你应用程序分层。分层可以“解耦”你的代码——允许新的组件被添加进来,而且让你的代码易于维护。这里用到的技术只是专注于把“解偶”做好。不管怎样,使用这样的构架可以让你用其他技术代替现在的层。例如,你可能不使用Hibernate实现持久化。既然你在DAO中面向接口的编程的,所以你完全可以用iBATIS来代替。或者,你也可能想用Struts外的其他的技术或者框架替换现在的UI层(转换久层,实现层并不应该直接影响到你的业务逻辑和业务服务层)。用适当的框架搭建你的Web应用,其实也不是一件烦琐的工作,更主要的是它“解耦”了你程序中的各个层。

后记:

看完这篇文章后,只是觉得很喜欢,于是就翻译了,当然同时也准备着挨大家扔来的鸡蛋:)。这篇文章里并没有太多的技术细节,和详细的步骤。如果你从未使用过这些框架而在运行实例程序遇上困难的话,可以到CSDN论坛JavaOpenSource版发贴,我一定会详细解答的(啊哦,这不算做广告吧?),文章是从一个构架的角度讲述了如何搭配现有的开源框架进行分层,有太多的术语我都不知道怎么表达,而且可能有很多语句存在错误。如果影响了你的阅读,请你直接点原文地址,我同时也向你说声抱歉。
作者简介:MarkEagle高级软件工程师,亚特兰大。
翻译:Totodo(zhangli@telecomjs.com)软件工程师
参考:
Struts:http://jakarta.apache.org/struts/index.html
Spring:http://www.springframework.org
Hibernate:http://www.hibernate.org
http://www.hibernate.org.cn
关于控制反转IOC和依赖注射:http://www.martinfowler.com/articles/injection.html

wiring的运行(使用opensource产品组装你的web应用架构)
前一阵在csdn上看到totodo翻译的使用opensource产品组装你的web应用架构,感觉不错,于是乎将wiring.zip下载下来,结果运行的比较不顺利,现将运行过程出现的磕磕绊绊以及解决办法简单介绍一下,希望同行们不要再走那么多弯路,才疏学浅,希望大家谅解。
因为我是基本按照readme.txt文件的步骤来的,所以一下的几个步骤也是和readme.txt的步骤对应,不过是对它的一个补充,因此原文就不再叙述。
STEP1:
除了提到的5个软件包外还要下载db2java,主要是为了ant用。
STEP5:
还要修改build.xml,根据本人的情况设置如下几个property.xdoclet.lib.home;hibernate.lib.home;mysql.lib.home;db2.lib.home
还要在WEB-INF下建立一个lib目录。
还要修改web.xml,将<param-value>WEB-INF/struts-config.xml</param-value>修改为<param-value>/WEB-INF/struts-config.xml</param-value>
STEP7:
在运行WebServer之前,先将一下一些jar文件拷贝到WEB-INF/lib下:
aopalliance.jar;asm.jar;cglib-2.0.jar;commons-beanutils.jar;commons-collections.jar;commons-digester.jar;commons-lang.jar;commons-logging.jar;
dom4j.jar;ehcache.jar;hibernate2.jar;jakarta-oro.jar;jstl.jar;jta.jar;odmg.jar;proxool-0.8.3.jar;commons-validator.jar;
spring.jar;spring-aop.jar;spring-context.jar;spring-core.jar;spring-dao.jar;spring-orm.jar;spring-web.jar;spring-webmvc.jar;standard.jar;
struts.jar
以上jar文件大部分可以在spring-framework下找到。

我在运行发布过程中,出现的错误有一下这些:
1."org.springframework.beans.factory.BeanDefinitionStoreException:Errorregisteringbeanwithname'orderService'definedinresource[/WEB-INF/applicationContext-hibernate.xml]ofServletContext:Classthatbeanclass[org.springframework.transaction.interceptor.TransactionProxyFactoryBean]dependsonnotfound;nestedexceptionisjava.lang.NoClassDefFoundError:org/aopalliance/aop/Advice",原因是aopalliance.jar没有加入到WEB-INF/lib下去。
2."java.lang.ClassNotFoundException:org.apache.taglibs.standard.tlv.JstlCoreTLV",原因是jstl.jar没有加入到WEB-INF/lib下。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: