基于J2EE的Blog平台

来源:互联网 发布:电信光猫iptv端口设置 编辑:程序博客网 时间:2024/04/26 10:31
基于J2EE的Blog平台  http://www.j2medev.com/Article/ShowArticle.asp?ArticleID=106    Blog(WebLog)在Internet上越来越流行。许多网友都有了自己的Blog,通过Blog展示自己,结识更过的网友。比较著名的Blog平台是基于ASP.net的开源项目.Text。但是它的逻辑全部以存储过程的形式放在数据库中。虽然存储过程能大大提高数据操作的效率,但是存储过程本身是结构化的程序,无法发挥面向对象的威力,也不便于实现代码复用。因此,我决定实现一个基于J2EE体系的多层结构的Blog平台,功能和界面和.Text非常类似,暂命名为Crystal Blog。实现的功能有:发表和编辑文章;多用户支持;全文检索;RSS支持;图片管理;SMTP邮件发送等常见功能。选择平台和框架 (目录)  由于使用J2EE平台,我们准备采用WebLogic Server 8.1作为运行平台,使用WebLogic Workshop8.1这个强大的集成化IDE作为开发工具。   数据库选择MS SQL Server 2000 SP3,建立一个名为blog的数据库存储所有的用户数据。由于我们并没有针对特定数据库编码,稍后我们会使用其他数据库测试。在系统设计之前,选择一个优秀的框架能大大提高开发效率。Spring是一个轻量级的J2EE框架。它覆盖了从后台数据库的JDBC封装到前台Web框架的几乎所有方?面。并且,Spring的各个模块耦合非常松散,我们既可以用它作为整个应用程序的框架,也可以仅仅使用它的某一个模块。此外,Spring非常强大的集成功能使我们可以轻易地集成Struts编写的Web端,或者使用Hibernate作为后端的O/R Mapping方案。  Spring的核心思想便是IoC和AOP,Spring本身是一个轻量级容器,和EJB容器不同,Spring的组件就是普通的Java Bean,这使得单元测试可以不再依赖容器,编写更加容易。Spring负责管理所有的Java Bean组件,同样支持声明式的事务管理。我们只需要编写好Java Bean组件,然后将它们“装配”起来就可以了,组件的初始化和管理均由Spring完成,只需在配置文件中声明即可。这种方式最大的优点是各组件的耦合极为松散,并且无需我们自己实现Singleton模式。   由于后台要使用关系数据库存储数据,使用O/R Mapping必不可少。iBatis是又一个类似于Hibernate的O/R Mapping方案,特点是小巧,配置简单,查询灵活,完全符合我们的要求。  除了Spring和iBatis,用到的第三方组件还有:用于全文搜索的Lucene引擎,用于文件上传的common-file-upload 1.0,用于输出RSS的RSSLibJ1.0 RC2。  由于使用Spring这个轻量级框架,就无需EJB服务器,只需要Web服务器即可。因此,系统可以运行在WebLogic Server,Tomcat和Resin等支持Servlet和JSP的Web服务器上。系统设计 (目录)  很显然,多层结构的J2EE架构能保证系统的灵活性和可扩展性。我们仍然采用表示层/逻辑层/持久层三层设计。  整个系统以Spring为基础,持久层采用DAO模式和iBatis O/R Mapping,封装所有数据库操作;中间层是由Spring管理的普通的JavaBean,采用Fa?ade模式;表示层使用Spring提供的MVC框架。由于Spring对其他框架的良好集成,我们采用Velocity作为View。由于Velocity不能调用Java代码,从而强制使用MVC模式而不是在View中嵌入逻辑代码。配置服务器 (目录)  在WebLogic中新建一个Configuration,命名为blog,添加一个数据源,命名为jdbc/blog:  整个应用程序的目录结构如下:crystalblog/+ doc/ (存放API文档)+ report/ (存放JUnit测试结果)+ src/ (存放java源程序)+ web/ (web目录)| + manage/ (存放blog管理页)| + skin/ (存放blog界面页)| + upload/ (存放用户上传的图片)| + WEB-INF/| + classes/ (存放编译的class文件)| + lib/ (存放用到的所有jar文件)| + search/ (存放Lucene的index)| + c.tld (使用jstl必须的文件)| + dispatcher-servlet.xml (Spring配置文件)| + web.xml (标准web配置文件)+ blog.war (打包的可部署应用)+ build.xml (ant脚本)编写Ant?脚本 (目录)  Ant是一个非常棒的执行批处理任务的工具。使用Ant能使编译、测试、打包、部署和生成文档等一系列任务全自动化,从而大大节省开发时间。  首先我们把用到的所有.jar文件放到/web/WEB-INF/lib中,然后编写compile任务,生成的class文件直接放到web/WEB-INF/classes目录下。如果编译成功,就进行单元测试,单元测试的结果以文本文件存放在report目录中。如果测试通过,下一步便是打包成blog.war文件。接着把应用部署到服务器上,直接将web目录的内容复制到%BEA_HOME%/user_projects/domains/blogdomain/applications/blog/目录下即可。如果要在Tomcat上部署,直接将整个web目录复制到%TOMCAT%/webapps/blog/下。   最后,如果需要,可以用javadoc生成api文档。系统设计 (目录)  Crystal Blog共分成三层结构:后台数据持久层,采用DAO模式;中间逻辑层,采用Facade模式;前端Web层,采用MVC结构,使用JSP作为视图。以下是Rational Rose的UML图:设计Domain对象 (目录)  设计Domain对象  Domain层是抽象出的实体。根据我们要实现的功能,设计以下实体,它们都是普通的Java Bean:   Account:封装一个用户,包括用户ID,用户名,口令,用户设置等等。   Category:封装一个分类,一共有3种Category,分别用来管理Article,Image和Link,一个Account对应多个Category。   Article:封装一篇文章,包括Title,Summary,Content等等,一个Category对应多个Article。   Feedback:封装一个回复,包括Title,Username,Url和Content,一个Article对应多个Feedback。   Image:封装一个图片,Image只包含图片信息(ImageId,Type),具体的图片是以用户上传到服务器的文件的形式存储的。一个Category对应多个Image。   Link:封装一个链接,和Category是多对一的关系。有Title,Url,Rss等属性。   Message:封装一个消息,使其他用户在不知道Email地址的情况下能够通过系统发送邮件给某个用户。  最后,为了唯一标识每条数据库记录,我们需要一个主键。在MS SQL Server和Oracle中可以使用自动递增的主键生成方式。但是很多数据库不支持自动递增的主键,考虑到移植性,我们自己定义一个Sequence表,用于生成递增的主键。Sequence表有且仅有7条记录,分别记录Account到Message对象的当前最大主键值。系统启动时,由SqlConfig负责初始化Sequence表。   SequenceDao负责提供下一个主键,为了提高效率,一次缓存10个主键。 配置iBatis (目录)  接下来,使用iBatis实现O/R Mapping。首先从http://www.ibatis.com下载iBatis 2.0,将所需的jar文件复制到web/WEB-INF/lib/目录下。iBatis使用XML配置数据库表到Java对象的映射,先编写一个sql-map-config.xml:   将sql-map-config.xml放到web/WEB-INF/classes/目录下,iBatis就能搜索到这个配置文件,然后编写一个初始化类:public class SqlConfig { private SqlConfig() {} private static final SqlMapClient sqlMap; static { try { java.io.Reader reader = Resources.getResourceAsReader ("sql-map-config.xml"); sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("Error initializing SqlConfig. Cause: " + e); } } public static SqlMapClient getSqlMapInstance () { return sqlMap; }}  SqlMapClient封装了访问数据库的大部分操作,可以直接使用SqlConfig.getSqlMapInstance()获得这个唯一实例。使用DAO模1 式 (目录)  为了分离逻辑层和数据库持久层,定义一系列DAO接口:AccountDao,CategoryDao,ArticleDao……其实现类对应为SqlMapAccountDao,SqlMapCategoryDao,SqlMapArticleDao……这样就使得逻辑层完全脱离了数据库访问代码。如果将来需要使用其它的O/R Mapping方案,直接实现新的DAO接口替代现有的SqlMapXxxDao即可。   以SqlMapAccountDao为例,实现一个login()方法是非常简单的:public int login(String username, String password) throws AuthorizationException { try { Map map = new HashMap(); map.put("username", username); map.put("password", password); Integer I = (Integer)sqlMap.queryForObject("login", map); if(I==null) throw new RuntimeException("Failed: Invalid username or password."); return I.intValue(); } catch(SQLException sqle) { throw new RuntimeException("Sql Exception: " + sqle); }}  在Account.xml配置文件中定义login查询:逻辑层设计 (目录)  由于DAO模式已经实现了所有的数据库操作,业务逻辑主要是检查输入,调用DAO接口,因此业务逻辑就是一个简单的Facade接口: public class FacadeImpl implements Facade { private AccountDao accountDao; private ArticleDao articleDao; private CategoryDao categoryDao; private FeedbackDao feedbackDao; private ImageDao imageDao; private LinkDao linkDao; private SequenceDao sequenceDao;}  对于普通的getArticle()等方法,Facade仅仅简单地调用对应的DAO接口:public Article getArticle(int articleId) throws QueryException { return articleDao.getArticle(articleId);}  对于需要身份验证的操作,如deleteArticle()方法,Facade需要首先验证用户身份:public void deleteArticle(Identity id, int articleId) throws DeleteException { Article article = getArticleInfo(articleId); if(article.getAccountId()!=id.getAccountId()) throw new AuthorizationException("Permission denied."); articleDao.deleteArticle(articleId);}  要分离用户验证逻辑,可以使用Proxy模式,或者使用Spring的AOP,利用MethodInterceptor实现,不过,由于逻辑很简单,完全可以直接写在一块,不必使用过于复杂的设计。   至此,我们的Blog已经实现了所有的后台业务逻辑,并且提供统一的Facade接口。前台Web层仅仅依赖这个Facade接口,这样,Web层和后台耦合非常松散,即使替换整个Web层也非常容易。Web层设计 (目录)使用MVC模式 (目录)  对于复杂的Web层,使用MVC模式是必不可少的。虽然Spring能轻易集成Struts,WebWorks等Web框架,但Spring本身就提供了一个非常好的Web框架,能完全实现MVC模式。   Spring使用一个DispatcherServlet,所有的特定请求都被转发到DispatcherServlet,然后由相应的Controller处理,Controller返回一个ModelAndView对象(因为Java语言的方法调用只能返回一个结果,而且不支持ref参数,所以将Model和View对象合在一起返回),Model是一个Java对象,通常是Map,View是视图的逻辑名字,通常是JSP文件名,但也可以使用Velocity等作为视图。返回的View通过viewResolver得到真正的文件名。  首先配置Spring的MVC,在web.xml中声明DispatcherServlet,处理所有以.c结尾的请求: dispatcher org.springframework.web.servlet.DispatcherServlet 1 dispatcher *.c   Spring会在WEB-INF下查找一个名为dispatcher-servlet.xml的文件,我们需要创建这个文件: 用到的所有的Java Bean组件都要在这个文件中声明和配置,以下是配置URL映射的Bean: articleController   凡是匹配/article.c的Request都会被名为articleController的Bean处理,同样需要声明这个articleController: ViewArticleController处理请求,然后生成Model,并选择一个View:public class ViewArticleController implements Controller { private Facade facade; public void setFacade(Facade facade) { this.facade = facade; }public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { // 获得参数: int articleId = Integer.parseInt(request.getParameter("articleId")); // 使用facade处理请求: Article article = facade.getArticle(articleId); // 生成Model: Map map = new HashMap(); map.put("article", article); // 返回Model和视图名“skin/blueskysimple/article”: return new ModelAndView("skin/blueskysimple/article", map); }}  最后,skin/bluesky/article视图会将结果显示给用户。   我们注意到,ViewArticleController并不自己查找或者创建Facade,而是由容器通过setFacade(Facade)方法设置的,这就是所谓的IoC(Inversion of Control)或者Dependency Injection。容器通过配置文件完成所有组件的初始化工作:   以上配置文件实现的功能大致为:Facade facade = new Facade();ViewArticleController articleController = new ViewArticleController();articleController.setFacade(facade);  但是我们不必编写以上代码,只需在xml文件中装配好我们的组件就可以了。所有组件由Spring管理,并且,缺省的创建模式是Singleton,确保了一个组件只有一个实例。   此外,所有自定义异常都是RuntimeException,Spring提供的AOP使我们能非常方便地实现异常处理。在Web层定义ExceptionHandler,处理所有异常并以统一的error页面把出错信息显示给用户,因此,在代码中只需抛出异常,完全不必在Controller中处理异常:使用Velocity作为View  采用Velocity作为View的最大的优点是简洁,能通过简单明了的语法直接在Html中输出Java变量。Velocity本身是一个模板引擎,通过它来渲染Model输出Html非常简单,比如显示用户名: ${account.username}  换成JSP不得不写成:<%=account.getUsername()%>而<%...%>标记在Dreamwaver中无法直接看到其含义。如果需要在标签中嵌入JSP就更晦涩了:">Link  这种嵌套的标签往往使得可视化Html编辑器难以正常显示,而Velocity用直接嵌入的语法解决了“ ”的问题。   在Spring中集成Velocity是非常简单的事情,甚至比单独使用Velocity更简单,只需在dispatcher-servlet.xml中申明: / /WEB-INF/velocity.properties  虽然标准的Velocity模板以.vm命名,但我们的所有Velocity模板页均以.html作为扩展名,不但用Dreamwaver编辑极为方便,甚至可以直接用IE观看页面效果。实现Skin (目录)  许多Blog系统都允许用户选择自己喜欢的界面风格。要实现Skin功能非常简单,为每个Skin编写Velocity模板,存放在web/skin/目录下,然后在Controller中返回对应的视图:String viewpath = skin.getSkin(account.getSkinId()) + "viewArticle";  由于使用了MVC模式,我们已经为每个页面定义好了Model,添加一个新的skin非常容易,只需要编写几个必须的.html文件即可,可以参考现有的skin。   SkinManager的作用是在启动时自动搜索/skin/目录下的所有子目录并管理这些skin,将用户设定的skinId映射到对应的目录。目前只有一个skin,您可以直接用可视化Html编辑器如Dreamwaver改造这个skin。附加功能 (目录)实现图片上传 (目录)  用户必须能够上传图片,因此需要文件上传的功能。比较常见的文件上传组件有Commons FileUpload(http://jakarta.apache.org/commons/fileupload/a>)和COS FileUpload(http://www.servlets.com/cos),Spring已经完全集成了这两种组件,这里我们选择Commons FileUpload。   由于Post一个包含文件上传的Form会以multipart/form-data请求发送给服务器,必须明确告诉DispatcherServlet如何处理MultipartRequest。首先在dispatcher-servlet.xml中声明一个MultipartResolver: 1048576   这样一旦某个Request是一个MultipartRequest,它就会首先被MultipartResolver处理,然后再转发相应的Controller。   在UploadImageController中,将HttpServletRequest转型为MultipartHttpServletRequest,就能非常方便地得到文件名和文件内容: public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { // 转型为MultipartHttpRequest: MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; // 获得文件: MultipartFile file = multipartRequest.getFile("file"); // 获得文件名: String filename = file.getOriginalFilename(); // 获得输入流: InputStream input = file.getInputStream(); // 写入文件...}生成缩略图 (目录)  当用户上传了图片后,必须生成缩略图以便用户能快速浏览。我们不需借助第三方软件,JDK标准库就包含了图像处理的API。我们把一张图片按比例缩放到120X120大小,以下是关键代码:public static void createPreviewImage(String srcFile, String destFile) { try { File fi = new File(srcFile); // src File fo = new File(destFile); // dest BufferedImage bis = ImageIO.read(fi); int w = bis.getWidth(); int h = bis.getHeight(); double scale = (double)w/h; int nw = IMAGE_SIZE; // final int IMAGE_SIZE = 120; int nh = (nw * h) / w; if( nh>IMAGE_SIZE ) { nh = IMAGE_SIZE; nw = (nh * w) / h; } double sx = (double)nw / w; double sy = (double)nh / h; transform.setToScale(sx,sy); AffineTransformOp ato = new AffineTransformOp(transform, null); BufferedImage bid = new BufferedImage(nw, nh, BufferedImage.TYPE_3BYTE_BGR); ato.filter(bis,bid); ImageIO.write(bid, "jpeg", fo); } catch(Exception e) { e.printStackTrace(); throw new RuntimeException("Failed in create preview image. Error: " + e.getMessage()); }}实现RSS (目录)  RSS是一个标准的XML文件,Rss阅读器可以读取这个XML文件获得文章的信息,使用户可以通过Rss阅读器而非浏览器阅读Blog,我们只要动态生成这个XML文件便可以了。RSSLibJ是一个专门读取和生成RSS的小巧实用的Java库,大小仅25k,可以从http://sourceforge.net/projects/rsslibj/下载rsslibj-1_0RC2.jar和它需要的EXMLjar两个文件,然后复制到web/WEB-INF/lib/下。   使用RSSLibJ异常简单,我们先设置好HttpServletResponse的Header,然后通过RSSLibJ输出XML即可:Channel channel = new Channel();channel.setDescription(account.getDescription());baseUrl = baseUrl.substring(0, n);channel.setLink("http://server-name/home.c?accountId=" + accountId);channel.setTitle(account.getTitle());List articles = facade.getArticles(accountId, account.getMaxPerPage(), 1);Iterator it = articles.iterator();while(it.hasNext()) { Article article = (Article)it.next(); channel.addItem("http://server-name/article.c?articleId=" + article.getArticleId(), article.getSummary(), article.getTitle() );}// 输出xml:response.setContentType("text/xml");PrintWriter pw = response.getWriter();pw.print(channel.getFeed("rss"));pw.close();实现全文搜索 (目录)  全文搜索能大大方便用户快速找到他们希望的文章,为blog增加一个全文搜索功能是非常必要的。然而,全文搜索不等于SQL的LIKE语句,因为关系数据库的设计并不是为全文搜索设计的,数据库索引对全文搜索无效,在一个几百万条记录中检索LIKE '%A%'可能会耗时几分钟,这是不可接受的。幸运的是,我们能使用免费并且开源的纯Java实现的Lucene全文搜索引擎,Lucene可以非常容易地集成到我们的blog中。   Lucene不提供直接对文件,数据库的索引,只提供一个高性能的引擎,但接口却出人意料地简单。我们只需要关心以下几个简单的接口:   Document:代表Lucene数据库的一条记录,也代表搜索的一条结果。   Field:一个Document包含一个或多个Field,类似关系数据库的字段。   IndexWriter:用于创建新的索引,也就是向数据库添加新的可搜索的大段字符串。   Analyzer:将字符串拆分成单词(Token),不同的文本对应不同的Analyzer,如HtmlAnalyzer,PDFAnalyzer。   Query:封装一个查询,用于解析用户输入。例如,将“bea blog”解析为“同时包含bea和blog的文章”。   Searcher:搜索一个Query,结果将以Hits返回。   Hits:封装一个搜索结果,包含Document集合,能非常容易地输出结果。   下一步,我们需要为Article表的content字段建立全文索引。首先为Lucene新建一个数据库,请注意这个数据库是Lucene专用的,我们不能也不必知道它的内部结构。Lucene的每个数据库对应一个目录,只需要指定目录即可:String indexDir = "C:/search/blog";IndexWriter indexWriter = new IndexWriter(indexDir, new StandardAnalyzer(), true);indexWriter.close();  然后添加文章,让Lucene对其索引:String title = "文章标题" // 从数据库读取String content = "文章内容" // 从数据库读取// 打开索引:IndexWriter indexWriter = new IndexWriter(indexDir, new StandardAnalyzer(), false);// 添加一个新记录:Document doc = new Document();doc.add(Field.Keyword("title", title));doc.add(Field.Text("content", content));// 建立索引:indexWriter.addDocument(doc);// 关闭:indexWriter.close(); 要搜索文章非常简单: 然后添加文章,让对其索引:String title = "文章标题" // 从数据库读取String content = "文章内容" // 从数据库读取// 打开索引:IndexWriter indexWriter = new IndexWriter(indexDir, new StandardAnalyzer(), false);// 添加一个新记录:Document doc = new Document();doc.add(Field.Keyword("title", title));doc.add(Field.Text("content", content));// 建立索引:indexWriter.addDocument(doc);// 关闭:indexWriter.close();  要搜索文章非常简单:Searcher searcher = new IndexSearcher(dir);Query query = QueryParser.parse(keyword, "content", new StandardAnalyzer());Hits hits = searcher.search(query);if(hits != null){ for(int i = 0;i < hits.length(); i++){ Document doc = hits.doc(i); System.out.println("found in " + doc.get("title")); System.out.println(doc.get("content")); }}searcher.close();  我们设计一个LuceneSearcher类封装全文搜索功能,由于必须锁定数据库所在目录,我们把数据库设定在/WEB-INF/search/下,确保用户不能访问,并且在配置文件中初始化目录: /WEB-INF/search/ 效果如下: (图4:search)发送Email (目录)  Blog用户可以让系统将来访用户的留言发送到注册的Email地址,为了避免使用SMTP发信服务器,我们自己手动编写一个SendMail组件,直接通过SMTP协议将Email发送到用户信箱。   SendMail组件只需配置好DNS服务器的IP地址,即可向指定的Email信箱发送邮件。并且,SendMail使用缓冲队列和多线程在后台发送Email,不会中断正常的Web服务。具体代码请看SendMail.java。测试 (目录)  服务器配置为:P4 1.4G,512M DDR,100M Ethernet,Windows XP Professional SP2。   测试服务器分别为WebLogic Server 8.1,Tomcat 4.1/5.0,Resin 2.1.1。   测试数据库为MS SQL Server 2000 SP3。如果你使用Oracle或者DB2,MySQL等其他数据库并测试成功,请将SQL初始化脚本和详细配置过程发一份给我,谢谢。   由于时间有限,没有作进一步的调优。WebLogic Server和iBatis有很多优化选项,详细配置可以参考相关文档。中文支持 (目录)  测试发现,中文不能在页面中正常显示,为了支持中文,首先在web.xml加入Filter,用于将输入编码设置为gb2312: encodingFilter org.crystalblog.web.filter.EncodingFilter encoding gb2312 encodingFilter /*  然后用文本工具搜索所有的.htm,.html,.properties文件,将“iso-8859-1”替换为“gb2312”,现在页面中文已经能正常显示,但是Lucene仍不能正常解析中文,原因是标准的StandardA?nalyzer只能解析英文,可以从网上下载一个支持中文的Analyzer。总结 (目录)  Spring的确是一个优秀的J2EE框架,通过Spring强大的集成和配置能力,我们能轻松设计出灵活的多层J2EE应用而无需复杂的EJB组件支持。由于时间仓促,水平有限,文中难免有不少错误,恳请读者指正。源代码下载 (目录)  源代码可以从http://www.javasprite.com/crystal/index.htm下载。相关资源下载 (目录)JDK 1.4.2可以从http://java.sun.com下载。Spring framework 1.1可以从http://www.springframework.org下载。iBatis 2.0可以从http://www.ibatis.com下载。Tomcat 4.1/5.0、Ant 1.6可以从http://www.apache.org下载。Resin 2.1.1可以从http://www.caucho.com下载。WebLogic Server / Workshop 8.1可以从http://commerce.bea.com下载。JUnit 3.8可以从http://www.junit.org下载。MySQL 4及其JDBC驱动可以从http://www.mysql.com下载。MS SQL Server 2000 JDBC驱动可以从http://www.microsoft.com/downloads/details.aspx?FamilyID=07287b11-0502-461a-b138-2aa54bfdc03a&DisplayLang=en下载。RSSLibJ 1.0可以从http://sourceforge.net/projects/rsslibj/下载。参考 (目录)“Spring Reference”,Rod Johnson等。“iBatis SQL Maps Guide”。“Apache Ant 1.6.2 Manual”。“Lucene Getting Started”。Springframework的JPetStore示例是非常棒的设计,本文参考了JPetStore的许多设计模式。关于作者廖雪峰,北京邮电大学本科毕业,对J2EE/J2ME有浓厚兴趣,欢迎交流:asklxf@163.com。个人网站:http://www.javasprite.com,欢迎访问。个人Blog站点:http://blog.csdn.net/asklxf/,欢迎访问。
原创粉丝点击