返回目录列表
文件上传组件
图片文件上传与管理是论坛一大丰富功能,图片上传后持久保存在数据库还是本地文件系统中是一个比较费思量的问题,下面比较一下两者优点和缺点:
保存在数据库的优点:
部署方便,直接通过war包部署。
可伸缩性强,当访问量增加时,可以通过引入集群机制提高处理能力。
缺点主要是:
增加数据库负载。
使用缓存会耗费内存。
需要通过特殊处理激活客户端浏览器对图片的缓存。
保存在文件系统中优点:
单机性能快,自动激活客户端浏览器对图片的缓存。
缺点:
无法升级到集群环境。
图片或文件管理麻烦你,由于图片或文件保存在文件系统中,图片或文件的性质不一样,管理起来非常麻烦,需要靠文件目录分级来表明不同用处的图片或文件,例如200060402/user/*.jpg表示某段日期某个用户的图片;而/200060402/123535/*.jpg可能表示某段日期某个帖子编号下的图片。
根据以上优缺点,我们采取一下设计方案:
单独开发一个Web项目用来管理图片或文件系统的上传与管理。
专门的图片文件系统是通过数据库形式持久化的。
在本论坛中通过URL网址引用来显示图片。
目标:上传文件必须开发成一个可以重用在各类项目的通用复用组件,这样,才可以提高大大开发效率,也不枉费我们组件的专门开发。
组件子模块:
1. 上传文件的管理:上传管理其实也是一个围绕Model的CRUD管理过程,上传一个新图片可以看成是Create;修改一个上传后的图片是Update;删除上传后的图片是Delete。
2. 上传文件的显示,如果是图片,进行图片显示;如果是flash,实现Flash显示,本版本实现图片显示,必须为以后增加新的显示留有余地;新的显示拓展方式:第一步:上传时辨识,然后用Javascript将特殊标签写入;第二步:编辑一个新的过滤器,可通过后台管理上传该过滤器,实现特殊标签过滤为相应特定的html写法。
下面我们首先从建立Model开始,然后围绕Model实现CRUD。
Model设计
我们需要建立一个文件上传Model:com.jdon.jivejdon.model.UploadFile主要有以下内容:
public class UploadFile extends Model {
private String id;
private String name; //上传文件名
private String description;
//上传文件的相关描述
private FormFile theFile; //上传文件
private byte[] data; //上传文件体
private String contentType;
//类型
private int size; //大小
private String parentId ; //附件的主对象Id
private String tempId;
……
}
注意到UploadFile中有两个上传文件数据:FormFile theFile和byte[] data,刚开始设计UploadFile并没有能够想到设计FormFile theFile字段,只有byte[] data字段,当文件上传时,主体模型对象如Message还没有建立,所以,我们采取使用Session保存上传的文件,由于上传文件可能比较大,如果使用byte[] data字段,实际就是将整个上传文件数据字节放于Session中,非常浪费内存;而在上传时,使用FormFile theFile字段,则theFile只是一个虚拟对象,是Struts内部对象,它指向硬盘上临时目录的上传文件,只有通过getFileData方法才会将硬盘上文件读取到内存中,因此,我们将theFile放在Session中可以节省内存,当最后持久层保存数据库时,我们再通过getFileData方法读取文件真正字节,然后保存到数据库中。
而byte[] data字段也有存在必要,当上传的文件从数据库中读取时,文件字节需要保存再byte[]
data字段字段中,以便客户端显示。
上述Model反复修改也是随着后面步骤展开,然后逐渐丰富才进行调整,基于模型开发的特点非常类似画画,在一张白纸上,首先要进行第一遍基本简单布局绘画,然后各个部分布局再细节深入,这个过程是反复迭代,可以对前一个步骤进行调整和丰富。
Service实现
上传文件管理是模型Upload的新增删除查询实现,这里没有列出修改,因为修改上传文件,实际就是替换,替换可以通过删除后再新增,上传新的文件实际就是新增功能,上传后,我们会通过界面显示当前上传文件列表,用户可以从列表中删除一些已经上传的文件。
我们首先讨论上传文件的新增实现:
由于上传文件动作一般是在主体数据库提交之前完成的,例如上传文件和发帖提交总是错开的,因此,我们首先考虑将上传的文件暂存在什么地方?
如果将暂存地放在数据库,带来的是业务逻辑不统一,因为帖子内容还没有提交保存,那么帖子的自动主键ID还没有产生,所以UpLoadFile的parentId所属关联值还没有,这是一个方面;另外一个方面,将大的数据文件保存在数据库后,用户可能修改和删除,这种频繁数据库操作比较耗费资源;从真正业务逻辑来说,上传文件保存地是一个暂存地。而不是最终保存地(数据库),因此我们不能使用最终保存地概念替代暂存地概念,必须遵循实际要求设置对应虚拟位置,否则,需求变化后,软件处理会变得异常复杂。
暂存地是使用全局性Cache还是Session呢?
上传文件是某个用户自己一直在操作,因此没有必要使用全局性Cache,使用Session可以实现与用户相关的暂存地。
使用Session作为暂存地,必须注意保存的模型数据不能过大,否则会撑满内存,所以,在这里是将模型UploadFile对象保存Session中,而UploadFile模型我们需要考虑不要将上传文件全部字节读取到UploadFile对象中,在UploadFile模型设计中,我们使用了FormFile作为硬盘上上传的文件引用。
这样,我们业务功能有两个位置操作,第一个是将UploadFile模型放在Session中,冰球可以查询,删除;第二个是将UploadFile模型持久化到数据表中,也有新增、查询和删除,所以这两个操作实现方法都一致,但是两者实现,而且前者动作和后者动作有关系,我们可以考虑使用Proxy模式来实现。
定义UploadFile模型业务操作接口UploadService如下:
public interface
UploadService {
UploadFile
getUploadFile(String objectId);
void
saveUploadFile(EventModel em);
void
saveAllUploadFiles(String messageId, SessionContext sessionContext);
void
removeUploadFile(EventModel em);
Collection
getUploadFiles(Long messageId);
}
上面接口中方法除saveAllUploadFiles方法比较特殊以外,其他都是查询、新增保存、删除和多个查询方法,比较容易理解。
com.jdon.jivejdon.service.imp.UploadServiceShell是上述接口的一个实现,主要是操作Session;而com.jdon.jivejdon.service.imp.UploadServiceImp则是接口第二个实现,操作数据库方面。
我们看看UploadServiceShell的新增保存设计,基本可以了解上传文件是如何放入暂存地的:
public void saveUploadFile(EventModel
em) {
UploadFile uploadFile = (UploadFile)em.getModel();
logger.debug("saveUploadFile to session" );
List
uploads =
(List)sessionContext.getArrtibute(UPLOAD_NAME);
if
(uploads == null){
uploads = new ArrayList();
uploads.add(uploadFile);
sessionContext.setArrtibute(UPLOAD_NAME, uploads);
}else if(uploads.size() < 3){//we can modify by this user's grade
uploads.add(uploadFile);
}else{
logger.warn("max count is two upload files" );
}
}
我们利用Jdon框架的Session机制保存上传文件,这个Session其实用的就是HttpSession,为了保存一次性上传的多个文件,这里使用了List集合,目前缺省限制最多上传3个,以后可加入用户等级设计后,根据等级通过扣分来限制。
现在我们谈谈接口UploadService中特殊方法saveAllUploadFiles,这个方法其实是供ForumMessageServiceShell调用的,当上传文件都保存在暂存地以后,用户将输入发言内容,从而激发ForumMessageService的帖子创建方法,在这些创建方法中,我们随便将暂存地中上传文件保存到持久层中。
saveAllUploadFiles有一个参数SessionContext,这是由于Jdon框架的机制决定的:在实际运行时,ForumMessageShell对象中的UploadService对象并不是表现层使用的UploadService,表现层使用的UploadService对象是一个已经注射了与用户相关Session信息的,也就是说,表现层调用的UploadService,其SessionContext字段已经被Jdon框架自动赋予HttpSession实现了,将SessionContext和HttpSession联系起来,这是通过UploadService的setSessionContext方法将HttpSession注射进入的;但是ForumMessageShell中调用的UploadService对象则不是,所以,如果需要得到保存在暂存地Session中所有上传文件,必须得到正确的SessionContext,saveAllUploadFiles的SessionContext参数是供调用者ForumMessageShell调用时,将ForumMessageShell自己当时的SessionContext赋予,因为ForumMessageShell的SessionContext也是表现层直接调用的,因此必然已经与HttpSession联系起来了。
在com.jdon.jivejdon.service.imp.ForumMessageShell的createTopicMessage方法中增加关于上传文件持久化的代码如下:
public void
createTopicMessage(EventModel em){
ForumMessage forumMessage = (ForumMessage)em.getModel();
try
{
Account
account = sessionContextUtil.getLoginAccount(sessionContext);
forumMessage.setAccount(account);
super.createTopicMessage(em);
//以下是这次开发增加的有关上传文件保存代码
uploadService.saveAllUploadFiles(forumMessage.getMessageId().toString(),
this.sessionContext);
}
catch (Exception e) {
logger.error(e);
em.setErrors(Constants.ERRORS);
}
}
CRUD实现
上传管理是一个围绕Model的CRUD管理过程,上节UploadFile和UploadFileForm已经建立完成,下面是围绕Model的CRUD的实现,主要有两步:表现层CRUD配置和业务层Service的CRUD配置和代码完成。
首先,表现层CRUD配置如下,专门建立struts-config-upload.xml:
<action
name="upLoadFileForm" path="/message/upload/uploadAction"
scope="request"
type="com.jdon.strutsutil.ModelViewAction"
validate="false">
<forward
name="create" path="/message/upload/upload.jsp" />
<forward
name="edit" path="/message/upload/upload.jsp" />
</action>
<action
name="upLoadFileForm"
path="/message/upload/saveUploadAction" scope="request"
type="com.jdon.strutsutil.ModelSaveAction">
<forward
name="success" path="/message/upload/uploadOk.jsp" />
<forward
name="failure" path="/message/upload/uploadOk.jsp" />
</action>
然后在jdonframework.xml中建立业务层Service配置如下:
<model
class="com.jdon.strutsutil.file.UploadFile" key="id">
<actionForm
name="upLoadFileForm" />
<handler>
<service ref="uploadService">
<getMethod name="getUploadFile"/>
<createMethod
name="saveUploadFile"/>
<deleteMethod
name="removeUploadFile"/>
</service>
</handler>
</model>
界面表单设计
按照基于Jdon框架的通用开发开发思路,下一步是建立边界视图对象UploadFileForm。在UploadFileForm中有FormFile字段,这是struts规定的字段,该字段保存中Jsp表单中提交的上传文件。
然后,我们编制UploadFileForm对应的Jsp页面upload.jsp,在upload.jsp中有两个特点:
1.
表单的提交类型是enctype="multipart/form-data"。
2.
表单中上传文件使用html:file字段。
大小限制
结合Struts的上传大小功能限制,我们可以在UploadFileForm对上传文件的长度大小进行限制,大小值是在struts-config-upload.xml(我们建立专门的上传配置,利于修改和维护)中配置。首先,在UploadFileForm中doValidate方法增加以下代码(适合所有上传):
public void
doValidate(ActionMapping mapping, HttpServletRequest request, List errors) {
//has the maximum length been
exceeded?
Boolean maxLengthExceeded = (Boolean) request.getAttribute(
MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED);
if
((maxLengthExceeded != null) && (maxLengthExceeded.booleanValue())) {
errors.add(….. );
}
……
}
类型校验
上传文件只能是指定的几种类型,而且这种类型能够方便地扩展,如通过表现层简单修改就能够拓展。
类型校验包括javascript前台和ActionForm后台校验。
在UpLoadFileForm中设计字段List fileTypes,用来存放所有被允许上传的文件类型,这个字段可在UpLoadFileForm初始化时初始。同时为了区分图片和普通文件,使用imagesTypes和othersTypes来区分。
在doValidate方法中增加如下代码即可进行验证:
//retrieve the file name
String fileName =
theFile.getFileName();
int extInt =
fileName.lastIndexOf(".");
String ext =
fileName.substring(extInt+1); //获得上传文件名的点后面的字符串。
if
(!fileTypes.contains(ext))
errors.add(new
ActionMessage("illegal file type! "));
在upload.jsp加入js的验证,编写一个函数如下:
function isAuth(field){
<logic:iterate
id="fileType" name="upLoadFileForm"
property="fileTypes" >
if
(field.toLowerCase().indexOf(".<bean:write
name="fileType"/>") > -1){
return true;
}
</logic:iterate>
return false;
}
该函数是在用户按“上传”按钮时进行调用检验。
输入过滤器
所谓融合,就是当文件上传后,需要把相关上传信息置入宿主帖子中,如果是图片,我们需要在帖子ForumMessage的body字段字符串后加入一串图片显示标识。
如果上传的是图片,那么用于显示语法标签就应该是:
[img] [/img]
如果是普通文件,用于显示的语法标签就应该是:
[url=xxx]xxx[/url]
这样,通过过滤器转换为Html的链接语法,从而在显示时,能够通过链接下载显示。
另外,由于可以上传多个文件,可能各种类型混杂,应该根据不同类型输出的语法不一样,而且这个语法是可以方便修改和拓展,例如增加显示Flash新的标签。
这里涉及两种过滤器:
1.
当文件上传后,在用户发表的新帖子中加入图片或文件等显示标签。称为输入过滤器。这将是本节讨论的。
2.
当帖子显示时,需要将帖子中的图片和文件标签转换为html标签,这称为输出过滤器。这已经在前面“过滤显示”章节讨论过。
输入过滤器原理也参考“过滤显示”的过滤器原理,在ForumMessageShell的创建帖子之前,我们将上传文件标签都打入帖子内容中,代码如下:
Collection uploadFiles =
uploadService.saveAllUploadFiles(
forumMessage.getMessageId().toString(),
this.sessionContext);
forumMessage.setUploadFiles(uploadFiles);
//展开过滤器集合
InFilterManager inFilterManager =
new InFilterManager();
ForumMessageDecorator[] filters =
inFilterManager.getFilters();
ForumMessageDecorator
forumMessageDecorator = null;
for (int i = 0; i <
filters.length; i++) {
if (filters[i]
!= null) {
logger.debug("enter inFilter: " + filters[i].getClass().getName());
forumMessageDecorator = filters[i].clone(forumMessage);
//激活过滤器。
forumMessage.setBody(forumMessageDecorator.getBody());
}
}
2008年8月22日星期五重构
将[img]写入message body中有一个很大问题就是:图片信息和message强烈耦合在一起,我们不但处理模型上message和upload之间关联关系,还要处理具体[img]耦合,增加复杂性。
重构后,将输入过滤器改为输出过滤器,但查询输出时,如果message和uploads之间存在关联关系,那么在输出filterBody中直接写入<img src=””>标识。
同时,输出过滤器和其他html等管理器一样,可以通过admin在后台对Bean进行定义,比如定义显示图片的jsp页面,缺省是../imageShow.jsp,这是相对路径。以后可以通过指定其他服务器网址,分担负载。
用户头像上传
未完成。
需求:每个用户可以上传一张自己的照片或者其他Photo。
分析这个需求有几个特殊点:
1.每个用户只需要一张图片,数量是一个;上传文件类型只是图片,因此,本功能实现无需前面那么复杂,对以后扩展性也没有那么高,因为用户头像两张以上可能性很小,除非专门的照片集。
2.实体关联设计:之前是ForumMessage和UploadFile之间1:N一对多关系,现在是一对一关系,也使用UploadFile来表达上传的头像。使用User对应的Property来保存头像信息。
Property的name是jiveIcon;Property的value是<img src=’#{url_path}#/imageShow.jsp?type=images/gif&id=XXX’
border='0' width=’64’ height=’64’>
其中id的值XXX就是UploadFiler持久化保存生成的id;type类型是images/gif类型。
这里有一个有一个问题:Property的value应该保存的是UploadFile的id,不应该直接是显示头像信息,显示信息“<img src=…”等应该是在输出显示时进行合成。
那么哪些地方上需要显示头像呢?目前在帖子显示作者时,都有可能需要显示头像,而显示图像需要一系列动作,因此可以使用struts的tiels在对应的jsp中插入对应的action,而在这个Action中,我们根据寻找User寻找出Proptery的name值为jiveIcon的Property(这里需要一个缓存),然后合成<img src=值显示。
更多Jdon框架专题讨论
JdonFramework作为一个免费开源软件开发平台,可以商用开发大多数数据库应用软件和管理软件: 电子商务软件 在线教育软件 税务软件 Web快速开发软件 财务软件 购物车软件 医院帐务软件 crm software medical software 人事薪资软件payroll software 在线购物软件 销售软件 项目管理软件 房产不动产管理软件 生产软件 PDM软件 制造业软件 仓库软件 采购软件 进销存软件 危险源监控软件 物流软件 超市软件 银行软件 保险软件 汽车软件 医疗软件 电子软件 自动化软件 服装软件 烟草软件 分销管理软件 供应商管理软件
|