笔者在项目中所搭建的架构参考了 “Mark Eagle” 的《Wiring Your Web Application with Open Source Java》 一文中所阐述的架构思想。
从架构图中可以看出系统分为四层:
-
UI层:借助Struts实现
-
业务层:借助SpringFramework进行业务组件的组装关联。
-
数据持久层:借助Hibernate实现
-
域对象层:将所有域对象划分在一个层面
为什么采用这样的四层架构?
-
通过成熟的开源产品实现各层,同自己编写代码实现,相比之下能缩短开发周期,且架构所用到的开源产品均有 很广泛的用户群,经受过实践的考验,质量和性能更有保障。
-
层与层之间松散耦合,增加代码重用率。
-
各层分工明确,这样也利于团队的明确分工。
UI 是User Interface的缩写,这一层是面向用户的界面,是用户与系统之间交互的媒介。如,用户在界面发送请求, 系统接收请求,进行处理,然后通过界面将结果呈现于用户。这一过程包括了用户动作、数据传递、界面显示。大 家熟悉的MVC模式就是将这三者分离,减少三者耦合。
我们在该层借助了Struts来实现:
-
用ActionForm类封装与用户互动的数据元素。
-
用Action类实现业务逻辑、动作处理、链接转向。实现MVC中的C
-
借助Struts标签来完成数据呈现。实现MVC中的V。
关于struts具体的应用请参考相关章节。
在 实际的项目开发中,每个领域都会有自己独特的业务逻辑,正因为这样,致使项目中代码高度耦合,原本有可能被重用的代码 或功能,因为与具体的业务逻辑绑定在一块而导致很难被重用。因此我们将实现这些具体逻辑的代码抽取出来分为单独的一层, 其目的是希望通过层,来降低它与系统其他部分的耦合度。
现实中世界是变化的,既然该层实现的是现实中具体的业务逻辑,那该层的实现代码 不可避免的会发生变更。怎样让该层适应 最大的变化,做到最小的改动?通常我们在编码的时候会尽量考虑到同一业务多种实现的兼容和可扩展的能力。因此我们在 该层借助了Spring,通过依赖注入、AOP应用、面向接口编程,来降低业务组件之间的耦合度,增强系统扩展性。
关于Spring的具体使用请参考相关章节。
开发中与数据库进行数据交互必不可少,通常我们归为CRUD(添加、读取、修改、删除),这些操作占据了系统开发中大部分的时间, 同时我们还需要考虑与数据库交互的性能问题,如连接池、数据缓存等等。因此该层实现我们借助了Hibernate。
Hibernate 是一个ORM工具,它不仅仅是实现了数据库访问性能优化和与数据库交互的常用操作(CRUD),还将数据表与对象进行了 关联,让我们可以脱离数据表,而直接针对对象来与数据库交互,我们不再需要用字符串去描述表中字段,不再需要一个个”+“号去 组装Sql语句。这使得编码中可书写性提高。
该层的具体实现,请参看Hibernate相关章节。
Struts是用于实现Web项目中UI层的开源产品,是MVC模式的经典实现案例。它属于Apache组织中的开源产品之一, 我们可以从官方网站http://struts.apache.org得到其所有资料及源码。
为什么使用Struts?
-
Struts将业务数据、页面显示、动作处理进行分离,这有利各部分的维护。
-
Struts采用Front Controller模式来实现动作处理,让所有的动作请求都是经过一个统一入口, 然后进行分发。这样方便我们在入口中加入一些全局控制代码的实现,如安全控制、日志管理、国际化 编码等。
-
通过Struts提供的ActionForm封装web form 中的元素,使重用web表单成为可能。
-
借助Struts Validator框架帮助完成web层的验证工作,通常情况下我们不用再去为每个web页面写其验证代码,只需通 过配置即可实现。这也减少了我们的开发量,由于验证代码的集中管理,也为维护带来便利。
1、下载Struts,将其相关jar包引用到Web项目。
2、在web项目的web.xml中配置Action影射,将相应请求动作交给Struts提供的ActionServlet类进行统一控制。
例 2.1. web.xml
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>debug</param-name>
<param-value>2</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
在web.xml中定义了所有带.do后缀的文件请求都会触发ActionServlet类。在url-pattern节点可以灵活定义 适合自身的映射表达式,如,对某个目录下请求的映射:/myDirectory/*.do
在配置org.apache.struts.action.ActionServlet类时设置一些参数其含义如下:
-
config:制定Struts配置文件路径,默认为/WEB-INF/struts-config.xml
-
debug:设定日志记录级别。
3、在web.xm配置所需要用到的的Struts标签文件
例 2.2. web.xml
<taglib>
<taglib-uri>struts-bean.tld</taglib-uri>
<taglib-location>/WEB-INF/struts-bean.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>struts-html.tld</taglib-uri>
<taglib-location>/WEB-INF/struts-html.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>struts-logic.tld</taglib-uri>
<taglib-location>/WEB-INF/struts-logic.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>struts-template.tld</taglib-uri>
<taglib-location>/WEB-INF/struts-template.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>struts-tiles.tld</taglib-uri>
<taglib-location>/WEB-INF/struts-tiles.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>c.tld</taglib-uri>
<taglib-location>/WEB-INF/c.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>Validate.tld</taglib-uri>
<taglib-location>/WEB-INF/Validate.tld</taglib-location>
</taglib>
4、建立ActionForm、Action。
例 2.3. struts-config.xml
<struts-config>
<form-beans>
<form-bean name="helloWorldForm" type ="struts.test.HelloWorldForm">
<form-property name="name" type="java.lang.String" />
</form-bean>
</form-beans>
<action-mappings>
<action path="/helloWorld" name="helloWorldForm" type="struts.test.HelloWorldAction"
scope="request" input="/HelloWorld.jsp" validate="false" />
</action-mappings>
</struts-config>
例 2.4. HelloWorldForm.java
public class HelloWorldForm extends ActionForm
{
private String name = null;
public String getName()
{
return this.name;
}
public void setName(String name)
{
this.name = name;
}
}
例 2.5. HelloWorldAction.java
public class HelloWorldAction extends Action
{
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request,
HttpServletResponse response) throws Exception
{
HelloWorldForm loginForm = (HelloWorldForm)form;
String name = loginForm.getName();
request.setAttribute("HelloWorldForm",loginForm);
return mapping.getInputForward();
}
}
5、视图呈现
例 2.6. HelloWorld.jsp
<%@page language="java" contentType = "text/html;charset=GBK"%>
<%@ taglib uri="struts-html.tld" prefix="html" %>
<%@ taglib uri="struts-tiles.tld" prefix="tiles" %>
<%@ taglib uri="struts-bean.tld" prefix="bean" %>
<table border="1">
<tr>
<html:form action="helloWorld.do" method="post">
<td>
<table>
<tr>
<td>用户名:</td>
<td><html:text property="name" /></td>
</tr>
<tr colspan="2">
<td><html:submit property="show" value="提交" /></td>
</tr>
</table>
</td>
</html:form>
</tr>
</table>
<bean:write name="name" />
6、演示效果
本章演示了一个简单的示例,在文本框输入内容,提交后显示其内容。功能虽然很简单但是涉及到了struts应用的大部分知识:
-
在web.xml中配置Action影射规则,如,*.do文件的请求触发Action。
-
ActionFrom中的属性对应到表单域中表单元素,如,jsp页面名为name的Text,对应到ActionForm中name属性。
-
继承Action类后即可实现具体的控制器,如,HelloAction类接受到请求后将ActionForm对象存放到request 范围,然后转发给配置的链接地址。
-
借助struts提供的标签进行视图呈现。如,bean:write 标签访问ActionForm对象的属性值。
ActionFrom是用来传输表单数据的对象,通过配置文件可以关联到对应的Action,实现在UI层与业务层之间的数据传输。 实现机制如下:
-
Struts 提供了一个org.apache.struts.action.ActionForm类,里面实现了将请求表单中的元素赋值给其具体的 实现类属性。因此自定义ActionForm类时,只需继承该类即可。在自定义ActionForm类时请保证其属性名称与所对应 表单名称一致。
-
当触发Action时,通过配置文件匹配到对应的ActionFrom实例,以参数形式传入。
ActionForm 的实现虽然简单,但是随着界面的增加,ActionForm类也会增加,造成代码膨胀。在Struts1.1以上版本提供了 ActionForm的另一个实现类,org.apache.struts.action.DynaActionForm,该类充当所有 ActionForm的代理类,只需在 配置ActionFrom时指定用该类实现即可:
例 2.7. struts-config.xml
<form-beans>
<form-bean name="helloWorldForm" type ="org.apache.struts.action.DynaActionForm">
<form-property name="name" type="java.lang.String" />
</form-bean>
</form-beans>
在访问helloWorldForm实例时如下:
Action充当了控制器的角色,每个请求都会触发到Action类,我们在这里处理逻辑业务,分发数据,链接转向。 其实现机制:
-
在web.xml 配置影射规则,符合其影射条件的请求都会交给org.apache.struts.action.ActionServlet类处理, 在该类中将会实现org.apache.struts.action.Action类中的相应方法。在自定义Action类时只需继承该类, 即可让自定义的Action得到触发。
-
execute()方法中 ActionMapping、ActionForm实例通过读取配置文件获得。
在UI中对用户提交的数据作验证是必不可少的,Struts也提供了相应的实现。下面将演示一个登录界面的校验:
1、在配置文件中申明校验实现类,Struts中校验工作通过org.apache.struts.validator.ValidatorPlugIn类实现。
例 2.9. struts-config.xml
<plug-in className="org.apache.struts.validator.ValidatorPlugIn">
<set-property property="pathnames" value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml"/>
</plug-in>
在实例化ValidatorPlugIn类时会装载validator-rules.xml、validation.xml文件,这些文件中包含了验证规则 和要被验证的具体表单信息。
2、完善校验规则文件。在validator-rules.xml中定义了常用的客户端验证规则,如,不为空,只允许 数字等。特殊的实现需要在此扩充。
3、定义要被验证的表单信息。
例 2.10. validation.xml
<form-validation>
<formset>
<form name="loginForm"> (1)
<field property="name" page="0" depends="required"> (2)
<arg0 key="register.username"/> (3)
</field>
</form>
</formset>
</form-validation>
(1) | 定义要被验证的ActionForm名称。 |
(2) | 定义要被验证的具体属性,及规则。depends="required"指明将调用validator-rules.xml中的required规则。 |
(3) | 从资源文件中匹配具体的验证信息。 |
4、建立存放验证提示信息的资源文件。如当用户名为空时显示提示信息“必须填写用户名”
5、在界面设置触发条件,如onsubmit时响应验证动作。
例 2.12. Login.jsp
<%@page language="java" contentType = "text/html;charset=GBK"%>
<%@ taglib uri="struts-html.tld" prefix="html" %>
<table border="1">
<tr>
<html:form action="login.do" method="post" οnsubmit="return validateCheckLoginForm(this);">
<html:hidden property="loginType" value="0"/>
<td>
<table>
<tr>
<td>用户名:</td>
<td><html:text property="name" /></td>
</tr>
<tr>
<td>密码:</td>
<td><html:password property="password" /></td>
</tr>
<tr colspan="2">
<td><html:submit property="login" value="登录" /></td>
</tr>
</table>
</td>
</html:form>
</tr>
<html:javascript formName="checkLoginForm" dynamicJavascript="true" staticJavascript="true"/>
</table>
当用户名为空时点击登录,将出现如下提示:
对spring的描述莫过于作者本人之言
Developing software applications is hard enough even with good tools and technologies. Implementing applications using platforms which promise everything but turn out to be heavy-weight, hard to control and not very efficient during the development cycle makes it even harder. Spring provides a light-weight solution for building enterprise-ready applications, while still supporting the possibility of using declarative transaction management, remote access to your logic using RMI or webservices, mailing facilities and various options in persisting your data to a database. Spring provides an MVC framework, transparent ways of integrating AOP into your software and a well-structured exception hierarchy including automatic mapping from proprietary exception hierarchies. 即使拥有良好的工具和优秀技术,应用软件开发也是困难重重。如果使用了超重量级,难于控制,不能有效控制开 发周期的平台 那么就让应用开发变得更为困难。Spring为已建立的企业级应用提供了一个轻量级的解决方案,这个方案包括声明 性事务管理, 通过RMI或webservices远程访问业务逻辑,mail支持工具以及对于数据和数据库之间持久层的各种配置的支持。 Spring还提供了 一个MVC应用框架,可以通过集成AOP透明的嵌入你的软件和一个优秀的异常处理体系,这个异常体系可以自动 从属性异常体系 进行映射。 | ||
--springFramework reference |
springFramework 是种非侵入式轻量级框架,允许自由选择和组装各部分功能,还提供和其他软件集成的接口,如与Hibernate、Struts 的集成(后面的章节中会提到)。它提供的功能有Spring IOC、spring AOP、Spring ORM、Spring DAO、Spring MVC.笔者在项目 中用到的主要是IOC和AOP功能,ORM用hibernate取代,MVC用Struts取代。本文讲述springFramework在web环境下 的使用。
1、利用延时注入思想组装代码,提高系统扩展性,灵活性,实现插件式编程。
2、利用AOP思想,集中处理业务逻辑,减少重复代码,构建优雅的解决方案。
3、利用其对Hibernate的SessionFactory、事务管理的封装,更简洁的应用Hibernate。
要使用Spring非常简单,来体验下:
通过Spring注入MyClassImpl的实例,需在配置文件中做如下配置:
例 3.3. SpringConfig.xml
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans default-lazy-init="false" default-dependency-check="none" default-autowire="no">
<bean id="myClass" class="MyClassImpl" />
</beans>
这样在代码中就可以通过Spring体验到什么叫延时装载了
例 3.4.
ApplicationContext ac = new FileSystemXmlApplicationContext("SpringConfig.xml"); (1)
MyClass cls = (MyClass)ac.getBean("myClass"); (2)
cls.execute();
(1) | 载入Spring配置文档,上例中SpringConfig.xml放置在工作路径根目录中。这种引用方式其配制文件只能相对于工作路径 的引用。 |
(2) | 实例化配置的对象,以配置文件的bean节点ID值作为实例引用的关键字。 |
上面的例子中得到了实现类的实例,但是代码中并没有硬编码具体实现类,而是将这种依赖转移到配置文件中。
1、下载springFramework最新版本http://www.springframework.org,将springFramework下的*.jar 拷贝到项目lib中,并引用。
2、在Web.xml中配置spring的启动方式。
springFramework提供两种形式的web context,基于Listener接口的实现和基于Servlet接口的实现。
例 3.5. web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
<display-name>Test</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring_bean.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--
<servlet>
<servlet-name>context</servlet-name>
<servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
-->
<servlet>
<servlet-name>Init</servlet-name>
<servlet-class>com.m5.Initializtion</servlet-class>
<load-on-startup>3</load-on-startup>
</servlet>
</web-app>
tomcat 启动时会装载/WEB-INF/spring_bean.xml文件,如果不指定contextConfigLocation参数, 默认装载/WEB-INF/applicationContext.xml文件。然后在tomcat启动时初始化一个自定义servlet, 在这里实现springFramework的装载。
例 3.6. Initializtion.java
public class Initializtion extends HttpServlet
{
public void init(ServletConfig config) throws ServletException
{
WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext
(config.getServletContext());
super.init(config);
InitSpring.Init((AbstractApplicationContext)wac); (1)
}
}
(1) | InitSpring是一个符合单例模式的类。 |
例 3.7. InitSpring.java
public class InitSpring
{
AbstractApplicationContext wac = null;
private static InitSpring instance = new InitSpring();
private InitSpring()
{
}
public static void Init(AbstractApplicationContext wac)
{
instance.wac = wac;
}
public static Object getInstance(String objName)
{
return instance.wac.getBean(objName);
}
public static Object getInstance(Class objClass)
{
return getInstance(objClass.getName());
}
}
经过以上处理,在项目任何一个类中如需得到Spring配置文件中的一个类实例,如下即可:
InitSpring.getObject(beanId);
IOC(Inversion of Control),译作反转控制,其功能是将类之间的依赖转移到外部的配置文件中, 避免在调用类中硬编码实现类,因此也被称作依赖注入(Dependency Injection)。在以往的开发中, 通常利用工厂模式(Factory)来解决此类问题,其实不管是工厂模式还是依赖注入,调用类与实现类不可 能没有任何依赖,工厂模式中工厂类通常根据参数来判断该实例化哪个实现类,Spring IOC将需要实例的 类在配置文件文件中配置。使用Spring IOC能得到工厂模式同样的效果,而且编码更加简洁。看段代码比较 一下:
1、用工厂模式来实现
例 3.9. ConcreteProductA.java
public class ConcreteProductA implements Product
{
public void execute()
{
...
}
}
例 3.10. ConcreteProductB.java
public class ConcreteProductB implements Product
{
public void execute()
{
...
}
}
例 3.11. Factory.java
public class Factory
{
public Product CreateProduct(object param)
{
return ConstructObjects(param);
}
private Product ConstructObjects(object param)
{
...
}
}
例 3.12. Client.java(调用类)
public class Client
{
public Client()
{
//实例化ConcreteProductA
Product product = Factory.CreateProduct(paramA);
//实例化ConcreteProductB
Product product = Factory.CreateProduct(paramB);
...
}
}
在ConstructObjects方法中设定实例化实现类的逻辑,这样对于调用类来说,不直接实例化实现类,纵然实现类发生变化, 调用代码仍然可以不作修改,给维护与扩展带来便利。
2、Spring IOC实现
例 3.13. SpringConfig.xml
<bean id="productA" class="ConcreteProductA" />
<bean id="productB" class="ConcreteProductB" />
例 3.14. Client.java(调用类)
public class Client
{
public Client()
{
//实例化ConcreteProductA
Product product = (Product)InitSpring.getObject("productA");
//实例化ConcreteProductB
Product product = (Product)InitSpring.getObject("productB");
...
}
}
调用代码中没有硬编码实现类,比较工厂模式,少了Factory类。
Spring为依赖注入提供三种实现方式:接口注入、设值注入、构造注入。利用这些可以灵活的解决 类之间的依赖关系,让你为所欲为的组装代码。与其说Spring IOC是一个工具,还不如说搭建了一 个思想的舞台。继续看代码:
来实现一个操作多个数据源的切换
例 3.15. DataSource.java
public class DataSource
{
private String driverClassName;
private String url;
private String username;
private String password;
public String getDriverClassName()
{
return this.driverClassName;
}
public void setDriverClassName(String driverClassName)
{
this.driverClassName = driverClassName;
}
public String getUrl()
{
return this.url;
}
public void setUrl(String url)
{
this.url = url;
}
public String getUsername()
{
return this.Username;
}
public void setUsername(String username)
{
this.username = username;
}
public String getPassword()
{
return this.password;
}
public void setPassword(String password)
{
this.password = password;
}
}
例 3.16. DataAccessor.java
public class DataAccessor
{
private DataSource dataSource;
public void setDriver(DataSource dataSource)
{
this.dataSource = dataSource;
}
public void save(String sql)
{
Statement s = getStatement();
try
{
s.getConnection().setAutoCommit(false);
int rows = s.executeUpdate(sql);
s.getConnection().commit();
}
catch(Exception e)
{
s.getConnection().rollback();
...
}
finally
{
...
}
}
private Statement getStatement()
{
Statement s;
try
{
Class.forName(dataSource.getDriverClassName()).newInstance();
java.sql.Connection conn =
java.sql.DriverManager.getConnection(dataSource.getUrl(),dataSource.getUser(),dataSource.getPassword());
try
{
s = c.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE);
}
}
catch(Exception e)
{
...
}
return s;
}
}
例 3.17. BussinessA.java
public class BussinessA
{
private DataAccessor dao;
public void setDao(DataAccessor dao)
{
this.dao = dao;
}
public void execute()
{
dao.save("insert into tb1 ...");
}
}
例 3.18. BussinessB.java
public class BussinessB
{
private DataAccessor dao;
public void setDao(DataAccessor dao)
{
this.dao = dao;
}
public void execute()
{
dao.save("insert into tb2 ...");
}
}
全部代码就这样了,执行BussinessA.java、BussinessB.java代码即可完成数据插入操作,从代码中看, 这两个类具体操作的是什么数据库?什么样的操作细节?让你失望了,代码中找不到这样的关联,看配置文件吧:
例 3.19. SpringConfig.xml
<bean id="dataSourceA" class="DataSource" destroy-method="close">
<property name="driverClassName"><value>org.gjt.mm.mysql.Driver</value></property>
<property name="url">
<value>jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=GBK</value>
</property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<bean id="dataSourceB" class="DataSource" destroy-method="close">
<property name="driverClassName"><value>org.gjt.mm.mysql.Driver</value></property>
<property name="url">
<value>jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=GBK</value>
</property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<bean id="daoA" class="DataAccessor">
<property name="dataSource">
<ref local="dataSourceA"/>
</property>
</bean>
<bean id="daoB" class="DataAccessor">
<property name="dataSource">
<ref local="dataSourceB"/>
</property>
</bean>
<bean id="bussinessA" class="BussinessA">
<property name="dao">
<ref local="daoA"/>
</property>
</bean>
<bean id="bussinessB" class="BussinessB">
<property name="dao">
<ref local="daoB"/>
</property>
</bean>
看完配置文件应该明白了,这里承担了所有的依赖关系。
-
首先,我们通过设值注入方法设置数据源相关参数
-
然后,我们将数据源实例注入给数据访问类
-
最后,我们为每个具体业务类注入相应访问器
是不是感觉想玩积木似的,在组装你的代码?
例 3.20. DaoTest.java
public void testSave()
{
BussinessA bussinessA = (BussinessA)InitSpring.getObject("bussinessA");
bussinessA.execute();
bussinessB bussinessB = (BussinessB)InitSpring.getObject("bussinessB");
bussinessB.execute();
}
执行这段测试代码,数据库Test1、Test2中tb1、tb2表将分别插入对应的数据,从实现代码来看操作多个数据库和 操作一个数据库完全一样,即使当数据源,数据访问类不断变化,应用代码也可以做到不用任何修改。
希望看完本章节的内容能让读者与我共鸣,Spring IOC是一种优雅的思想,借助它发挥你无穷的想象吧。
AOP 的全称为Aspect Oriented Programming,译为面向方面编程,针对具体业务领域、业务逻辑。AOP目的是 将复杂逻辑进行分离,抽取共性,让各部分实现实现的功能更为专一。如,Spring AOP思想实现的经典案例“事务 管理”,每个访问数据库的业务类都有可能用到事务控制,将其事务管理代码从具体类中抽取出来,放到一个单独的 地方处理,这样大大简化了具体业务类的代码。如图:
从图中能清楚地看出分离的效果,下面继续借助图例来阐述AOP的一些观念
-
Aspect(方面):我们所关注的,可以被抽取的业务逻辑,如图中的“事务管理”。
-
JoinPoint(连接点):程序在执行过程中明确的点,如图中execute()方法。
-
Advice(通知):在特定的连接点执行的动作。如在执行execute()方法前的预处理, 在执行完execute() 方法后的后处理。
-
PointCut(切入点):如图中在客户调用execute()时才产生图中所示动作,还也可以 设定产生同样动作的方法,如save(),update(),甚至申明成“save.*”,这些申明的 集合就称之为切入点。
-
TargetObject(目标对象):包含连接点的对象,也称为被代理的对象。如图中的“业务组件”
理解完概念,接下来看实际例子:
在Spring IOC章节中,DataAccessor.java的Save方法中是 未经事务管理分离的实现,我们采用AOP实现后,期望将实现代码变成如下:
例 3.21. DataAccessor.java
public void save(String sql)
{
Statement s = getStatement();
int rows = s.executeUpdate(sql);
}
多么美妙的想法,看实现方法:
1、 AOP中的“方面”已经明确,即“事务管理”,通常用拦截器来实现其功能。我们可以通过实现 AOP联盟提供的通用AOP接口MethodInterceptor自定义拦截器。本例借助Spring中提供的 org.springframework.jdbc.datasource.DataSourceTransactionManager事务管理 类实现。
2、”目标类“我们也已经明确,即DataAccessor.java。
3、定义切入点,该例中我们期望在执行save方法时做事务管理控制。
4、在SpringConfig.xml文件中进行相关配置
例 3.22. SpringConfig.xml
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName"><value>org.gjt.mm.mysql.Driver</value></property>
<property name="url">
<value>jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=GBK</value>
</property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<bean id="dataAccessor" class="DataAccessor">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> (1)
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
<bean id="dataAccessorProxy"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> (2)
<property name="transactionManager"> (3)
<ref bean="transactionManager" />
</property>
<property name="target"> (4)
<ref local="dataAccessor" />
</property>
<property name="transactionAttributes"> (5)
<props>
<prop key="save">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
经 过以上的配置,改动下DataAccessor.java类的实现,因为这里我们用的dataSource类为Spring提供的 org.apache.commons.dbcp.BasicDataSource,且我们可以通过JdbcTemplate执行相关数据库访问操作。
例 3.23. DataAccessor.java
public class DataAccessor
{
private DataSource dataSource;
public void setDataSource(DataSource dataSource)
{
this.dataSource = dataSource;
}
public void save()
{
JdbcTemplate template = new JdbcTemplate(this.dataSource);
template.update("insert into game (name) values ('test')");
}
}
save ()中代码完全是我们所期望完全样子,不用关系异常处理,不用关系事务管理,设想一下,如果项目 中有100百个这样的业务类,那能节省你不少敲这种重复代码的时间了,最重要的是我们让某个业务逻辑得 到集中处理,便于日后维护与扩展。 这就是借助AOP的好处。以后面对一个复杂的业务逻辑处理时,别忘了 想想是否能借助面向方面的思想去解决问题。
对象到关系型数据映射(ORM)是架构中热门的话题,hibernate是诸多优秀的ORM工具之一,使用和受推崇程度较高。 国内也有专门的hibernate网站与论坛,其中人气最高当属 java视线论坛, 感谢他们的无私与辛勤,以致hibernate官方发行包中多了一份中文指南。
1、减轻了编写Sql语句工作量
例 4.1. 传统的数据库访问代码
insert into table (field_1,field_2,field_n) values('"+ field_value1 +"','" + field_value2 + "','" + field_value3 + "'")
由代码比较可以看出,数据表可以跟对象一样被操作,这样代码显得更加简洁,可读性也增强。在实际开发中,这里是业务变动 频繁的地方,保证代码的可读性和易维护,很有价值。
2、Hibernate封装了数据库访问、事务管理、数据缓存等工作。省去了自己去编写这些代码。
3、将数据表数据映射到对象中,以对象作为传输媒介,能更好的在系统各层传输数据。
在体验hibernate前,请首先下载hibernate最新发行包,www.hibernate.org。 然后将hibernate相关jar包加入项目lib,并引用。(本章节的所有例子均以Mysql数据库为例)
1、建立表:
2、建立映射文件:
例 4.3. book.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 2.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd" >
<hibernate-mapping>
<class
name="com.m5.Book" (1)
table="book" (2)
>
<id
name="id"
type="java.lang.Integer"
column="id"
>
<generator class="native" />
</id>
<property
name="name"
type="java.lang.String"
column="name"
not-null="true"
length="100"
/>
<property
name="price"
type="long"
column="price"
not-null="true"
length="10"
/>
</class>
</hibernate-mapping>
通常增一个表就需要增加类似上面的映射文件。(别紧张,这样的工作可以借助工具生成,后续的内容会介绍)。
3、建立影射对象:
例 4.4. book.java
package com.m5;
public class Book
{
private int id;
private String name;
private long price;
public Book()
{
}
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public long getPrice()
{
return price;
}
public void setPrice(long price)
{
this.price = price;
}
}
4、建立hibernate的配置文件
例 4.5. hibernate.cfg.xml
<?xml version='1.0' encoding='gb2312'?>
<!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
<hibernate-configuration>
<session-factory >
<property name="hibernate.connection.driver_class">org.gjt.mm.mysql.Driver</property> (1)
<property name="hibernate.connection.url">(2)
jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=GBK
</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password"></property>
<property name="show_sql">true</property>
<property name="dialect">net.sf.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.cglib.use_reflection_optimizer">true</property>
<property name="hibernate.query.substitutions">true 1, false 0</property>
<property name="hibernate.query.substitutions">male 1, female 0</property>
<mapping resource="book.hbm.xml"/> (3)
</session-factory>
</hibernate-configuration>
(1) | 指定数据库驱动类,该文件配置的数据库驱动为mysql,hibernate还支持其它的数据库,更改为对应的驱动类即可。 |
(2) | 数据库的连接字符串 |
(3) | 指定映射文件,该文件配置的book.hbm.xml表明放在类引用路径的根目录。如web项目的classes目录。 |
5、数据库访问代码
例 4.6. MyHibernate.java
import java.io.File;
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.Configuration;
public class MyHibernate {
public static void main(String[] args)
{
String name = "java";
long price = 10;
try
{
File file = new File("d:hibernate.cfg.xml");
Configuration config = new Configuration().configure(file); (1)
SessionFactory sessionFactory = config.buildSessionFactory();
Session session = sessionFactory.openSession();
Book book = new Book();
book.setName(name);
book.setPrice(price);
session.save(book); (2)
session.flush();
}
catch(Exception ex)
{
System.out.println(ex.toString());
}
}
}
执 行上面的代码,book数据表将插入一条数据,这就是ORM。hibernate的使用是不是也没有想象中的复杂?当然 实际开发中不仅仅只会有如此简单的数据表操作,后续章节将逐步深入讲述hibernate的使用。希望该例子能让你 对hibernate有了较直观的理解,至少明白hibernate的映射规则:
-
首先,通过影射文件设定映射类与数据表之间的结构关联。
-
然后,装载配置文件
-
最后执行操作,将数据表数据映射到对象,或将对象数据持久到数据表中。
在写本节前首先感谢夏昕提供的《hibernate开发指南》,该文中详细的介绍了怎样生成映射文件 和值对象,以及开发中借助工具常用的方法。
前 面的例子已经介绍了,hibernate完成持久操作需要一个映射文件和影射类。从映射文件可以看出其结构类似 对数据表的描述,我们只要根据数据表就能够找出生成映射文件的规律。于是我们有了工具来替代手动书写, 从而减少工作量,提高开发效率。下面介绍hibernate官方提供的Middlegen-Hibernate工具。
1、下载Middlegen-Hibernate,www.hibernate.org,解压缩到c:Middlegen目录。
2、修改Middlegen-Hibernate的配置文件,Middlegen-Hibernate通过ant来构建运行(如果对ant不熟悉请先阅读 ant章节的内容),打开根目录的build.xml文件:
例 4.7. build.xml
<?xml version="1.0"?>
<!DOCTYPE project [
<!ENTITY database SYSTEM "file:./config/database/mysql.xml"> (1)
]>
<project name="Middlegen Hibernate" default="all" basedir=".">
<property file="${basedir}/build.properties"/>
<property name="name" value="MyTest"/> (2)
...
<property name="build.dir" value="c:sample"/> (3)
...
<hibernate
destination="${build.gen-src.dir}"
package="com.m5" (4)
genXDocletTags="true" (5)
genIntergratedCompositeKeys="false"
javaTypeMapper="middlegen.plugins.hibernate.HibernateJavaTypeMapper"/>
...
</project>
(1) | 改成对应数据库的映射文件,本例以mysql为例,因此引用mysql.xml。 |
(2) | 项目名称。 |
(3) | 映射文件的输出路径。 |
(4) | 对应的包名。 |
(5) | 是否生成XDoclet标签,这里设置成true,以便利用映射文件生成值对象。 |
3、找到configdatabase目录下对应的数据库配置文件,本文用的是mysql,因此打开mysql.xml
例 4.8. mysql.xml
<property name="database.script.file" value="${src.dir}/sql/${name}-mysql.sql"/>
<property name="database.driver.file" value="${lib.dir}/mysql-connector-java-3.0.14-production-bin.jar"/>
<property name="database.driver.classpath" value="${database.driver.file}"/>
<property name="database.driver" value="org.gjt.mm.mysql.Driver"/>
<property name="database.url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&
characterEncoding=GBK"/> (1)
<property name="database.userid" value="root"/> (2)
<property name="database.password" value=""/> (3)
<property name="database.schema" value=""/>
<property name="database.catalog" value=""/>
<property name="jboss.datasource.mapping" value="mySQL"/>
4、进入Middlegen-Hibernate,运行ant,将会看到如下图的界面:
根据界面的标题进行各项对应的操作,最后单击“Generate”按钮即可生成映射文件。如,本文以book表为例,生成 book表的映射文件:
例 4.9. book.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 2.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd" >
<hibernate-mapping>
<!--
Created by the Middlegen Hibernate plugin 2.1
http://boss.bekk.no/boss/middlegen/
http://www.hibernate.org/
-->
<class
name="com.m5.Book"
table="book"
>
<meta attribute="class-description" inherit="false">
@hibernate.class
table="book"
</meta>
<id
name="id"
type="java.lang.Integer"
column="id"
>
<meta attribute="field-description">
@hibernate.id
generator-class="assigned"
type="java.lang.Integer"
column="id"
</meta>
<generator class="assigned" />
</id>
<property
name="name"
type="java.lang.String"
column="name"
not-null="true"
length="100"
>
<meta attribute="field-description">
@hibernate.property
column="name"
length="100"
not-null="true"
</meta>
</property>
<property
name="price"
type="long"
column="price"
not-null="true"
length="10"
>
<meta attribute="field-description">
@hibernate.property
column="price"
length="10"
not-null="true"
</meta>
</property>
<!-- Associations -->
</class>
</hibernate-mapping>
从book.hbm.xml 文件中可以发现有类似这样的代码“ @hibernate.class table=‘book’”,这就是XDoclet 标签。可以通过Hibernate Extension工具包中的hbm2java工具来转换这些标签生称对应的值对象。
1、下载Hibernate Extension工具包,http://www.hibernate.org
2、hbm2java对本地类库引用的配置文件为:toolsinsetenv.bat
例 4.10. setenv.bat
@echo off
rem -------------------------------------------------------------------
rem Setup environment for hibernate tools
rem -------------------------------------------------------------------
set JDBC_DRIVER=C:jarsmysql-connector-java-3.0.14-production-bin.jar (1)
set HIBERNATE_HOME=C:hibernate-2.1 (2)
set HIBERNATETOOLS_HOME=%~dp0..
echo HIBERNATETOOLS_HOME set to %HIBERNATETOOLS_HOME%
if "%HIBERNATE_HOME%" == "" goto noHIBERNATEHome
set CORELIB=%HIBERNATE_HOME%lib
set LIB=%HIBERNATETOOLS_HOME%lib
set CP=%CLASSPATH%;%JDBC_DRIVER%;%HIBERNATE_HOME%hibernate2.jar;%CORELIB%commons-logging-1.0.4.jar;
%CORELIB%commons-lang-1.0.1.jar;%CORELIB%cglib-full-2.0.2.jar;%CORELIB%dom4j-1.4.jar;
%CORELIB%odmg-3.0.jar;%CORELIB%xml-apis.jar;%CORELIB%xerces-2.4.0.jar;%CORELIB%xalan-2.4.0.jar;
%LIB%jdom.jar;%CORELIB%commons-collections-2.1.1.jar;%LIB%..hibernate-tools.jar (3)
if not "%HIBERNATE_HOME%" == "" goto end
:noHIBERNATEHome
echo HIBERNATE_HOME is not set. Please set HIBERNATE_HOME.
goto end
:end
(1) | 对应到本地JDBC_DRIVER jar包路径。 |
(2) | hibernate根目录。 |
(3) | 对hibernate中jar包的引用,hibernate因版本不同而jar包的名字也会有可能不同,因此 请核对。 |
3、 进入hbm2java.bat所在目录,运行 hbm2java 影射文件路径 --output=输出值对象路径。如: hbm2java C:samplegen-srccomm5Book.hbm.xml --output=C:sampleclasses。在 C:sampleclasses目录下生成以包名组织的文件comm5Book.java
例 4.11. Book
package com.m5;
import java.io.Serializable;
import org.apache.commons.lang.builder.ToStringBuilder;
/**
* @hibernate.class
* table="book"
*/
public class Book implements Serializable {
/** identifier field */
private Integer id;
/** persistent field */
private String name;
/** persistent field */
private long price;
/** full constructor */
public Book(Integer id, String name, long price) {
this.id = id;
this.name = name;
this.price = price;
}
/** default constructor */
public Book() {
}
/**
* @hibernate.id
* generator-class="native"
* type="java.lang.Integer"
* column="id"
*/
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
/**
* @hibernate.property
* column="name"
* length="100"
* not-null="true"
*/
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
/**
* @hibernate.property
* column="price"
* length="10"
* not-null="true"
*/
public long getPrice() {
return this.price;
}
public void setPrice(long price) {
this.price = price;
}
public String toString() {
return new ToStringBuilder(this)
.append("id", getId())
.toString();
}
}
上 节提到了用hbm2java将影射文件生成映射对象,依靠xdoclet标签完成。xdoclet也是依靠此标签完成与影射文件的 同步。这样实际开发中会带来很大的便利,我们只要维护代码,而不需要手动维护与影射文件的同步。xdoclet标签 可以由上节讲的方法去转化得来,当然如果熟悉了xdoclet标签,手动完成即可。xdoclet的使用很方便,可以加入我 们已有的ant任务中(如果尚未了解Ant,请参看相关章节)。
1、下载xdoclet。
2、建立构建文件
例 4.12. build.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="XDoclet Examples" default="hibernate" basedir=".">
<property name="xdoclet.root.dir" value="c:/xdoclet-1.2.2"/>
<property name="xdoclet.lib.dir" value="${xdoclet.root.dir}/lib"/>
<property name="samples.gen-src.dir" value="./gen-src"/>
<path id="classpath">
<fileset dir="${xdoclet.lib.dir}">
<include name="*.jar"/>
</fileset>
</path>
<taskdef
name="hibernatedoclet"
classname="xdoclet.modules.hibernate.HibernateDocletTask"
classpathref="classpath"
/>
<target name="hibernate" description="Generate mapping documents">
<echo>+---------------------------------------------------+</echo>
<echo>| |</echo>
<echo>| R U N N I N G H I B E R N A T E D O C L E T |</echo>
<echo>| |</echo>
<echo>+---------------------------------------------------+</echo>
<hibernatedoclet
destdir="${samples.gen-src.dir}"
excludedtags="@version,@author,@todo,@see"
addedtags="@xdoclet-generated at ${TODAY},@copyright The XDoclet Team,@author XDoclet,@version ${version}"
force="false"
verbose="true">
<fileset dir="${samples.gen-src.dir}">
<include name="com/m5/Book.java"/>
</fileset>
<hibernate version="2.1"/>
</hibernatedoclet>
</target>
</project>
build.xml中的目录结构均为笔者环境的,使用时请修改成对应的目录。
3、运行ant,在输出目录生成对应的影射文件。
建 议:如果你觉得hibernate的映射文件放在一个xml文件更为方便,可以通过修改xdoclet的源码, 使其生成的映射文件全部放置在制定的xml文件中,这样生成新的映射文件时不需要去维护hibernate 的配置文件中对影射文件的引用,当然也有弊端,多人开发时,关于版本控制冲突,以及可读性降低。 以上建议仅供参考。
在讲述关联关系之前,有必要解释下一些专用词的含义
级 联在编程中经常接触,写过触发器来修改或删除关联表相记录的一定会知道,触发器的作用是当 主控表信息改变时,用来保证其关联表中数据同步更新。比如一个employee存放职员信息,一个 timecard存放职员的考勤信息,当从职员表中删除一个职员时,timecard表中对应的考勤信息 已经没有意义,因为其所属的职员已不存在,如果继续留在timecard表中就成了没用的也称脏数据。 理想的做法是在删除职员信息的同时将该职员信息对应的考勤信息也删除。在hibernate中如果要达到这个 效果只需要设置cascade属性值即可。当然是否进行级联关系要根据实际情况慎重考虑。
表 与表之间的关联,我们通常将主动发起关联请求的表称为主动表,被关联的表成为被动表,hibernate中 将此概念冠以在表所对应的对象上,因此将主动发起关联请求的对象称为主动对象或主控对象,被关联的对象 称为被动对象或被控对象。hibernate由主动对象维护关联关系,在实际中经常碰到一个对象的关联角色并不 那么明确,如双向关联,这时inverse值用来标明由谁来维护关联关系。设为true时反转控制角色,即由该 属性关联的对象维护关联关系。
前面章节的例子是单表的操作,实际开发中表之间的关联操作是必不可少的。 本章以书籍与出版社之间的关联为例,来讲述一对一的关联操作。
一对一关系在hibernate中以one-to-one表示,本例中以Book类为主动连接方,因此在Book.java中加入 关联Publish的属性。一对一关联在hibernate中有两种方式:
-
主键关联:不需借助外部字段,直接通过两个表的主键进行关联,因此必须保证两个表的主键值一 致,这通常通常借助foreign标识符生成器策略来完成。简单来说,这种情况就是两个表的主键 相等的内连接。
-
唯一外键关联:在主动方加入外键进行关联,这样主动方与被动方的影射关系实际上就成了多对一的关联。
为方便查询,在此描述one-to-one节点的属性含义(也可参考hibernate的官方指导手册,有中英文对照很方便)
<one-to-one
name="propertyName" (1)
class="ClassName" (2)
cascade="all|none|save-update|delete" (3)
constrained="true|false" (4)
outer-join="true|false|auto" (5)
property-ref="propertyNameFromAssociatedClass" (6)
access="field|property|ClassName" (7)
/>
(1) | name:映射属性的名称。 |
(2) | class(可选):被关联的类的名称,如果省略此属性,则通过反射机制得到与此属性名称一致的类。 |
(3) | cascade(可选):表明操作是否从父对象级联到被关联的对象,all,为所有变更动作都进行级联操作;none,为 从来不作级联操作;save-update,为insert,update动作时作级联操作。delete,为delete动作时作级联操作。 |
(4) | constrained(可选):表明该类对应的表对应的数据库表,和被关联的对象所对应的数据库表之间,通过一个外键 引用对主键进行约束。这个选项影响save()和delete()在级联执行时的先后顺序。 |
(5) | outer-join(可选):是否允许外连接抓取;默认是auto,关联对象没有采用proxy机制时使用外联接。 |
(6) | property-ref(可选):指定关联类的一个属性,这个属性将会和本外键相对应。默认为关联类的主键。 |
(7) | access(可选):Hibernate用来访问属性的策略,默认是property. |
首先来看通过主键进行一对一的关联操作:
1、建立映射文件
例 4.13. hibernate_map.xml
<hibernate-mapping>
<!--one to one-->
<class name="hibernate.relation.oneToOne.Book" table="Book">
<id name="id" column="id" type="java.lang.Integer">
<generator class="foreign">
<param name="property">publish</param>
</generator>
</id>
<property name="name" type="java.lang.String" column="name" length="100" not-null="true" />
<property name="price" type="long" column="price" length="100" not-null="true" />
<one-to-one name="publish" class="hibernate.relation.oneToOne.Publish" cascade="none" outer-join="auto" constrained="false" />
</class>
<class name="hibernate.relation.oneToOne.Publish" table="Publish">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"></generator>
</id>
<property name="name" type="java.lang.String" column="name" length="100" not-null="true" />
<property name="address" type="java.lang.String" column="address" length="100" not-null="true" />
</class>
</hibernate-mapping>
2、建立映射类
例 4.14. Book.java
package hibernate.relation.oneToOne;
import java.io.Serializable;
import org.apache.commons.lang.builder.ToStringBuilder;
public class Book implements Serializable {
private Integer id;
private String name;
private long price;
private Publish publish = null;
public Book(Integer id, String name, long price) {
this.id = id;
this.name = name;
this.price = price;
}
public Book() {
}
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public long getPrice() {
return this.price;
}
public void setPrice(long price) {
this.price = price;
}
public Publish getPublish()
{
return this.publish;
}
public void setPublish(Publish publish)
{
this.publish = publish;
}
public String toString() {
return new ToStringBuilder(this)
.append("id", getId())
.toString();
}
}
例 4.15. Publish.java
package hibernate.relation.oneToOne;
import java.io.Serializable;
import org.apache.commons.lang.builder.ToStringBuilder;
public class Publish implements Serializable {
private Integer id;
private String name;
private String address;
public Publish(Integer id, String name, String address) {
this.id = id;
this.name = name;
this.address = address;
}
public Publish() {
}
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return this.address;
}
public void setAddress(String address) {
this.address = address;
}
public String toString() {
return new ToStringBuilder(this)
.append("id", getId())
.toString();
}
}
3、演示代码
例 4.16. BookTest.java
public class BookTest extends TestCase
{
private String hql;
private SessionFactory sessionFactory;
private Session session;
private List list;
protected void setUp() throws Exception
{
File file = new File("d:hibernate.cfg.xml");
Configuration config = new Configuration().configure(file);
sessionFactory = config.buildSessionFactory();
session = sessionFactory.openSession();
}
public void testSave() throws HibernateException
{
Book book = new Book();
book.setName("《Basic》");
book.setPrice(Long.parseLong("12"));
Publish publish = new Publish();
publish.setName("机械出版社");
publish.setAddress("北京朝阳");
book.setPublish(publish);
session.save(book);
session.flush();
}
public void tesQuery() throws HibernateException
{
String publishName = null;
hql = "from hibernate.relation.oneToOne.Book as book where book.id = 1";
List books = session.find(hql);
for ( int i=0; i < books.size(); i++ )
{
Book book = (Book)books.get(i);
publishName = book.getPublish().getName();
System.out.println("对应的出版社为:" + publishName);
}
}
}
插入时将执行如下语句:
Hibernate: insert into Publish (name, address) values (?, ?)
Hibernate: insert into Book (name, price, id) values (?, ?, ?)
这是因为我们在Book映射文件中设置了其id值为外键生成策略,所以Hibernate会先插入 Publish,然后用publish的主键值填充Book的主键值,以保证Publish与Book表的主键 值相等。
执行查询语句将执行如下语句:
Hibernate: select book0_.id as id, book0_.name as name, book0_.price as price from Book book0_ where (book0_.id=39 )
Hibernate: select publish0_.id as id0_, publish0_.name as name0_, publish0_.address as address0_ from Publish publish0_ where publish0_.id=?
以上示例通过Book、publish两个表的主键形成关联,接下来看如何通过外键关联完成该例子。
我们在Book表中添加publishId的外键,用来与publish表形成关联。
1、建立映射文件
例 4.17. hibernate_map.xml
<hibernate-mapping>
<!--one to one-->
<class name="hibernate.relation.oneToOne.Book" table="Book">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native" />
</id>
<property name="name" type="java.lang.String" column="name" length="100" not-null="true" />
<property name="price" type="long" column="price" length="100" not-null="true" />
<many-to-one name="publish" class="hibernate.relation.oneToOne.Publish" cascade="all" outer-join="auto"/>
</class>
...
</hibernate-mapping>
只 将book映射文件的one-to-one修改成many-to-one,其他的保持不变,执行BookTest.java文件,将看到 与one-to-one同样的效果。(注意:cascade="all",这里设置级联是必须的,因为在插入book时应该先得到 publishid的值。)
上面的例子都是以Book为主动关联方进行操作,如果需要在操作Publish时获取关联的Book对象, 我们需要在 Publish中加入与Book的关联映射,这样Book与Publish之间就形成了双向关联,这里假设Publish与Book是 一对多的关系,具体操作请看下一章节。
1、在publish映射中加入一对多关系
例 4.18. hibernate_map.xml
...
<class name="hibernate.relation.oneToOne.Publish" table="Publish">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"></generator>
</id>
<property name="name" type="java.lang.String" column="name" length="100" not-null="true" />
<property name="address" type="java.lang.String" column="address" length="100" not-null="true" />
<set name="book" table="book" lazy="false" cascade="none" sort="unsorted" inverse="false">
<key column="publishId" />
<one-to-many class="hibernate.relation.manyToOne.Book" />
</set>
</class>
2、在Publish映射类中加入book属性
例 4.19. Publish.java
private Set book = new HashSet();
public Set getBook()
{
return book;
}
public void setBook(Set book)
{
this.book = book;
}
这样就能在操作Publish时也能获取到与之关联的Book信息,看测试代码:
例 4.20. Publish.java
Publish publish = (Publish)session.get(Publish.class,Integer.valueOf(1));
Set books = publish.getBook();
for (Iterator it = books.iterator(); it.hasNext();)
{
Book book = (Book)it.next();
System.out.println("对应的书籍为:" + book.getName());
}
执行上面的代码显示的结果为:
-
Hibernate: select publish0_.id as id0_, publish0_.name as name0_, publish0_.address as address0_ from Publish publish0_ where publish0_.id=?
-
Hibernate: select book0_.id as id__, book0_.publishId as publishId__, book0_.id as id0_, book0_.name as name0_, book0_.price as price0_, book0_.publishId as publishId0_ from Book book0_ where book0_.publishId=?
-
对应的书籍为:《Basic》
多 对多的关联在实际的开发中也是经常被用到的,假设现有一个员工表来存放所有员工的信息, 一个福利项目表存放福利明细,要记录每个员工享有的福利明细,同一福利项,多个员工均可享有, 一个员工也可以享有多项福利,这就形成了多对多的关联,在这里我们加入一个福利明细表充当 关联其两者的中间表(多对多的关联一般都是通过中间表进行关联的)。看具体实现:
1、表结构如下:
表 4.7. Empolyee_Welfare(员工福利明细表)
id(主键递增) | EmpolyeeID(员工ID) | WelfareID(福利项ID) |
---|---|---|
1 | 1(王一) | 1(饭补) |
2 | 1(王一) | 2(交通补助) |
3 | 1(王一) | 3(岗位补助) |
4 | 2(李二) | 1(饭补) |
5 | 2(李二) | 2(交通补助) |
2、编写影射文件
例 4.21. Hibernate_map.xml
<class name="hibernate.relation.manyToMany.Employee" table="Employee">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"></generator>
</id>
<property name="name" type="java.lang.String" column="name" length="100" not-null="true" />
<property name="job" type="java.lang.String" column="job" length="100" not-null="true" />
<set name="welfare" table="Empolyee_Welfare" lazy="false">
<key column="EmployeeId" />
<many-to-many class="hibernate.relation.manyToMany.Welfare" column="WelfareId" />
</set>
</class>
<class name="hibernate.relation.manyToMany.Welfare" table="Welfare">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"></generator>
</id>
<property name="name" type="java.lang.String" column="name" length="100" not-null="true" />
<property name="money" type="java.lang.Double" column="money" length="18" not-null="true" />
<set name="employee" table="Empolyee_Welfare" lazy="false" inverse="true">
<key column="WelfareId" />
<many-to-many class="hibernate.relation.manyToMany.Employee" column="EmployeeId" />
</set>
</class>
3、编写映射类
例 4.22. Employee.java
public class Employee
{
private int id;
private String name;
private String job;
private Set welfare = new HashSet();
...
public Set getWelfare()
{
return this.welfare;
}
public void setWelfare(Set welfare)
{
this.welfare = welfare;
}
}
例 4.23. Welfare.java
public class Welfare
{
private int id;
private String name;
private Double money;
private Set employee = new HashSet();
...
public Set getEmployee()
{
return this.employee;
}
public void setEmployee(Set employee)
{
this.employee = employee;
}
}
4、测试代码
例 4.24. Employee.java
public void testSave() throws Exception
{
try
{
tx = session.beginTransaction();
Employee employee = new Employee();
employee.setName("王一");
employee.setJob("程序员");
Welfare welfare = new Welfare();
welfare.setMoney(Double.valueOf("250"));
welfare.setName("饭补");
employee.getWelfare().add(welfare);
welfare.getEmployee().add(employee);
session.save(employee);
session.save(welfare);
tx.commit();
}
catch(Exception ex)
{
if ( tx !=null )
{
tx.rollback();
}
}
finally
{
session.close();
}
}
执行以上代码Empolyee、Welfare、Empolyee_Welfare表中将各插入一条数据:
-
Hibernate: insert into Employee (name, job) values (?, ?)
-
Hibernate: insert into Welfare (name, money) values (?, ?)
-
Hibernate: insert into salary (EmployeeId, WelfareId) values (?, ?)
上面的例子中我的测试代码都是通过如下代码来建立hibernate的Session。
File file = new File("d:hibernate.cfg.xml");
Configuration config = new Configuration().configure(file);
sessionFactory = config.buildSessionFactory();
session = sessionFactory.openSession();
这样做的目的是为了更直观的说明对hibernate的使用,本节将演示结合Spring的使用,使代码更为简洁。
首先,修改spring的配置文件,如下:
<beans default-lazy-init="false" default-dependency-check="none" default-autowire="no">
<description>
</description>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName">
<value>org.gjt.mm.mysql.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=GBK</value>
</property>
<property name="username">
<value>root</value>
</property>
<property name="password">
<value></value>
</property>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="mappingResources">
<list>
<value>Hibernate_Map.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.cglib.use_reflection_optimizer">true</prop>
<prop key="hibernate.query.substitutions">true 1, false 0</prop>
</props>
</property>
</bean>
<beans>
上 面增加的“dataSource”和“sessionFactory”节点,可以完全替换掉hibernate.cfg.xml的配置信息, 并且在org.springframework.orm.hibernate.LocalSessionFactoryBean类中封装了对 hibernate的 session调用。请看如下调用代码:
SessionFactory sessionFactory = (SessionFactory)InitSpring.getInstance("sessionFactory");
Session session = SessionFactoryUtils.getSession(sessionFactory, false);
session.save(...);
上面的代码通过spring配置文件,实例sessionFactory,然后通过SessionFactoryUtils的getSession方法得到session实例。 关于在程序启动时怎样初始化Spring,请参考???
我们还可以通过Spring来实现Hibernate DAO,得到更简洁的调用:
<bean id="DAOTarget" class="hibernate.dao.test.MyDAO"
singleton="true"
lazy-init="default"
dependency-check="default"
autowire="default"
>
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
例 4.25. MyDAO.java
public class MyDAO extends HibernateDaoSupport
{
getHibernateTemplate().save(...);
}
上面的代码如果看的不是很明白,没有关系,在DAO章节将详细讲述其实现,这里代码只是想说明, 借助spring能让hibernate的应用更加简洁。
在 开始本章之前,先介绍DAO模式的概念,DAO是Data Access Object的缩写,DAO模式思想是将业务逻辑代码与 数据库交互代码分离,降低两者耦合。通过DAO模式可以使结构变得更为清晰,代码更为简洁,本节示例将结合Spring, 演练Hibernate Dao所带来的优越性。
为什么借助Spring实现Hibernate DAO:
-
Spring帮我们封装了针对Hibernate DAO的常用操作。
-
将业务对象与DAO对象的依赖转移到Spring配置文件中。
-
借助Spring AOP功能实现DAO对象中数据库访问的统一事务管理。
在开始演练Spring DAO之前,请看一段不借助DAO模式的代码:
例 4.26. MyApp.java
public void AddUser()
{
SessionFactory sessionFactory = (SessionFactory)InitSpring.getInstance("sessionFactory");
Session session = SessionFactoryUtils.getSession(sessionFactory, false);
User user = new User();
user.setName("王一");
String hql = " from User as user where user.name = '"+ user.name +"' "
List list = session.find(hql);
//进行逻辑判断,如果已经存在相同的用户则不允许添加
if ( list.size() > 0 )
{
...
}
else
{
try
{
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
}
catch(Exception ex)
{
if ( tx !=null )
{
tx.rollback();
...
}
}
finally
{
...
session.close();
}
}
}
以上的代码实现的功能是添加一个用户,且在添加用户之前判断数据库是否有同名用户,应该说逻辑不算复杂, 我们将这种业务逻辑与数据库访问代码写在一块有如下不足之处:
-
降低重用性:该例子中实现了查找用户和添加用户的代码,这种功能很肯能在项目的很多地方都需要用到,但是 我们又不能直接调用AddUser()中实现的类似代码,因为该方法与具体的业务逻辑邦定在一块。
-
有可能降低代码可读性:当业务逻辑变得复杂时,在该例中既要实现业务逻辑,又要实现数据库访问代码。
-
造成重复编码:从上面例子的代码中我们可以看到附加了业务逻辑以外的代码,如事务管理、异常捕获。 最理想的业务类完成的功能应该是只实现其自身的业务逻辑。
我们期望的代码是:
例 4.27. MyApp.java
public void AddUser()
{
User user = new User();
user.setName("王一");
if ( findUser(user).size() >0 )
{
...
}
else
{
saveUser(user);
}
}
下面借助Spring来实现Hibernate Dao,来看看是否能达到我们期望的效果:
1、将访问数据库的代码从业务类MyApp.java中分离出来,放在DAO对象UserDao.java中
例 4.28. UserDao.java
public class UserDao extends HibernateDaoSupport
{
public List findUser(User user)
{
String hql = " from User as user where user.name = '"+ user.name +"' ";
return getHibernateTemplate().find(hql);
}
public void saveUser(User user)
{
getHibernateTemplate().save(user);
}
}
2、将业务类MyApp.java与DAO对象关联(在MyApp.java类中增加DAO的属性,以便通过Spring 注入DAO实例)。
例 4.29. Spring_Config.xml
<bean id="userDao" class="hibernate.dao.test.UserDao"
singleton="true"
lazy-init="default"
dependency-check="default"
autowire="default"
>
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
<bean id="myApp" class="hibernate.dao.test.MyApp"
singleton="true"
lazy-init="default"
dependency-check="default"
autowire="default">
<property name="dao">
<ref local="userDao" />
</property>
</bean>
3、业务类MyApp.java的实现
例 4.30. MyApp.java
public class MyApp
{
private UserDao dao;
public void setDao(UserDao dao)
{
this.dao = dao;
}
public void AddUser()
{
User user = new User();
user.setName("王一");
if ( dao.findUser(user).size() >0 )
{
...
}
else
{
dao.saveUser(user);
}
}
}
从MyApp.java可以看到与我们期望的效果几乎一样,在实际开发中我们应该让业务类与DAO类分别针对其接口 做实现,这样在代码中可以只针对于接口做引用,从而降低调用类与具体实现类的耦合,这里为更简洁的说明问 题省略对其各自接口的定义。
log4j 是用于java语言的日志记录工具,一个完整的商业软件,日志是必不可少的。现实开发 中日志记录多种多样,有打印在控制台中,有记录成文本文件,有保存到数据库中等。日志信息也许需要 分为调试日志,运行日志,异常日志等。这些虽然实现简单,但是也繁琐。本章将介绍用log4j来实现日志 记录的种种情况。
1、下载log4j,http://logging.apache.org/log4j,将log4j.jar拷贝到项目的lib中, 并引用。
2、建立log4j的配置文件,本文中命名为log4j.xml,内容如下:
例 5.1. log4j.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> (1)
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="file" class="org.apache.log4j.FileAppender"> (2)
<param name="File" value="D:/mytest.log" /> (3)
<layout class="org.apache.log4j.PatternLayout"> (4)
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c:(%F:%L) %n - %m%n"/>
</layout>
</appender>
<root>
<priority value ="INFO"/> (5)
<appender-ref ref="file" /> (6)
</root>
</log4j:configuration>
(1) | 申明验证该文档的dtd文件,”SYSTEM“说明是从本地寻找,因此需将log4j.dtd文件放入申明的路径中 |
(2) | 该 节点配置成日志以文件形式输出(org.apache.log4j.FileAppender)。log4j还提供打印日志到控制台 (org.apache.log4j.ConsoleAppender),以信息流格式传送到任何地方 (org.apache.log4j.WriterAppender)。 |
(3) | 指定日志文件的路径和名称。 |
(4) | 指 定记录日志的布局格式。log4j提供以html格式的布局(org.apache.log4j.HTMLLayout),自定义布局格式 (org.apache.log4j.PatternLayout),包含一些简单的日志信息,级别和信息字符串 (org.apache.log4j.SimpleLayout) 包含详细的日志信息,如时间、线程、类别等信息。 |
(5) | 设置日志输出的级别。log4j的日志常用级别如下,按优先级别从高到低分为: Fatal:显示致命错误。 Error:显示错误信息。 Warn:显示警告信息。 Info:显示程序运行日志。 debug:显示调试信息。 只有日志级别大于或等于被设置级别,相应的日志才被记录。如本配置文件配置级别为Info,程序中除了debug级别的日志, 其它级别的日志都会被输出。 |
(6) | 引用输出日志的方式。 |
3、演示使用log4j记录日志
例 5.2. MyLog.java
package com.m5;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
public class MyLog {
public static void main(String[] args)
{
String log4j = "d:log4j.xml" ;
DOMConfigurator.configure(log4j);
Logger logger = Logger.getLogger(MyLogServlet.class.getName());
try
{
int i = 10;
int n = 0 ;
int m = 10/0;
}
catch(Exception ex)
{
logger.info(ex.toString());
}
}
}
上 面的程序会将捕捉到的异常信息写入D:/mytest.log文件。日志的书写格式输出目的地均可以通过log4j进行配置。 关于具体配置请参考log4j.xml及其说明,在此不再一一演示其效果。从上面的演示代码中可以看出对log4j的引用 也非常简洁,在实际运用中可以优化下导入log4j的配置文件部分。MyLog.java中的代码是为了最简捷清晰的说明对 log4j的使用。
4、实际环境中的应用(以web服务为tomcat的web项目为例)
在每个类中记录日志之前都敲一次装载log4j的配置文件的代码,显然是不合理的。通常我们在程序启动之前完成这些初始化工作。 spring一文中描述了对spring配置文件的初始化方法,同样,初始化log4j配置文件的装载也可以通过这个自定义的启动类完成。
例 5.3. web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
<display-name>Test</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring_bean.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--
<servlet>
<servlet-name>context</servlet-name>
<servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
-->
<servlet>
<servlet-name>Init</servlet-name>
<servlet-class>com.m5.Base</servlet-class>
<init-param>
<param-name>log4jConfigLocation</param-name> (1)
<param-value>/WEB-INF/log4j.xml</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
</web-app>
(1) | 配置log4j的配置文件路径。 |
例 5.4. Base.java
public class Base extends HttpServlet
{
WebApplicationContext wac = null;
public void init(ServletConfig config) throws ServletException
{
//ApplicationContext ac = new FileSystemXmlApplicationContext("bean.xml");
wac = WebApplicationContextUtils.getRequiredWebApplicationContext(config.getServletContext());
super.init(config);
InitSpring.Init((AbstractApplicationContext)wac);
String root = getServletConfig().getServletContext().getRealPath("/");
String log4j = getInitParameter("log4jConfigLocation"); (1)
DOMConfigurator.configure(root + log4j); (2)
}
}
通过以上配置,项目中可以通过如下引用:
例 5.5. MyLog.java
package com.m5;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
public class MyLog {
public static void main(String[] args)
{
//String log4j = "d:log4j.xml" ;
//DOMConfigurator.configure(log4j);
Logger logger = Logger.getLogger(MyLogServlet.class.getName());
try
{
int i = 10;
int n = 2 ;
int m = i/n;
}
catch(Exception ex)
{
logger.info(ex.toString());
}
}
}
规范合理的日志记录能让开发人员和维护人员事半功倍,在记录日志时还应该考虑不同的角色对日志内容可能会有 不同的需求。比如,软件正常情况下提供给用户的日志应该简洁明了,调试时提供给程序员的日志应该详细明确。 请看如下代码:
package com.m5;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
public class MyLog {
public static void main(String[] args)
{
String log4j = "d:log4j.xml" ;
DOMConfigurator.configure(log4j);
Logger logger = Logger.getLogger(MyLogServlet.class.getName());
try
{
int i = 10;
int n = 2 ;
int m = i/n;
logger.debug("以下信息为除法器运算跟踪:");
logger.warn("请注意被除数不能为0");
logger.info("除数为" + Integer.toString(i));
logger.info("被除数为" + Integer.toString(n));
logger.info("运算结果为:" + Integer.toString(m) );
}
catch(Exception ex)
{
logger.error(ex.toString());
}
}
}
调 试的时候我们可以在log4j.xml配置文中指定级别为debug,输出所有日志。正式运行时将级别设定为error,只输出 错误日志。这样就不用每次在软件正式使用前注释或者删除调试的信息了。可以想象一下,如果要注释成百上千个段调试 代码,也是项繁琐的工作,再说在正式运行的时候如果出错,想看详细信息又得修改原代码,然后再编译。特别是异地非 远程控制的情况下如果要得到详细的调试日志那是件苦不堪言的事情,因为用户不会帮你去改代码。
log4j的配置文件还可以是属性文件,在此不再另述。