Tag Archives: Spring

使用 Spring Boot AOP 实现 Web 日志处理和分布式锁

AOP

AOP 的全称为 Aspect Oriented Programming,译为面向切面编程。实际上 AOP 就是通过预编译和运行期动态代理实现程序功能的统一维护的一种技术。在不同的技术栈中 AOP 有着不同的实现,但是其作用都相差不远,我们通过 AOP 为既有的程序定义一个切入点,然后在切入点前后插入不同的执行内容,以达到在不修改原有代码业务逻辑的前提下统一处理一些内容(比如日志处理、分布式锁)的目的。

为什么要使用 AOP

在实际的开发过程中,我们的应用程序会被分为很多层。通常来讲一个 Java 的 Web 程序会拥有以下几个层次:

  • Web 层:主要是暴露一些 Restful API 供前端调用。
  • 业务层:主要是处理具体的业务逻辑。
  • 数据持久层:主要负责数据库的相关操作(增删改查)。

虽然看起来每一层都做着全然不同的事情,但是实际上总会有一些类似的代码,比如日志打印和安全验证等等相关的代码。如果我们选择在每一层都独立编写这部分代码,那么久而久之代码将变的很难维护。所以我们提供了另外的一种解决方案: AOP。这样可以保证这些通用的代码被聚合在一起维护,而且我们可以灵活的选择何处需要使用这些代码。

AOP 的核心概念

  • 切面(Aspect):通常是一个类,在里面可以定义切入点和通知。
  • 连接点(Joint Point):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截的到的方法,实际上连接点还可以是字段或者构造器。
  • 切入点(Pointcut):对连接点进行拦截的定义。
  • 通知(Advice):拦截到连接点之后所要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。
  • AOP 代理:AOP 框架创建的对象,代理就是目标对象的加强。Spring 中的 AOP 代理可以使 JDK 动态代理,也可以是 CGLIB 代理,前者基于接口,后者基于子类。

Spring AOP

Spring 中的 AOP 代理还是离不开 Spring 的 IOC 容器,代理的生成,管理及其依赖关系都是由 IOC 容器负责,Spring 默认使用 JDK 动态代理,在需要代理类而不是代理接口的时候,Spring 会自动切换为使用 CGLIB 代理,不过现在的项目都是面向接口编程,所以 JDK 动态代理相对来说用的还是多一些。在本文中,我们将以注解结合 AOP 的方式来分别实现 Web 日志处理和分布式锁。

Spring AOP 相关注解

  • @Aspect: 将一个 java 类定义为切面类。
  • @Pointcut:定义一个切入点,可以是一个规则表达式,比如下例中某个 package 下的所有函数,也可以是一个注解等。
  • @Before:在切入点开始处切入内容。
  • @After:在切入点结尾处切入内容。
  • @AfterReturning:在切入点 return 内容之后切入内容(可以用来对处理返回值做一些加工处理)。
  • @Around:在切入点前后切入内容,并自己控制何时执行切入点自身的内容。
  • @AfterThrowing:用来处理当切入内容部分抛出异常之后的处理逻辑。

其中 @Before@After@AfterReturning@Around@AfterThrowing 都属于通知。

AOP 顺序问题

在实际情况下,我们对同一个接口做多个切面,比如日志打印、分布式锁、权限校验等等。这时候我们就会面临一个优先级的问题,这么多的切面该如何告知 Spring 执行顺序呢?这就需要我们定义每个切面的优先级,我们可以使用 @Order(i) 注解来标识切面的优先级, i 的值越小,优先级越高。假设现在我们一共有两个切面,一个 WebLogAspect,我们为其设置 @Order(100);而另外一个切面 DistributeLockAspect 设置为 @Order(99),所以 DistributeLockAspect 有更高的优先级,这个时候执行顺序是这样的:在 @Before 中优先执行 @Order(99) 的内容,再执行 @Order(100) 的内容。而在 @After 和 @AfterReturning 中则优先执行 @Order(100) 的内容,再执行 @Order(99) 的内容,可以理解为先进后出的原则。

基于注解的 AOP 配置

使用注解一方面可以减少我们的配置,另一方面注解在编译期间就可以验证正确性,查错相对比较容易,而且配置起来也相当方便。相信大家也都有所了解,我们现在的 Spring 项目里面使用了非常多的注解替代了之前的 xml 配置。而将注解与 AOP 配合使用也是我们最常用的方式,在本文中我们将以这种模式实现 Web 日志统一处理和分布式锁两个注解。下面就让我们从准备工作开始吧。

准备工作

准备一个 Spring Boot 的 Web 项目

你可以通过 Spring Initializr 页面生成一个空的 Spring Boot 项目,当然也可以下载 springboot-pom.xml 文件,然后使用 maven 构建一个 Spring Boot 项目。项目创建完成后,为了方便后面代码的编写你可以将其导入到你喜欢的 IDE 中,我这里选择了 Intelli IDEA 打开。

添加依赖

我们需要添加 Web 依赖和 AOP 相关依赖,只需要在 pom.xml 中添加如下内容即可:

清单 1. 添加 web 依赖
1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
清单 2. 添加 AOP 相关依赖
1
2
3
4
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

其他准备工作

为了方便测试我还在项目中集成了 Swagger 文档,具体的集成方法可以参照在 Spring Boot 项目中使用 Swagger 文档。另外编写了两个接口以供测试使用,具体可以参考本文源码。由于本教程所实现的分布式锁是基于 Redis 缓存的,所以需要安装 Redis 或者准备一台 Redis 服务器。

利用 AOP 实现 Web 日志处理

为什么要实现 Web 日志统一处理

在实际的开发过程中,我们会需要将接口的出请求参数、返回数据甚至接口的消耗时间都以日志的形式打印出来以便排查问题,有些比较重要的接口甚至还需要将这些信息写入到数据库。而这部分代码相对来讲比较相似,为了提高代码的复用率,我们可以以 AOP 的方式将这种类似的代码封装起来。

Web 日志注解

清单 3. Web 日志注解代码
1
2
3
4
5
6
7
8
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ControllerWebLog {
     String name();
     boolean intoDb() default false;
}

其中 name 为所调用接口的名称,intoDb 则标识该条操作日志是否需要持久化存储,Spring Boot 连接数据库的配置,可以参考 SpringBoot 项目配置多数据源这篇文章,具体的数据库结构可以点击这里获取。现在注解有了,我们接下来需要编写与该注解配套的 AOP 切面。

实现 WebLogAspect 切面

  1. 首先我们定义了一个切面类 WebLogAspect 如清单 4 所示。其中@Aspect 注解是告诉 Spring 将该类作为一个切面管理,@Component 注解是说明该类作为一个 Spring 组件。
    清单 4. WebLogAspect
    1
    2
    3
    4
    5
    @Aspect
    @Component
    @Order(100)
    public class WebLogAspect {
    }
  2. 接下来我们需要定义一个切点。
    清单 5. Web 日志 AOP 切点
    1
    2
    @Pointcut("execution(* cn.itweknow.sbaop.controller..*.*(..))")
    public void webLog() {}

    对于 execution 表达式,官网的介绍为(翻译后):

    清单 6. 官网对 execution 表达式的介绍
    1
    execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

    其中除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。这个解释可能有点难理解,下面我们通过一个具体的例子来了解一下。在 WebLogAspect 中我们定义了一个切点,其 execution 表达式为 * cn.itweknow.sbaop.controller..*.*(..),下表为该表达式比较通俗的解析:

    表 1. execution() 表达式解析
  3. @Before 修饰的方法中的内容会在进入切点之前执行,在这个部分我们需要打印一个开始执行的日志,并将请求参数和开始调用的时间存储在 ThreadLocal 中,方便在后面结束调用时打印参数和计算接口耗时。
    清单 7. @Before 代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Before(value = "webLog()& &  @annotation(controllerWebLog)")
        public void doBefore(JoinPoint joinPoint, ControllerWebLog controllerWebLog) {
            // 开始时间。
            long startTime = System.currentTimeMillis();
            Map<String, Object> threadInfo = new HashMap<>();
            threadInfo.put(START_TIME, startTime);
            // 请求参数。
            StringBuilder requestStr = new StringBuilder();
            Object[] args = joinPoint.getArgs();
            if (args != null && args.length > 0) {
                for (Object arg : args) {
                    requestStr.append(arg.toString());
                }
            }
            threadInfo.put(REQUEST_PARAMS, requestStr.toString());
            threadLocal.set(threadInfo);
            logger.info("{}接口开始调用:requestData={}", controllerWebLog.name(), threadInfo.get(REQUEST_PARAMS));
     }
  4. @AfterReturning,当程序正常执行有正确的返回时执行,我们在这里打印结束日志,最后不能忘的是清除 ThreadLocal 里的内容。
    清单 8. @AfterReturning 代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @AfterReturning(value = "webLog()&& @annotation(controllerWebLog)", returning = "res")
    public void doAfterReturning(ControllerWebLog controllerWebLog, Object res) {
            Map<String, Object> threadInfo = threadLocal.get();
            long takeTime = System.currentTimeMillis() - (long) threadInfo.getOrDefault(START_TIME, System.currentTimeMillis());
            if (controllerWebLog.intoDb()) {
                insertResult(controllerWebLog.name(), (String) threadInfo.getOrDefault(REQUEST_PARAMS, ""),
                            JSON.toJSONString(res), takeTime);
            }
            threadLocal.remove();
            logger.info("{}接口结束调用:耗时={}ms,result={}", controllerWebLog.name(),
                    takeTime, res);
    }
  5. 当程序发生异常时,我们也需要将异常日志打印出来:
    清单 9. @AfterThrowing 代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @AfterThrowing(value = "webLog()& &  @annotation(controllerWebLog)", throwing = "throwable")
        public void doAfterThrowing(ControllerWebLog controllerWebLog, Throwable throwable) {
            Map< String, Object> threadInfo = threadLocal.get();
            if (controllerWebLog.intoDb()) {
                insertError(controllerWebLog.name(), (String)threadInfo.getOrDefault(REQUEST_PARAMS, ""),
                        throwable);
            }
            threadLocal.remove();
            logger.error("{}接口调用异常,异常信息{}",controllerWebLog.name(), throwable);
    }
  6. 至此,我们的切面已经编写完成了。下面我们需要将 ControllerWebLog 注解使用在我们的测试接口上,接口内部的代码已省略,如有需要的话,请参照本文源码
    清单 10. 测试接口代码
    1
    2
    3
    4
    5
    @PostMapping("/post-test")
    @ApiOperation("接口日志 POST 请求测试")
    @ControllerWebLog(name = "接口日志 POST 请求测试", intoDb = true)
    public BaseResponse postTest(@RequestBody BaseRequest baseRequest) {
    }
  7. 最后,启动项目,然后打开 Swagger 文档进行测试,调用接口后在控制台就会看到类似图 1 这样的日志。
    图 1. 基于 Redis 的分布式锁测试效果

    基于 Redis 的分布式锁测试效果

利用 AOP 实现分布式锁

为什么要使用分布式锁

我们程序中多多少少会有一些共享的资源或者数据,在某些时候我们需要保证同一时间只能有一个线程访问或者操作它们。在传统的单机部署的情况下,我们简单的使用 Java 提供的并发相关的 API 处理即可。但是现在大多数服务都采用分布式的部署方式,我们就需要提供一个跨进程的互斥机制来控制共享资源的访问,这种互斥机制就是我们所说的分布式锁。

注意

  1. 互斥性。在任时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。这个其实只要我们给锁加上超时时间即可。
  3. 具有容错性。只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

分布式锁注解

清单 11. 分布式锁注解
1
2
3
4
5
6
7
8
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributeLock {
    String key();
    long timeout() default 5;
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

其中,key 为分布式所的 key 值,timeout 为锁的超时时间,默认为 5,timeUnit 为超时时间的单位,默认为秒。

注解参数解析器

由于注解属性在指定的时候只能为常量,我们无法直接使用方法的参数。而在绝大多数的情况下分布式锁的 key 值是需要包含方法的一个或者多个参数的,这就需要我们将这些参数的位置以某种特殊的字符串表示出来,然后通过参数解析器去动态的解析出来这些参数具体的值,然后拼接到 key 上。在本教程中我也编写了一个参数解析器 AnnotationResolver。篇幅原因,其源码就不直接粘在文中,需要的读者可以查看源码

获取锁方法

清单 12. 获取锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private String getLock(String key, long timeout, TimeUnit timeUnit) {
        try {
            String value = UUID.randomUUID().toString();
            Boolean lockStat = stringRedisTemplate.execute((RedisCallback< Boolean>)connection ->
                    connection.set(key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")),
                            Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
            if (!lockStat) {
                // 获取锁失败。
                return null;
            }
            return value;
        } catch (Exception e) {
            logger.error("获取分布式锁失败,key={}", key, e);
            return null;
        }
}

RedisStringCommands.SetOption.SET_IF_ABSENT 实际上是使用了 setNX 命令,如果 key 已经存在的话则不进行任何操作返回失败,如果 key 不存在的话则保存 key 并返回成功,该命令在成功的时候返回 1,结束的时候返回 0。我们随机产生了一个 value 并且在获取锁成功的时候返回出去,是为了在释放锁的时候对该值进行比较,这样可以做到解铃还须系铃人,由谁创建的锁就由谁释放。同时还指定了超时时间,这样可以保证锁释放失败的情况下不会造成接口永远不能访问。

释放锁方法

清单 13. 释放锁
1
2
3
4
5
6
7
8
9
10
11
12
13
private void unLock(String key, String value) {
        try {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            boolean unLockStat = stringRedisTemplate.execute((RedisCallback< Boolean>)connection ->
                    connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
                            key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8"))));
            if (!unLockStat) {
                logger.error("释放分布式锁失败,key={},已自动超时,其他线程可能已经重新获取锁", key);
            }
        } catch (Exception e) {
            logger.error("释放分布式锁失败,key={}", key, e);
        }
}

切面

切点和 Web 日志处理的切点一样,这里不再赘述。我们在切面中使用的通知类型为 @Around,在切点之前我们先尝试获取锁,若获取锁失败则直接返回错误信息,若获取锁成功则执行方法体,当方法结束后(无论是正常结束还是异常终止)释放锁。

清单 14. 环绕通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Around(value = "distribute()&& @annotation(distributeLock)")
public Object doAround(ProceedingJoinPoint joinPoint, DistributeLock distributeLock) throws Exception {
        String key = annotationResolver.resolver(joinPoint, distributeLock.key());
        String keyValue = getLock(key, distributeLock.timeout(), distributeLock.timeUnit());
        if (StringUtil.isNullOrEmpty(keyValue)) {
            // 获取锁失败。
            return BaseResponse.addError(ErrorCodeEnum.OPERATE_FAILED, "请勿频繁操作");
        }
        // 获取锁成功
        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            return BaseResponse.addError(ErrorCodeEnum.SYSTEM_ERROR, "系统异常");
        } finally {
            // 释放锁。
            unLock(key, keyValue);
        }
}

测试

清单 15. 分布式锁测试代码
1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/post-test")
@ApiOperation("接口日志 POST 请求测试")
@ControllerWebLog(name = "接口日志 POST 请求测试", intoDb = true)
@DistributeLock(key = "post_test_#{baseRequest.channel}", timeout = 10)
public BaseResponse postTest(@RequestBody BaseRequest baseRequest) {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return BaseResponse.addResult();
}

在本次测试中我们将锁的超时时间设置为 10 秒钟,在接口中让当前线程睡眠 10 秒,这样可以保证 10 秒钟之内锁不会被释放掉,测试起来更加容易些。启动项目后,我们快速访问两次该接口,注意两次请求的 channel 传值需要一致(因为锁的 key 中包含该值),会发现第二次访问时返回如下结果:

图 2. 基于 Redis 的分布式锁测试效果

基于 Redis 的分布式锁测试效果

这就说明我们的分布式锁已经生效。

结束语

在本教程中,我们主要了解了 AOP 编程以及为什么要使用 AOP。也介绍了如何在 Spring Boot 项目中利用 AOP 实现 Web 日志统一处理和基于 Redis 的分布式锁。你可以在 Github 上找到本教程的完整实现,如果你想对本教程做补充的话欢迎发邮件(gancy.programmer@gmail.com)给我或者直接在 Github 上提交 Pull Reqeust。

参考资源

from:https://www.ibm.com/developerworks/cn/java/j-spring-boot-aop-web-log-processing-and-distributed-locking/index.html

Spring

1. Spring框架的作用

  • 轻量:Spring是轻量级的,基本的版本大小为2MB
  • 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
  • 面向切面的编程AOP:Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
  • 容器:Spring包含并管理应用中对象的生命周期和配置
  • MVC框架: Spring-MVC
  • 事务管理:Spring提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务JTA
  • 异常处理:Spring提供方便的API把具体技术相关的异常

2. Spring的组成

Spring由7个模块组成:

  • Spring Core: 核心容器提供 Spring 框架的基本功能。核心容器的主要组件是BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转 (IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
  • Spring 上下文:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。
  • Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。
  • Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。
  • Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
  • Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
  • Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。

3. Spring容器

Sping的容器可以分为两种类型

  1. BeanFactory:(org.springframework.beans.factory.BeanFactory接口定义)是最简答的容器,提供了基本的DI支持。最常用的BeanFactory实现就是XmlBeanFactory类,它根据XML文件中的定义加载beans,该容器从XML文件读取配置元数据并用它去创建一个完全配置的系统或应用。
  2. ApplicationContext应用上下文:(org.springframework.context.ApplicationContext)基于BeanFactory之上构建,并提供面向应用的服务。

4. ApplicationContext通常的实现

  • ClassPathXmlApplicationContext:从类路径下的XML配置文件中加载上下文定义,把应用上下文定义文件当做类资源。
  • FileSystemXmlApplicationContext:读取文件系统下的XML配置文件并加载上下文定义。
  • XmlWebApplicationContext:读取Web应用下的XML配置文件并装载上下文定义。
1
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

5. IOC & DI

Inversion of Control, 一般分为两种类型:依赖注入DI(Dependency Injection)和依赖查找(Dependency Lookup).依赖注入应用比较广泛。

Spring IOC负责创建对象,管理对象(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。

优点:把应用的代码量降到最低。容器测试,最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载。
DI依赖注入是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用床架对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述组件需要哪些服务,之后一个IOC容器辅助把他们组装起来。
IOC的注入方式:1. 构造器依赖注入;2. Setter方法注入。

6. 如何给spring容器提供配置元数据

  • XML配置文件
  • 基于注解的配置
  • 基于Java的配置@Configuration, @Bean

7. bean标签中的属性:

  • id
  • name
  • class
  • init-method:Bean实例化后会立刻调用的方法
  • destory-method:Bean从容器移除和销毁前,会调用的方法
  • factory-method:运行我们调用一个指定的静态方法,从而代替构造方法来创建一个类的实例。
  • scope:Bean的作用域,包括singleton(默认),prototype(每次调用都创建一个实例), request,session, global-session(注意spring中的单例bean不是线程安全的)
  • autowired:自动装配 byName, byType, constructor, autodetect(首先阐释使用constructor自动装配,如果没有发现与构造器相匹配的Bean时,Spring将尝试使用byType自动装配)

8. beans标签中相关属性

default-init-method
default-destory-method
default-autowire:默认为none,应用于Spring配置文件中的所有Bean,注意这里不是指Spring应用上下文,因为你可以定义多个配置文件

9. Bean的生命周期

1) 创建Bean的实例(factory-method, autowireConstrutor)
2) 属性注入(autowireByName, autowireByType)
3) 初始化Bean

3.1 激活Aware方法:(invokeAwaresMethods)Spring中提供了一些Aware相关接口,比如BeanNameAware, BeanFactoryAware, ApplicationContextAware等,实现这些Aware接口的bean在被初始化之后,可以取得一些相对应的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void invokeAwareMethods(final String beanName, final Object bean){
    if(bean instanceof Aware)
    {
        if(bean instanceof BeanNameAware){
            ((BeanNameAware) bean).setBeanName(beanName);
        }
        if(bean instanceof BeanClassLoaderAware){
            ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
        }
        if(bean instanceof BeanFactoryAware){
            ((BeanFactoryAware) bean).setBeanFactory(AbstactAutowire CapableBeanFactory.this);
        }
    }
}

3.2 处理器的应用(BeanPostProcessor接口):调用客户自定义初始化方法前以及调用自定义初始化方法后分别会调用BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization方法,使用户可以根据自己的业务需求进行响应的处理。

3.3 激活自定义的init方法(init-method & 自定义实现InitializingBean接口)

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
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinetion mbd){
    if(System.getSecurityManager() != null){
        AccessController.doPrivileged(new PrivilegedAction<Object>(){
            @Override
            public Object run()
            {
                invokeAwareMethods(beanName,bean);
                return null;
            }
        });
    }
    else{
        //对特殊的bean处理:Aware, BeanClassLoaderAware, BeanFactoryAware
        invokeAwareMethods(beanName,bean);
    }
    Object wrappedBean = bean;
    if(mbd == null !! !mbd.isSynthetic()){
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wappedBean,beanName);
    }
    try{
        invokeInitMethods(beanName, wappedBean, mbd);
    }
    catch(Throwable ex){
        throw new BeanCreationException((mbd != null ? mbd.getResourceDescription():null),beanName,"Invocation of init method failed",ex);
    }
    if(mbd == null || !mbd.isSynthetic()){
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wappedBean;
}

4) 使用Bean。 驻留在应用的上下文中,直到该应用上下文被销毁。
5) 销毁(destory-mthod & 实现DisposableBean接口)

Or represent like this:

1. Bean的构造
2. 调用setXXX()方法设置Bean的属性
3. 调用BeanNameAware的setBeanName()
4. 调用BeanFactoryAware的setBeanFactory()方法
5. 调用BeanPostProcessor的postProcessBeforeInitialization()方法
6. 调用InitializingBean的afterPropertiesSet()方法
7. 调用自定义的初始化方法
8. 调用BeanPostProcessor类的postProcessAfterInitialization()方法
9. 调用DisposableBean的destroy()方法
10. 调用自定义的销毁方法

10. Spring中注入集合

  1. <list>允许值相同
  2. <set>不允许值相同
  3. <map><entry key=”” value=”“></map>键和值都可以为任意类型,key, key-ref, value-ref, value可以任意搭配
  4. <props><prop key=”“>XXX</prop></props>键和值都只能是String类型

11. 装配空值

1
<property name="xxx"><null/></property>

12. 自动装配(autowiring)

有助于减少甚至消除配置<property>和<constructor-arg>元素,让Spring自动识别如何装配Bean的依赖关系。<context:annotation-config/>
与之对应的是:自动检测(autodiscovery),比自动装配更近了一步,让Spring能够自动识别哪些类需要被配置成SpringBean,从而减少对<bean>元素的使用。<context:component-scan>

13. 注解

Spring容器默认禁用注解装配。最简单的开启方式<context:annotation-config/>。
Spring支持的几种不同的用于自动装配的注解:

  • Spring自带的@Autowired注解
  • JSR-330的@Inject注解
  • JSR-250的@Resource注解

14. @Autowired

@Autowired具有强契约特征,其所标注的属性或参数必须是可装配的。如果没有Bean可以装配到@Autowired所标注的属性或参数中,自动装配就会失败,抛出NoSuchBeanDefinitionException.
属性不一定非要装配,null值也是可以接受的。在这种场景下可以通过设置@Autowired的required属性为false来配置自动装配是可选的,如:

1
2
@Autowired(required=false)
private Object obj;

注意required属性可以用于@Autowired注解所使用的任意地方。但是当使用构造器装配时,只有一个构造器可以将@Autowired的required属性设置为true。其他使用@Autowired注解所标注的构造器只能将required属性设置为false。此外,当使用@Autowired标注多个构造器时,Spring就会从所有满足装配条件的构造器中选择入参最多的那个构造器。
可以使用@Qualifier明确指定要装配的Bean.如下:

1
2
3
@Autowired
@Qualifier("objName")
private Object obj;

15. 自定义的限定器

1
2
3
4
@Target({ElementType.FIELF, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @Interface SpecialQualifier{}

此时,可以通过自定义的@SpecialQualifier注解来代替@Qualifier来标注,也可以和@Autowired一起使用:

1
2
3
@Autowired
@SpecialQualifier
private Object obj;

此时,Spring会把自动装配的范围缩小到被@SpecialQualifier标注的Bean中。如果被@SpecialQualifier标注的Bean有多个,我们还可以通过自定义的另一个限定器@SpecialQualifier2来进一步缩小范围。

16. @Autowired优缺点

Spring的@Autowired注解是减少Spring XML配置的一种方式。但是它的类会映入对Spring的特定依赖(即使依赖只是一个注解)。

17. @Inject

和@Autowired注解一样,@Inject可以用来自动装配属性、方法和构造器;与@Autowired不同的是,@Inject没有required属性。因此@Inject注解所标注的依赖关系必须存在,如果不存在,则会抛出异常。

18. @Named

相对于@Autowired对应的Qualifier,@Inject所对应的是@Named注解。

1
2
3
@Inject
@Named("objName")
private Object obj;

19. SpEL表达式

语法形式在#{}中使用表达式,如:

1
<property name="count" value="#{5}"/>

20. @Value

@Value是一个新的装配注解,可以让我们使用注解装配String类型的值和基本类型的值,如int, boolean。我们可以通过@Value直接标注某个属性,方法或者方法参数,并传入一个String类型的表达式来装配属性,如:

1
2
@Value("Eruption")
private String song;

@Value可以配合SpEL表达式一起使用,譬如有些情况下需要读取properties文件中的内容,可以使用:

1
@Value("#{configProperties['ora_driver']}")

详细可以参考Spring+Mybatis多数据源配置(三)——Spring如何获取Properties文件的信息

21. 自动检测Bean

<context:component-scan>元素除了完成与<context:annotation-config>一样的工作,还允许Spring自动检测Bean和定义Bean.<context:component-scan>元素会扫描指定的包和其所有子包,如下:

1
<context:component-scan base-package="com.zzh.dao" />

22. 为自动检测标注Bean

默认情况下,查找使用构造型(stereotype)注解所标注的类,这些特殊的注解如下:
– @Component:通用的构造型注解,标志此类为Spring组件
– @Controller:标识将该类定义为SpringMVC controller
– @Repository:标识将该类定义为数据仓库
– @Service:标识将该类定义为服务
以@Component为例:

1
2
@Component
public class Guitar implements Intrument{}

这里@Component会自动注册Guitar 为Spring Bean,并设置默认的Bean的Id为guitar,首字母大写变小写。注意如果第一个和第二个字母都是大写,默认的Bean的id会有特殊处理。
也可以指定Bean的Id如:

1
2
@Component("guitarOne")
public class Guitar implements Intrument{}

23. AOP

面向切面的编程AOP,是一种编程技术,允许程序模块化横向切割关注点,或横切典型的责任划分,如日志和事务管理。

AOP的核心是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在SpringAOP中,切面通过带有@Aspect注解的类实现。

关注点是应用中的一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。

横切关注点一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输,几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。

连接点代表一个应用程序的某个位置,在这个位置我们可以插入一个AOP切面,它实际上是个应用程序执行Spring AOP的位置。

切点是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方式指明切入点。

引入运行我们在已存在的类中添加新的方法和属性。

24. AOP通知

通知是个在方法执行前后要做的动作,实际上是程序执行时要通过SpringAOP框架触发的代码
Spring切面可以应用五种类型的通知:
before:前置通知,在一个方法执行前被调用。@Before
after: 在方法执行之后调用的通知,无论方法执行是否成功。@After
after-returning: 仅当方法成功完成后执行的通知。@AfterReturning
after-throwing: 在方法抛出异常退出时执行的通知。@AfterThrowing
around: 在方法执行之前和之后调用的通知。@Around

25. Spring的事务类型

编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。
声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。

26. ACID

  1. Atomic原子性:事务是由一个或多个活动所组成的一个工作单元。原子性确保事务中的所有操作全部发生或者全部不发生。
  2. Consistent一致性:一旦事务完成,系统必须确保它所建模的业务处于一致的状态
  3. Isolated隔离线:事务允许多个用户对象头的数据进行操作,每个用户的操作不会与其他用户纠缠在一起。
  4. Durable持久性:一旦事务完成,事务的结果应该持久化,这样就能从任何的系统崩溃中恢复过来。

27. JDBC事务

如果在应用程序中直接使用JDBC来进行持久化,譬如博主采用的是Mybatis,DataSourceTransactionManager会为你处理事务边界。譬如:

1
2
3
4
5
6
7
8
9
10
11
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="${driver}" />
    <property name="url" value="${url}" />
    <property name="username" value="zzh" />
    <property name="password" value="zzh" />
    <property name="validationQuery" value="SELECT 1"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

28. JTA事务

如果你的事务需要跨多个事务资源(例如:两个或多个数据库;或者如Sping+ActiveMQ整合需要将ActiveMQ和数据库的事务整合起来),就需要使用JtaTransactionManager:

1
<bean id="jtaTransactionManager"class="org.springframework.transaction.jta.JtaTransactionManager"/>

JtaTransactionManager将事务管理的职责委托给了一个JTA的实现。JTA规定了应用程序与一个或多个数据源之间协调事务的标准API。transactionManagerName属性指明了要在JNDI上查找的JTA事务管理器。
JtaTransactionManager将事务管理的职责委托给javax.transaction.UserTransaction和javax.transaction.TransactionManager对象。通过UserTransaction.commit()方法来提交事务。类似地,如果事务失败,UserTransaction的rollback()方法将会被调用。

29. 声明式事务

尽管Spring提供了多种声明式事务的机制,但是所有的方式都依赖这五个参数来控制如何管理事务策略。因此,如果要在Spring中声明事务策略,就要理解这些参数。(@Transactional)

1. 隔离级别(isolation)

  • ISOLATION_DEFAULT: 使用底层数据库预设的隔离层级
  • ISOLATION_READ_COMMITTED: 允许事务读取其他并行的事务已经送出(Commit)的数据字段,可以防止Dirty read问题
  • ISOLATION_READ_UNCOMMITTED: 允许事务读取其他并行的事务还没送出的数据,会发生Dirty、Nonrepeatable、Phantom read等问题
  • ISOLATION_REPEATABLE_READ: 要求多次读取的数据必须相同,除非事务本身更新数据,可防止Dirty、Nonrepeatable read问题
  • ISOLATION_SERIALIZABLE: 完整的隔离层级,可防止Dirty、Nonrepeatable、Phantom read等问题,会锁定对应的数据表格,因而有效率问题

2. 传播行为(propagation)

  • PROPAGATION_REQUIRED–支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
  • PROPAGATION_SUPPORTS–支持当前事务,如果当前没有事务,就以非事务方式执行。
  • PROPAGATION_MANDATORY–支持当前事务,如果当前没有事务,就抛出异常。
  • PROPAGATION_REQUIRES_NEW–新建事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED–以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NEVER–以非事务方式执行,如果当前存在事务,则抛出异常。
  • PROPAGATION_NESTED–如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

3. 只读(read-only)

如果事务只进行读取的动作,则可以利用底层数据库在只读操作时发生的一些最佳化动作,由于这个动作利用到数据库在只读的事务操作最佳化,因而必须在事务中才有效,也就是说要搭配传播行为PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED来设置。

4. 事务超时(timeout)

有的事务操作可能延续很长一段的时间,事务本身可能关联到数据表的锁定,因而长时间的事务操作会有效率上的问题,对于过长的事务操作,考虑Roll back事务并要求重新操作,而不是无限时的等待事务完成。 可以设置事务超时期间,计时是从事务开始时,所以这个设置必须搭配传播行为PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED来设置。

5. 回滚规则(rollback-for, no-rollback-for)

rollback-for指事务对于那些检查型异常应当回滚而不提交;no-rollback-for指事务对于那些异常应当继续运行而不回滚。默认情况下,Spring声明事务对所有的运行时异常都进行回滚。

1
2
3
4
5
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" />
    </tx:attributes>
</tx:advice>

30. SpringMVC

核心架构的具体流程:

  1. 首先用户发送请求——>DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制;
  2. DispatcherServlet——>HandlerMapping, HandlerMapping将会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象、多个HandlerInterceptor拦截器)对象,通过这种策略模式,很容易添加新的映射策略;
  3. DispatcherServlet——>HandlerAdapter,HandlerAdapter将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器;
  4. HandlerAdapter——>处理器功能处理方法的调用,HandlerAdapter将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理;并返回一个ModelAndView对象(包含模型数据、逻辑视图名);
  5. ModelAndView的逻辑视图名——> ViewResolver, ViewResolver将把逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术;
  6. View——>渲染,View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构,因此很容易支持其他视图技术;
  7. 返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户,到此一个流程结束。

31. DispatcherServlet

SpringMVC的核心是DispatcherServlet,这个Servlet充当SpringMVC的前端控制器。与其他Servlet一样,DispatcherServlet必须在Web应用程序的web.xml文件中进行配置。

1
2
3
4
5
<servlet>
    <servlet-name>viewspace</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>2</load-on-startup>
</servlet>

默认情况下,DispatcherServlet在加载时会从一个基于这个Servlet名字的XML文件中加载Spring应用上下文。因为servlet的名字是viewspace,所以配置文件的名称为viewspace-servlet.xml。
接下来,必须申明DispatcherServlet处理那些URL:

1
2
3
4
<servlet-mapping>
    <servlet-name>viewspace</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

通过将DispatcherServlet映射到/,声明了它会作为默认的servlet并且会处理所有的请求,包括对静态资源的请求。
可以配置:

1
2
3
4
5
6
<mvc:resources mapping="/images/**" location="/images/"
    cache-period="31556926" />
<mvc:resources mapping="/js/**" location="/js/"
    cache-period="31556926" />
<mvc:resources mapping="/css/**" location="/css/"
    cache-period="31556926" />

处理静态资源。

32. 配置HandlerMapping

Spring自带了多个处理器映射实现:

  • BeanNameUrlHandlerMapping:根据控制器Bean的名字将控制器映射到URL。
  • ControllerBeanNameHandlerMapping:与BeanNameUrlHandlerMapping类似,根据控制器Bean的名字将控制器映射到URL。使用该处理器映射实现,Bean的名字不需要遵循URL的约定。
  • ControllerClassNameHandlerMapping:通过使用控制器的类名作为URL基础将控制器映射到URL。
  • DefaultAnnotationHandlerMapping:将请求映射给使用@RequestingMapping注解的控制器和控制器方法。
  • SimpleUrlHandlerMapping:使用定义在Spring应用上下文的熟悉集合将控制器映射到URL。
  • 使用如上这些处理器映射通常只需在Spring中配置一个Bean。如果没有找到处理器映射Bean,DisapatchServlet将创建并使用BeanNameUrlHandlerMapping和DefaultAnnotationHandlerMapping。我们一般使用基于注解的控制器类。
1
2
3
4
<mvc:annotation-driven />
<bean id="defaultAnnotationHandlerMapping"
    class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
</bean>

在构建控制器的时候,我们还需要使用注解将请求参数绑定到控制器的方法参数上进行校验以及信息转换。提供注解驱动的特性。

33. 配置HandlerAdapter

1
2
<bean id="annotationMethodHandlerAdapter"
class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />

34. 配置视图

在SpringMVC中大量使用了约定优于配置的开发模式。InternalResourceViewResolver就是一个面向约定的元素。它将逻辑视图名称解析为View对象,而该对象将渲染的任务委托给Web应用程序上下文中的一个模板。

1
2
3
4
5
6
7
8
<!-- 配置视图解析器,将ModelAndView及字符串解析为具体的页面 -->
<bean
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass"
        value="org.springframework.web.servlet.view.JstlView" />
    <property name="prefix" value="/WEB-INF/jsp/" />
    <property name="suffix" value=".jsp" />
</bean>

当DispatcherServlet要求InternalResourceViewResolver解析视图的时候,它将获取一个逻辑视图名称,添加”/WEB-INF/jsp/”前缀和”.jsp”后缀。等待的结果就是渲染输出的JSP路径。在内部,InternalResourceViewResolver接下来会将这个路径传递给View对象,View对象将请求传递给JSP.

参考文献

1. 《Sping In Action》 Craig Walls
2. 69道Spring面试题和答案
3. Sping+ActiveMQ整合

关于Spring的69个面试问答

这篇文章总结了一些关于Spring框架的重要问题,这些问题都是你在面试或笔试过程中可能会被问到的。下次你再也不用担心你的面试了,Java Code Geeks这就帮你解答。
大多数你可能被问到的问题都列举在下面的列表中了。所有的核心模块,从基础的Spring功能(如Spring Beans)到上层的Spring MVC框架,文章中都会进行简短的讲解。看完这些面试问题,你应该看看我们的Spring教程

我们开始吧!

目录

Spring概述

依赖注入

Spring Beans

Spring注解

Spring的对象访问

Spring面向切面编程

Spring MVC框架

Spring概述

1.什么是Spring?

Spring是一个开源的Java EE开发框架。Spring框架的核心功能可以应用在任何Java应用程序中,但对Java EE平台上的Web应用程序有更好的扩展性。Spring框架的目标是使得Java EE应用程序的开发更加简捷,通过使用POJO为基础的编程模型促进良好的编程风格。

2.Spring有哪些优点?

  • 轻量级:Spring在大小和透明性方面绝对属于轻量级的,基础版本的Spring框架大约只有2MB。
  • 控制反转(IOC):Spring使用控制反转技术实现了松耦合。依赖被注入到对象,而不是创建或寻找依赖对象。
  • 面向切面编程(AOP): Spring支持面向切面编程,同时把应用的业务逻辑与系统的服务分离开来。
  • 容器:Spring包含并管理应用程序对象的配置及生命周期。
  • MVC框架:Spring的web框架是一个设计优良的web MVC框架,很好的取代了一些web框架。
  • 事务管理:Spring对下至本地业务上至全局业务(JAT)提供了统一的事务管理接口。
  • 异常处理:Spring提供一个方便的API将特定技术的异常(由JDBC, Hibernate, 或JDO抛出)转化为一致的、Unchecked异常。

3.Spring框架有哪些模块?

Spring框架的基本模块如下所示:

  • Core module
  • Bean module
  • Context module
  • Expression Language module
  • JDBC module
  • ORM module
  • OXM module
  • Java Messaging Service(JMS) module
  • Transaction module
  • Web module
  • Web-Servlet module
  • Web-Struts module
  • Web-Portlet module

4.解释核心容器(应用上下文)模块

这是Spring的基本模块,它提供了Spring框架的基本功能。BeanFactory 是所有Spring应用的核心。Spring框架是建立在这个模块之上的,这也使得Spring成为一个容器。

5.BeanFactory – BeanFactory 实例

BeanFactory是工厂模式的一种实现,它使用控制反转将应用的配置和依赖与实际的应用代码分离开来。

最常用的BeanFactory实现是XmlBeanFactory类。

6.XmlBeanFactory

最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory,它根据XML文件中定义的内容加载beans。该容器从XML文件中读取配置元数据,并用它来创建一个完备的系统或应用。

7.解释AOP模块

AOP模块用来开发Spring应用程序中具有切面性质的部分。该模块的大部分服务由AOP Aliance提供,这就保证了Spring框架和其他AOP框架之间的互操作性。另外,该模块将元数据编程引入到了Spring。

8.解释抽象JDBC和DAO模块

通过使用抽象JDBC和DAO模块保证了与数据库连接代码的整洁与简单,同时避免了由于未能关闭数据库资源引起的问题。它在多种数据库服务器的错误信息之上提供了一个很重要的异常层。它还利用Spring的AOP模块为Spring应用程序中的对象提供事务管理服务。

9.解释对象/关系映射集成模块

Spring通过提供ORM模块在JDBC的基础上支持对象关系映射工具。这样的支持使得Spring可以集成主流的ORM框架,包括Hibernate, JDO, 及iBATIS SQL Maps。Spring的事务管理可以同时支持以上某种框架和JDBC。

10.解释web模块

Spring的web模块建立在应用上下文(application context)模块之上,提供了一个适合基于web应用程序的上下文环境。该模块还支持了几个面向web的任务,如透明的处理多文件上传请求及将请求参数同业务对象绑定起来。

11.解释Spring MVC模块

Spring提供MVC框架构建web应用程序。Spring可以很轻松的同其他MVC框架结合,但Spring的MVC是个更好的选择,因为它通过控制反转将控制逻辑和业务对象完全分离开来。

12.Spring的配置文件

Spring的配置文件是一个XML文件,文件包含了类信息并描述了这些类是如何配置和互相调用的。

13.Spring IoC容器是什么?

Spring IOC负责创建对象、管理对象(通过依赖注入)、整合对象、配置对象以及管理这些对象的生命周期。

14.IOC有什么优点?

IOC或依赖注入减少了应用程序的代码量。它使得应用程序的测试很简单,因为在单元测试中不再需要单例或JNDI查找机制。简单的实现以及较少的干扰机制使得松耦合得以实现。IOC容器支持勤性单例及延迟加载服务。

15.应用上下文是如何实现的?

FileSystemXmlApplicationContext 容器加载XML文件中beans的定义。XML Bean配置文件的完整路径必须传递给构造器。

FileSystemXmlApplicationContext 容器也加载XML文件中beans的定义。注意,你需要正确的设置CLASSPATH,因为该容器会在CLASSPATH中查看bean的XML配置文件。

WebXmlApplicationContext:该容器加载xml文件,这些文件定义了web应用中所有的beans。

16.Bean Factory和ApplicationContext有什么区别?

ApplicationContext提供了一种解决文档信息的方法,一种加载文件资源的方式(如图片),他们可以向监听他们的beans发送消息。另外,容器或者容器中beans的操作,这些必须以bean工厂的编程方式处理的操作可以在应用上下文中以声明的方式处理。应用上下文实现了MessageSource,该接口用于获取本地消息,实际的实现是可选的。

17.Spring应用程序看起来像什么?

  • 一个定义功能的接口
  • 实现包括属性,setter和getter方法,功能等
  • Spring AOP
  • Spring的XML配置文件
  • 使用该功能的客户端编程

依赖注入

18.Spring中的依赖注入是什么?

依赖注入作为控制反转(IOC)的一个层面,可以有多种解释方式。在这个概念中,你不用创建对象而只需要描述如何创建它们。你不必通过代码直接的将组件和服务连接在一起,而是通过配置文件说明哪些组件需要什么服务。之后IOC容器负责衔接。

19.有哪些不同类型的IOC(依赖注入)?

  • 构造器依赖注入:构造器依赖注入在容器触发构造器的时候完成,该构造器有一系列的参数,每个参数代表注入的对象。
  • Setter方法依赖注入:首先容器会触发一个无参构造函数或无参静态工厂方法实例化对象,之后容器调用bean中的setter方法完成Setter方法依赖注入。

20.你推荐哪种依赖注入?构造器依赖注入还是Setter方法依赖注入?

你可以同时使用两种方式的依赖注入,最好的选择是使用构造器参数实现强制依赖注入,使用setter方法实现可选的依赖关系。

Spring Beans

21.什么是Spring Beans?

Spring Beans是构成Spring应用核心的Java对象。这些对象由Spring IOC容器实例化、组装、管理。这些对象通过容器中配置的元数据创建,例如,使用XML文件中定义的创建。

在Spring中创建的beans都是单例的beans。在bean标签中有一个属性为”singleton”,如果设为true,该bean是单例的,如果设为false,该bean是原型bean。Singleton属性默认设置为true。因此,spring框架中所有的bean都默认为单例bean。

22.Spring Bean中定义了什么内容?

Spring Bean中定义了所有的配置元数据,这些配置信息告知容器如何创建它,它的生命周期是什么以及它的依赖关系。

23.如何向Spring 容器提供配置元数据?

有三种方式向Spring 容器提供元数据:

24.你如何定义bean的作用域?

在Spring中创建一个bean的时候,我们可以声明它的作用域。只需要在bean定义的时候通过’scope’属性定义即可。例如,当Spring需要产生每次一个新的bean实例时,应该声明bean的scope属性为prototype。如果每次你希望Spring返回一个实例,应该声明bean的scope属性为singleton。

25.说一下Spring中支持的bean作用域

Spring框架支持如下五种不同的作用域:

  • singleton:在Spring IOC容器中仅存在一个Bean实例,Bean以单实例的方式存在。
  • prototype:一个bean可以定义多个实例。
  • request:每次HTTP请求都会创建一个新的Bean。该作用域仅适用于WebApplicationContext环境。
  • session:一个HTTP Session定义一个Bean。该作用域仅适用于WebApplicationContext环境.
  • globalSession:同一个全局HTTP Session定义一个Bean。该作用域同样仅适用于WebApplicationContext环境.

bean默认的scope属性是’singleton‘。

26.Spring框架中单例beans是线程安全的吗?

不是,Spring框架中的单例beans不是线程安全的。

27.解释Spring框架中bean的生命周期

  • Spring容器读取XML文件中bean的定义并实例化bean。
  • Spring根据bean的定义设置属性值。
  • 如果该Bean实现了BeanNameAware接口,Spring将bean的id传递给setBeanName()方法。
  • 如果该Bean实现了BeanFactoryAware接口,Spring将beanfactory传递给setBeanFactory()方法。
  • 如果任何bean BeanPostProcessors 和该bean相关,Spring调用postProcessBeforeInitialization()方法。
  • 如果该Bean实现了InitializingBean接口,调用Bean中的afterPropertiesSet方法。如果bean有初始化函数声明,调用相应的初始化方法。
  • 如果任何bean BeanPostProcessors 和该bean相关,调用postProcessAfterInitialization()方法。
  • 如果该bean实现了DisposableBean,调用destroy()方法。

28.哪些是最重要的bean生命周期方法?能重写它们吗?

有两个重要的bean生命周期方法。第一个是setup方法,该方法在容器加载bean的时候被调用。第二个是teardown方法,该方法在bean从容器中移除的时候调用。

bean标签有两个重要的属性(init-method 和 destroy-method),你可以通过这两个属性定义自己的初始化方法和析构方法。Spring也有相应的注解:@PostConstruct 和 @PreDestroy。

29.什么是Spring的内部bean?

当一个bean被用作另一个bean的属性时,这个bean可以被声明为内部bean。在基于XML的配置元数据中,可以通过把元素定义在 或元素内部实现定义内部bean。内部bean总是匿名的并且它们的scope总是prototype。

30.如何在Spring中注入Java集合类?

Spring提供如下几种类型的集合配置元素

  • list元素用来注入一系列的值,允许有相同的值。
  • set元素用来注入一些列的值,不允许有相同的值。
  • map用来注入一组”键-值”对,键、值可以是任何类型的。
  • props也可以用来注入一组”键-值”对,这里的键、值都字符串类型。

31.什么是bean wiring?

Wiring,或者说bean Wiring是指beans在Spring容器中结合在一起的情况。当装配bean的时候,Spring容器需要知道需要哪些beans以及如何使用依赖注入将它们结合起来。

32.什么是bean自动装配?

Spring容器可以自动配置相互协作beans之间的关联关系。这意味着Spring可以自动配置一个bean和其他协作bean之间的关系,通过检查BeanFactory 的内容里没有使用和< property>元素。

33.解释自动装配的各种模式?

自动装配提供五种不同的模式供Spring容器用来自动装配beans之间的依赖注入:

  • no:默认的方式是不进行自动装配,通过手工设置ref 属性来进行装配bean。
  • byName:通过参数名自动装配,Spring容器查找beans的属性,这些beans在XML配置文件中被设置为byName。之后容器试图匹配、装配和该bean的属性具有相同名字的bean。
  • byType:通过参数的数据类型自动自动装配,Spring容器查找beans的属性,这些beans在XML配置文件中被设置为byType。之后容器试图匹配和装配和该bean的属性类型一样的bean。如果有多个bean符合条件,则抛出错误。
  • constructor:这个同byType类似,不过是应用于构造函数的参数。如果在BeanFactory中不是恰好有一个bean与构造函数参数相同类型,则抛出一个严重的错误。
  • autodetect:如果有默认的构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。

34.自动装配有哪些局限性?

自动装配有如下局限性:

  • 重写:你仍然需要使用 和< property>设置指明依赖,这意味着总要重写自动装配。
  • 原生数据类型:你不能自动装配简单的属性,如原生类型、字符串和类。
  • 模糊特性:自动装配总是没有自定义装配精确,因此,如果可能尽量使用自定义装配。

35.你可以在Spring中注入null或空字符串吗?

完全可以。

Spring注解

36.什么是Spring基于Java的配置?给出一些注解的例子

基于Java的配置允许你使用Java的注解进行Spring的大部分配置而非通过传统的XML文件配置。

以注解@Configuration为例,它用来标记类,说明作为beans的定义,可以被Spring IOC容器使用。另一个例子是@Bean注解,它表示该方法定义的Bean要被注册进Spring应用上下文中。

37.什么是基于注解的容器配置?

另外一种替代XML配置的方式为基于注解的配置,这种方式通过字节元数据装配组件而非使用尖括号声明。开发人员将直接在类中进行配置,通过注解标记相关的类、方法或字段声明,而不再使用XML描述bean之间的连线关系。

38.如何开启注解装配?

注解装配默认情况下在Spring容器中是不开启的。如果想要开启基于注解的装配只需在Spring配置文件中配置元素即可。

39.@Required 注解

@Required表明bean的属性必须在配置时设置,可以在bean的定义中明确指定也可通过自动装配设置。如果bean的属性未设置,则抛出BeanInitializationException异常。

40.@Autowired 注解

@Autowired 注解提供更加精细的控制,包括自动装配在何处完成以及如何完成。它可以像@Required一样自动装配setter方法、构造器、属性或者具有任意名称和/或多个参数的PN方法。

41. @Qualifier 注解

当有多个相同类型的bean而只有其中的一个需要自动装配时,将@Qualifier 注解和@Autowire 注解结合使用消除这种混淆,指明需要装配的bean。

Spring数据访问

42.在Spring框架中如何更有效的使用JDBC?

使用Spring JDBC框架,资源管理以及错误处理的代价都会减轻。开发人员只需通过statements和queries语句从数据库中存取数据。Spring框架中通过使用模板类能更有效的使用JDBC,也就是所谓的JdbcTemplate(例子)。

43.JdbcTemplate

JdbcTemplate类提供了许多方法,为我们与数据库的交互提供了便利。例如,它可以将数据库的数据转化为原生类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据库错误处理功能。

44.Spring对DAO的支持

Spring对数据访问对象(DAO)的支持旨在使它可以与数据访问技术(如 JDBC, Hibernate 及JDO)方便的结合起来工作。这使得我们可以很容易在的不同的持久层技术间切换,编码时也无需担心会抛出特定技术的异常。

45.使用Spring可以通过什么方式访问Hibernate?

使用Spring有两种方式访问Hibernate:

  • 使用Hibernate Template的反转控制以及回调方法
  • 继承HibernateDAOSupport,并申请一个AOP拦截器节点

46.Spring支持的ORM

Spring支持一下ORM:

  • Hibernate
  • iBatis
  • JPA (Java -Persistence API)
  • TopLink
  • JDO (Java Data Objects)
  • OJB

47.如何通过HibernateDaoSupport将Spring和Hibernate结合起来?

使用Spring的SessionFactory 调用LocalSessionFactory。结合过程分为以下三步:

  • 配置Hibernate SessionFactory
  • 继承HibernateDaoSupport实现一个DAO
  • 使用AOP装载事务支持

48.Spring支持的事务管理类型

Spring支持如下两种方式的事务管理:

  • 编程式事务管理:这意味着你可以通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。
  • 声明式事务管理:这种方式意味着你可以将事务管理和业务代码分离。你只需要通过注解或者XML配置管理事务。

49.Spring框架的事务管理有哪些优点?

  • 它为不同的事务API(如JTA, JDBC, Hibernate, JPA, 和JDO)提供了统一的编程模型。
  • 它为编程式事务管理提供了一个简单的API而非一系列复杂的事务API(如JTA).
  • 它支持声明式事务管理。
  • 它可以和Spring 的多种数据访问技术很好的融合。

50.你更推荐那种类型的事务管理?

许多Spring框架的用户选择声明式事务管理,因为这种方式和应用程序的关联较少,因此更加符合轻量级容器的概念。声明式事务管理要优于编程式事务管理,尽管在灵活性方面它弱于编程式事务管理(这种方式允许你通过代码控制业务)。

Spring面向切面编程(AOP)

51.解释AOP

面向切面编程,或AOP允许程序员模块化横向业务逻辑,或定义核心部分的功能,例如日志管理和事务管理。

52.切面(Aspect)

AOP的核心就是切面,它将多个类的通用行为封装为可重用的模块。该模块含有一组API提供 cross-cutting功能。例如,日志模块称为日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。

53.在Spring AOP中concern和 cross-cutting concern的区别是什么?

Concern(核心逻辑):表示在应用程序中一个模块的行为。Concern可以定义为我们想要实现的功能。

Cross-cutting concern(横向的通用逻辑):指的是整个应用程序都会用到的功能,它影响整个应用程序。例如,日志管理(Logging)、安全管理(Security)以及数据交互是应用程序的每个模块都要涉及到的,因此这些都属于Cross-cutting concern。

54.连接点(Join point)

连接点代表应用程序中插入AOP切面的地点。它实际上是Spring AOP框架在应用程序中执行动作的地点。

55.通知(Advice)

通知表示在方法执行前后需要执行的动作。实际上它是Spring AOP框架在程序执行过程中触发的一些代码。

Spring切面可以执行一下五种类型的通知:

  • before(前置通知):在一个方法之前执行的通知。
  • after(最终通知):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
  • after-returning(后置通知):在某连接点正常完成后执行的通知。
  • after-throwing(异常通知):在方法抛出异常退出时执行的通知。
  • around(环绕通知):在方法调用前后触发的通知。

56.切入点(Pointcut)

切入点是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方式指明切入点。

57.什么是引入?

引入允许我们在已有的类上添加新的方法或属性。

58.什么是目标对象?

被一个或者多个切面所通知的对象。它通常是一个代理对象。也被称做被通知(advised)对象。

59.什么是代理?

代理是将通知应用到目标对象后创建的对象。从客户端的角度看,代理对象和目标对象是一样的。

60.有几种不同类型的自动代理?

  • BeanNameAutoProxyCreator:bean名称自动代理创建器
  • DefaultAdvisorAutoProxyCreator:默认通知者自动代理创建器
  • Metadata autoproxying:元数据自动代理

61.什么是织入?什么是织入应用的不同点?

织入是将切面和其他应用类型或对象连接起来创建一个通知对象的过程。织入可以在编译、加载或运行时完成。

62.解释基于XML Schema方式的切面实现

在这种情况下,切面由使用XML文件配置的类实现。

63.解释基于注解方式(基于@AspectJ)的切面实现

在这种情况下(基于@AspectJ的实现),指的是切面的对应的类使用Java 5注解的声明方式。

Spring的MVC框架

64.什么是Spring的MVC框架?

Spring提供了一个功能齐全的MVC框架用于构建Web应用程序。Spring框架可以很容易的和其他的MVC框架融合(如Struts),该框架使用控制反转(IOC)将控制器逻辑和业务对象分离开来。它也允许以声明的方式绑定请求参数到业务对象上。

65.DispatcherServlet

Spring的MVC框架围绕DispatcherServlet来设计的,它用来处理所有的HTTP请求和响应。

66.WebApplicationContext

WebApplicationContext继承了ApplicationContext,并添加了一些web应用程序需要的功能。和普通的ApplicationContext 不同,WebApplicationContext可以用来处理主题样式,它也知道如何找到相应的servlet。

67.什么是Spring MVC框架的控制器?

控制器提供对应用程序行为的访问,通常通过服务接口实现。控制器解析用户的输入,并将其转换为一个由视图呈现给用户的模型。Spring 通过一种极其抽象的方式实现控制器,它允许用户创建多种类型的控制器。

68.@Controller annotation

@Controller注解表示该类扮演控制器的角色。Spring不需要继承任何控制器基类或应用Servlet API。

69.@RequestMapping annotation

@RequestMapping注解用于将URL映射到任何一个类或者一个特定的处理方法上。

好了,现在你可以去面试了!不要忘了访问我们的Spring教程

原文链接: javacodegeeks 翻译: ImportNew.com 人晓
译文链接: http://www.importnew.com/11657.html

Spring MVC Guide and Resources

 Spring MVC Guide

Step Description
1 Create a Dynamic Web Project with a name HelloWeb and create a package com.tutorialspoint under the src folder in the created project.
2 Drag and drop below mentioned Spring and other libraries into the folder WebContent/WEB-INF/lib.
3 Create a Java class HelloController under the com.tutorialspoint package.
4 Create Spring configuration files web.xml and HelloWeb-servlet.xml under the WebContent/WEB-INF folder.
5 Create a sub-folder with a name jsp under the WebContent/WEB-INF folder. Create a view file hello.jsp under this sub-folder.
6 The final step is to create the content of all the source and configuration files and export the application as explained below.

 

Spring MVC Hello World Example
http://www.tutorialspoint.com/spring/spring_mvc_hello_world_example.htm

 

Download Spring jar package:
http://maven.springframework.org/release/org/springframework/spring/ or http://repo.spring.io/release/org/springframework/spring/

Where can I download Spring Framework jars without using Maven
http://stackoverflow.com/questions/19082860/where-can-i-download-spring-framework-jars-without-using-maven

http://mvnrepository.com

使用 Spring 进行单元测试

概述

单元测试和集成测试在我们的软件开发整个流程中占有举足轻重的地位,一方面,程序员通过编写单元测试来验证自己程序的有效性,另外一方面,管理者通过持续自动的执行单元测试和分析单元测试的覆盖率等来确保软件本身的质量。这里,我们先不谈单元测试本身的重要性,对于目前大多数的基于 Java 的企业应用软件来说,Spring 已经成为了标准配置,一方面它实现了程序之间的低耦合度,另外也通过一些配置减少了企业软件集成的工作量,例如和 Hibernate、Struts 等的集成。那么,有个问题,在普遍使用 Spring 的应用程序中,我们如何去做单元测试?或者说,我们怎么样能高效的在 Spring 生态系统中实现各种单元测试手段?这就是本文章要告诉大家的事情。

单元测试目前主要的框架包括 Junit、TestNG,还有些 MOCK 框架,例如 Jmock、Easymock、PowerMock 等,这些都是单元测试的利器,但是当把他们用在 Spring 的开发环境中,还是那么高效么?还好,Spring 提供了单元测试的强大支持,主要特性包括:

  • 支持主流的测试框架 Junit 和 TestNG
  • 支持在测试类中使用依赖注入 Denpendency Injection
  • 支持测试类的自动化事务管理
  • 支持使用各种注释标签,提高开发效率和代码简洁性
  • Spring 3.1 更是支持在测试类中使用非 XML 配置方法和基于 Profile 的 bean 配置模式

通过阅读本文,您能够快速的掌握基于 Spring TestContext 框架的测试方法,并了解基本的实现原理。本文将提供大量测试标签的使用方法,通过这些标签,开发人员能够极大的减少编码工作量。OK,现在让我们开始 Spring 的测试之旅吧!


原来我们是怎么做的

这里先展示一个基于 Junit 的单元测试,这个单元测试运行在基于 Spring 的应用程序中,需要使用 Spring 的相关配置文件来进行测试。相关类图如下:

数据库表

假设有一个员工账号表,保存了员工的基本账号信息,表结构如下:

  • ID:整数类型,唯一标识
  • NAME:字符串,登录账号
  • SEX:字符串,性别
  • AGE:字符串,年龄

假设表已经建好,且内容为空。

测试工程目录结构和依赖 jar 包

在 Eclipse 中,我们可以展开工程目录结构,看到如下图所示的工程目录结构和依赖的 jar 包列表:

您需要引入的 jar 包括:

  • cglib-nodep-2.2.3.jar
  • commons-logging.jar
  • hsqldb.jar
  • Junit-4.5.jar
  • log4j-1.2.14.jar
  • Spring-asm-3.2.0.M1.jar
  • Spring-beans-3.2.0.M1.jar
  • Spring-context-3.2.0.M1.jar
  • Spring-core-3.2.0.M1.jar
  • Spring-expression-3.2.0.M1.jar
  • Spring-jdbc-3.2.0.M1.jar
  • Spring-test-3.2.0.M1.jar
  • Spring-tx-3.2.0.M1.jar
  • testng-6.8.jar

其中的 hsqldb 是我们测试用数据库。

图 1. 工程目录结构

图 1. 工程目录结构

类总体介绍

假设我们现在有一个基于 Spring 的应用程序,除了 MVC 层,还包括业务层和数据访问层,业务层有一个类 AccountService,负责处理账号类的业务,其依赖于数据访问层 AccountDao 类,此类提供了基于 Spring Jdbc Template 实现的数据库访问方法,AccountService 和 AccountDao 以及他们之间的依赖关系都是通过 Spring 配置文件进行管理的。

现在我们要对 AccountService 类进行测试,在不使用 Spring 测试方法之前,我们需要这样做:

此类代表账号的基本信息,提供 getter 和 setter 方法。

清单 1. Account.Java
 package domain; 

 public class Account { 
	 public static final String SEX_MALE = "male"; 
	 public static final String SEX_FEMALE = "female"; 
	
	 private int id; 
	 private String name; 
	 private int age; 
	 private String sex; 
     public String toString() { 
	    return String.format("Account[id=%d,name=%s,age:%d,sex:%s]",id,name,age,sex); 
	 } 
	 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 int getAge() { 
		 return age; 
	 } 
	 public void setAge(int age) { 
		 this.age = age; 
	 } 
	 public String getSex() { 
		 return sex; 
	 } 
	 public void setSex(String sex) { 
		 this.sex = sex; 
	 } 
	
     public static Account getAccount(int id,String name,int age,String sex) { 
		 Account acct = new Account(); 
		 acct.setId(id); 
		 acct.setName(name); 
		 acct.setAge(age); 
		 acct.setSex(sex); 
		 return acct; 
	 } 
 }

注意上面的 Account 类有一个 toString() 方法和一个静态的 getAccount 方法,getAccount 方法用于快速获取 Account 测试对象。

这个 DAO 我们这里为了简单起见,采用 Spring Jdbc Template 来实现。

清单 2. AccountDao.Java
 package DAO; 

 import Java.sql.ResultSet; 
 import Java.sql.SQLException; 
 import Java.util.HashMap; 
 import Java.util.List; 
 import Java.util.Map; 

 import org.Springframework.context.ApplicationContext; 
 import org.Springframework.context.support.ClassPathXmlApplicationContext; 
 import org.Springframework.jdbc.core.RowMapper; 
 import org.Springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport; 
 import org.Springframework.jdbc.core.simple.ParameterizedRowMapper; 

 import domain.Account; 

 public class AccountDao extends NamedParameterJdbcDaoSupport { 
	 public void saveAccount(Account account) { 
		 String sql = "insert into tbl_account(id,name,age,sex) " + 
				"values(:id,:name,:age,:sex)"; 
		 Map paramMap = new HashMap(); 
		 paramMap.put("id", account.getId()); 
		 paramMap.put("name", account.getName()); 
		 paramMap.put("age", account.getAge()); 
		 paramMap.put("sex",account.getSex()); 
		 getNamedParameterJdbcTemplate().update(sql, paramMap); 
	 } 
	
	 public Account getAccountById(int id) { 
		 String sql = "select id,name,age,sex from tbl_account where id=:id"; 
		 Map paramMap = new HashMap(); 
		 paramMap.put("id", id); 
		 List<Account> matches = getNamedParameterJdbcTemplate().query(sql, 
		 paramMap,new ParameterizedRowMapper<Account>() { 
					 @Override 
					 public Account mapRow(ResultSet rs, int rowNum) 
							 throws SQLException { 
						 Account a = new Account(); 
						 a.setId(rs.getInt(1)); 
						 a.setName(rs.getString(2)); 
						 a.setAge(rs.getInt(3)); 
						 a.setSex(rs.getString(4)); 
						 return a; 
					 } 
			
		 }); 
		 return matches.size()>0?matches.get(0):null; 
	 } 
	
 }

AccountDao 定义了几个账号对象的数据库访问方法:

  • saveAccount:负责把传入的账号对象入库
  • getAccountById:负责根据 Id 查询账号
清单 3. AccountService.Java
 package service; 

 import org.apache.commons.logging.Log; 
 import org.apache.commons.logging.LogFactory; 
 import org.Springframework.beans.factory.annotation.Autowired; 

 import DAO.AccountDao; 
 import domain.Account; 

 public class AccountService { 
	 private static final Log log = LogFactory.getLog(AccountService.class); 
	
	 @Autowired 
	 private AccountDao accountDao; 
	
	 public Account getAccountById(int id) { 
		 return accountDao.getAccountById(id); 
	 } 
	
	 public void insertIfNotExist(Account account) { 
		 Account acct = accountDao.getAccountById(account.getId()); 
		 if(acct==null) { 
			 log.debug("No "+account+" found,would insert it."); 
             accountDao.saveAccount(account); 
		 } 
		 acct = null; 
	 } 
		
 }

AccountService 包括下列方法:

  • getAccountById:根据 Id 查询账号信息
  • insertIfNotExist:根据传入的对象插入数据库

其依赖的 DAO 对象 accountDao 是通过 Spring 注释标签 @Autowired 自动注入的。

上述几个类的依赖关系是通过 Spring 进行管理的,配置文件如下:

清单 4. Spring 配置文件
 <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"
 xsi:schemaLocation="http://www.Springframework.org/schema/beans 
 http://www.Springframework.org/schema/beans/Spring-beans-3.0.xsd 
 http://www.Springframework.org/schema/context 
 http://www.Springframework.org/schema/context/Spring-context-3.0.xsd "> 
	
 <context:annotation-config/> 
 <bean id="datasource" class="
 org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
		 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
		 <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
		 <property name="username" value="sa" /> 
		 <property name="password" value="" /> 
	 </bean> 
	 <bean id="initer" init-method="init" class="service.Initializer"> 
	 </bean> 
 <bean id="accountDao" depends-on="initer" class="DAO.AccountDao"> 
		 <property name="dataSource" ref="datasource" /> 
	 </bean> 
 <bean id="accountService" class="service.AccountService"> 
	 </bean> 
 </beans>

注意其中的“<context:annotation-config/>”的作用,这个配置启用了 Spring 对 Annotation 的支持,这样在我们的测试类中 @Autowired 注释才会起作用(如果用了 Spring 测试框架,则不需要这样的配置项,稍后会演示)。另外还有一个 accountDao 依赖的 initer bean, 这个 bean 的作用是加载 log4j 日志环境,不是必须的。

另外还有一个要注意的地方,就是 datasource 的定义,由于我们使用的是 Spring Jdbc Template,所以只要定义一个 org.Springframework.jdbc.datasource.DriverManagerDataSource 类型的 datasource 即可。这里我们使用了简单的数据库 HSQL、Single Server 运行模式,通过 JDBC 进行访问。实际测试中,大家可以选择 Oracle 或者 DB2、Mysql 等。

好,万事具备,下面我们来用 Junit4 框架测试 accountService 类。代码如下:

清单 5. AccountServiceOldTest.Java
 package service; 

 import static org.Junit.Assert.assertEquals; 

 import org.Junit.BeforeClass; 
 import org.Junit.Test; 
 import org.Springframework.context.ApplicationContext; 
 import org.Springframework.context.support.ClassPathXmlApplicationContext; 

 import domain.Account; 


 public class AccountServiceOldTest { 
	 private static AccountService service; 
	
	 @BeforeClass 
	 public static void init() { 
		 ApplicationContext 
 context = new ClassPathXmlApplicationContext("config/Spring-db-old.xml"); 
		 service = (AccountService)context.getBean("accountService"); 
	 } 	
	
	 @Test 
	 public void testGetAcccountById() { 
 Account acct = Account.getAccount(1, "user01", 18, "M"); 
		 Account acct2 = null; 
		 try { 
 service.insertIfNotExist(acct); 
			 acct2 = service.getAccountById(1); 
			 assertEquals(acct, acct2); 
		 } catch (Exception ex) { 
			 fail(ex.getMessage()); 
		 } finally { 
			 service.removeAccount(acct); 
		 } 
 } 
 }

注意上面的 Junit4 注释标签,第一个注释标签 @BeforeClass,用来执行整个测试类需要一次性初始化的环境,这里我们用 Spring 的 ClassPathXmlApplicationContext 从 XML 文件中加载了上面定义的 Spring 配置文件,并从中获得了 accountService 的实例。第二个注释标签 @Test 用来进行实际的测试。

测试过程:我们先获取一个 Account 实例对象,然后通过 service bean 插入数据库中,然后通过 getAccountById 方法从数据库再查询这个记录,如果能获取,则判断两者的相等性;如果相同,则表示测试成功。成功后,我们尝试删除这个记录,以利于下一个测试的进行,这里我们用了 try-catch-finally 来保证账号信息会被清除。

执行测试:(在 Eclipse 中,右键选择 AccountServiceOldTest 类,点击 Run as Junit test 选项),得到的结果如下:

执行测试的结果

在 Eclipse 的 Junit 视图中,我们可以看到如下的结果:

图 2. 测试的结果

图 2. 测试的结果

对于这种不使用 Spring test 框架进行的单元测试,我们注意到,需要做这些工作:

  • 在测试开始之前,需要手工加载 Spring 的配置文件,并获取需要的 bean 实例
  • 在测试结束的时候,需要手工清空搭建的数据库环境,比如清除您插入或者更新的数据,以保证对下一个测试没有影响

另外,在这个测试类中,我们还不能使用 Spring 的依赖注入特性。一切都靠手工编码实现。好,那么我们看看 Spring test 框架能做到什么。

首先我们修改一下 Spring 的 XML 配置文件,删除 <context:annotation-config/> 行,其他不变。

清单 6. Spring-db1.xml
 <beans xmlns="http://www.Springframework.org/schema/beans"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.Springframework.org/schema/beans 
 http://www.Springframework.org/schema/beans/Spring-beans-3.2.xsd"> 
 <bean id="datasource" 
 class="org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
		 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
		 <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
		 <property name="username" value="sa"/> 
		 <property name="password" value=""/> 
	 </bean> 
 <bean id="transactionManager" 
 class="org.Springframework.jdbc.datasource.DataSourceTransactionManager"> 
 		 <property name="dataSource" ref="datasource"></property> 
 	 </bean> 
 	 <bean id="initer" init-method="init" class="service.Initializer"> 
 	 </bean> 
 <bean id="accountDao" depends-on="initer" class="DAO.AccountDao"> 
 		 <property name="dataSource" ref="datasource"/> 
 	 </bean> 
 	 <bean id="accountService" class="service.AccountService"> 
 	 </bean> 
 </beans>

其中的 transactionManager 是 Spring test 框架用来做事务管理的管理器。

清单 7. AccountServiceTest1.Java
 package service; 
 import static org.Junit.Assert.assertEquals; 

 import org.Junit.Test; 
 import org.Junit.runner.RunWith; 
 import org.Springframework.beans.factory.annotation.Autowired; 
 import org.Springframework.test.context.ContextConfiguration; 
 import org.Springframework.test.context.Junit4.SpringJUnit4ClassRunner; 
 import org.Springframework.transaction.annotation.Transactional; 

 import domain.Account; 

 @RunWith(SpringJUnit4ClassRunner.class) 
 @ContextConfiguration("/config/Spring-db1.xml") 
 @Transactional 
 public class AccountServiceTest1 { 
	 @Autowired 
	 private AccountService service; 
	
	 @Test 
	 public void testGetAcccountById() { 
 Account acct = Account.getAccount(1, "user01", 18, "M"); 
		 service.insertIfNotExist(acct); 
		 Account acct2 = service.getAccountById(1); 
		 assertEquals(acct,acct2); 
	 } 
 }

对这个类解释一下:

  • @RunWith 注释标签是 Junit 提供的,用来说明此测试类的运行者,这里用了 SpringJUnit4ClassRunner,这个类是一个针对 Junit 运行环境的自定义扩展,用来标准化在 Spring 环境中 Junit4.5 的测试用例,例如支持的注释标签的标准化
  • @ContextConfiguration 注释标签是 Spring test context 提供的,用来指定 Spring 配置信息的来源,支持指定 XML 文件位置或者 Spring 配置类名,这里我们指定 classpath 下的 /config/Spring-db1.xml 为配置文件的位置
  • @Transactional 注释标签是表明此测试类的事务启用,这样所有的测试方案都会自动的 rollback,即您不用自己清除自己所做的任何对数据库的变更了
  • @Autowired 体现了我们的测试类也是在 Spring 的容器中管理的,他可以获取容器的 bean 的注入,您不用自己手工获取要测试的 bean 实例了
  • testGetAccountById 是我们的测试用例:注意和上面的 AccountServiceOldTest 中相同的测试方法的对比,这里我们不用再 try-catch-finally 了,事务管理自动运行,当我们执行完成后,所有相关变更会被自动清除

执行结果

在 Eclipse 的 Junit 视图中,我们可以看到如下的结果:

图 3. 执行结果

图 3. 执行结果

小结

如果您希望在 Spring 环境中进行单元测试,那么可以做如下配置:

  • 继续使用 Junit4 测试框架,包括其 @Test 注释标签和相关的类和方法的定义,这些都不用变
  • 您需要通过 @RunWith(SpringJUnit4ClassRunner.class) 来启动 Spring 对测试类的支持
  • 您需要通过 @ContextConfiguration 注释标签来指定 Spring 配置文件或者配置类的位置
  • 您需要通过 @Transactional 来启用自动的事务管理
  • 您可以使用 @Autowired 自动织入 Spring 的 bean 用来测试

另外您不再需要:

  • 手工加载 Spring 的配置文件
  • 手工清理数据库的每次变更
  • 手工获取 application context 然后获取 bean 实例

Spring 测试注释标签

我们已经看到利用 Spring test framework 来进行基于 Junit4 的单元测试是多么的简单,下面我们来看一下前面遇到的各种注释标签的一些可选用法。

@ContextConfiguration 和 @Configuration 的使用

刚才已经介绍过,可以输入 Spring xml 文件的位置,Spring test framework 会自动加载 XML 文件,得到 application context,当然也可以使用 Spring 3.0 新提供的特性 @Configuration,这个注释标签允许您用 Java 语言来定义 bean 实例,举个例子:

现在我们将前面定义的 Spring-db1.xml 进行修改,我们希望其中的三个 bean:initer、accountDao、accountService 通过配置类来定义,而不是 XML,则我们需要定义如下配置类:

注意:如果您想使用 @Configuration,请在 classpath 中加入 cglib 的 jar 包(cglib-nodep-2.2.3.jar),否则会报错。

清单 8. SpringDb2Config.Java
 package config; 

 import org.Springframework.beans.factory.annotation.Autowired; 
 import org.Springframework.context.annotation.Bean; 
 import org.Springframework.context.annotation.Configuration; 
 import org.Springframework.jdbc.datasource.DriverManagerDataSource; 

 import service.AccountService; 
 import service.Initializer; 
 import DAO.AccountDao; 

 @Configuration 
 public class SpringDb2Config { 
	 private @Autowired DriverManagerDataSource datasource; 
	 @Bean 
	 public Initializer initer() { 
		 return new Initializer(); 
	 } 
	
	 @Bean 
	 public AccountDao accountDao() { 
 AccountDao DAO = new AccountDao(); 
 DAO.setDataSource(datasource); 
 return DAO; 
	 } 
	
	 @Bean 
	 public AccountService accountService() { 
 return new AccountService(); 
	 } 
 }

注意上面的注释标签:

  • @Configuration:表明这个类是一个 Spring 配置类,提供 Spring 的 bean 定义,实际效果等同于 XML 配置方法
  • @Bean:表明这个方法是一个 bean 的定义,缺省情况下,方法名称就是 bean 的 Id
  • @Autowired:这个 datasource 采用自动注入的方式获取

注意,我们采用的是 XML+config bean 的方式进行配置,这种方式比较符合实际项目的情况。相关的 Spring 配置文件也要做变化,如下清单所示:

清单 9. Spring-db2.xml
 <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"
 xsi:schemaLocation="http://www.Springframework.org/schema/beans 
 http://www.Springframework.org/schema/beans/Spring-beans-3.0.xsd 
 http://www.Springframework.org/schema/context 
 http://www.Springframework.org/schema/context/Spring-context-3.0.xsd"> 
 <context:annotation-config/> 
 <bean id="datasource" 
 class="org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
		 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
		 <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
		 <property name="username" value="sa"/> 
		 <property name="password" value=""/> 
	 </bean> 
 <bean id="transactionManager" 
          class="org.Springframework.jdbc.datasource.DataSourceTransactionManager"> 
 		 <property name="dataSource" ref="datasource"></property> 
 	 </bean> 
 	
 	 <bean class="config.SpringDb2Config"/> 
 </beans>

注意里面的 context 命名空间的定义,如代码中黑体字所示。另外还必须有 <context:annotaiton-config/> 的定义,这个定义允许采用注释标签的方式来控制 Spring 的容器,最后我们看到 beans 已经没有 initer、accountDao 和 accountService 这些 bean 的定义,取而代之的是一个 SpringDb2Config bean 的定义,注意这个 bean 没有名称,因为不需要被引用。

现在有了这些配置,我们的测试类只要稍稍修改一下,即可实现加载配置类的效果,如下:

 @ContextConfiguration("/config/Spring-db2.xml")

通过上面的配置,测试用例就可以实现加载 Spring 配置类,运行结果也是成功的 green bar。

@DirtiesContext

缺省情况下,Spring 测试框架一旦加载 applicationContext 后,将一直缓存,不会改变,但是,

由于 Spring 允许在运行期修改 applicationContext 的定义,例如在运行期获取 applicationContext,然后调用 registerSingleton 方法来动态的注册新的 bean,这样的情况下,如果我们还使用 Spring 测试框架的被修改过 applicationContext,则会带来测试问题,我们必须能够在运行期重新加载 applicationContext,这个时候,我们可以在测试类或者方法上注释:@DirtiesContext,作用如下:

  • 如果定义在类上(缺省),则在此测试类运行完成后,重新加载 applicationContext
  • 如果定义在方法上,即表示测试方法运行完成后,重新加载 applicationContext

@TransactionConfiguration 和 @Rollback

缺省情况下,Spring 测试框架将事务管理委托到名为 transactionManager 的 bean 上,如果您的事务管理器不是这个名字,那需要指定 transactionManager 属性名称,还可以指定 defaultRollback 属性,缺省为 true,即所有的方法都 rollback,您可以指定为 false,这样,在一些需要 rollback 的方法,指定注释标签 @Rollback(true)即可。


对 Junit4 的注释标签支持

看了上面 Spring 测试框架的注释标签,我们来看看一些常见的基于 Junit4 的注释标签在 Spring 测试环境中的使用方法。

@Test(expected=…)

此注释标签的含义是,这是一个测试,期待一个异常的发生,期待的异常通过 xxx.class 标识。例如,我们修改 AccountService.Java 的 insertIfNotExist 方法,对于传入的参数如果为空,则抛出 IllegalArgumentException,如下:

 public void insertIfNotExist(Account account) { 
	 if(account==null) 
		 throw new IllegalArgumentException("account is null"); 
	 Account acct = accountDao.getAccountById(account.getId()); 
	 if(acct==null) { 
		 log.debug("No "+account+" found,would insert it."); 
		 accountDao.saveAccount(account); 
	 } 
	 acct = null; 
 }

然后,在测试类中增加一个测试异常的方法,如下:

 @Test(expected=IllegalArgumentException.class) 
 public void testInsertException() { 
	 service.insertIfNotExist(null); 
 }

运行结果是 green bar。

@Test(timeout=…)

可以给测试方法指定超时时间(毫秒级别),当测试方法的执行时间超过此值,则失败。

比如在 AccountService 中增加如下方法:

 public void doSomeHugeJob() { 
	 try { 
		 Thread.sleep(2*1000); 
	 } catch (InterruptedException e) { 
	 } 
 }

上述方法模拟任务执行时间 2 秒,则测试方法如下:

 @Test(timeout=3000) 
 public void testHugeJob() { 
	 service.doSomeHugeJob(); 
 }

上述测试方法期待 service.doSomeHugeJob 方法能在 3 秒内结束,执行测试结果是 green bar。

@Repeat

通过 @Repeat,您可以轻松的多次执行测试用例,而不用自己写 for 循环,使用方法:

 @Repeat(3) 
 @Test(expected=IllegalArgumentException.class) 
 public void testInsertException() { 
	 service.insertIfNotExist(null); 
 }

这样,testInsertException 就能被执行 3 次。


在测试类中基于 profile 加载测试 bean

从 Spring 3.2 以后,Spring 开始支持使用 @ActiveProfiles 来指定测试类加载的配置包,比如您的配置文件只有一个,但是需要兼容生产环境的配置和单元测试的配置,那么您可以使用 profile 的方式来定义 beans,如下:

清单 10. Spring-db.xml
 <beans xmlns="http://www.Springframework.org/schema/beans"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.Springframework.org/schema/beans 
 http://www.Springframework.org/schema/beans/Spring-beans-3.2.xsd"> 
 	 <beans profile="test"> 
 <bean id="datasource" 
 class="org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
	 <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
	 <property name="username" value="sa"/> 
	 <property name="password" value=""/> 
	 </bean> 
</beans> 
	
<beans profile="production"> 
 <bean id="datasource" 
 class="org.Springframework.jdbc.datasource.DriverManagerDataSource"> 
	 <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
	 <property name="url" value="jdbc:hsqldb:hsql://localhost/prod" /> 
	 <property name="username" value="sa"/> 
	 <property name="password" value=""/> 
	 </bean> 
 </beans> 
 <beans profile="test,production"> 
 <bean id="transactionManager" 
     class="org.Springframework.jdbc.datasource.DataSourceTransactionManager"> 
	 <property name="dataSource" ref="datasource"></property> 
	 </bean> 
	 <bean id="initer" init-method="init" class="service.Initializer"> 
	 </bean> 
 <bean id="accountDao" depends-on="initer" class="DAO.AccountDao"> 
	 		 <property name="dataSource" ref="datasource"/> 
	 	 </bean> 
	 	
	 	 <bean id="accountService" class="service.AccountService"> 
	 	 </bean> 
	 	 <bean id="envSetter" class="EnvSetter"/> 
 	 </beans> 
 </beans>

上面的定义,我们看到:

  • 在 XML 头中我们引用了 Spring 3.2 的 beans 定义,因为只有 Spring 3.2+ 才支持基于 profile 的定义
  • 在 <beans> 根节点下可以嵌套 <beans> 定义,要指定 profile 属性,这个配置中,我们定义了两个 datasource,一个属于 test profile,一个输入 production profile,这样,我们就能在测试程序中加载 test profile,不影响 production 数据库了
  • 在下面定义了一些属于两个 profile 的 beans,即 <beans profile=”test,production”> 这样方便重用一些 bean 的定义,因为这些 bean 在两个 profile 中都是一样的
清单 11. AccountServiceTest.Java
 @RunWith(SpringJUnit4ClassRunner.class) 
 @ContextConfiguration("/config/Spring-db.xml") 
 @Transactional 
 @ActiveProfiles("test") 
 public class AccountServiceTest { 
 ... 
 }

注意上面的 @ActiveProfiles,可以指定一个或者多个 profile,这样我们的测试类就仅仅加载这些名字的 profile 中定义的 bean 实例。


对 TestNG 的支持

Spring 2.5 以后,就开始支持 TestNG 了,支持的方法包括:

  • 将您的 TestNG 测试类继承 Spring 的测试父类:AbstractTransactionalTestNGSpringContextTests 或者 AbstractTestNGSpringContextTests,这样您的 TestNG 测试类内部就可以访问 applicationContext 成员变量了
  • 不继承 Spring 父类,在测试类上使用 @TestExecutionListeners 注释标签,可以引入的监听器包括
    • DependencyInjectionTestExecutionListener:使得测试类拥有依赖注入特性
    • DirtiesContextTestExecutionListener:使得测试类拥有更新 applicationContext 能力
    • TransactionalTestExecutionListener:使得测试类拥有自动的事务管理能力

这里我们演示一下如何使用 Spring 提供的 TestNG 父类来进行测试。

清单 12. AccountServiceTestNGTest.Java
 package testng; 

 import static org.Junit.Assert.assertEquals; 

 import org.Springframework.beans.factory.annotation.Autowired; 
 import org.Springframework.test.context.ActiveProfiles; 
 import org.Springframework.test.context.ContextConfiguration; 
 import org.Springframework.test.context.testng. 
 AbstractTransactionalTestNGSpringContextTests; 
 import org.Springframework.transaction.annotation.Transactional; 

 import service.AccountService; 
 import domain.Account; 

 @ContextConfiguration("/config/Spring-db.xml") 
 @Transactional 
 @ActiveProfiles("test") 
 public class AccountServiceTestNGTest extends 
 AbstractTransactionalTestNGSpringContextTests { 
	 @Autowired 
	 private AccountService service; 
	
	 @org.testng.annotations.Test 
	 public void testGetAcccountById() { 
		 Account acct = Account.getAccount(1, "user01", 18, "M"); 
		 service.insertIfNotExist(acct); 
		 Account acct2 = service.getAccountById(1); 
		 assertEquals(acct,acct2); 
	 } 
 }

执行测试,我们将看到测试成功。

图 4. 测试成功

图 4. 测试成功

搜索数据库对应的表,我们看到里面没有数据,说明自动事务起作用了。


基本原理

Spring test framework 主要位于 org.Springframework.test.context 包中,主要包括下面几个类:

图 5. Spring 测试框架类图(查看大图

图 5. Spring 测试框架类图

  • TestContextManager:主要的入口类,提供 TestContext 实例的管理,负责根据各种事件来通知测试监听器
  • TestContext:实体类,提供访问 Spring applicatoin context 的能力,并负责缓存 applicationContext
  • TestExecutionListener:测试监听器,提供依赖注入、applicationContext 缓存和事务管理等能力
  • ContextLoader:负责根据配置加载 Spring 的 bean 定义,以构建 applicationContext 实例对象
  • SmartContextLoader:Spring 3.1 引入的新加载方法,支持按照 profile 加载

Spring 通过 AOP hook 了测试类的实例创建、beforeClass、before、after、afterClass 等事件入口,执行顺序主要如下:

图 6. Spring 测试框架执行序列图(查看大图

图 6. Spring 测试框架执行序列图

  • 测试执行者开始执行测试类,这个时候 Spring 获取消息,自动创建 TestContextManager 实例
  • TestContextManager 会创建 TestContext,以记录当前测试的上下文信息,TestContext 则通过 ContextLoader 来获取 Spring ApplicationContext 实例
  • 当测试执行者开始执行测试类的 BeforeClass、Before、After、AfterClass 的时候,TestContextManager 将截获事件,发通知给对应的 TestExecutionListener

总结

根据上面的例子和介绍,我们可以看到,Spring 测试框架的主要特点如下:

  • 完美的支持了 Junit4(提供特别的 SpringJunit4ClassRunner),比较好的支持了 TestNG
  • 在支持原有单元测试能力的基础上,通过各种监听器,支持了测试类的依赖注入、对 Spring applicationContext 的访问以及事务管理能力,为使用 Spring 架构的应用程序的测试带来了极大的便利性
  • Spring 3.1 引入的基于 profile 的加载能力使得测试环境和正式环境可以在一个 XML 定义中完美的结合

总之,如果您的程序中使用了 Spring,且对用 Junit 或者 testNG 来对他们进行单元测试感到力不从心,可以考虑使用 Spring test framework,它将使您的应用程序的质量上一个新的台阶。

参考资料

学习

讨论

  • 加入 developerWorks 中文社区:查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

其他参考:
Using Spring MVC Test to unit test multipart POST request
Unit Testing of Spring MVC Controllers: REST API

from:http://www.ibm.com/developerworks/cn/java/j-lo-springunitest/index.html