12.6 服务层功能实现

BookService.java

/MyBookApp/src/com/service/BookService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.service;

import java.util.List;
import com.domain.Book;

public interface BookService {
/**
* 查找所有的图书.
*
* @return Book对象的List集合.
*/
List<Book> getAllBooks();
}

BookServiceImpl.java

/MyBookApp/src/com/service/BookServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.service;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.domain.Book;
import com.mapper.BookMapper;

/**
* Book服务层接口实现类.
*/
// @Service("bookService")注解高数Spring创建当前类的一个名为bookService的bean.
@Service("bookService")
public class BookServiceImpl implements BookService {
/**
* 自动注入BookMapper.
*/
@Autowired
private BookMapper bookMapper;

/**
* BookService接口的getAllBooks()方法的实现.
*
* @see{BookService}
*/
@Override
public List<Book> getAllBooks() {
return bookMapper.selectAllBooks();
}
}

UserService.java

/MyBookApp/src/com/service/UserService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.service;

import com.domain.User;

public interface UserService {
/**
* 判断用户登录.
*
* @param loginname
* @param password
* @return 找到用户则返回User对象,找不到返回null
*/
User login(String loginname, String password);
}

UserServiceImpl.java

/MyBookApp/src/com/service/UserServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.domain.User;
/**
* User服务层接口实现.
*/
// @Service("userService")表示当前类作为Spring的一个bean,名为userService
import com.mapper.UserMapper;

@Service("userService")
public class UserServiceImpl implements UserService {
/**
* 自动注入UserMapper
*/
@Autowired
private UserMapper userMapper;

/**
* UserServiec接口的login方法实现.
*
* @see{UserService}
*/
@Override
public User login(String loginname, String password) {
return userMapper.selectUserByLoginnamePassword(loginname, password);
}
}

服务层使用了Spring@Autowired注解自动注入持久层的Mapper对象,并且使用了@Service注解将类注释成为SpringBean

12.5 持久层功能实现

PO类

User.java

/MyBookApp/src/com/domain/User.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.domain;
public class User {
private Integer id;
private String loginname;
private String password;
private String username;
private String phone;
private String address;
public User() {
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString() {
return "User [id=" + id + ", loginname=" + loginname + ", password=" + password + ", username=" + username
+ ", phone=" + phone + ", address=" + address + "]";
}
}

Book.java

/MyBookApp/src/com/domain/Book.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.domain;
import java.util.Date;
public class Book {
// id
private Integer id;
// 书名
private String name;
// 作者
private String author;
// 出版日期
private Date publicationdate;
// 出版社
private String publication;
// 价格
private Double price;
// 封面图片
private String image;
// 详细描述
private String remark;
public Book() {
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString() {
return "Book [id=" + id + ", name=" + name + ", author=" + author + ", publicationdate=" + publicationdate
+ ", publication=" + publication + ", price=" + price + ", image=" + image + ", remark=" + remark + "]";
}
}

Mapper接口

UserMapper.java

/MyBookApp/src/com/mapper/UserMapper.java
1
2
3
4
5
6
7
8
9
10
package com.mapper;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import com.domain.User;

public interface UserMapper {
@Select("select * from tb_user where loginname=#{loginname} and password=#{password}")
User selectUserByLoginnamePassword(@Param("loginname") String loginname, @Param("password") String password);
}

BookMapper.java

/MyBookApp/src/com/mapper/BookMapper.java
1
2
3
4
5
6
7
8
9
10
package com.mapper;

import java.util.List;
import org.apache.ibatis.annotations.Select;
import com.domain.Book;

public interface BookMapper {
@Select("select * from tb_book")
List<Book> selectAllBooks();
}

持久层包括和数据库表映射的User.javaBook.java两个Java Bean对象,并使用了MyBatis的注解映射了对应的SQL语句

12.4 完成配置文件

新建一个项目MyBookApp,加入在12.2节中准备的jar包。

然后在项目中添加如下文件。

db.properties

/MyBookApp/src/db.properties
1
2
3
4
5
6
7
8
dataSource.driverClass=com.mysql.jdbc.Driver
dataSource.jdbcUrl=jdbc:mysql://127.0.0.1:3306/mybatis
dataSource.user=root
dataSource.password=root
dataSource.maxPoolSize=20
dataSource.maxIdleTime = 1000
dataSource.minPoolSize=6
dataSource.initialPoolSize=5

log4j.xml

/MyBookApp/src/log4j.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration
PUBLIC "-//LOG4J//DTD LOG4J//EN"
"https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd" >
<log4j:configuration>
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%5p [%t] %m%n" />
</layout>
</appender>
<logger name="com.mapper">
<level value="DEBUG" />
</logger>
<root>
<level value="ERROR" />
<appender-ref ref="STDOUT" />
</root>
</log4j:configuration>

applicationContext.xml

/MyBookApp/WebContent/WEB-INF/applicationContext.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd">
<!-- mybatis:scan会将org.fkit.mapper包里的所有接口当作mapper配置,之后可以自动引入mapper类 -->
<mybatis:scan base-package="org.fkit.mapper" />
<!-- 扫描org.fkit包下面的java文件,有Spring的相关注解的类,则把这些类注册为Spring的bean -->
<context:component-scan base-package="org.fkit" />
<!-- 使用PropertyOverrideConfigurer后处理器加载数据源参数 -->
<context:property-override
location="classpath:db.properties" />
<!-- 配置c3p0数据源 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource" />
<!-- 配置SqlSessionFactory,org.mybatis.spring.SqlSessionFactoryBean是Mybatis社区开发用于整合Spring的bean -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean"
p:dataSource-ref="dataSource" />
<!-- JDBC事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource" />
<!-- 启用支持annotation注解方式事务管理 -->
<tx:annotation-driven
transaction-manager="transactionManager" />
</beans>

springmvc-config.xml

/MyBookApp/WebContent/WEB-INF/springmvc-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 自动扫描该包,SpringMVC会将包下用了@controller注解的类注册为Spring的controller -->
<context:component-scan
base-package="org.fkit.controller" />
<!-- 设置默认配置方案 -->
<mvc:annotation-driven />
<!-- 使用默认的Servlet来响应静态文件 -->
<mvc:default-servlet-handler />
<!-- 视图解析器 p:prefix属性表示前缀 p:suffix 表示后缀 -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/content/" p:suffix=".jsp" />
</beans>

web.xml

/MyBookApp/WebContent/WEB-INF/web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID"
version="3.1">
<!-- 定义Spring MVC的前端控制器 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- 让Spring MVC的前端控制器拦截所有请求 -->
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 配置spring核心监听器,默认会以 /WEB-INF/applicationContext.xml作为配置文件 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- contextConfigLocation参数用来指定Spring的配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>
<!-- 编码过滤器 -->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

12.3 准备数据库资源

mybatis数据库中创建两个表tb_usertb_book,并插入测试数据,SQL脚本如下:

G:\workspace_web2\MyBookApp\src\mybatis.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
Navicat MySQL Data Transfer

Source Server : localhost_3306
Source Server Version : 50508
Source Host : localhost:3306
Source Database : mybatis

Target Server Type : MYSQL
Target Server Version : 50508
File Encoding : 65001

Date: 2019-09-11 20:47:15
*/
-- 关闭外键检查
SET FOREIGN_KEY_CHECKS=0;

-- 创建tb_user表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`loginname` varchar(50) DEFAULT NULL,
`password` varchar(18) DEFAULT NULL,
`username` varchar(18) DEFAULT NULL,
`phone` varchar(18) DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `loginname` (`loginname`)
);
-- 插入数据
INSERT INTO `tb_user` VALUES (1, 'Donald Trump', '123456', '唐纳德·特朗普', '123456789123', '上海');
DROP TABLE IF EXISTS `tb_book`;
-- 创建tb_book
CREATE TABLE `tb_book` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(54) DEFAULT NULL,
`author` varchar(54) DEFAULT NULL,
`publicationdate` date DEFAULT NULL,
`publication` varchar(150) DEFAULT NULL,
`price` double DEFAULT NULL,
`image` varchar(54) DEFAULT NULL,
`remark` varchar(600) DEFAULT NULL,
PRIMARY KEY (`id`)
);
-- 插入数据
INSERT INTO `tb_book` VALUES (1, '创:美国商界巨子特朗普的商业法则', '唐纳德·特朗普', '2018-10-01', ' 中华工商联合出版社', 72.1, 'images/创:美国商界巨子特朗普的商业法则2.jpg', '在本书中,美国地产大王唐纳德·特朗普和北美最大继续教育学校总裁比尔·赞克通过正反两方面的大量案例和他们东山再起、飞速发展、逆市飞扬的真实故事,从独特的视角分析和看待美国商业界中的传奇,告诉读者他们的商业法则和成功秘密――胸怀大志、敢想敢干。');
-- 开启外键检查
SET FOREIGN_KEY_CHECKS=1;

9.3.5 命令模式

考虑这样一种场景:某个方法需要完成某一个功能,完成这个功能的大部分步骤已经确定了,但可能有少量具体步骤无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。
这个要求看起来有点奇怪:这个方法不仅要求参数可以变化,甚至要求方法执行体的代码也可以变化,需要能把“处理行为”作为一个参数传入该方法。
在某些编程语言(如RubyPerl等)中,确实允许传入一个代码块作为参数。从Java8开始支持的Lambda表达式也可以实现这种功能.
对于这样的需求,要求把”处理行为”作为参数传入该方法,而”处理行为”用编程来实现就是段代码。那如何把这段代码传入某个方法呢?在Java语言中,类才是一等公民,方法也不能独立存在,所以实际传入该方法的应该是一个对象,该对象通常是某个接口的匿名实现类的实例,该接口通常被称为命令接口,这种设计方式也被称为命令模式。

程序示例

1
2
3
4
5
6
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Command
└─src\
├─Command.java
├─CommandTest.java
├─LambdaTest.java
└─ProcessArray.java

下面的程序先定义一个ProcessArray类,该类里包含一个each方法用于处理数组,但具体如何处理暂时不能确定,所以each()方法里定义了一个Command参数。

1
2
3
4
5
6
7
8
public class ProcessArray
{
// 定义一个each()方法,用于处理数组,
public void each(int[] target, Command cmd)
{
cmd.process(target);
}
}

上面定义each()方法时,指定了一个Command形参,这个Command接口用于定义一个process()方法,该方法用于封装对数组的”处理行为”。下面是该Command接口代码。

1
2
3
4
5
public interface Command
{
// 接口里定义的process()方法用于封装“处理行为”
void process(int[] target);
}

上面的Command接口里定义了一个process()方法,这个方法用于封装”处理行为”,但这个方法没有方法体—因为现在还无法确定这个处理行为。
下面是主程序调用ProcessArray对象each()方法的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CommandTest
{
public static void main(String[] args)
{
ProcessArray pa = new ProcessArray();
int[] target ={3, -4, 6, 4};
// 第一次处理数组,具体处理行为取决于Command对象
pa.each(target, new Command()
{
// 重写process()方法,决定具体的处理行为
public void process(int[] target)
{
for (int tmp : target)
{
System.out.println("迭代输出目标数组的元素:" + tmp);
}
}
});
System.out.println("------------------");
// 第二次处理数组,具体处理行为取决于Command对象
pa.each(target, new Command()
{
// 重写process()方法,决定具体的处理行为
public void process(int[] target)
{
int sum = 0;
for (int tmp : target)
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
}
});
}
}

正如上面的程序中两段粗体字代码所示,程序两次调用ProcessArray对象的each()方法来处理数组对象,每次调用each()方法时传入不同的Command匿名实现类的实例,不同的Command实例封装了不同的”处理行为”。
运行上面程序,将看到如下结果。

1
2
3
4
5
6
迭代输出目标数组的元素:3
迭代输出目标数组的元素:-4
迭代输出目标数组的元素:6
迭代输出目标数组的元素:4
------------------
数组元素的总和是:9

上面的运行结果显示了两次不同处理行为的结果,也就实现了process()方法和”处理行为”的分离,两次不同的处理行为分别由两个不同的Command对象来提供。

Java8新增了Lambda表达式功能,Java8允许使用Lambda表达式创建函数式接口的实例。

什么是函数式接口

所谓函数式接口,指的是只包含一个抽象方法的接口。上面程序中的Command接口就是函数式接口。因此CommandTest可改写为如下形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LambdaTest
{
public static void main(String[] args)
{
ProcessArray pa = new ProcessArray();
int[] target ={3, -4, 6, 4};
// 第一次处理数组,具体处理行为取决于Lambda表达式
pa.each(target, array ->
{
for (int tmp : array)
{
System.out.println("迭代输出目标数组的元素:" + tmp);
}
});
System.out.println("------------------");
// 第二次处理数组,具体处理行为取决于Lambda表达式
pa.each(target, array ->
{
int sum = 0;
for (int tmp : array)
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
});
}
}

理解了这个命令模式后,相信读者对Spring框架中HibernateTemplateexecute方法找到了一点感觉, HibernateTemplate使用了execute()方法弥补了HibernateTemplate的不足,该方法需要接受一个HibernateCallback接口,该接口的代码如下:

1
2
3
4
5
// 定义一个`HibernateCallback`接口,该接口封装持久化处理行为
public interface HibernateCallback
{
Object doInHibernate(Session session);
}

上面的HibernateCallback接口就是一个典型的Command接口,一个HibernateCallback对象封装了自定义的持久化处理
HibernateTemplate而言,大部分持久化操作都可通过一个方法来实现, HibernateTemplate对象简化了Hibernate的持久化操作,但丢失了使用Hibernate持久化操作的灵活性.
通过HibernateCallback就可以弥补HibernateTemplate灵活性不足的缺点,当调用HibernateTemplateexecute()方法时,传入Hibernate Callback对象的dolnhibernateo方法就是自定义的持久化处理—即将自定义的持久化处理传入了execute()方法。下面的代码片段使用Lambda表达式来实现该功能。

1
2
3
4
5
6
7
8
9
10
List list=getHibernateTemplate()
.execute(session ->{
//执行 Hibernate分页查询
List result=session.createQuery(hql)
.setFirstResult(offset);
.setMaxResults(pageSize);
.list();
return result;
});
return list;

上面程序中的粗体字代码块将直接传给HibernatTemplate,HibernatTemplate将直接使用该代码块来执行持久化查询,并将查询得到的结果作为execute()方法的返回值。

9.3.2 简单工厂

依赖关系

对于一个典型的Java应用而言,应用之中各实例之间存在复杂的调用关系(Spring把这种调用关系称为依赖关系,例如A实例调用B实例的方法,则称为A依赖于B)。

硬编码耦合

当A对象需要调用B对象的方法时,许多初学者会选择使用new关键字来创建一个B实例,然后调用B实例的方法。从语法的角度来看,这种做法没有任何问题,这种做法的坏处在于:A类的方法实现直接调用了B类的类名(这种方式也被称为硬编码耦合),一旦系统需要重构:需要使用C类来代替B类时,程序不得不改写A类代码。如果应用中有100个或10000个类以硬编码方式耦合了B类,则需要重新改写100个、10000个地方……这显然是一种非常可怕的事情。

换一个角度来看这个问题:对于A对象而言,它只需要调用B对象的方法,并不是关心B对象的实现、创建过程。考虑让B类实现一个IB接口,而A类只需要IB接口耦合。具体做法是:
定义一个工厂类:IBFactory,IBFactory工厂类来负责创建IB实例;而A类通过调用IBFactory工厂的方法来得到IB的实例。这样A类就不用直接使用new关键字来创建B实例。

通过改用上面设计,则A类不但需要与IBFactory耦合,还需要与IB接口耦合;如果系统需要重构:需要使用C类代替B类,则只需要让C类也实现IB接口,并改写IBFactory工厂中创建IB实例的实现代码,让该工厂产生实现了IB接口的C实例即可。由于所有依赖IB实例的对象都是通过工厂来获取IB实例的,所以它们都将改为获得C实例,这就完成了系统重构。

这种将多个类对象交给工厂类来生成的设计方式被称为简单工厂模式

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\SimpleFactory
└─src\
├─BetterPrinter.java
├─Computer.java
├─Output.java
├─OutputFactory.java
└─Printer.java

下面以一个简单的场景来介绍简单工厂模式。假设程序中有个Computer对象需要依赖一个输出设备,现在有两个选择:直接让Computer对象依赖一个Printer(实现类)对象,或者让Computer依赖Output(接口)属性。
在这种应用场景下,使用简单工厂模式可以让系统具有更好的可维护性、可扩展性。根据工厂模式,程序应该让Computer依赖一个Output属性,将Computer类与Printer实现类分离开来。 Computer对象只需面向Output接口编程即可。

下面是这个Computer类定义的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Computer
{
private Output out;

public Computer(Output out)
{
this.out = out;
}
// 定义一个模拟获取字符串输入的方法
public void keyIn(String msg)
{
out.getData(msg);
}
// 定义一个模拟打印的方法
public void print()
{
out.out();
}
public static void main(String[] args)
{
// 创建OutputFactory
OutputFactory of = new OutputFactory();
// 将Output对象传入,创建Computer对象
Computer c = new Computer(of.getOutput());
c.keyIn("轻量级Java EE企业应用实战");
c.keyIn("疯狂Java讲义");
c.print();
}
}

从上面粗体字代码可以看出,该Computer类已经完全与Output实现类分离了,它只与该接口耦合。而且, Computer不再负责创建Output对象,系统将提供一个Output工厂来负责生成Output对象。这个OutputFactory工厂类代码如下。

1
2
3
4
5
6
7
public class OutputFactory
{
public Output getOutput()
{
return new Printer();
}
}

在该OutputFactory类中包含了一个getOutput()方法,该方法返回一个Output实现类的实例。该方法负责创建Output实例,具体创建哪一个实现类的对象由该方法决定,
Printer类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 让Printer类实现Output
public class Printer implements Output
{
private String[] printData = new String[MAX_CACHE_LINE];
// 用以记录当前需打印的作业数
private int dataNum = 0;
public void out()
{
// 只要还有作业,继续打印
while(dataNum > 0)
{
System.out.println("打印机打印:" + printData[0]);
// 把作业队列整体前移一位,并将剩下的作业数减1
System.arraycopy(printData , 1, printData, 0, --dataNum);
}
}
public void getData(String msg)
{
if (dataNum >= MAX_CACHE_LINE)
{
System.out.println("输出队列已满,添加失败");
}
else
{
// 把打印数据添加到队列里,已保存数据的数量加1。
printData[dataNum++] = msg;
}
}
}

上面的Printer类模拟了一个简单的打印机,如果系统需要重构,需要使用BetterPrinter来代替Printer类,则只需要让BetterPrinter实现Output接口,并改写OutputFactory类的getOutput()方法即可。
下面是BetterPrinter实现类的代码。 BetterPrinter只是对原有的Printer进行简单修改,这里用来模拟系统重构后的改进。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class BetterPrinter implements Output
{
private String[] printData = new String[MAX_CACHE_LINE * 2];
// 用以记录当前需打印的作业数
private int dataNum = 0;
public void out()
{
// 只要还有作业,继续打印
while(dataNum > 0)
{
System.out.println("高速打印机正在打印:" + printData[0]);
// 把作业队列整体前移一位,并将剩下的作业数减1
System.arraycopy(printData , 1, printData, 0, --dataNum);
}
}
public void getData(String msg)
{
if (dataNum >= MAX_CACHE_LINE * 2)
{
System.out.println("输出队列已满,添加失败");
}
else
{
// 把打印数据添加到队列里,已保存数据的数量加1。
printData[dataNum++] = msg;
}
}
}

上面程序中的BetterPrinter类与Printer并无太大区别,仅仅略微改变了Printer实现,且BetterPrinter也实现了Output接口,因此也可当成Output对象使用,因此只要把OutputFactory工厂类的getOutput()方法中改为如下代即可:

1
2
3
4
5
6
7
public class OutputFactory
{
public Output getOutput()
{
return new BetterPrinter();
}
}

再次运行前面的Computer.java程序,发现Computer所依赖的Output对象已改为Better Printer对象,而不再是原来的Printer对象。

通过这种方式,可以把所有生成Output对象的逻辑集中在OutputFactory工厂类中管理,而所有需要使用Output对象的类只需与Output接口耦合,而不是与具体的实现类耦合。即使系统中有很多类依赖了Printer对象,只要OutputFactory类的getOutput()方法返回BetterPrinter对象,则它们全部将会改为依赖BetterPrinter对象,而其他程序无须修改,只需要修改OutputFactory工厂的getOutput()的方法实现即可。

简单工厂模式的优势

使用简单工厂模式的优势是:让对象的调用者和对象创建过程分离,当对象调用者需要对象时,直接向工厂请求即可;从而避免了对象的调用者与对象的实现类以硬编码方式耦合,以提高系统的可维护性、可扩展性。

工厂模式的缺陷

工厂模式也有一个小小的缺陷:当产品修改时,工厂类也要做相应的修改。

Spring容器而言,它首先是一个巨大的工厂,它负责创建所有Bean实例,整个应用的所有组件都由Spring容器负责创建。不仅如此, Spring容器扩展了这种简单工厂模式,它还可以管理Bean实例之间的依赖关系;而且,如果容器中Bean实例具有singleton行为特征,则Spring容器还会缓存该Bean实例,从而保证程序通过Spring工厂来获取该Bean实例时, Spring工厂将会返回同一个Bean实例。

实现简单的IoC容器功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\IoC
├─beans.xml
└─src\
├─lee\
│ ├─BetterPrinter.java
│ ├─Computer.java
│ ├─IoCTest.java
│ ├─Output.java
│ └─Printer.java
└─org\
└─crazyit\
└─ioc\
├─ApplicationContext.java
└─CrazyitXmlApplicationContext.java

下面的示例提供一份类似于Spring配置文件的XML文件,程序提供一个扩展的工厂类,该工厂类也可提供类似于Spring loc容器的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="GBK"?>
<beans>
<bean id="computer" class="lee.Computer">
<!-- 为name注入基本类型的值 -->
<property name="name" value="孙悟空的电脑" />
<!-- 为out注入容器中其他Bean -->
<property name="out" ref="betterPrinter" />
</bean>
<!-- 配置两个Bean实例 -->
<bean id="printer" class="lee.Printer" />
<bean id="betterPrinter" class="lee.BetterPrinter" />
<!-- 配置一个prototype行为的Bean实例 -->
<bean id="now" class="java.util.Date" scope="prototype" /> <!--① -->
</beans>

细心的读者可能已经发现:该配置文件和Spring配置文件如此相似。实际上这个配置文件只是通过简单修改了Spring配置文件得到的。上面的配置文件一样配置了computer Bean,且为该Bean依赖注入了两个属性:nameout除此之外,上面的配置文件中①号代码处还配置了一个prototype行为的Bean实例。
本程序中也提供了一个简化的ApplicationContext接口,该接口仅包含一个getBean()方法。

1
2
3
4
5
6
7
package org.crazyit.ioc;

public interface ApplicationContext
{
// 获取指定Bean实例的方法
Object getBean(String name) throws Exception;
}

本示例将为该接口提供一个简单的实现类,该实现类就是一个功能强大的工厂。它使用Dom4j来解析XML配置文件,并根据配置文件来创建工厂中的Bean实例。下面是该实现类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package org.crazyit.ioc;

import java.lang.reflect.*;
import java.util.*;
import java.io.*;
import org.dom4j.*;
import org.dom4j.io.*;

public class CrazyitXmlApplicationContext implements ApplicationContext
{
// 保存容器中所有单例模式的Bean实例
private Map<String, Object> objPool = Collections
.synchronizedMap(new HashMap<String, Object>());
// 保存配置文件对应的Document对象
private Document doc;
// 保存配置文件里的根元素
private Element root;
public CrazyitXmlApplicationContext(String filePath) throws Exception
{
SAXReader reader = new SAXReader();
doc = reader.read(new File(filePath));
root = doc.getRootElement();
initPool();
initProp();
}

public Object getBean(String name) throws Exception
{
Object target = objPool.get(name);
// 对于singleton Bean,容器已经初始化了所有Bean实例,直接返回即可
if (target.getClass() != String.class)
{
return target;
} else
{
String clazz = (String) target;
// 对于prototype对象并未注入属性值
return Class.forName(clazz).getConstructor().newInstance();
}
}
// 初始化容器中所有singleton Bean
private void initPool() throws Exception
{
// 遍历配置文件里的每个<bean.../>元素
for (Object obj : root.elements())
{
Element beanEle = (Element) obj;
// 取得<bean.../>元素的id属性
String beanId = beanEle.attributeValue("id");
// 取得<bean.../>元素的class属性
String beanClazz = beanEle.attributeValue("class");
// 取得<bean.../>元素的scope属性
String beanScope = beanEle.attributeValue("scope");
// 如果<bean.../>元素的scope属性不存在,或为singleton
if (beanScope == null || beanScope.equals("singleton"))
{
// 以默认构造器创建Bean实例,并将其放入objPool中
objPool.put(beanId, Class.forName(beanClazz).getConstructor()
.newInstance());
} else
{
// 对于非singlton Bean,存放该Bean实现类的类名。
objPool.put(beanId, beanClazz);
}
}
}
// 初始化容器中singleton Bean的属性
private void initProp() throws Exception
{
// 遍历配置文件里的每个<bean.../>元素
for (Object obj : root.elements())
{
Element beanEle = (Element) obj;
// 取得<bean.../>元素的id属性
String beanId = beanEle.attributeValue("id");
// 取得<bean.../>元素的scope属性
String beanScope = beanEle.attributeValue("scope");
// 如果<bean.../>元素的scope属性不存在,或为singleton
if (beanScope == null || beanScope.equals("singleton"))
{
// 取出objPool的指定的Bean实例
Object bean = objPool.get(beanId);
// 遍历<bean.../>元素的每个<property.../>子元素
for (Object prop : beanEle.elements())
{
Element propEle = (Element) prop;
// 取得<property.../>元素的name属性
String propName = propEle.attributeValue("name");
// 取得<property.../>元素的value属性
String propValue = propEle.attributeValue("value");
// 取得<property.../>元素的ref属性
String propRef = propEle.attributeValue("ref");
// 将属性名的首字母大写
String propNameCamelize = propName.substring(0, 1)
.toUpperCase()
+ propName.substring(1, propName.length());
// 如果<property.../>元素的value属性值存在
if (propValue != null && propValue.length() > 0)
{
// 获取设值注入所需的setter方法
Method setter = bean.getClass().getMethod(
"set" + propNameCamelize, String.class);
// 执行setter注入
setter.invoke(bean, propValue);
}
if (propRef != null && propRef.length() > 0)
{
// 取得需要被依赖注入的Bean实例
Object target = objPool.get(propRef);
// objPool池中不存在指定Bean实例
if (target == null)
{
// 此处还应处理Singleton Bean依赖prototype Bean的情形
}
// 定义设值注入所需的setter方法
Method setter = null;
// 遍历target对象所所实现的所有接口
for (Class<?> superInterface : target.getClass()
.getInterfaces())
{
try
{
// 获取设值注入所需的setter方法
setter = bean.getClass().getMethod(
"set" + propNameCamelize,
superInterface);
// 如果成功取得该接口对应的方法,直接跳出循环
break;
} catch (NoSuchMethodException ex)
{
// 如果没有找到对应的setter方法,继续下次循环
continue;
}
}
// 如果setter方法依然为null,
// 则直接取得target实现类对应的setter方法
if (setter == null)
{
setter = bean.getClass().getMethod(
"set" + propNameCamelize,
target.getClass());
}
// 执行setter注入
setter.invoke(bean, target);
}
}
}
}
}
}

上面的CrazyitXmlApplicationContext类当然不能与SpringApplicationContext实现类相比,该容器类仅仅实现了简单的IoC功能,而且并未为prototype行为的Bean的属性提供依赖注入功能。读者可以通过该工厂类大致了解Spring底层的实现原理。
下面是测试该工厂类的主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package lee;

import org.crazyit.ioc.*;

public class IoCTest
{
public static void main(String[] args) throws Exception
{
// 创建IoC容器
ApplicationContext ctx = new CrazyitXmlApplicationContext("beans.xml");
// 从IoC容器中取出computer Bean
Computer c = (Computer) ctx.getBean("computer");
// 测试Computer对象
c.keyIn("轻量级Java EE企业应用实战");
c.keyIn("疯狂Java讲义");
c.print();
System.out.println(ctx.getBean("now"));
}
}

从上面程序中的粗体字代码可以看出,本程序的IoC容器具有和Spring容器类似的功能,同样可以创建并管理容器中所有的Bean实例
与简单工厂模式类似的还有工厂方法和抽象工厂模式,下面将进一步讲解工厂方式和抽象工厂模式的设计方式.

9.3.4 代理模式

代理模式是一种应用非常广泛的设计模式,当客户端代码需要调用某个对象时,客户端实际上也不关心是否准确得到该对象,它只要一个能提供该功能的对象即可,此时就可返回该对象的代理(Proxy)。
在这种设计方式下,系统会为某个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个Java对象代表另一个Java对象来采取行动。在某些情况下,客户端代码不想或不能够直接调用被调用者,代理对象可以在客户和目标对象之间起到中介的作用。
对客户端而言,它不能分辨出代理对象与真实对象的区别,它也无须分辨代理对象和真实对象的区别。客户端代码并不知道真正的被代理对象,客户端代码面向接口编程,它仅仅持有一个被代理对象的接。
总而言之,只要客户端代码不能或不想直接访问被调用对象—这种情况有很多原因,比如需要创建一个系统开销很大的对象,或者被调用对象在远程主机上,或者目标对象的功能还不足以满足需求…而是额外创建一个代理对象返回给客户端使用,那么这种设计方式就是代理模式

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Proxy
└─src\
├─BigImage.java
├─BigImageTest.java
├─Image.java
└─ImageProxy.java

下面示范一个简单的代理模式,程序首先提供了一个Image接口,代表大图片对象所实现的接口。

1
2
3
4
public interface Image
{
void show();
}

该接口提供了一个实现类,该实现类模拟了一个大图片对象,该实现类的构造器使用Thread.sleep()方法来暂停3s。下面是该BigImage的程序代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用该BigImage模拟一个很大图片
public class BigImage implements Image
{
public BigImage()
{
try
{
// 程序暂停3s模式模拟系统开销
Thread.sleep(3000);
System.out.println("图片装载成功...");
} catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
// 实现Image里的show()方法
public void show()
{
System.out.println("绘制实际的大图片");
}
}

上面程序中的粗体字代码暂停了3s,这表明创建一个BigImage对象需要3s的时间开销程序使用这种延迟来模拟装载此图片所导致的系统开销。如果不采用代理模式,当程序中创建BioiMage时,系统将会产生3s的延迟。为了避免这种延迟,程序为BigImage对象提供了一个代理对象, BigImage类的代理类如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ImageProxy implements Image
{
// 组合一个image实例,作为被代理的对象
private Image image;
// 使用抽象实体来初始化代理对象
public ImageProxy(Image image)
{
this.image = image;
}
/**
* 重写Image接口的show()方法 该方法用于控制对被代理对象的访问,
* 并根据需要负责创建和删除被代理对象
*/
public void show()
{
// 只有当真正需要调用image的show方法时才创建被代理对象
if (image == null)
{
image = new BigImage();
}
image.show();
}
}

上面的ImageProxy代理类实现了与BigImage相同的show()方法,这使得客户端代码获取到该代理对象之后,可以将该代理对象当成BigImage来使用。
ImageProxy类的show方法中增加了控制逻辑,这段控制逻辑用于控制当系统真正调用Imageshow()时,才会真正创建被代理的BioImage对象。下面程序需要使用BigImage对象,但程序并不是直接返回BigImage实例,而是先返回BigImage的代理对象,如下面的程序所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BigImageTest
{
public static void main(String[] args)
{
long start = System.currentTimeMillis();
// 程序返回一个Image对象,该对象只是BigImage的代理对象
Image image = new ImageProxy(null);
System.out.println(
"系统得到Image对象的时间开销:" + (System.currentTimeMillis() - start));
// 只有当实际调用image代理的show()方法时,程序才会真正创建被代理对象。
image.show();
}
}

上面的程序初始化Image非常快,因为程序并未真正创建BigImage对象,只是得到了ImageProxy代理对象直到程序调用image.show()方法时,程序需要真正调用BioImage对象的showO方法,程序此时才真正创建BigImage对象。运行上面程序,看到如下所示的结果。

1
2
3
系统得到Image对象的时间开销:0
图片装载成功...
绘制实际的大图片

读者应该能认同:使用代理模式提高了获取Image对象的系统性能,但可能有读者会提出疑问:程序调用ImageProxy对象的show()方法时一样需要创建BigImage对象,系统开销并未真正减少,只是这种系统开销延迟了而已?
可以从如下两个角度来回答这个问题。

  1. 把创建BigImage推迟到真正需要它时才创建,这样能保证前面程序运行的流畅性,而且能减少BigImage在内存中的存活时间,从宏观上节省了系统的内存开销。
  2. 在有些情况下,也许程序永远不会真正调用ImageProxy对象的show()方法—意味着系统根本无须创建BigImage对象。在这种情形下,使用代理模式可以显著地提高系统运行性能。

使用代理节省开销

第二种情况正是Hibernate延迟加载所采用的设计模式,相信读者还记得前面介绍Hibernate关联映射时的知识,当A实体和B实体之间存在关联关系时, Hibernate默认启用延迟加载,当系统加载A实体时,A实体关联的B实体并未被加载出来,A实体所关联的B实体全部是代理对象——只有等到A实体真正需要访问B实体时,系统才会去数据库里抓取B实体所对应的记录。
Hibernate的延迟加载充分体现了代理模式的优势:当系统加载A实体时,也许只需要访问A实体对应的记录,根本不会访问A的关联实体。如果不采用代理模式,系统需要在加载A实体时,同时加载A实体的所有关联实体,这是很大的系统开销。

代理对象增强目标对象的功能

除了上面出于性能考虑使用代理模式之外,代理模式还有另一种常用场景:当目标对象的功能不足以满足客户端需求时,系统可以为该对象创建一个代理对象,而代理对象可以增强原目标对象的功能
借助于Java提供的ProxyInvocationHandler,可以实现在运行时生成动态代理的功能,而动态代理对象就可作为目标对象使用,而且增强了目标对象的功能

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\DynaProxy\src
├─Dog.java
├─GunDog.java
├─MyInvokationHandler.java
├─MyProxyFactory.java
├─Test.java
└─TxUtil.java

由于**JDK动态代理只能创建指定接口的动态代理**,所以下面先提供一个Dog接口,该接口代码非常简单,仅仅在该接口里定义了两个方法。

1
2
3
4
5
6
7
public interface Dog
{
// info()方法声明
void info();
// run()方法声明
void run();
}

上面接口里只是简单定义了两个方法,并未提供方法实现。下面程序先为该接口提供一个实现类该实现类的实例将会作为被代理的目标对象。下面是该接口实现类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class GunDog implements Dog
{
// info方法实现,仅仅打印一个字符串
public void info()
{
System.out.println("我是一只猎狗");
}
// run方法实现,仅仅打印一个字符串
public void run()
{
System.out.println("我奔跑迅速");
}
}

上面的代码没有丝毫的特别之处,该Dog的实现类仅仅为每个方法提供了一个简单实现。现在假设该目标对象(GunDog)实例的两个方法不能满足实际需要,因此客户端不想直接调用该目标对象。假设客户端需要在GunDog为两个方法增加事务控制:在目标方法被调用之前开始事务,在目标方法被调用之后结束事务。
为了实现该功能,可以为GunDog对象创建一个代理对象,该代理对象提供与GunDog对象相同的方法,而代理对象增强了GunDog对象的功能。
下面先提供一个TxUtil类(这个类通常被称为拦截器),该类里包含两个方法,分别用于开始事务、提交事务。下面是TxUtil类的源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TxUtil
{
// 第一个拦截器方法:模拟事务开始
public void beginTx()
{
System.out.println("=====模拟开始事务=====");
}
// 第二个拦截器方法:模拟事务结束
public void endTx()
{
System.out.println("=====模拟结束事务=====");
}
}

借助于ProxyInvocationHandler就可以实现:当程序调用info()方法和run()方法时,系统可以”自动”将beginTx()endTx()两个通用方法插入info()run()方法执行中。
JDK动态代理的关键在于下面的MyInvokationHandler类,该类是一个InvocationHandler实现类,该实现类的invoke方法将会作为代理对象的方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.reflect.*;

public class MyInvokationHandler implements InvocationHandler
{
// 需要被代理的对象
private Object target;
public void setTarget(Object target)
{
this.target = target;
}
// 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Exception
{
TxUtil tx = new TxUtil();
// 执行TxUtil对象中的beginTx()。
tx.beginTx();
// 以target作为主调来执行method方法
Object result = method.invoke(target, args);
// 执行TxUtil对象中的endTx()。
tx.endTx();
return result;
}
}

上面的invoke方法将会作为动态代理对象的所有方法的实现体。上面方法中
tx.beginTx();这行代码调用了开始事务的方法,
Object result = method.invoke(target, args);这行代码通过反射回调了被代理对象的目标方法,
tx.endTx();这行代码调用了结束事务的方法。
通过这种方式,使得代理对象的方法既回调了被代理对象的方法,并为被代理对象的方法增加了事务功能。

下面再为程序提供一个MyProxyFactory类,该对象专为指定的target生成动态代理实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.reflect.*;

public class MyProxyFactory
{
// 为指定target生成动态代理对象
public static Object getProxy(Object target) throws Exception
{
// 创建一个MyInvokationHandler对象
MyInvokationHandler handler = new MyInvokationHandler();
// 为MyInvokationHandler设置target对象
handler.setTarget(target);
// 创建、并返回一个动态代理
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), handler);
}
}

上面的动态代理工厂类提供了一个getProxy()方法,该方法为target对象生成一个动态代理对象,这个动态代理对象与target实现了相同的接口,所以具有相同的public方法—从这个意义上来看,动态代理对象可以当成target对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvokationHandler对象的invoke方法。例如调用动态代理对象的info()方法,程序将开始执行invoke()方法,其执行步骤如下。

  1. 创建TXUtil实例。
  2. 执行TxUtil实例的beginTx()方法。
  3. 使用反射以target作为调用者执行info()方法。
  4. 执行TxUtil实例的endTx()方法。

看到上面的执行过程,读者应该已经发现:使用动态代理对象来代替被代理对象时,代理对象的方法就实现了前面的要求——程序执行info()run()方法时增加事务功能。而且这种方式有一个额外的好处:GunDog的方法中没有以硬编码的方式调用beginTx()endTx()—这就为系统扩展增加了无限可能性:当系统需要扩展(GunDog实例的功能时,程序只需要提供额外的拦截器类,并在MyInvokationHandlerinvoke()方法中回调这些拦截器方法即可。
下面提供一个主程序来测试动态代理的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test
{
public static void main(String[] args) throws Exception
{
// 创建一个原始的GunDog对象,作为target
Dog target = new GunDog();
// 以指定的target来创建动态代理
Dog dog = (Dog) MyProxyFactory.getProxy(target);
// 调用代理对象的info()和run()方法
dog.info();
dog.run();
}
}

上面程序中的dog对象实际上是动态代理对象,只是该动态代理对象也实现了Dog接口,所以也可以当成Dog对象使用。程序执行doginfo()run()方法时,实际上会先执行TxUtilbeginTx()方法,再执行target对象的info()run()方法,最后再执行TxUtilendTx()方法。执行上面的程序,将看到如下所示的结果。

1
2
3
4
5
6
7
=====模拟开始事务=====
我是一只猎狗
=====模拟结束事务=====
=====模拟开始事务=====
我奔跑迅速
=====模拟结束事务=====

从运行结果来看,不难发现采用动态代理可以非常灵活地实现解耦。通过使用这种动态代理,程序就为被代理对象增加了额外的功能

这种动态代理在AOP(Aspect Orient Program,面向切面编程)里被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理
AOP代理所包含的方法与目标对象所包含的方法示意图如图9.6所示。
这里有一张图片
看到此处,相信读者应该对SpringAOP框架有点感觉了:当Spring容器中的被代理Bean实现了一个或多个接口时, Spring所创建的AOP代理就是这种动态代理。 Spring AOP与此示例应用的区别在哪里呢? Spring AOP更灵活,当Sping定义InvocationHandler类的invoke时,它并没有以硬编码方式决定调用哪些拦截器,而是通过配置文件来决定在invoke方法中要调用哪些拦截器,这就实现了更彻底的解耦——当程序需要为目标对象扩展新功能时,根本无须改变Java代理,只需要在配置文件中增加更多的拦截器配置即可。

9.3.3 工厂方法和抽象工厂

在简单工厂模式里,系统使用工厂类生产所有产品实例,且该工厂类决定生产哪个类的实例,即该工厂类负责所有的逻辑判断、实例创建等工作。
如果不想在工厂类中进行逻辑判断,程序可以为不同产品类提供不同的工厂,不同的工厂类生产不同的产品。例如为上面的PrinterBetter Printer分别提供PrinterFactoryBetterPrinterFactory工厂类,这就无须在工厂类进行复杂的逻辑判断。

程序示例

1
2
3
4
5
6
7
8
9
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\FactoryMethod
└─src\
├─BetterPrinter.java
├─BetterPrinterFactory.java
├─Computer.java
├─Output.java
├─OutputFactory.java
├─Printer.java
└─PrinterFactory.java

本示例应用将OutputFactory改为一个接口,并为该接口提供两个实现类:PrinterFactory.javaBetterPrinterFactory.java。下面是OutputFactory接口的代码。

1
2
3
4
5
public interface OutputFactory
{
// 仅定义一个方法用于返回输出设备。
Output getOutput();
}

上面的OutputFactory只是一个接口,该接口提供了一个getOutput0方法,该方法可直接返回一个输出设备。
下面为OutputFactory接口提供一个PrinterFactory实现类,该实现类专门负责生成Printer实例。

1
2
3
4
5
6
7
8
public class PrinterFactory implements OutputFactory
{
public Output getOutput()
{
// 该工厂只负责产生Printer对象
return new Printer();
}
}

上面的PrinterFactory实现了OutputFactory接口,并实现了该接口里的getOutput()方法,该方法直接返回一个简单的Printer对象。
下面再为OutputFactory接口提供一个BetterPrinterFactory实现类,该实现类专门负责生成BetterPrinter实例。

1
2
3
4
5
6
7
8
public class BetterPrinterFactory implements OutputFactory
{
public Output getOutput()
{
// 该工厂只负责产生BetterPrinter对象
return new BetterPrinter();
}
}

本示例应用中各类之间的类图如图9.2所示。
这里有一张图片
当使用工厂方法设计模式时,对象调用者需要与具体的工厂类耦合:当需要不同对象时,程序需要调用相应工厂对象的方法来得到所需的对象。如下是Computer类中创建Output对象并调用该对象方法的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Computer
{
private Output out;

public Computer(Output out)
{
this.out = out;
}
// 定义一个模拟获取字符串输入的方法
public void keyIn(String msg)
{
out.getData(msg);
}
// 定义一个模拟打印的方法
public void print()
{
out.out();
}
public static void main(String[] args)
{
// 使用PrinterFactory子类来创建OutputFactory
OutputFactory of = new BetterPrinterFactory();
// 将Output对象传入,创建Computer对象
Computer c = new Computer(of.getOutput());
c.keyIn("轻量级Java EE企业应用实战");
c.keyIn("疯狂Java讲义");
c.print();
}
}

正如程序中main方法中第一行代码所示,当客户端代码需要调用Ouput对象的方法时,为了得到不同的Output实例,程序必须显式创建不同的OutputFactory实例,程序中创建的是PrinterFactory实例。
从上面的代码可以看出,对于采用工厂方法的设计架构,客户端代码成功与被调用对象的实现类分离,但带来了另一种耦合:客户端代码与不同的工厂类耦合。这依然是一个问题!

抽象工厂模式

为了解决客户端代码与不同工厂类耦合的问题,接着考虑再增加一个工厂类,该工厂类不是生产Output对象,而是生产OutputFactory实例,简而言之,这个工厂类不制造具体的被调用对象,而是制造不同工厂对象。这个特殊的工厂类被称呼抽象工厂类,这种设计方式也被称为抽象工厂模式。如图9.3所示是抽象工厂模式示例的UML类图。
这里有一张图片
从图9.3中可以看出,在这种模式下系统新增了一个OutputFactoryFactory工厂类,该工厂类提供了一个getOutputFactory(String type)方法,该方法用于返回一个OutputFactory工厂实例。

程序示例

1
2
3
4
5
6
7
8
9
10
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\AbstractFactory
└─src\
├─BetterPrinter.java
├─BetterPrinterFactory.java
├─Computer.java
├─Output.java
├─OutputFactory.java
├─OutputFactoryFactory.java
├─Printer.java
└─PrinterFactory.java

下面是该抽象工厂类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OutputFactoryFactory
{
// 仅定义一个方法用于返回输出设备。
public static OutputFactory getOutputFactory(String type)
{
if (type.equalsIgnoreCase("better"))
{
return new BetterPrinterFactory();
} else
{
return new PrinterFactory();
}
}
}

从上面的粗体字代码可以看出,抽象工厂根据type参数进行判断,决定需要生成哪种工厂实例。通过这种设计模式,就可让客户端程序只需与抽象工厂类耦合。下面是客户端调用被调用者对象方法的主方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Computer
{
...
public static void main(String[] args)
{
// 使用OutputFactoryFactory工厂类创建OutputFactory
OutputFactory of = OutputFactoryFactory.getOutputFactory("better");
// 调用OuputFactory的方法获取Output对象,
// 并将Output对象传入,创建Computer对象
Computer c = new Computer(of.getOutput());
c.keyIn("轻量级Java EE企业应用实战");
c.keyIn("疯狂Java讲义");
c.print();
}
}

上面程序中的粗体字代码用于产生一个OutputFactory工厂,但具体产生哪个工厂则由OutputFactoryFactory抽象工厂决定,不同的工厂对象将产生不同的Output对象。

通过采用抽象工厂的设计模式,系统可以让客户端代码与被调用对象的实现类、具体的工厂类分离。

读者掌握了这种抽象工厂模式后,应该对Spring IoC容器感到迷惑:它到底是简单工厂,还是抽象工厂?本书倾向于认为Spring loc容器是抽象工厂,因为Sping loc容器可以包括万象,它不仅可以管理普通Bean实例,也可管理工厂实例。
不要过分纠缠于简单工厂模式、抽象工厂模式这些概念,可以把它们统称为工厂模式

  • 如果工厂直接生产被调用对象,那就是简单工厂模式;
  • 如果工厂生产了工厂对象,那就会升级成抽象工厂模式。

9.3.1 单例模式

有些时候,允许自由创建某个类的实例没有意义,还可能造成系统性能下降(因为创建对象所带来的系统开销问题)。例如整个系统只有一个窗口管理器,只有一个假脱机打印设备;在Java EE应用中可能只需要一个数据库引擎访问点, Hibernate访问时只需要一个SessionFactory实例,如果在系统中为它们创建多个实例就没有太大的意义。
如果一个类始终只能创建一个实例,则这个类被称为单例类,这种模式就被称为单例模式。
Spring框架而言,可以在配置Bean实例时指定scope="singleton"来配置单例模式。不仅如此,如果配置<bean>元素时没有指定scope属性,则该Bean实例默认是单例的行为方式。
Spring推荐将所有业务逻辑组件、**DAO组件数据源组件**等配置成单例的行为方式,因为这些组件无须保存任何用户状态,故所有客户端都可共享这些业务逻辑组件、DAO组件,因此推荐将这些组件配置成单例模式的行为方式。
即使不借助Spring框架,也可手动实现单例模式。为了保证该类只能产生一个实例,程序不能允许自由创建该类的对象,而是只允许为该类创建一个对象。

实现单例模式的步骤

  1. 为了避免程序自由创建该类的实例,使用private修饰该类的构造器,从而将该类的构造器隐藏起来
  2. 将该类的构造器隐藏起来之后,则需要提供一个public方法用于创建该类的对象,因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象只能是类,所以该方法必须使用static修饰。
  3. 除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过实例,也就无法保证只创建一个实例。为此该类需要使用一个属性来保存曾经创建的实例,因为该属性需要被静态方法访问,所以该属性要使用static修饰.

单例类实例

1
2
3
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Singleton\src
├─Singleton.java
└─SingletonTest.java

基于上面的介绍,下面的程序创建了一个单例类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton
{
// 3.使用一个类变量缓存曾经创建的实例
private static Singleton instance;
// 1.将构造器使用private修饰,隐藏该构造器
private Singleton()
{
}
// 2.提供一个静态方法,用于返回Singleton实例
// 该方法可以加入自定义的控制,保证只产生一个Singleton对象
public static Singleton getInstance()
{
// 如果instance为null,表明还不曾创建Singleton对象
// 如果instance不为null,则表明已经创建了Singleton对象,将不会执行该方法
if (instance == null)
{
// 创建一个Singleton对象,并将其缓存起来
instance = new Singleton();
}
return instance;
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
public class SingletonTest
{
public static void main(String[] args)
{
// 创建Singleton对象不能通过构造器,只能通过getInstance方法
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
// 将输出true
System.out.println(s1 == s2);
}
}

Java EE应用中,单例模式是一种应用非常广泛的设计模式,应用中许多组件都只需要单个实例下面介绍的工厂模式里的工厂也只需要单个实例。

单例模式优势

使用单例模式主要有如下两个优势。

  1. 减少创建Java实例所带来的系统开销。
  2. 便于系统跟踪单个Java实例的生命周期、实例状态等。

9.3 常见设计模式精讲

设计模式定义

设计模式是对处于特定环境下,经常出现的某类软件开发问题的,一种相对成熟的设计方案

设计模式有什么用

通过设计模式可以直接运用前面成功的经验,从而避免重复设计
所有资深软件设计师,他们积累了足够的经验,这些经验可以让他们快速、优雅地解决软件开发中的大量重复问题。而设计模式的最终目标就是帮助人们利用软件设计师的集体经验,从而设计出更加优秀的软件

本节将会联系实际Java EE应用开发来介绍设计模式,并深入分析SpringHibernate等框架,以及Java EE应用中常用设计模式的应用场景。

设计模式分类

设计模式常常被分成如下三类。

  1. 创建型:创建对象时,不再直接实例化对象;而是根据特定场景,由程序来确定创建对象的方式,从而保证更高的性能、更好的架构优势。创建型模式主要有简单工厂模式工厂方法抽象工厂模式单例模式生成器模式原型模式。PS:(简单工厂模式并不是23种设计模式之一)
  2. 结构型:用于帮助将多个对象组织成更大的结构。结构型模式主要有适配器模式桥接模式组合器模式装饰器模式门面模式享元模式代理模式
  3. 行为型:用于帮助系统间各对象的通信,以及如何控制复杂系统中的流程。行为型模式主要有命令模式解释器模式迭代器模式中介者模式备忘录模式观察者模式状态模式策略模式模板模式访问者模式