关于Web开发的思考
理论是灰色的,唯生命之树常青。
---歌德
缺失的一环(The Lost Ring)
在上一篇《关于Web界面开发的思考》中,我曾经谈过,在基于PowerBuilder和Delphi等4GL的开发工具开发时,应用的开发效率是很高的,而发展到用J2EE来进行Web应用程序开发时,程序的开发效率就下降了很多。之后我仔细思考了一番,寻找其中的差异所在。终于发现了其中的根本差异所在。
在C/S的程序开发中,界面和数据是绑定在一起的;而在B/S的程序开发中,界面和数据是分离的。
下面举一个实际的例子来详细说明一下我的观点。下图是一个用PowerBuilder开发的应用程序界面。
请注意这个界面的中央部分,这里使用了一个PowerBuilder特有的Datawindow控件对象,通过这个控件对象,程序的界面可以直接连接到后台的数据表。在上面这个数据窗口中,他所对应的数据表结构如下图所示。
大家可以看到,用户界面上显示的人员号、问题编号、A、B、C、D、E这五个选择项,是直接和数据库表结构一一对应的。采用这种方式,在界面上对数据的修改,包括插入、更新、乃至删除,都可以被控件自动转换成所需要的SQL语句,提交到后台进行执行。
采用这种方式,大大简化了应用程序的开发;降低了编写SQL语句的要求,大多数SQL语句都可以自动生成,并提交执行。
接下来,我们再看看一个Web方式的界面,是如何来做的。这里没有实际的产品,是一个基于常见开发框架的假想页面。
界面虽然类似,但是从这个界面上,我们能否知道这个界面是如何和后面的数据表关联的呢?答案是:不能。因为按照MVC的标准模式,jsp页面已经属于了View层,负责给用户来提供一个展示界面,它只能利用其他模块提供给他的数据,而不能直接和数据库发生关联。
在一个采用Struts作为基础架构的Web应用中。基本结构如下,也许不同项目会各有增减,此处只讨论最常见的模式。
JSP页面 |
Struct Default Controller |
开发人员定义的Action |
数据表访问DAO |
数据库数据表 |
ActionForm |
用户请求 |
在这种模式中,页面仅仅作为一个视图来展现给用户,它所需要的数据,是由后台的Action负责进行提供,因此页面上并没有任何这些数据从何而来的信息。有的仅仅是展示所需要的信息。
在我看来,这里就构成了缺失的一环,而这一环的缺失,正是造成目前Web程序开发拖沓,冗长,效率低下,成本高昂的关键原因。
在PowerBuilder这种开发环境中,从某种意义上来讲,界面和后台的数据库是绑定在一起的,像Datawindow这种对象,它的内部实际上存储了对应数据表的表结构定义,查询检索条件,更新的主键条件,甚至也包括数据的校验规则等很多信息,因此当用户在前台修改数据以后,控件本身可以利用这些元数据,动态生成后台访问所需要的SQL指令,发送给后台进行执行,这样就以一种非常直观的方式把后台的数据表展示给用户,用户的操作可以直接反映到数据库内容的变化上,便于用户理解和操作,也便于程序开发维护。
用户 |
修改界面上的数据 |
控件生成数据访问的SQL语句 |
数据窗口对象存储相关信息
|
SQL发送给数据库,更新数据库 |
在上述过程中,正是由于在界面,也就是数据窗口对象中存储了界面元素和数据库元素之间的对应关系,因此才使得自动生成SQL命令成为可能。而在Web开发方式中,由于界面上只有显示用的数据,没有任何的额外信息来记录界面数据和数据表的对应关系,因此当界面数据改动以后。修改的流程如下:
用户 |
修改界面数据,并点击提交按钮 |
浏览器向后台Web服务器发送请求 |
Web服务器转发给 Struct控制器 |
Stucts控制器转发给对应Action处理 |
Action调用DAO类 |
DAO拼装成所需要的SQL语句,执行 |
现在我们来看看DAO类是如何实现的。下面是一段假想代码。
Public void insertTable(String bookid,String bookname,String bookmemo){ String strsql=”insert into T_BOOK(book_id,book_name,book_memo)” +” Values(‘”+bookid+”’,” +”’”+ bookname +”’,” +”’” + bookmemo +”’)”; Dao.executeUpdate(strsql); } |
在这段代码中,所需要的这三个参数又是从那里来的呢?是从前台界面传过来的,换句话说,这段代码的实质工作,就是把前台的数据,按照和数据库的对应关系,拼装成合适的SQL语句,再传给数据库执行而已。
下面是我对两种不同开发方式的数据信息的一个对比。
比较项 | PowerBuilder方式 | Structs 方式 |
前台界面包含信息 | 数据内容 数据和数据表的对应关系 数据项和数据列的对应关系 数据表的组成信息 数据字段的校验规则 | 数据展示内容 |
前台向后传送内容 | 向数据库发送SQL命令 | 向Web服务器发送Http请求,包括GET,POST方式 |
SQL生成地点 | 前台控件自动生成 | 后台DAO用代码生成 |
数据项和数据表字段的对应关系体现 | 控件的内部存储保存 | 根据采用的技术而异。如果直接采用JDBC编程,这种对应关系是在程序中体现的;如果采用OR产品,这个对应关系可能体现在一个xml的配置文件中。 |
这里顺便要批判一下Hibernate这个著名的O/R映射工具,按照上面的分析,Hibernate从实质上来讲,不过是把数据库的一张数据表的记录,映射成了一个指定的java类,但最令人恼火的是,这种映射关系还必须另外编写一个xml文件来进行存储。而维护这种大范围的java文件和xml文件,将不可避免的成为项目的一个又一个黑洞。以我看来,它只是用带来一系列问题的方法,试图解决原来的一些问题而已。
总结一下,Web开发变得如此复杂的原因,在于原来在C/S模式下同一个界面的信息内容,被人为割裂到各个部分去实现,而且原来很多自动生成的内容,现在都需要编程手工来实现,这样就造成了系统整体复杂性的增加,系统变得不够直观,难以理解,难以维护。而这一切,又带来了开发成本的增加,费用的提高和利润的缩减。
Web开发新思路
基于上面的讨论分析,我认为,web开发可以借鉴C/S的开发模式,通过在网页上增加一些和后台数据库相关的额外数据信息,达到在数据变化时,动态生成数据操作所需SQL的目的,这样前台和后台之间的交互,除了页面以外,就都是直接的SQL语句,这种开发思路是对C/S开发方式的借鉴和模仿。
用户 |
修改界面上的数据 |
前台生成数据访问的SQL语句 |
页面上存储相关信息
|
SQL发送给Web服务器,转发数据库,更新数据库 |
要达到这一目的,首先要明确的是,页面上除了显示内容以外,还需要存储那些信息,这些信息应当以何种方式提供,前台在生成SQL语句时,又该如何使用这些信息。
那么页面上除了通常的显示内容以外,还需要提供那些信息呢?这个问题可以转换成:为了能够从前台界面上自动生成后台访问的SQL命令,前台还需要那些信息呢?
在上一个版本的框架设计中,我的思路是将数据表的各种元数据存储到后台的数据表中去,在需要的时候动态读取到前台来进行使用和访问。但在本版本的设计中,我认为这种处理方式是不够好的。一来是系统所需要的元数据会越来越多,放到后台来不胜其烦;二来是这样的设计侵入了应用数据库设计的范围,可能会造成应用设计的混乱。为了避免这种后果,在这一版本设计中,所有的元数据,都直接存储在前台的javascript中。
前台功能需求分析
本章节将进入正题,尝试用比较正式的方式来描述对于一个开发框架的具体要求,进而分析要实现这些功能要求的技术实现难度和基本算法,存在问题等。
本文的需求分析很大部分是参考PowerBuilder的DataWindow,谨在此向PowerBuilder致敬。
数据检索(Retrieve)
Web页面层要摆脱对后台的依赖,首先要摆脱对数据提供的依赖。如果按照所谓标准的J2ee架构来设计系统,Web页面层永远只是系统开发中的二等公民,仰人鼻息而已。所以Web页面层必须提供自己的数据检索功能,能够从后台数据库中直接检索得到所需要的数据。
每个页面所需要的数据可能是不一致的,因此数据检索的条件应当保存在页面上。
数据检索可能是带有条件和参数的,例如数据库表中有1000条记录,但可能需要按照主键来检索出一条记录出来,或者检察特定的主键存在与否。
数据检索的操作流程如下:
从页面上获得数据检索用的SQL语句 |
如果带有参数的话,填充检索参数,得到新的SQL语句 |
发送SQL命令给Web服务器,得到查询结果 |
检索完毕 |
输入的数据条件是:查询用的SQL语句(可能带有参数),检索参数。
查询完成的结果存储在javascript定义的变量中。
数据填充(Filled)
在采用新的开发框架以后,前台界面除了显示的格式以外,其具体内容是通过“拉”的方式直接从后台得到的,而不是采用原来“推”的方式被动得到的。
Web页面 |
Web Server |
Web页面 |
Web Server |
在原来的页面模式中,Web服务器返回的页面信息和数据是直接混合在一起的,因此不需要一个填充的过程。
而在以Web页面为中心的新框架中,数据是页面直接发请求得到的,返回的数据中并不包含如何进行数据填充的信息,因此前台必须自己完成数据填充的工作。
数据填充的操作流程如下:
数据检索 |
按照数据填充条件(预定义),将返回数据和界面元素对应起来 |
如果是单表显示,那么填充一个现有页面 |
如果是多行显示,那么填充出多个新的行记录出来 |
填充完毕 |
输入的数据条件是:数据填充条件,保存了业务数据和界面元素之间的对应关系;
是单表填充,还是多行显示填充。
填充数据(通过数据检索来得到,存储在javascript变量中)
插入数据(Insert)
插入数据,是在数据库中单表插入一条新记录的操作。
插入数据的流程如下:
根据数据表元数据,生成插入的空白SQL语句;或者直接取得自定义的插入语句,其中含有占位符号 |
根据界面元素和数据表列的对应关系,从界面上逐一得到数据,填充到插入的SQL命令中去 |
提交SQL命令,执行插入操作 |
输入的数据条件是: 数据表元数据定义;
插入用的SQL语句预定义;
数据表字段和界面元素之间的对应关系;
空白字段的插入默认值定义;
删除数据(Delete)
删除数据,是指从数据库表中删除一条记录的操作。
删除数据的流程如下:
根据数据表元数据,得到删除的SQL语句;或者直接获得原始定义的删除用SQL语句 |
根据主键定义,以及界面元素和数据表列的对应关系,从界面上得到主键数据,填充到SQL语句中去。 |
提交SQL请求,以实际删除数据 |
输入的数据条件包括: 数据表元数据定义;
删除用SQL语句定义;
数据表主键定义;
界面元素和数据表字段的对应关系;
更新数据(Update)
更新数据,是指更新数据库中一条或者多条记录的操作。
更新数据的流程如下:
根据数据表元数据,得到更新的SQL语句;或者直接获得原始定义的更新用SQL语句 |
根据主键定义,以及界面元素和数据表列的对应关系,从界面上得到字段数据,填充到SQL语句中去。 |
提交SQL请求,以实际更新数据 |
输入的数据包括:
数据表元数据定义;
更新用SQL语句定义;
数据表主键定义;
界面元素和数据库字段的对应关系;
可更新字段定义;(因为并非所有字段可以在界面上直接修改的)
数据校验(Verify)
数据校验是应用程序开发时必须的功能,如果在数据录入时没有合适的校验,那么系统中将会存在很多垃圾数据,造成日后不可预知的程序错误。
数据校验暂时针对每个数据字段来设计进行。
在界面设计时,通过配置属性的方法来定义每个字段所需要的校验内容。
在前台界面上可以定义一个通用的数据校验器,可以根据给定的规则对界面数据进行校验。
数据校验的原始定义,针对的是数据表的列定义;而两者的对应关系,通过另外的数据进行对应关系的定义。
数据校验一般在更新数据库之前进行调用。、
数据校验功能由开发人员手工编程调用。
异常处理(Exception)
异常处理是系统定义的一个通用功能。当调用后台服务发生异常时,前台框架应当提供一个统一的错误显示功能,供用户进行错误定位分析和错误显示之用。
目前的异常处理流程如下:
后台操作发生异常情况,返回错误描述信息 |
前台的状态表示栏位显示出现错误信息 |
用户单击错误信息栏位,利用javascript弹出一个对话框,显示错误信息。 |
错误信息清空,不再显示出现错误。 |
WebDW架构设计
在基于上面功能需求分析的基础上,提出WebDW的架构设计。
WebDW,名字来源于PowerBuilder的DataWindow技术,webDW的含义,就是基于Web方式的DataWindow技术。
webDW在设计上,吸收了借鉴了PB中Datawindow的设计思想,但针对web开发的特点,进行了大量的简化,也许在未来的发展中会逐渐复杂化,但在目前的第一版本中,还是相当简单的。
下图是WebDW的技术架构图,描述了WebDW的详细组成部分。及各部分之间的交互关系。
webSQL |
Ajax |
界面渲染 |
Web Server |
浏览器 |
查询定义 |
数据表元数据定义 |
界面映射定义 |
数据校验规则定义 |
数据填充 |
数据检索 |
插入数据 |
删除数据 |
更新数据 |
数据校验 |
异常处理 |
扩展功能定义 |
扩展功能解释器 |
webSql 网页SQL支持
webSql是一个运行在Web Server上的服务器组件,可以用Servlet,jsp,php等多种方式实现,对外提供一个访问接口,可以通过这个接口向webSql发送SQL请求,webSql执行SQL命令后,将结果返回给调用者。
一个用java实现的Servlet代码参见附件。
入口定义:从request对象中取出operType和command参数,其中command中存储了要执行的SQL命令。
出口定义:各数据项之间用tab分割,各行之间用回车键分割。
前台通过Ajax技术来访问webSql,访问结束后返回值存储在一个固定的变量中。
扩展功能解释器(Very Important)
目前在后台,webSql仅提供SQL语句的支持,未来考虑对这部分功能进行扩展,可以提供更多的支持功能。在前台和后台之间,定义出一个命令集合类似的东西,通过发送请求过来,可以在后台执行相应的操作。譬如:服务器上的文件访问功能,Session访问功能等。
这部分功能还需要重新设计和仔细研究。
扩展功能解释器在Web Server上运行,与WebSQL类似,不同的是,它提供的不是数据服务,而是应用层次的服务。
入口定义:待定
出口定义:待定
浏览器是新的命令行!命令行能做的事情,其实浏览器一样可以做到,通过请求的转发,浏览器实际上具有了一样强大的功能。把浏览器解放出来!!! |
扩展功能解释器,就像webSql 一样,不一定是纯粹独立开发出来的一个产品,也可以是一个功能的代理和转发,可以把界面上的要求转发给后台对应的功能模块来执行。这里的扩展功能解释器,或者叫转发器,起到的是一个沟通和交流的作用,而未必一定要由自己来具体完成。
Ajax支持
Ajax支持是IE等主流浏览器支持的,不需要额外开发。
界面渲染
界面渲染指的是针对给定的HTML源文件,把他们变成一个图形界面,这个是浏览器的基本功能,不需要额外开发。
WebDW的技术框架部分,所有界面的显示,均采用标准的HTML来实现,WebDW本身并不提供任何显示层的增强和功能。
数据填充
数据填充是将从后台得到的数据,填充到界面元素上的过程。
根据界面上元素的不同,填充方法可能略有区别。
元素可能包括:文本框,标签,文本,选择框,下拉框,图片,多行文本框等。
根据元素类型的不同,填充方法可能略有区别。
填充的输入参数格式定义如下:
字段1=值1 (TAB键分割)字段2=值2(TAB键分割)
输入的数据参数,其格式也可以和webSql返回的数据格式完全一致,这样就不需要再进行额外的转换工作。
具体的实现细节待定。
数据校验
数据校验的设计目的,是为了简化在客户端的输入数据校验功能。在以往的程序中,数据校验一般都是通过JavaScript编程来一一实现的,这种实现方式的缺点是:程序代码工作量很大,修改起来也很难看懂,难以维护。增加数据校验这一模块以后,每个输入域的校验用一个指定规则的校验字符串来进行描述,一个输入域可以定义多个校验字符串,这样数据校验器一旦被调用,就可以对所有已经注册的输入域一一进行校验,校验成功以后返回成功信息,失败以后返回失败标志和错误提示。这样就把原来针对一个一个域的单独校验功能代码,转换成了统一编写的数据校验字符串。假设界面上原来有三个输入域需要进行输入校验。
校验第一个字段的JS代码 |
校验第二个字段的JS代码 |
校验第三个字段的JS代码 |
第一个字段的校验字符串 第二个字段的校验字符串 第三个字段的校验字符串 |
统一编写的数据校验功能模块 |
上面框图中,统一编写的数据校验功能模块是预先定义好的,也可以在以后进行扩充修改,这样每个界面上的数据校验,都不再需要编写一段代码,而只需要编写一个字符串即可以。在未来版本中,甚至可以把这一字符串存储到后台的数据库中,实现一处定义,处处可用的目的。
未来的系统可以提供对数据校验字符串的维护功能。
异常处理
系统提供统一的异常处理机制。因为这里指的异常,都是指后台访问操作发生的异常,当发生这些情况时,系统应当可以给用户适当的提示,当用户需要时显示详细的错误信息(后台提供)。
用户 |
服务器返回错误信息 包括:错误代码,错误级别,简单描述,详细描述,后续处理方式 |
前台界面提示用户服务发生错误 |
用户点击某个固定图标,显示详细错误信息 |
数据检索
数据检索功能通过调用WebSQL提供的接口来实现和数据库的交互。
数据检索的核心功能是一个Select语句生成器,根据相关条件来生成一个Select语句,并传递到后台进行执行。在这一过程中,一部分内容是事先定义好的,而一部分数据内容是根据界面元素的值来进行填充的。
简单来说,就是原来采用Struct架构时,DAO部分生成Select语句的过程,被迁移到前台来,用javascript重新进行实现,所不同的是,原来每个功能的Select语句都是独立写出来的,而现在则定义一个相关的Select生成器,或者直接把Select语句写出来就是了。
在目前版本的实现,先弱化这部分功能,直接定义出所需要的Select语句,把参数留出来供填充就是了。
例如,定义的Select语句可能是:
Select book_id,book_name,book_memo from t_book Where book_id= :book_id; |
这里的:book_id就是一个定义的检索参数,填入不同的值就可以检索出不同的数据内容出来。
当用户调用wbsql_retrieve(‘123’)时,数据检索功能可以得到对应的SQL语句
Select book_id,book_name,book_memo from t_book Where book_id= ‘123’; |
并用这个SQL语句来调用后台,检索结果出来。
未来可以考虑设计一个查询生成器,专门协助开发人员来编写检索用的Select方法,这一功能待实现。
插入数据
插入数据同样需要调用webSql来向后台数据库提交。
插入数据的功能,准确地来说是一个Insert语句的生成器。与数据检索的不同点在于,插入所需要的SQL语句是根据当前页面所给出的数据表元数据动态生成的,而不是在设计时指定的。
插入所需要的SQL语句一般是:
Insert into T_BOOK(book_id,book_name,book_memo) Value(:book_id,:book_name,:book_memo); |
其中数据表名称,数据列的列表,都属于数据表的元数据,在WebDW中都给以显示,定义。
所需要的其他数据,从页面上的元素中获取。
页面元素和数据表列的对应关系,在WebDW中给与定义,存储。
删除数据
删除数据,同样需要调用webSql来向后台数据库提交。
删除数据的功能,准确来说是一个Delete语句生成器。根据数据表的元数据定义,以及界面元素和列名称之间的对应关系,从界面上取出主键对应字段,生成Delete命令发送给后台,进行数据删除。
更新数据
更新数据,同样需要调用webSql来向后台数据库进行提交。
更新数据的功能,是一个Update语句生成器。根据数据表元数据定义,以及界面元素和列名称之间的对应关系,从界面上取出相应字段,生成Update命令发送给后台,进行数据更新。
更新数据时只针对一行数据进行更新,判断标准是按照数据库主键来进行定义。
更新数据时那些列可以被更新是webDW描述时可选的,选择以后只更新指定列,未指定列不会被更新。
数据表元数据定义格式
鉴于在上述的各种操作中,数据表的元数据都是要用到的基础数据,因此需要定义数据表的元数据格式出来,以达到标准化处理的目的。
下面是mysql用来创建一张表的sql命令。
create table T_BOOK( book_id integer not null, book_name char(10) not null, book_memo char(20) not null, book_others char(255) null, primary key(book_id) ); |
从这里进行分析,一张数据表的元数据包括如下信息:表名称,列名称,列数据类型,主键。或者说:
表元数据 = 表名称 + 列名称 + 列数据类型 + 主键。
整个webDW在界面显示时,是作为一个整体的DIV来显示的。因此这里定义的每个属性,都是代表这个DIV的内部属性定义,而不是整个页面范围内的属性定义。也不是采用JavaScript标准变量的方式来表示的。
定义数据表元数据格式如下:
表名称,指的是webDW要更新处理的表名称,命名为updateTableName,存储一个字符串。
列列表,指的是数据表的所有字段列表,各个字段之间用逗号分割,最后一个列后面也加一个逗号,命名为columnList.存储一个字符串。
列类型列表,指的是数据表的所有字段数据类型列表,各个类型之间用分号分割,这主要是考虑到各个数据类型内部可能用到了逗号表示,为了编程方便而设计的。
主键列表,指的是数据表定义的主键,多个主键列之间用逗号分割,名为primarykey.数值以逗号结束。
象上面提到的那个数据表定义,它的数据表元数据定义就是:
<div id=’dw_table’ updateTableName=’T_BOOK’ columnlist=’book_id,book_name, book_memo,’ columntypelist=’char(10),char(20),char(255),’ primarykey=’book_id,’> </div> |
通过这个Div的元数据定义,前面的插入,删除,更新都可以根据上述的元数据来动态生成SQL命令。
未来可以提供自动生成所需元数据的功能,将元数据生成当成系统的一个附加功能,给出数据表名称,自动生成所需要的元数据,用户只需要粘贴拷贝就可以完成程序代码的编写过程。
附件1 webSQL的Java Servlet实现
package com.liu;
import javax.servlet.http.*; import javax.servlet.*; import java.sql.*; import java.util.*; import javax.naming.*; import javax.sql.DataSource;
//import oracle.jdbc.pool.*;
/** * @author user * */ public class TableServlet extends HttpServlet{ private String s_ok = "OK"; private String s_error = "ERROR"; private String s_oper_query ="1"; private String s_oper_exec ="2";
public void init(ServletConfig config)throws ServletException{ System.out.println("____________TableServlet init...."); } /** * 提供后台调用服务的Servlet程序 */ public void doGet(HttpServletRequest request,HttpServletResponse response) throws javax.servlet.ServletException ,java.io.IOException{ request.setCharacterEncoding("GBK"); //设置为GBK编码方式,处理中文
System.out.println("enter service TableServlet.Welcome."); String operType= request.getParameter("opertype"); String command = request.getParameter("command"); String param = request.getParameter("param"); int col=0;
if( operType==null) operType=""; if( command ==null) command=""; if( param == null) param="";
response.setContentType("text/html;charset=GBK"); ServletOutputStream out = response.getOutputStream();
Connection conn =null; Statement stat = null; ResultSet rs = null;
try{ conn = getConnection(); conn.setAutoCommit(false); stat = conn.createStatement(); }catch(Exception e){ e.printStackTrace(); out.print("OK"); return; }
if(operType.equals("")){ out.print(s_ok); return; }
//查询请求 if(operType.equals(s_oper_query)){ try{ System.out.println("Sql = "+command); rs = stat.executeQuery(command); ResultSetMetaData meta = rs.getMetaData(); String sline=""; for(col=1;col<=meta.getColumnCount();col++){ if(col<meta.getColumnCount()){ sline += meta.getColumnName(col)+"/t"; }else{ sline += meta.getColumnName(col); } } out.println(sline); System.out.println("colname="+sline); while(rs.next()){ sline =""; for(col=1;col<=meta.getColumnCount();col++){ if(col<meta.getColumnCount()){ sline += rs.getString(col)+"/t"; }else{ sline += rs.getString(col); } } out.println(sline); } }catch(Exception e){ e.printStackTrace(); out.println(e.toString()); } }
//执行请求 if(operType.equals(s_oper_exec)){ try{ System.out.println("Sql = "+command); stat.execute(command); conn.commit(); }catch(Exception e){ e.printStackTrace(); out.println(e.toString()); } } //其他情况下,直接返回s_ok. out.print(s_ok);
//TODO:关闭数据库连接 try{ if(rs!=null) rs.close(); if(conn!=null) conn.close(); }catch(Exception e){ e.printStackTrace(); System.out.println("Database connection close failed."); } }
/** * 得到数据库连接 * @return * @throws Exception */ private Connection getConnection() throws Exception{ Context initCtx = new InitialContext(); Context envCtx = (Context) initCtx.lookup("java:comp/env"); DataSource ds = (DataSource)envCtx.lookup("jdbc/tableedit"); Connection conn = ds.getConnection(); return conn; } } |
-------------------------
由于无法贴图,带图全文请访问
http://liujunsong.javaeye.com/admin/blogs/261085