Java 面试题解析(Java EE)
什么是 ORM?
答:对象关系映射(Object-Relational Mapping,简称 ORM)是一种为了解决程序的面向对象模型与数据库的关系模型互不匹配问题的技术;简单的说,ORM 是通过使用描述对象和数据库之间映射的元数据(在 Java 中可以用 XML 或者是注解),将程序中的对象自动持久化到关系数据库中或者将关系数据库表中的行转换成 Java 对象,其本质上就是将数据从一种形式转换到另外一种形式。
持久层设计要考虑的问题有哪些?你用过的持久层框架有哪些?
答:所谓“持久”就是将数据保存到可掉电式存储设备中以便今后使用,简单的说,就是将内存中的数据保存到关系型数据库、文件系统、消息队列等提供持久化支持的设备中。持久层就是系统中专注于实现数据持久化的相对独立的层面。
持久层设计的目标包括:
- 数据存储逻辑的分离,提供抽象化的数据访问接口。
- 数据访问底层实现的分离,可以在不修改代码的情况下切换底层实现。
- 资源管理和调度的分离,在数据访问层实现统一的资源调度(如缓存机制)。
- 数据抽象,提供更面向对象的数据操作。
持久层框架有:
- Hibernate
- MyBatis
- TopLink
- Guzz
- jOOQ
- Spring Data
- ActiveJDBC
Hibernate 中 SessionFactory 是线程安全的吗?Session 是线程安全的吗(两个线程能够共享同一个 Session 吗)?
答:SessionFactory 对应 Hibernate 的一个数据存储的概念,它是线程安全的,可以被多个线程并发访问。SessionFactory 一般只会在启动的时候构建。对于应用程序,最好将 SessionFactory 通过单例模式进行封装以便于访问。Session 是一个轻量级非线程安全的对象(线程间不能共享 Session),它表示与数据库进行交互的一个工作单元。Session 是由 SessionFactory 创建的,在任务完成之后它会被关闭。Session 是持久层服务对外提供的主要接口。Session 会延迟获取数据库连接(也就是在需要的时候才会获取)。为了避免创建太多的 Session,可以使用 ThreadLocal
将 Session 和当前线程绑定在一起,这样可以让同一个线程获得的总是同一个 Session。Hibernate 3 中 SessionFactory 的 getCurrentSession()
方法就可以做到。
Hibernate 中 Session 的 load
和 get
方法的区别是什么?
答:主要有以下三项区别:
- 如果没有找到符合条件的记录,
get
方法返回null
,load
方法抛出异常。 get
方法直接返回实体类对象,load
方法返回实体类对象的代理。- 在 Hibernate 3 之前,
get
方法只在一级缓存中进行数据查找,如果没有找到对应的数据则越过二级缓存,直接发出 SQL 语句完成数据读取;load
方法则可以从二级缓存中获取数据;从 Hibernate 3 开始,get
方法不再是对二级缓存只写不读,它也是可以访问二级缓存的。
说明:对于 load()
方法 Hibernate 认为该数据在数据库中一定存在可以放心的使用代理来实现延迟加载,如果没有数据就抛出异常,而通过 get()
方法获取的数据可以不存在。
Session 的 save()
、update()
、merge()
、lock()
、saveOrUpdate()
和 persist()
方法分别是做什么的?有什么区别?
答:Hibernate 的对象有三种状态:瞬时态(transient)、持久态(persistent)和游离态(detached),如第 135 题中的图所示。瞬时态的实例可以通过调用 save()
、persist()
或者 saveOrUpdate()
方法变成持久态;游离态的实例可以通过调用 update()
、saveOrUpdate()
、lock()
或者 replicate()
变成持久态。save()
和 persist()
将会引发 SQL 的 INSERT
语句,而 update()
或 merge()
会引发 UPDATE
语句。save()
和 update()
的区别在于一个是将瞬时态对象变成持久态,一个是将游离态对象变为持久态。merge()
方法可以完成 save()
和 update()
方法的功能,它的意图是将新的状态合并到已有的持久化对象上或创建新的持久化对象。对于 persist()
方法,按照官方文档的说明:(1)persist()
方法把一个瞬时态的实例持久化,但是并不保证标识符被立刻填入到持久化实例中,标识符的填入可能被推迟到 flush
的时间;(2)persist()
方法保证当它在一个事务外部被调用的时候并不触发一个 INSERT
语句,当需要封装一个长会话流程的时候,persist()
方法是很有必要的;(3)save()
方法不保证第(2)条,它要返回标识符,所以它会立即执行 INSERT
语句,不管是在事务内部还是外部。至于 lock()
方法和 update()
方法的区别,update()
方法是把一个已经更改过的脱管状态的对象变成持久状态;lock()
方法是把一个没有更改过的脱管状态的对象变成持久状态。
阐述 Session 加载实体对象的过程。
答:Session 加载实体对象的步骤是:
- Session 在调用数据库查询功能之前,首先会在一级缓存中通过实体类型和主键进行查找,如果一级缓存查找命中且数据状态合法,则直接返回;
- 如果一级缓存没有命中,接下来 Session 会在当前
NonExists
记录(相当于一个查询黑名单,如果出现重复的无效查询可以迅速做出判断,从而提升性能)中进行查找,如果NonExists
中存在同样的查询条件,则返回null
; - 如果一级缓存查询失败则查询二级缓存,如果二级缓存命中则直接返回;
- 如果之前的查询都未命中,则发出 SQL 语句,如果查询未发现对应记录则将此次查询添加到 Session 的
NonExists
中加以记录,并返回null
; - 根据映射配置和 SQL 语句得到
ResultSet
,并创建对应的实体对象; - 将对象纳入 Session(一级缓存)的管理;
- 如果有对应的拦截器,则执行拦截器的
onLoad
方法; - 如果开启并设置了要使用二级缓存,则将数据对象纳入二级缓存;
- 返回数据对象。
Query
接口的 list
方法和 iterate
方法有什么区别?
答:
list()
方法无法利用一级缓存和二级缓存(对缓存只写不读),它只能在开启查询缓存的前提下使用查询缓存;iterate()
方法可以充分利用缓存,如果目标数据只读或者读取频繁,使用iterate()
方法可以减少性能开销。list()
方法不会引起 N+1 查询问题,而iterate()
方法可能引起 N+1 查询问题
说明:关于 N+1 查询问题,可以参考 CSDN 的一篇文章《什么是 N+1 查询》
Hibernate 如何实现分页查询?
答:通过 Hibernate 实现分页查询,开发人员只需要提供 HQL 语句(调用 Session 的 createQuery()
方法)或查询条件(调用 Session 的 createCriteria()
方法)、设置查询起始行数(调用 Query
或 Criteria
接口的 setFirstResult()
方法)和最大查询行数(调用 Query
或 Criteria
接口的 setMaxResults()
方法),并调用 Query
或 Criteria
接口的 list()
方法,Hibernate 会自动生成分页查询的 SQL 语句。
锁机制有什么用?简述 Hibernate 的悲观锁和乐观锁机制。
答:有些业务逻辑在执行过程中要求对数据进行排他性的访问,于是需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制。
Hibernate 支持悲观锁和乐观锁两种锁机制。
悲观锁,顾名思义悲观的认为在数据处理过程中极有可能存在修改数据的并发事务(包括本系统的其他事务或来自外部系统的事务),于是将处理的数据设置为锁定状态。悲观锁必须依赖数据库本身的锁机制才能真正保证数据访问的排他性,关于数据库的锁机制和事务隔离级别在前面已经讨论过了。
乐观锁,顾名思义,对并发事务持乐观态度(认为对数据的并发操作不会经常性的发生),通过更加宽松的锁机制来解决由于悲观锁排他性的数据访问对系统性能造成的严重影响。最常见的乐观锁是通过数据版本标识来实现的,读取数据时获得数据的版本号,更新数据时将此版本号加 1,然后和数据库表对应记录的当前版本号进行比较,如果提交的数据版本号大于数据库中此记录的当前版本号则更新数据,否则认为是过期数据无法更新。Hibernate 中通过 Session 的
get()
和load()
方法从数据库中加载对象时可以通过参数指定使用悲观锁;而乐观锁可以通过给实体类加整型的版本字段再通过 XML 或@Version
注解进行配置。
注意:使用乐观锁会增加了一个版本字段,很明显这需要额外的空间来存储这个版本字段,浪费了空间,但是乐观锁会让系统具有更好的并发性,这是对时间的节省。因此乐观锁也是典型的空间换时间的策略。
阐述实体对象的三种状态以及转换关系。
答:最新的 Hibernate 文档中为 Hibernate 对象定义了四种状态(原来是三种状态,面试的时候基本上问的也是三种状态),分别是:瞬时态(new, or transient)、持久态(managed, or persistent)、游状态(detached)和移除态(removed,以前 Hibernate 文档中定义的三种状态中没有移除态),如下图所示,就以前的 Hibernate 文档中移除态被视为是瞬时态。
瞬时态:当 new 一个实体对象后,这个对象处于瞬时态,即这个对象只是一个保存临时数据的内存区域,如果没有变量引用这个对象,则会被 JVM 的垃圾回收机制回收。这个对象所保存的数据与数据库没有任何关系,除非通过 Session 的
save()
、saveOrUpdate()
、persist()
、merge()
方法把瞬时态对象与数据库关联,并把数据插入或者更新到数据库,这个对象才转换为持久态对象。持久态:持久态对象的实例在数据库中有对应的记录,并拥有一个持久化标识(ID)。对持久态对象进行
delete
操作后,数据库中对应的记录将被删除,那么持久态对象与数据库记录不再存在对应关系,持久态对象变成移除态(可以视为瞬时态)。持久态对象被修改变更后,不会马上同步到数据库,直到数据库事务提交。游离态:当 Session 进行了
close()
、clear()
、evict()
或flush()
后,实体对象从持久态变成游离态,对象虽然拥有持久和与数据库对应记录一致的标识值,但是因为对象已经从会话中清除掉,对象不在持久化管理之内,所以处于游离态(也叫脱管态)。游离态的对象与临时状态对象是十分相似的,只是它还含有持久化标识。
注意:关于这个问题,在 Hibernate 的官方文档中有更为详细的解读。
如何理解 Hibernate 的延迟加载机制?在实际应用中,延迟加载与 Session 关闭的矛盾是如何处理的?
答:延迟加载就是并不是在读取的时候就把数据加载进来,而是等到使用时再加载。Hibernate 使用了虚拟代理机制实现延迟加载,我们使用 Session 的 load()
方法加载数据或者一对多关联映射在使用延迟加载的情况下从一的一方加载多的一方,得到的都是虚拟代理,简单的说返回给用户的并不是实体本身,而是实体对象的代理。代理对象在用户调用 getter
方法时才会去数据库加载数据。但加载数据就需要数据库连接。而当我们把会话关闭时,数据库连接就同时关闭了。
延迟加载与 session 关闭的矛盾一般可以这样处理:
- 关闭延迟加载特性。这种方式操作起来比较简单,因为 Hibernate 的延迟加载特性是可以通过映射文件或者注解进行配置的,但这种解决方案存在明显的缺陷。首先,出现 "no session or session was closed" 通常说明系统中已经存在主外键关联,如果去掉延迟加载的话,每次查询的开销都会变得很大。
- 在 session 关闭之前先获取需要查询的数据,可以使用工具方法
Hibernate.isInitialized()
判断对象是否被加载,如果没有被加载则可以使用Hibernate.initialize()
方法加载对象。 - 使用拦截器或过滤器延长 Session 的生命周期直到视图获得数据。Spring 整合 Hibernate 提供的
OpenSessionInViewFilter
和OpenSessionInViewInterceptor
就是这种做法。
举一个多对多关联的例子,并说明如何实现多对多关联映射。
答:例如:商品和订单、学生和课程都是典型的多对多关系。可以在实体类上通过 @ManyToMany
注解配置多对多关联或者通过映射文件中的和标签配置多对多关联,但是实际项目开发中,很多时候都是将多对多关联映射转换成两个多对一关联映射来实现的。
谈一下你对继承映射的理解。
答:继承关系的映射策略有三种:
- 每个继承结构一张表(table per class hierarchy),不管多少个子类都用一张表。
- 每个子类一张表(table per subclass),公共信息放一张表,特有信息放单独的表。
- 每个具体类一张表(table per concrete class),有多少个子类就有多少张表。
第一种方式属于单表策略,其优点在于查询子类对象的时候无需表连接,查询速度快,适合多态查询;缺点是可能导致表很大。后两种方式属于多表策略,其优点在于数据存储紧凑,其缺点是需要进行连接查询,不适合多态查询。
简述 Hibernate 常见优化策略。
答:这个问题应当挑自己使用过的优化策略回答,常用的有:
- 制定合理的缓存策略(二级缓存、查询缓存)。
- 采用合理的 Session 管理机制。
- 尽量使用延迟加载特性。
- 设定合理的批处理参数。
- 如果可以,选用 UUID 作为主键生成器。
- 如果可以,选用基于版本号的乐观锁替代悲观锁。
- 在开发过程中, 开启
hibernate.show_sql
选项查看生成的 SQL,从而了解底层的状况;开发完成后关闭此选项。 - 考虑数据库本身的优化,合理的索引、恰当的数据分区策略等都会对持久层的性能带来可观的提升,但这些需要专业的 DBA(数据库管理员)提供支持。
谈一谈 Hibernate 的一级缓存、二级缓存和查询缓存。
答:Hibernate 的 Session 提供了一级缓存的功能,默认总是有效的,当应用程序保存持久化实体、修改持久化实体时,Session 并不会立即把这种改变提交到数据库,而是缓存在当前的 Session 中,除非显示调用了 Session 的 flush()
方法或通过 close()
方法关闭 Session。通过一级缓存,可以减少程序与数据库的交互,从而提高数据库访问性能。
SessionFactory 级别的二级缓存是全局性的,所有的 Session 可以共享这个二级缓存。不过二级缓存默认是关闭的,需要显示开启并指定需要使用哪种二级缓存实现类(可以使用第三方提供的实现)。一旦开启了二级缓存并设置了需要使用二级缓存的实体类,SessionFactory 就会缓存访问过的该实体类的每个对象,除非缓存的数据超出了指定的缓存空间。
一级缓存和二级缓存都是对整个实体进行缓存,不会缓存普通属性,如果希望对普通属性进行缓存,可以使用查询缓存。查询缓存是将 HQL 或 SQL 语句以及它们的查询结果作为键值对进行缓存,对于同样的查询可以直接从缓存中获取数据。查询缓存默认也是关闭的,需要显示开启。
Hibernate 中 DetachedCriteria 类是做什么的?
答:DetachedCriteria 和 Criteria 的用法基本上是一致的,但 Criteria 是由 Session 的 createCriteria()
方法创建的,也就意味着离开创建它的 Session,Criteria 就无法使用了。DetachedCriteria 不需要 Session 就可以创建(使用 DetachedCriteria.forClass()
方法创建),所以通常也称其为离线的 Criteria,在需要进行查询操作的时候再和 Session 绑定(调用其 getExecutableCriteria(Session)
方法),这也就意味着一个 DetachedCriteria 可以在需要的时候和不同的 Session 进行绑定。
@OneToMany
注解的 mappedBy
属性有什么作用?
答:@OneToMany
用来配置一对多关联映射,但通常情况下,一对多关联映射都由多的一方来维护关联关系,例如学生和班级,应该在学生类中添加班级属性来维持学生和班级的关联关系(在数据库中是由学生表中的外键班级编号来维护学生表和班级表的多对一关系),如果要使用双向关联,在班级类中添加一个容器属性来存放学生,并使用 @OneToMany
注解进行映射,此时 mappedBy
属性就非常重要。如果使用 XML 进行配置,可以用 <set>
标签的 inverse="true"
设置来达到同样的效果。
MyBatis
中使用 #
和 $
书写占位符有什么区别?
答:#
将传入的数据都当成一个字符串,会对传入的数据自动加上引号;$
将传入的数据直接显示生成在 SQL 中。
注意:使用,写 order by
子句的时候应该用 $
而不是 #
。
解释一下 MyBatis 中命名空间(namespace)的作用。
答:在大型项目中,可能存在大量的 SQL 语句,这时候为每个 SQL 语句起一个唯一的标识(ID)就变得并不容易了。为了解决这个问题,在 MyBatis 中,可以为每个映射文件起一个唯一的命名空间,这样定义在这个映射文件中的每个 SQL 语句就成了定义在这个命名空间中的一个 ID。只要我们能够保证每个命名空间中这个 ID 是唯一的,即使在不同映射文件中的语句 ID 相同,也不会再产生冲突了。
MyBatis 中的动态 SQL 是什么意思?
答:对于一些复杂的查询,我们可能会指定多个查询条件,但是这些条件可能存在也可能不存在,例如在 58 同城上面找房子,我们可能会指定面积、楼层和所在位置来查找房源,也可能会指定面积、价格、户型和所在位置来查找房源,此时就需要根据用户指定的条件动态生成 SQL 语句。如果不使用持久层框架我们可能需要自己拼装 SQL 语句,还好 MyBatis 提供了动态 SQL 的功能来解决这个问题。MyBatis 中用于实现动态 SQL 的元素主要有:
- if
- choose / when / otherwise
- trim
- where
- set
- foreach
下面是映射文件的片段。
1 | <select id="foo" parameterType="Blog" resultType="Blog"> |
当然也可以像下面这些书写。
1 | <select id="foo" parameterType="Blog" resultType="Blog"> |
再看看下面这个例子。
1 | <select id="bar" resultType="Blog"> |
什么是 IoC 和 DI?DI 是如何实现的?
答:IoC 叫控制反转,是 Inversion of Control 的缩写,DI(Dependency Injection)叫依赖注入,是对 IoC 更简单的诠释。控制反转是把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”就是对组件对象控制权的转移,从程序代码本身转移到了外部容器,由容器来创建对象并管理对象之间的依赖关系。 IoC 体现了好莱坞原则 - "Don’t call me, we will call you"。依赖注入的基本原则是应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由容器负责,查找资源的逻辑应该从应用组件的代码中抽取出来,交给容器来完成。DI 是对 IoC 更准确的描述,即组件之间的依赖关系由容器在运行期决定,形象的来说,即由容器动态的将某种依赖关系注入到组件之中。
举个例子:一个类 A 需要用到接口 B 中的方法,那么就需要为类 A 和接口 B 建立关联或依赖关系,最原始的方法是在类 A 中创建一个接口 B 的实现类 C 的实例,但这种方法需要开发人员自行维护二者的依赖关系,也就是说当依赖关系发生变动的时候需要修改代码并重新构建整个系统。如果通过一个容器来管理这些对象以及对象的依赖关系,则只需要在类 A 中定义好用于关联接口 B 的方法(构造器或 setter 方法),将类 A 和接口 B 的实现类 C 放入容器中,通过对容器的配置来实现二者的关联。
依赖注入可以通过 setter 方法注入(设值注入)、构造器注入和接口注入三种方式来实现,Spring 支持 setter 注入和构造器注入,通常使用构造器注入来注入必须的依赖关系,对于可选的依赖关系,则 setter 注入是更好的选择,setter 注入需要类提供无参构造器或者无参的静态工厂方法来创建对象。
Spring 中 Bean 的作用域有哪些?
答:在 Spring 的早期版本中,仅有两个作用域:singleton
和 prototype
,前者表示 Bean 以单例的方式存在;后者表示每次从容器中调用 Bean 时,都会返回一个新的实例,prototype
通常翻译为原型。
补充:设计模式中的创建型模式中也有一个原型模式,原型模式也是一个常用的模式,例如做一个室内设计软件,所有的素材都在工具箱中,而每次从工具箱中取出的都是素材对象的一个原型,可以通过对象克隆来实现原型模式。
Spring 2.x 中针对 WebApplicationContext
新增了 3 个作用域,分别是:request
(每次 HTTP 请求都会创建一个新的 Bean)、session
(同一个 HttpSession 共享同一个 Bean,不同的 HttpSession 使用不同的 Bean)和 globalSession
(同一个全局 Session 共享一个 Bean)。
说明:单例模式和原型模式都是重要的设计模式。一般情况下,无状态或状态不可变的类适合使用单例模式。在传统开发中,由于 DAO 持有 Connection 这个非线程安全对象因而没有使用单例模式;但在 Spring 环境下,所有 DAO 类对可以采用单例模式,因为 Spring 利用 AOP 和 Java API 中的 ThreadLocal 对非线程安全的对象进行了特殊处理。
ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。ThreadLocal,顾名思义是线程的一个本地化对象,当工作于多线程中的对象使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程分配一个独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。
ThreadLocal 类非常简单好用,只有四个方法,能用上的也就是下面三个方法:
void set(T value)
:设置当前线程的线程局部变量的值。T get()
:获得当前线程所对应的线程局部变量的值。void remove()
:删除当前线程中线程局部变量的值。
ThreadLocal 是如何做到为每一个线程维护一份独立的变量副本的呢?在 ThreadLocal 类中有一个 Map
,键为线程对象,值是其线程对应的变量的副本,自己要模拟实现一个 ThreadLocal 类其实并不困难,代码如下所示:
1 | import java.util.Collections; |
解释一下什么叫 AOP(面向切面编程)?
答:AOP(Aspect-Oriented Programming)指一种程序设计范型,该范型以一种称为切面(aspect)的语言构造为基础,切面是一种新的模块化机制,用来描述分散在对象、类或方法中的横切关注点(crosscutting concern)。
你是如何理解“横切关注”这个概念的?
答:“横切关注”是会影响到整个应用程序的关注功能,它跟正常的业务逻辑是正交的,没有必然的联系,但是几乎所有的业务逻辑都会涉及到这些关注功能。通常,事务、日志、安全性等关注就是应用中的横切关注功能。
你如何理解 AOP 中的连接点(Joinpoint)、切点(Pointcut)、增强(Advice)、引介(Introduction)、织入(Weaving)、切面(Aspect)这些概念?
答:
- 连接点(Joinpoint):程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring 仅支持方法的连接点。
- 切点(Pointcut):如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。Spring AOP 的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。
- 增强(Advice):增强是织入到目标类连接点上的一段程序代码。Spring 提供的增强接口都是带方位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice 等。很多资料上将增强译为“通知”,这明显是个词不达意的翻译,让很多程序员困惑了许久。
说明: Advice 在国内的很多书面资料中都被翻译成"通知",但是很显然这个翻译无法表达其本质,有少量的读物上将这个词翻译为"增强",这个翻译是对 Advice 较为准确的诠释,我们通过 AOP 将横切关注功能加到原有的业务逻辑上,这就是对原有业务逻辑的一种增强,这种增强可以是前置增强、后置增强、返回后增强、抛异常时增强和包围型增强。
- 引介(Introduction):引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过引介功能,可以动态的未该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
- 织入(Weaving):织入是将增强添加到目标类具体连接点上的过程,AOP 有三种织入方式:
- 编译期织入:需要特殊的 Java 编译期(例如 AspectJ 的 ajc);
- 装载期织入:要求使用特殊的类加载器,在装载类的时候对类进行增强;
- 运行时织入:在运行时为目标类生成代理实现增强。Spring 采用了动态代理的方式实现了运行时织入,而 AspectJ 采用了编译期织入和装载期织入的方式。
- 切面(Aspect):切面是由切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义。
补充:代理模式是 GoF 提出的 23 种设计模式中最为经典的模式之一,代理模式是对象的结构模式,它给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。简单的说,代理对象可以完成比原对象更多的职责,当需要为原对象添加横切关注功能时,就可以使用原对象的代理对象。我们在打开 Office 系列的 Word 文档时,如果文档中有插图,当文档刚加载时,文档中的插图都只是一个虚框占位符,等用户真正翻到某页要查看该图片时,才会真正加载这张图,这其实就是对代理模式的使用,代替真正图片的虚框就是一个虚拟代理; Hibernate 的 load
方法也是返回一个虚拟代理对象,等用户真正需要访问对象的属性时,才向数据库发出 SQL 语句获得真实对象。
下面用一个找枪手代考的例子演示代理模式的使用:
1 | /** |
1 | /** |
1 | /** |
1 | public class ProxyTest1 { |
说明:从 JDK 1.3 开始,Java 提供了动态代理技术,允许开发者在运行时创建接口的代理实例,主要包括 Proxy
类和 InvocationHandler
接口。下面的例子使用动态代理为 ArrayList
编写一个代理,在添加和删除元素时,在控制台打印添加或删除的元素以及 ArrayList
的大小:
1 | import java.lang.reflect.InvocationHandler; |
1 | import java.lang.reflect.Proxy; |
说明:使用 Java 的动态代理有一个局限性就是代理的类必须要实现接口,虽然面向接口编程是每个优秀的 Java 程序都知道的规则,但现实往往不尽如人意,对于没有实现接口的类如何为其生成代理呢?继承!继承是最经典的扩展已有代码能力的手段,虽然继承常常被初学者滥用,但继承也常常被进阶的程序员忽视。CGLib 采用非常底层的字节码生成技术,通过为一个类创建子类来生成代理,它弥补了 Java 动态代理的不足,因此 Spring 中动态代理和 CGLib 都是创建代理的重要手段,对于实现了接口的类就用动态代理为其生成代理类,而没有实现接口的类就用 CGLib 通过继承的方式为其创建代理。
Spring 中自动装配的方式有哪些?
答:
no
:不进行自动装配,手动设置 Bean 的依赖关系。byName
:根据 Bean 的名字进行自动装配。byType
:根据 Bean 的类型进行自动装配。constructor
:类似于byType
,不过是应用于构造器的参数,如果正好有一个 Bean 与构造器的参数类型相同则可以自动装配,否则会导致错误。autodetect
:如果有默认的构造器,则通过constructor
的方式进行自动装配,否则使用byType
的方式进行自动装配。
说明:自动装配没有自定义装配方式那么精确,而且不能自动装配简单属性(基本类型、字符串等),在使用时应注意。
Spring 中如何使用注解来配置 Bean?有哪些相关的注解?
答:首先需要在 Spring 配置文件中增加如下配置:
1 | <context:component-scan base-package="org.example"/> |
然后可以用 @Component
、@Controller
、@Service
、@Repository
注解来标注需要由 Spring IoC 容器进行对象托管的类。这几个注解没有本质区别,只不过 @Controller
通常用于控制器,@Service
通常用于业务逻辑类,@Repository
通常用于仓储类(例如我们的 DAO 实现类),普通的类用 @Component
来标注。
Spring 支持的事务管理类型有哪些?你在项目中使用哪种方式?
答:Spring 支持编程式事务管理和声明式事务管理。许多 Spring 框架的用户选择声明式事务管理,因为这种方式和应用程序的关联较少,因此更加符合轻量级容器的概念。声明式事务管理要优于编程式事务管理,尽管在灵活性方面它弱于编程式事务管理,因为编程式事务允许你通过代码控制业务。
事务分为全局事务和局部事务。全局事务由应用服务器管理,需要底层服务器 JTA 支持(如 WebLogic、WildFly 等)。局部事务和底层采用的持久化方案有关,例如使用 JDBC 进行持久化时,需要使用 Connetion 对象来操作事务;而采用 Hibernate 进行持久化时,需要使用 Session 对象来操作事务。
Spring 提供了如下所示的事务管理器。
事务管理器实现类 | 目标对象 |
---|---|
DataSourceTransactionManager | 注入 DataSource |
HibernateTransactionManager | 注入 SessionFactory |
JdoTransactionManager | 管理 JDO 事务 |
JtaTransactionManager | 使用 JTA 管理事务 |
PersistenceBrokerTransactionManager | 管理 Apache 的 OJB 事务 |
这些事务的父接口都是 PlatformTransactionManager
。Spring 的事务管理机制是一种典型的策略模式,PlatformTransactionManager
代表事务管理接口,该接口定义了三个方法,该接口并不知道底层如何管理事务,但是它的实现类必须提供 getTransaction()
方法(开启事务)、commit()
方法(提交事务)、rollback()
方法(回滚事务)的多态实现,这样就可以用不同的实现类代表不同的事务管理策略。使用 JTA 全局事务策略时,需要底层应用服务器支持,而不同的应用服务器所提供的 JTA 全局事务可能存在细节上的差异,因此实际配置全局事务管理器是可能需要使用 JtaTransactionManager
的子类,如:WebLogicJtaTransactionManager
(Oracle 的 WebLogic 服务器提供)、UowJtaTransactionManager
(IBM 的 WebSphere 服务器提供)等。
编程式事务管理如下所示。
1 |
|
1 | package com.jackfrued.dao.impl; |
1 | package com.jackfrued.biz.impl; |
声明式事务如下图所示,以 Spring 整合 Hibernate 3 为例,包括完整的 DAO 和业务逻辑代码。
1 |
|
1 | package com.jackfrued.dao; |
1 | package com.jackfrued.dao; |
1 | package com.jackfrued.dao; |
1 | package com.jackfrued.comm; |
1 | package com.jackfrued.comm; |
1 | package com.jackfrued.dao; |
1 | package com.jackfrued.dao.impl; |
1 | package com.jackfrued.comm; |
1 | package com.jackfrued.comm; |
1 | package com.jackfrued.biz; |
1 | package com.jackfrued.biz.impl; |
如何在 Web 项目中配置 Spring 的 IoC 容器?
答:如果需要在 Web 项目中使用 Spring 的 IoC 容器,可以在 Web 项目配置文件 web.xml 中做出如下配置:
1 | <context-param> |
如何在 Web 项目中配置 Spring MVC?
答:要使用 Spring MVC 需要在 Web 项目配置文件中配置其前端控制器 DispatcherServlet
,如下所示:
1 | <web-app> |
说明:上面的配置中使用了 *.html
的后缀映射,这样做一方面不能够通过 URL 推断采用了何种服务器端的技术,另一方面可以欺骗搜索引擎,因为搜索引擎不会搜索动态页面,这种做法称为伪静态化。
Spring MVC 的工作原理是怎样的?
答:Spring MVC 的工作原理如下图所示:
- 客户端的所有请求都交给前端控制器
DispatcherServlet
来处理,它会负责调用系统的其他模块来真正处理用户的请求。 DispatcherServlet
收到请求后,将根据请求的信息(包括 URL、HTTP 协议方法、请求头、请求参数、Cookie 等)以及HandlerMapping
的配置找到处理该请求的Handler
(任何一个对象都可以作为请求的Handler
)。- 在这个地方 Spring 会通过
Controller
对该处理器进行封装。 Controller
是一个适配器,它用统一的接口对各种Handler
中的方法进行调用。Handler
完成对用户请求的处理后,会返回一个ModelAndView
对象给DispatcherServlet
,ModelAndView
顾名思义,包含了数据模型以及相应的视图的信息。ModelAndView
的视图是逻辑视图,DispatcherServlet
还要借助ViewResolver
完成从逻辑视图到真实视图对象的解析工作。- 当得到真正的视图对象后,
DispatcherServlet
会利用视图对象对模型数据进行渲染。 - 客户端得到响应,可能是一个普通的 HTML 页面,也可以是 XML 或 JSON 字符串,还可以是一张图片或者一个 PDF 文件。
如何在 Spring IoC 容器中配置数据源?
答:
DBCP 配置:
1 | <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> |
C3P0 配置:
1 | <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> |
注意: DBCP 的详细配置在第 153 题中已经完整的展示过了。
如何配置配置事务增强?
答:
1 |
|
选择使用 Spring 框架的原因(Spring 框架为企业级开发带来的好处有哪些)?
答:可以从以下几个方面作答:
- 非侵入式:支持基于 POJO 的编程模式,不强制性的要求实现 Spring 框架中的接口或继承 Spring 框架中的类。
- IoC 容器:IoC 容器帮助应用程序管理对象以及对象之间的依赖关系,对象之间的依赖关系如果发生了改变只需要修改配置文件而不是修改代码,因为代码的修改可能意味着项目的重新构建和完整的回归测试。有了 IoC 容器,程序员再也不需要自己编写工厂、单例,这一点特别符合 Spring 的精神“不要重复的发明轮子”。
- AOP(面向切面编程):将所有的横切关注功能封装到切面(aspect)中,通过配置的方式将横切关注功能动态添加到目标代码上,进一步实现了业务逻辑和系统服务之间的分离。另一方面,有了 AOP 程序员可以省去很多自己写代理类的工作。
- MVC:Spring 的 MVC 框架是非常优秀的,从各个方面都可以甩 Struts 2 几条街,为 Web 表示层提供了更好的解决方案。
- 事务管理:Spring 以宽广的胸怀接纳多种持久层技术,并且为其提供了声明式的事务管理,在不需要任何一行代码的情况下就能够完成事务管理。
- 其他:选择 Spring 框架的原因还远不止于此,Spring 为 Java 企业级开发提供了一站式选择,你可以在需要的时候使用它的部分和全部,更重要的是,你甚至可以在感觉不到 Spring 存在的情况下,在你的项目中使用 Spring 提供的各种优秀的功能。
Spring IoC 容器配置 Bean 的方式?
答:
- 基于 XML 文件进行配置。
- 基于注解进行配置。
- 基于 Java 程序进行配置(Spring 3+)
1 | package com.jackfrued.bean; |
1 | package com.jackfrued.bean; |
1 | package com.jackfrued.config; |
1 | package com.jackfrued.test; |
阐述 Spring 框架中 Bean 的生命周期?
答:
- Spring IoC 容器找到关于 Bean 的定义并实例化该 Bean。
- Spring IoC 容器对 Bean 进行依赖注入。
- 如果 Bean 实现了
BeanNameAware
接口,则将该 Bean 的id
传给setBeanName
方法。 - 如果 Bean 实现了
BeanFactoryAware
接口,则将BeanFactory
对象传给setBeanFactory
方法。 - 如果 Bean 实现了
BeanPostProcessor
接口,则调用其postProcessBeforeInitialization
方法。 - 如果 Bean 实现了
InitializingBean
接口,则调用其afterPropertySet
方法。 - 如果有和 Bean 关联的
BeanPostProcessors
对象,则这些对象的postProcessAfterInitialization
方法被调用。 - 当销毁 Bean 实例时,如果 Bean 实现了
DisposableBean
接口,则调用其destroy
方法。
依赖注入时如何注入集合属性?
答:可以在定义 Bean 属性时,通过 <list>
/<set>
/<map>
/<props>
分别为其注入列表、集合、映射和键值都是字符串的映射属性。
Spring 中的自动装配有哪些限制?
答:
- 如果使用了构造器注入或者 setter 注入,那么将覆盖自动装配的依赖关系。
- 基本数据类型的值、字符串字面量、类字面量无法使用自动装配来注入。
- 优先考虑使用显式的装配来进行更精确的依赖注入而不是使用自动装配。
在 Web 项目中如何获得 Spring 的 IoC 容器?
答:
1 | WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext); |
大型网站在架构上应当考虑哪些问题?
答:
分层:分层是处理任何复杂系统最常见的手段之一,将系统横向切分成若干个层面,每个层面只承担单一的职责,然后通过下层为上层提供的基础设施和服务以及上层对下层的调用来形成一个完整的复杂的系统。计算机网络的开放系统互联参考模型(OSI/RM)和 Internet 的 TCP/IP 模型都是分层结构。
大型网站的软件系统也可以使用分层的理念将其分为持久层(提供数据存储和访问服务)、业务层(处理业务逻辑,系统中最核心的部分)和表示层(系统交互、视图展示)。需要指出的是:(1)分层是逻辑上的划分,在物理上可以位于同一设备上也可以在不同的设备上部署不同的功能模块,这样可以使用更多的计算资源来应对用户的并发访问;(2)层与层之间应当有清晰的边界,这样分层才有意义,才更利于软件的开发和维护。
分割:分割是对软件的纵向切分。我们可以将大型网站的不同功能和服务分割开,形成高内聚低耦合的功能模块(单元)。在设计初期可以做一个粗粒度的分割,将网站分割为若干个功能模块,后期还可以进一步对每个模块进行细粒度的分割,这样一方面有助于软件的开发和维护,另一方面有助于分布式的部署,提供网站的并发处理能力和功能的扩展。
分布式:除了上面提到的内容,网站的静态资源(JavaScript、CSS、图片等)也可以采用独立分布式部署并采用独立的域名,这样可以减轻应用服务器的负载压力,也使得浏览器对资源的加载更快。数据的存取也应该是分布式的,传统的商业级关系型数据库产品基本上都支持分布式部署,而新生的 NoSQL 产品几乎都是分布式的。当然,网站后台的业务处理也要使用分布式技术,例如查询索引的构建、数据分析等,这些业务计算规模庞大,可以使用 Hadoop 以及 MapReduce 分布式计算框架来处理。
集群:集群使得有更多的服务器提供相同的服务,可以更好的提供对并发的支持。
缓存:所谓缓存就是用空间换取时间的技术,将数据尽可能放在距离计算最近的位置。使用缓存是网站优化的第一定律。我们通常说的 CDN、反向代理、热点数据都是对缓存技术的使用。
异步:异步是实现软件实体之间解耦合的又一重要手段。异步架构是典型的生产者消费者模式,二者之间没有直接的调用关系,只要保持数据结构不变,彼此功能实现可以随意变化而不互相影响,这对网站的扩展非常有利。使用异步处理还可以提高系统可用性,加快网站的响应速度(用 Ajax 加载数据就是一种异步技术),同时还可以起到削峰作用(应对瞬时高并发)。“能推迟处理的都要推迟处理”是网站优化的第二定律,而异步是践行网站优化第二定律的重要手段。
冗余:各种服务器都要提供相应的冗余服务器以便在某台或某些服务器宕机时还能保证网站可以正常工作,同时也提供了灾难恢复的可能性。冗余是网站高可用性的重要保证。
你用过的网站前端优化的技术有哪些?
答:
浏览器访问优化:
- 减少 HTTP 请求数量:合并 CSS、合并 JavaScript、合并图片(CSS Sprite)
- 使用浏览器缓存:通过设置 HTTP 响应头中的 Cache-Control 和 Expires 属性,将 CSS、JavaScript、图片等在浏览器中缓存,当这些静态资源需要更新时,可以更新 HTML 文件中的引用来让浏览器重新请求新的资源
- 启用压缩
- CSS 前置,JavaScript 后置
- 减少 Cookie 传输
CDN 加速:CDN(Content Distribute Network)的本质仍然是缓存,将数据缓存在离用户最近的地方,CDN 通常部署在网络运营商的机房,不仅可以提升响应速度,还可以减少应用服务器的压力。当然,CDN 缓存的通常都是静态资源。
反向代理:反向代理相当于应用服务器的一个门面,可以保护网站的安全性,也可以实现负载均衡的功能,当然最重要的是它缓存了用户访问的热点资源,可以直接从反向代理将某些内容返回给用户浏览器。
你使用过的应用服务器优化技术有哪些?
答:
分布式缓存:缓存的本质就是内存中的哈希表,如果设计一个优质的哈希函数,那么理论上哈希表读写的渐近时间复杂度为 O(1)。缓存主要用来存放那些读写比很高、变化很少的数据,这样应用程序读取数据时先到缓存中读取,如果没有或者数据已经失效再去访问数据库或文件系统,并根据拟定的规则将数据写入缓存。对网站数据的访问也符合二八定律(Pareto 分布,幂律分布),即 80% 的访问都集中在 20% 的数据上,如果能够将这 20% 的数据缓存起来,那么系统的性能将得到显著的改善。当然,使用缓存需要解决以下几个问题:
- 频繁修改的数据;
- 数据不一致与脏读;
- 缓存雪崩(可以采用分布式缓存服务器集群加以解决,memcached 是广泛采用的解决方案);
- 缓存预热;
- 缓存穿透(恶意持续请求不存在的数据)。
异步操作:可以使用消息队列将调用异步化,通过异步处理将短时间高并发产生的事件消息存储在消息队列中,从而起到削峰作用。电商网站在进行促销活动时,可以将用户的订单请求存入消息队列,这样可以抵御大量的并发订单请求对系统和数据库的冲击。目前,绝大多数的电商网站即便不进行促销活动,订单系统都采用了消息队列来处理。
使用集群。
代码优化:
- 多线程:基于 Java 的 Web 开发基本上都通过多线程的方式响应用户的并发请求,使用多线程技术在编程上要解决线程安全问题,主要可以考虑以下几个方面:
- 将对象设计为无状态对象(这和面向对象的编程观点是矛盾的,在面向对象的世界中被视为不良设计),这样就不会存在并发访问时对象状态不一致的问题。
- 在方法内部创建对象,这样对象由进入方法的线程创建,不会出现多个线程访问同一对象的问题。使用 ThreadLocal 将对象与线程绑定也是很好的做法,这一点在前面已经探讨过了。
- 对资源进行并发访问时应当使用合理的锁机制。
- 非阻塞 I/O: 使用单线程和非阻塞 I/O 是目前公认的比多线程的方式更能充分发挥服务器性能的应用模式,基于 Node.js 构建的服务器就采用了这样的方式。Java 在 JDK 1.4 中就引入了 NIO(Non-blocking I/O),在 Servlet 3 规范中又引入了异步 Servlet 的概念,这些都为在服务器端采用非阻塞 I/O 提供了必要的基础。
- 资源复用:资源复用主要有两种方式,一是单例,二是对象池,我们使用的数据库连接池、线程池都是对象池化技术,这是典型的用空间换取时间的策略,另一方面也实现对资源的复用,从而避免了不必要的创建和释放资源所带来的开销。
- 多线程:基于 Java 的 Web 开发基本上都通过多线程的方式响应用户的并发请求,使用多线程技术在编程上要解决线程安全问题,主要可以考虑以下几个方面:
什么是 XSS 攻击?什么是 SQL 注入攻击?什么是 CSRF 攻击?
答:
XSS(Cross Site Script,跨站脚本攻击)是向网页中注入恶意脚本在用户浏览网页时在用户浏览器中执行恶意脚本的攻击方式。跨站脚本攻击分有两种形式:反射型攻击(诱使用户点击一个嵌入恶意脚本的链接以达到攻击的目标,目前有很多攻击者利用论坛、微博发布含有恶意脚本的 URL 就属于这种方式)和持久型攻击(将恶意脚本提交到被攻击网站的数据库中,用户浏览网页时,恶意脚本从数据库中被加载到页面执行,QQ 邮箱的早期版本就曾经被利用作为持久型跨站脚本攻击的平台)。XSS 虽然不是什么新鲜玩意,但是攻击的手法却不断翻新,防范 XSS 主要有两方面:消毒(对危险字符进行转义)和 HttpOnly(防范 XSS 攻击者窃取 Cookie 数据)。
SQL 注入攻击是注入攻击最常见的形式(此外还有 OS 注入攻击(Struts 2 的高危漏洞就是通过 OGNL 实施 OS 注入攻击导致的)),当服务器使用请求参数构造 SQL 语句时,恶意的 SQL 被嵌入到 SQL 中交给数据库执行。SQL 注入攻击需要攻击者对数据库结构有所了解才能进行,攻击者想要获得表结构有多种方式:(1)如果使用开源系统搭建网站,数据库结构也是公开的(目前有很多现成的系统可以直接搭建论坛,电商网站,虽然方便快捷但是风险是必须要认真评估的);(2)错误回显(如果将服务器的错误信息直接显示在页面上,攻击者可以通过非法参数引发页面错误从而通过错误信息了解数据库结构,Web 应用应当设置友好的错误页,一方面符合最小惊讶原则,一方面屏蔽掉可能给系统带来危险的错误回显信息);(3)盲注。防范 SQL 注入攻击也可以采用消毒的方式,通过正则表达式对请求参数进行验证,此外,参数绑定也是很好的手段,这样恶意的 SQL 会被当做 SQL 的参数而不是命令被执行,JDBC 中的
PreparedStatement
就是支持参数绑定的语句对象,从性能和安全性上都明显优于Statement
。CSRF 攻击(Cross Site Request Forgery,跨站请求伪造)是攻击者通过跨站请求,以合法的用户身份进行非法操作(如转账或发帖等)。CSRF 的原理是利用浏览器的 Cookie 或服务器的 Session,盗取用户身份,其原理如下图所示。防范 CSRF 的主要手段是识别请求者的身份,主要有以下几种方式:(1)在表单中添加令牌(token);(2)验证码;(3)检查请求头中的 Referer(前面提到防图片盗链接也是用的这种方式)。令牌和验证都具有一次消费性的特征,因此在原理上一致的,但是验证码是一种糟糕的用户体验,不是必要的情况下不要轻易使用验证码,目前很多网站的做法是如果在短时间内多次提交一个表单未获得成功后才要求提供验证码,这样会获得较好的用户体验。
补充:防火墙的架设是 Web 安全的重要保障,ModSecurity 是开源的 Web 防火墙中的佼佼者。企业级防火墙的架设应当有两级防火墙,Web 服务器和部分应用服务器可以架设在两级防火墙之间的 DMZ,而数据和资源服务器应当架设在第二级防火墙之后。
什么是领域模型(domain model)?贫血模型(anaemic domain model)和充血模型(rich domain model)有什么区别?
答:领域模型是领域内的概念类或现实世界中对象的可视化表示,又称为概念模型或分析对象模型,它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。贫血模型是指使用的领域对象中只有 setter
和 getter
方法(POJO),所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。有人将我们这里说的贫血模型进一步划分成失血模型(领域对象完全没有业务逻辑)和贫血模型(领域对象有少量的业务逻辑),我们这里就不对此加以区分了。充血模型将大多数业务逻辑和持久化放在领域对象中,业务逻辑(业务门面)只是完成对业务逻辑的封装、事务和权限等的处理。下面两张图分别展示了贫血模型和充血模型的分层架构。
贫血模型
充血模型
贫血模型下组织领域逻辑通常使用事务脚本模式,让每个过程对应用户可能要做的一个动作,每个动作由一个过程来驱动。也就是说在设计业务逻辑接口的时候,每个方法对应着用户的一个操作,这种模式有以下几个有点:
- 它是一个大多数开发者都能够理解的简单过程模型(适合国内的绝大多数开发者)。
- 它能够与一个使用行数据入口或表数据入口的简单数据访问层很好的协作。
- 事务边界的显而易见,一个事务开始于脚本的开始,终止于脚本的结束,很容易通过代理(或切面)实现声明式事务。
然而,事务脚本模式的缺点也是很多的,随着领域逻辑复杂性的增加,系统的复杂性将迅速增加,程序结构将变得极度混乱。开源中国社区上有一篇很好的译文《贫血领域模型是如何导致糟糕的软件产生》对这个问题做了比较细致的阐述。
谈一谈测试驱动开发(TDD)的好处以及你的理解。
答:TDD 是指在编写真正的功能实现代码之前先写测试代码,然后根据需要重构实现代码。在 JUnit 的作者 Kent Beck 的大作《测试驱动开发:实战与模式解析》(Test-Driven Development: by Example)一书中有这么一段内容:“消除恐惧和不确定性是编写测试驱动代码的重要原因”。因为编写代码时的恐惧会让你小心试探,让你回避沟通,让你羞于得到反馈,让你变得焦躁不安,而 TDD 是消除恐惧、让 Java 开发者更加自信更加乐于沟通的重要手段。TDD 会带来的好处可能不会马上呈现,但是你在某个时候一定会发现,这些好处包括:
- 更清晰的代码 — 只写需要的代码
- 更好的设计
- 更出色的灵活性 — 鼓励程序员面向接口编程
- 更快速的反馈 — 不会到系统上线时才知道 bug 的存在
补充:敏捷软件开发的概念已经有很多年了,而且也部分的改变了软件开发这个行业,TDD 也是敏捷开发所倡导的。
TDD 可以在多个层级上应用,包括单元测试(测试一个类中的代码)、集成测试(测试类之间的交互)、系统测试(测试运行的系统)和系统集成测试(测试运行的系统包括使用的第三方组件)。TDD 的实施步骤是:红(失败测试)- 绿(通过测试) - 重构。关于实施 TDD 的详细步骤请参考另一篇文章《测试驱动开发之初窥门径》。
在使用 TDD 开发时,经常会遇到需要被测对象需要依赖其他子系统的情况,但是你希望将测试代码跟依赖项隔离,以保证测试代码仅仅针对当前被测对象或方法展开,这时候你需要的是测试替身。测试替身可以分为四类:
- 虚设替身:只传递但是不会使用到的对象,一般用于填充方法的参数列表
- 存根替身:总是返回相同的预设响应,其中可能包括一些虚设状态
- 伪装替身:可以取代真实版本的可用版本(比真实版本还是会差很多)
- 模拟替身:可以表示一系列期望值的对象,并且可以提供预设响应
Java 世界中实现模拟替身的第三方工具非常多,包括 EasyMock、Mockito、jMock 等。