10k

Spring实战(第四版)- part1(基础部分)

Spring的核心

第一章简介Spring的框架包括DI和AOP的概况,以及它们是如何帮助解耦应用组件的;

第二章讨论如何将组件拼装在一起,其中包含Spring提供的自动配置、基于Java的配置以及基于XML的配置;

第三章介绍了条件化装配、处理自动装配时的歧义、作用域以及Spring表达式语言;

第四章展示了使用AOP将系统级服务从应用(系统级服务服务的对象)解耦出来。

第一章 Spring之旅

  • Spring的bean容器
  • 核心模块
  • 生态系统
  • Spring新功能

Spring诞生就是为了简化企业级Java开发

1.1 简化Java开发

  1. bean:应用组件

  2. 关键策略

    • 基于POJO的轻量级和最小侵入性编程
    • 通过依赖注入和面向接口实现松耦合
    • 通过切面和惯例进行声明式编程
    • 通过切面和模板减少样板式代码
1.1.1 POJO
  1. Spring 避免侵入式编程,强迫实现Spring规范的接口和继承它规范的类。

    java // 一个简单的bean,Spring不对其做任何侵入式改变 public class HelloWorldBean { public String sayHello() { return "Hello World"; } }

  2. POJO具有的魔力原因之一就是DI来装配。

1.1.2 DI
  1. 任何一个有实际意义的应用都会由两个或者更多类组成,它们相互协作完成特定业务逻辑。

  2. 传统的逻辑是每个对象负责管理与自己相互协作的对象(即它所依赖的对象)-> 高耦合,难以测试。

    ```java public class DamselRescuingKnight implements Knight { private RescuDamselQuest quest;

    public DamselRescuingKnight() {
        this.quest = new RescuDamselQuest();
    }
    
    public void embarkOnQuest() {
        quest.embark();
    }
    

    } ```

    这段代码中,DamselRescuingKnight构造函数自行创建quest, 这种耦合限制了骑士的能力,它只能救援(不能杀恶龙,喝啤酒……);另外,在测试的时候需要保证embarkOnQuest()调用的时候embark()也被调用,但是没有简单的方法可以实现。

  3. 但是完全没有耦合代码什么也做不了,对象需要交互。

  4. 对象的依赖关系由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。

    ```java public class BraveKnight implements Knight { private Quest quest;

    public BraveKnight(Quest quest) {
        this.quest = quest;
    }
    
    public void embarkOnQuest() {
        quest.embark();
    }
    

    } // BraveKnight可以灵活处理各种请求 ```

    不同于之前的 DamselRescuingKnight,BraveKnight 没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入(constructor injection)。传入的探险类型是 Quest,也就是所有探险任务都必须实现的一个接口。所以,BraveKnight 能够响应 RescueDamselQuest、SlayDragonQuest、DrinkBeerQuest 等任意的 Quest 实现。

  5. 一个对象只通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。

  6. 对依赖进行替换的一个最常用方法就是在测试的时候使用 mock 实现。我们无法充分地测试 DamselRescuingKnight,因为它是紧耦合的;但是可以轻松地测试 BraveKnight,只需给它一个 Quest 的 mock 实现即可,如程序清单 1.4 所示。

    ```java package sia.knights; import static org.mockito.Mockito.*;

    import org.junit.Test;

    import sia.knights.BraveKnight; import sia.knights.Quest;

    public class BraveKnightTest {

    @Test
    public void knightShouldEmbarkOnQuest() {
        Quest mockQuest = mock(Quest.class);
        BraveKnight knight = new BraveKnight(mockQuest);
        knight.embarkOnQuest();
        verify(mockQuest, times(1)).embark();
    }
    

    } ```

  7. BraveKnight 类可以接受你传递给它的任意一种 Quest 的实现

    ```java package sia.knights;

    import java.io.PrintStream;

    public class SlayDragonQuest implements Quest {

    private PrintStream stream;
    
    public SlayDragonQuest(PrintStream stream) {
        this.stream = stream;
    }
    
    public void embark() {
        stream.println("Embarking on quest to slay the dragon!");
    }
    

    } // 实现了Quest,就可以注入到BraveKnight中了! ```

  8. 创建组件之间协作的行为称为装配(wiring)。可以采用XML的方式,如下:

    ```xml <?xml version="1.0" encoding="UTF-8"?> <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.xsd">

    <bean id="knight" class="sia.knights.BraveKnight"> <constructor-arg ref="quest" /> </bean>

    <bean id="quest" class="sia.knights.SlayDragonQuest"> <constructor-arg value="#{T(System).out}" /> </bean>

    </beans> ```

    在这里,BraveKnight 和 SlayDragonQuest 被声明为 Spring 中的 bean。就 BraveKnight bean 来讲,它在构造时传入了对 SlayDragonQuest bean 的引用,将其作为构造器参数。同时, SlayDragonQuest bean 的声明使用了 Spring 表达式语言(Spring Expression Language),将 System.out(这是一个 PrintStream)传入到了 SlayDragonQuest 的构造器中。

  9. 或者你更喜欢基于Java的配置

    ```java package sia.knights.config;

    import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;

    import sia.knights.BraveKnight; import sia.knights.Knight; import sia.knights.Quest; import sia.knights.SlayDragonQuest;

    @Configuration public class KnightConfig {

    @Bean public Knight knight() { return new BraveKnight(quest()); }

    @Bean public Quest quest() { return new SlayDragonQuest(System.out); }

    } ```

  10. Spring通过应用上下文(Application Context)装载bean的定义把它们组装起来。

1.1.3 应用切面

image-20220727091047042

  1. 系统功能代码会重复出现在多个组件中。组件会因为与自身核心业务无关的代码而变得混乱。

  2. 面向切面编程允许你将分散的功能提取出为可重用的组件。借助 AOP可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,你的核心应用甚至根本不知道它们的存在。这是一个非常强大的理念,可以将安全、事务和日志关注点与核心业务逻辑相分离。

    image-20220727091350165

    AOP应用

    每一个人都熟知骑士所做的任何事情,这是因为吟游诗人用诗歌记载了骑士的事迹并将其进行传唱。假设我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。程序清单 1.9 展示了我们会使用的 Minstrel 类。

    ```java package sia.knights;

    import java.io.PrintStream;

    public class Minstrel {

    private PrintStream stream;

    public Minstrel(PrintStream stream) { this.stream = stream; }

    public void singBeforeQuest() { stream.println("Fa la la, the knight is so brave!"); }

    public void singAfterQuest() { stream.println("Tee hee hee, the brave knight " + "did embark on a quest!"); }

    } ```

    ```java package com.springinaction.knights;

    public class BraveKnight implements Knight {

    private Quest quest; private Minstrel minstrel;

    public BraveKnight(Quest quest, Minstrel minstrel) { this.quest = quest; this.minstrel = minstrel; }

    public void embarkOnQuest() throws QuestException { minstrel.singBeforeQuest(); quest.embark(); minstrl.singAfterQuest(); }

    } ```

    然后将Minstrel配置到XML中的bean,并将其注入在BraveKnight的构造器……但是,管理诗人并不是骑士的工作。而且如果后面想要一个不需要传唱的骑士该怎么办?(将Minstrel置为null然后加一个判断条件?)

  3. 将Minstrel声明为一个切面

    ```xml <?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="knight" class="sia.knights.BraveKnight"> <constructor-arg ref="quest" /> </bean>

    <bean id="quest" class="sia.knights.SlayDragonQuest"> <constructor-arg value="#{T(System).out}" /> </bean>

    <bean id="minstrel" class="sia.knights.Minstrel"> <constructor-arg value="#{T(System).out}" /> </bean>

    <aop:config> <aop:aspect ref="minstrel"> <aop:pointcut id="embark" expression="execution(* *.embarkOnQuest(..))"/>

      <aop:before pointcut-ref="embark" 
          method="singBeforeQuest"/>
    
      <aop:after pointcut-ref="embark" 
          method="singAfterQuest"/>
    </aop:aspect>
    

    </aop:config>

    </beans> ```

    这里使用了 Spring 的 aop 配置命名空间把 Minstrel bean 声明为一个切面。首先,需要把 Minstrel 声明为一个 bean,然后在元素中引用该 bean。为了进一步定义切面,声明 (使用)在 embarkOnQuest() 方法执行前调用 Minstrel 的 singBeforeQuest() 方法。这种方式被称为前置通知(before advice)。同时声明(使用)在 embarkOnQuest() 方法执行后调用 singAfterQuest() 方法。这种方式被称为后置通知(after advice)。

    在这两种方式中,pointcut-ref 属性都引用了名字为 embark 的切入点。该切入点是在前边的元素中定义的,并配置 expression 属性来选择所应用的通知。表达式的语法采用的是 AspectJ 的切点表达式语言。

  4. 这样做的好处就是,MInstrel仍然是一个POJO, 我们只需在配置文件配置后它就变成了一个切面。BraveKnight不需要显式调用它。

1.1.4 使用模板消除样式代码
  1. 使用 Java API 而导致的样板式代码,例如使用 JDBC 访问数据库查询数据。
public Employee getEmployeeById(long id) {
  
  Connection conn = null;
  PreparedStatement stmt = null;
  Result rs = null;
  
  try {
    conn = dataSource.getConnection();
    stmt = conn.prepareStatment(
      "select id, firstname, lastname, salary from " +
      "employee where id=?");
    stmt.setLong(1, id);
    rs = stmt.executeQuery();
    Employee employee = null;
    if (rs.next()) {
      employee = new Employee();
      employee.setId(rs.getLong("id"));
      employee.setFirstName(rs.getString("firstname"));
      employee.setLastName(rs.getString("lastname"));
      employee.setSalary(rs.getBigDecimal("salary"));
    }
    return employee;
  } catch (SQLException e) {
  } finally {
    if (rs != null) {
      try {
        rs.close();
      } catch (SQLException e) {
      }
    }
    
    if (stmt != null) {
      try {
        stmt.close();
      } catch (SQLException e) {
      }
    }
    
    if (conn != null) {
      try {
        conn.close();
      } catch (SQLException e) {
      }
    }    
  }
  return null;
}
  1. Spring希望通过模板封装消除样板代码。Spring的JdbcTemplate 使得执行数据库操作时,避免传统的 JDBC 样板代码成为了可能。使用 Spring 的 JdbcTemplate(利用了 Java 5 特性的 JdbcTemplate 实现)重写的 getEmployeeById() 方法仅仅关注于获取员工数据的核心逻辑,而不需要迎合 JDBC API 的需求。

    java public Employee getEmployeeById(long id) { return jdbcTemplate.queryForObject( "select id, firstname, lastname, salary " + "from employee where id=?", new RowMapper<Employee>() { public Employee mapRow(ResultSet rs, int rowNum) throws SQLException { Employee employee = new Employee(); employee.setId(rs.getLong("id")); employee.setFirstName(rs.getString("firstname")); employee.setLastName(rs.getString("lastname")); employee.setSalary(rs.getBigDecimal("salary")); return employee; } }, id); } // 让你更关注业务逻辑

1.2 容纳bean

1.2.1 使用应用上下文
  1. 常见的应用上下文

    • AnnotationConfigApplicationContext:从一个或者多个基于Java的配置类中加载Spring应用上下文;
    • AnnotationConfigWebApplicationContext:从一个或者多个基于Java的配置类中加载Spring Web应用上下文;
    • ClassPathXmlApplicationContext:从类路径下一个或者多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源;
    • FileSystemApplicationContext:从文件系统下的一 个或多个 XML 配置文件中加载上下文定义;
    • XmlWebApplicationContext:从 Web 应用下的一个或多个 XML 配置文件中加载上下文定义。
  2. 将bean加载到bean工厂

    java ApplicationContext context = new ClassPathXmlApplicationContext("knight.xml");

    之后就可以调用上下文的getBean()方法从Spring容器中获取bean。

1.2.2 bean的生命周期
  1. 传统Java应用,bean被new实例化就可以使用,不使用后JVM会在GC回收。

  2. Spring容器中的bean的生命周期就相对复杂

    image-20220728084604520

    1. Spring对bean进行实例化;
    2. Spring将值和bean的引用注入到bean对应的属性中;
    3. 如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBeanName()方法;
    4. 如果 bean 实现了BeanFactoryAware接口,Spring 将调用setBeanFactory() 方法,将 BeanFactory 容器实例传入;
    5. 如果 bean 实现了 ApplicationContextAware接口,Spring将调用 setApplicationContext() 方法,将 bean 所在的应用上下文的引用传入进来;
    6. 如果 bean 实现了 BeanPostProcessor接口,Spring将调用它们的 postProcessBefore-Initialization() 方法;
    7. 如果 bean 实现了 InitializingBean 接口,Spring 将调用它们的 afterPropertiesSet() 方法。类似地,如果 bean 使用 initmethod 声明了初始化方法,该方法也会被调用;
    8. 如果 bean 实现了 BeanPostProcessor 接口,Spring 将调用它们的 postProcessAfter-Initialization() 方法;
    9. 此时,bean 已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;

    10. 如果 bean 实现了 DisposableBean 接口,Spring 将调用它的 destroy() 接口方法。

1.3 Spring风景线(生态系统)

1.3.1 Spring模块

1.7 spring 框架模块

核心容器

管理Spring应用的bean的创建、配置和管理。它包括了bean工厂提供了DI的功能;基于bean工厂还会有多种应用上下文的实现。该模块还提供许多企业服务例如Email、JNDI 访问、EJB 集成和调度。

AOP模块

帮助应用对象解耦。

数据访问与集成

Spring 的 JDBC 和 DAO(Data Access Object)模块抽象了数据库连接关闭处理等操作的样板式代码,简化代码同时还可以避免因为关闭数据库资源失败而引发的问题。该模块在多种数据库服务的错误信息之上构建了一个语义丰富的异常层,以后我们再也不需要解释那些隐晦专有的 SQL 错误信息了!

提供了ORM模块,集成Hibernate、Java Persisternce API、Java Data Object 和 iBATIS SQL Maps。

Web与远程调用

MVC帮助用户将界面逻辑与应用逻辑分离。

Spring 远程调用功能集成了 RMI(Remote Method Invocation)、Hessian、Burlap、JAX-WS,同时 Spring 还自带了一个 远程调用框架:HTTP invoker。Spring 还提供了暴露和使用 REST API 的良好支持。

Instrumentation

Spring 的 Instrumentation 模块提供了为 JVM 添加代理(agent)的功能。 具体来讲,它为 Tomcat 提供了一个织入代理,能够为 Tomcat 传递类文件,就像这些文件是被类加载器加载的一样。

测试

鉴于开发者自测的重要性,Spring 提供了测试模块以致力于 Spring 应用的测试。

通过该模块,你会发现 Spring 为使用 JNDI、Servlet 和 Portlet 编写单元测试提供了一系列的 mock 对象实现。对于集成测试,该模块为加载 Spring 应用上下文中的 bean 集合以及与 Spring 上下文中的 bean 进行交互提供了支持。

1.3.2 Spring Portfolio

Spring Portfolio 包括多个构建于核心 Spring 框架之上的框架和类库。

Spring Web Flow

Spring Web Flow 建立于 Spring MVC 框架之上,它为基于流程的会话式 Web 应用(可以想一下购物车或者向导功能)提供了支持。

Spring Web Service

虽然核心的 Spring 框架提供了将 Spring bean 以声明的方式发布为 Web Service 的功能,但是这些服务是基于一个具有争议性的架构(拙劣的契约后置模型)之上而构建的。这些服务的契约由 bean 的接口来决定。Spring Web Service 提供了契约优先的 Web Service 模型,服务的实现都是为了满足服务的契约而编写的。

Spring Security

利用 Spring AOP,Spring Security 为 Spring 应用提供了声明式的安全机制。

Spring Integration

Spring Integration 提供了多种通用应用集成模式的 Spring 声明式风格实现。

Spring Batch

如果需要开发一个批处理应用,你可以通过 Spring Batch,使用 Spring 强大的面向 POJO 的编程模型。

Spring Data

不管你使用文档数据库,如 MongoDB,图数据库,如 Neo4j,还是传统的关系型数据库,Spring Data 都为持久化提供了一种简单的编程模型。这包括为多种数据库类型提供了一种自动化的 Repository 机制, 它负责为你创建 Repository 的实现。

Spring Social

社交网络是互联网领域中新兴的一种潮流,越来越多的应用正在融入社交网络网站,例如 Facebook 或者 Twitter。如果对此感兴趣,你可以了解一下 Spring Social,这是 Spring 的一个社交网络扩展模块。

Spring Mobile

Spring Mobile 是 Spring MVC 新的扩展模块,用于支持移动 Web 应用开发。

Spring for Android

与 Spring Mobile 相关的是 Spring Android 项目。这个新项目,旨在通过 Spring 框架为开发基于 Android 设备的本地应用提供某些简单的支持。

Spring Boot

Spring Boot大量依赖于自动配置技术,它能够消除大部分(在很多场 景中,甚至是全部)Spring 配置。

1.4 Spring新功能

略。

1.5 小结

第二章 装配Bean

  • 声明bean
  • 构造器注入和Setter方法注入
  • 装配bean
  • 控制bean的创建和销毁

2.1 Spring配置的可选方案

方案没有优劣,只有选择你觉得最合适的即可。

  • 在 XML 中进行显式配置。
  • 在 Java 中进行显式配置。
  • 隐式的 bean 发现机制和自动装配。

即便如此,作者的建议是尽可能地使用自动配置的机制。显式配置越少越好。当你必须要显式配置 bean 的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置 bean 的时候),推荐使用类型安全并且比 XML 更加强大的 JavaConfig。最后,只有当你想要使用便利的 XML 命名空间,并且在 JavaConfig 中没有同样的实现时,才应该使用 XML。

2.2 自动化装配bean

  • 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。
  • 自动装配(autowiring):Spring自动满足bean之间的依赖。

下面会看到一个例子:我们会创建 CompactDisc 类,Spring 会发现它并将其创建为一个 bean。然后,再创建一个 CDPlayer 类,让 Spring 发现它,并将 CompactDiscbean 注入进来。

2.2.1 创建可被发现的bean
package soundsystem;

public interface CompactDisc {
  void play();
}

定义一个CompactDisc接口,定义了一个播放器对一盘CD所能进行的操作,通过这种方式降低耦合。

package soundsystem;

import org.springframework.stereotype.Component;

@Component
public class SgtPeppers implements CompactDisc {

  private String title = "Sgt. Pepper's Lonely Hearts Club Band";  
  private String artist = "The Beatles";
  
  public void play() {
    System.out.println("Playing " + title + " by " + artist);
  }
  
}

@Component注解表明该类会作为组件类,告诉Spring要为这个类创建bean。所以就不需要在其它地方显式配置SgtPepperbean了。

但是我们要显式配置Spring从而启动组件扫描。

package soundsystem;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class CDPlayerConfig { 
}

@ComponentScan会扫描与config类相同的包及其子包,找到带有@Component注解的类,并为其创建一个bean。为了验证我们的组件自动扫描,我们可以创建一个测试。

package soundsystem;

import static org.junit.Assert.*;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayerTest {

  @Autowired
  private CompactDisc cd;
  
  @Test
  public void cdShouldNotBeNull() {
    assertNotNull(cd);
  }
  
}
2.2.2 为组件扫描的bean命名
  1. Spring应用上下文中所有的bean都会有一个id。

  2. 一般来说这个id就是将bean的类名首字母小写得到,如果你想使用不同的id,可以将值传递给@Component注解。

    java @Componet("lonelyHeartsClub") public class SgtPeppers implements CompactDisc { ...... }

    还有另一种方法为bean命名,即使用Java依赖注入规范中提供的@Named注解:

    ```java package soundsystem;

    import javax.inject.Named;

    @Named("lonelyHeartsClub") public class SgtPeppers implements CompactDisc { ...... } ```

  3. Spring支持将@Named作为@Component的替代,它们会有些差异但是大部分场景可以互换。

2.2.3 设置组件扫描的基础包
  1. 如果我们将配置类单独放在一个包中,那么我们就需要配置组件扫描的基础包。

  2. 在@ComponentScan的value属性中指明包的名称:

    java @Configuration @ComponentScan("soundsystem") public class CDPlayerConfig { }

  3. 还可以更清晰的设置基础包:

    java @Configuration @ComponentScan(basePackages={"soundsystem", "video"}) public class CDPlayerConfig { }

    多个基础包可以传一个数组。

  4. 设置基础包使用String类型是类型不安全的。所以还可以设置成指定包中所包含的类或者接口。

    java @Configuration @ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.clas}) public class CDPlayerConfig { }

  5. 空标记接口:可以在包中建立一个用来扫描的空标记接口(一个存在于某个目标包中的接口,没有实际意义,只作为定位查找的依据),这样避免了引用实际的应用程序代码,对重构友好。

2.2.4 通过为bean添加注解实现自动装配
  1. 自动装配: 自动在Spring上下文寻找匹配某个bean需求的其它bean。可以借用@Autowired

    ```java package soundsystem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;

    @Component public class CDPlayer implements MediaPlayer { private CompactDisc cd;

    @Autowired public CDPlayer(CompactDisc cd) { this.cd = cd; }

    public void play() { cd.play(); }

    }

    // 当Spring创建CDPlayerbean的时候,会通过构造器实例化并且会传入一个可设置给CompactDisc类型的bean,将其注入到CDPlayer中。 ```

  2. @Autowired注解还能用在属性的Setter方法上。如果CDPlayer有一个setCompactDisc()方法,可以采用如下方式进行自动装配:

    java @Autowired public void setCompactDisc(CompactDisc cd){ this.cd = cd; }

  3. 其实@Autowired注解可以应用在任何方法上发挥作用。Spring会尝试满足方法参数声明的依赖。-> 如何指导我要的是DrPepper实例还是Coke实例?

  4. 如果没有匹配的bean,Spring会抛出异常,@Autowired的required属性设置为false可以避免异常抛出。这时这个bean处于未装配的状态。所以这种时候如果缺少空值检查可能会报空指针异常。

  5. 多个bean满足依赖关系的话,Spring也会抛出异常(自动装配的歧义性)。

  6. Java注入依赖提供了@Inject,它和@Autowired也是在大多数情况下可以互换。

2.2.5 验证自动装配
  1. 我们可以通过修改之前的Test来验证自动装配的bean是我们想要的:

    ```java package soundsystem;

    import static org.junit.Assert.*;

    import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.StandardOutputStreamLog; 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;

    @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=CDPlayerConfig.class) public class CDPlayerTest {

    @Rule public final StandardOutputStreamLog log = new StandardOutputStreamLog();

    @Autowired private MediaPlayer player;

    @Autowired private CompactDisc cd;

    @Test public void cdShouldNotBeNull() { assertNotNull(cd); }

    @Test public void play() { player.play(); assertEquals( "Playing Sgt. Pepper's Lonely Hearts Club Band by The Beatles\n", log.getLog()); }

    } ```

  2. 在测试代码中使用 System.out.println() 是稍微有点棘手的事情。因此,该样例中使用了 StandardOutputStreamLog,这是来源于 System Rules 库 的一个 JUnit 规则,该规则能够基于控制台的输出编写断言。在这里,我们断言 SgtPeppers.play() 方法的输出被发送到了控制台上。

2.3 通过Java代码装配

  1. 有时候使用第三方库中的组件,没办法在它的类添加@Component和@Autowired,这时就不能使用自动化装配。
  2. 一般会将Config代码放进单独的包,config类不应出现在业务代码的逻辑,config代码也不应该包含业务代码。
2.3.1 创建配置类
package soundsystem;

import org.spingframework.context.annotation.Configuration;

@Configuration
public class CDPlayerConfig {
}

@Configureation表明这个类是一个配置类。去掉了 CDPlayerConfig的 @ComponentScan 注解,因为这里要做显示配置。

2.3.2 声明简单的bean
  1. 要在 JavaConfig 中声明 bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加 @Bean 注解。比方说,下面的代码声明了 CompactDisc bean:
@Bean
public CompactDisc sgtPeppers() {
  return new SgtPeppers();
}

@Bean 注解会告诉 Spring 这个方法将会返回一个对象,该对象要注册为 Spring 应用上下文中的 bean。方法体中包含了最终产生 bean 实例的逻辑。

  1. bean的id和带有@Bean注解的方法名是一样的。也可以用name属性指定名称@Bean(name="lonelyHeartsClubBand")
2.3.3 借助JavaConfig实现注入
  1. 最简单的方法就是引用创建bean的方法。

    java @Bean public CDPlayer cdPlayer() { return new CDPlayer(sgtPeppers()); }

  2. sgtPepper()方法添加了@Bean注解,Spring会对其调用进行拦截,所以并非每次调用这个方法都会实际调用。

  3. ```java @Bean public CDPlayer cdPlayer() { return new CDPlayer(sgtPeppers()); }

    @Bean public CDPlayer anotherCDPlayer() { return new CDPlayer(sgtPeppers()); } ```

    上述两个CDPlayer会取到同一个CD,这是不符合逻辑的, 没有一个CD可以同时放在两个机子里。但是在软件领域,Spring中的bean都是单例。

  4. 另外一种调用方法:

    java @Bean public CDPlayer cdPlayer(CompactDisc compactDisc) { return new CDPlayer(compactDisc); }

    cdPlayer() 方法请求一个 CompactDisc 作为参数。当 Spring 调用 cdPlayer() 创建 CDPlayerbean 的时候,它会自动装配一个 CompactDisc 到配置方法之中。然后,方法体就可以按照合适的方式来使用它。

  5. 第二种方法比较好是因为它不会要求将CompactDisc声明在同一个JavaConfig类中,也不一定需要声明在JavaConfig中,它可以通过组件自动扫描或者XML配置。只要最终功能健全即可。

2.4 通过XML装配bean

2.4.1 创建XML配置规范

一个简单的例子:

<?xml version="1.0" encoding="UTF-8"?>
<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.xsd
http://www.springframework.org/schema/context" >
  <!-- configuration details go here />
</beans>
2.4.2 声明一个简单的\<bean>
  1. 类似JavaConfig中的@Bean: <bean class="soundsystem.SgtPeppers" />

  2. 后需要引用的话最好制定一个ID,不然默认生成的是例如“soundsystem.SgtPeppers#0”。所以最好在声明的时候也设置ID<bean id="compactDisc" class="soundsystem.SgtPeppers" />

    减少繁琐为了减少 XML 中繁琐的配置,只对那些需要按名字引用的 bean(比如,你需要将对它的引用注入到另外一个 bean中)进行明确地命名。

  3. 注意1:不需要直接创建SgtPeppers的实例,Spring在发现它的时候会调用其构造器创建bean。

  4. 注意2:给bean设置的类型class无法保证其是真正的类,可以借助IDE检查其合法性。

2.4.3 借助构造器注入初始化bean

在 XML 中声明 DI 时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择:

  • <constructor-arg> 元素
  • 使用 Spring 3.0 所引入的 c- 命名空间

构造器注入bean引用

现在已经声明了 SgtPeppers bean,并且 SgtPeppers 类实现了 CompactDisc 接口,所以实际上我们已经有了一个可以注入到 CDPlayer bean 中的 bean。我们所需要做的就是在 XML 中声明 CDPlayer 并通过 ID 引用 SgtPeppers:

<bean id="cdPlayer" class="soundsystem.CDPlayer">
  <constructor-arg ref="compactDisc">
</bean>

作为替代的方案,你也可以使用 Spring 的 c- 命名空间:

<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />

c 标签组成

将字面量注入到构造器

  1. 有时候,我们需要做的只是用一个字面量值来配置对象。

    ```java package soundsystem;

    import java.util.List;

    public class BlankDisc implements CompactDisc {

    private String title; private String artist;

    public BlankDisc(String title, String artist) { this.title = title; this.artist = artist; }

    public void play() { System.out.println("Playing " + title + " by " + artist); } } ```

  2. 使用了 value 属性,通过该属性表明给定的值要以字面量的形式注入到构造器之中

    xml <bean id="compactDisc" class="soundsystem.BlankDisc"> <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" /> <constructor-arg value="The Beatles" /> </bean>

装配集合

package soundsystem;

import java.util.List;

public class BlankDisc implements CompactDisc {

  private String title;
  private String artist;
  private List<String> tracks;

  public BlankDisc(String title, String artist, List<String> tracks) {
    this.title = title;
    this.artist = artist;
    this.tracks = tracks;
  }

  public void play() {
    System.out.println("Playing " + title + " by " + artist);
    for (String track : tracks) {
      System.out.println("-Track: " + track);
    }
  }

}
  1. 这个变更会对 Spring 如何配置 bean 产生影响,在声明 bean 的时候,我们必须要提供一个磁道列表。

  2. 声明列表

    ```java <?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:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="compactDisc" class="soundsystem.BlankDisc" c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles"> <constructor-arg> <list> <value>Sgt. Pepper's Lonely Hearts Club Band</value> <value>With a Little Help from My Friends</value> <value>Lucy in the Sky with Diamonds</value> <value>Getting Better</value> <value>Fixing a Hole</value> </list> </constructor-arg> </bean>

    </beans> ```

    其中,<list> 元素是 <constructor-arg> 的子元素,这表明一个包含值的列表将会传递到构造器中。其中,<value> 元素用来指定列表中的每个元素。

2.4.4 设置属性
  1. ```java package soundsystem; import org.springframework.beans.factory.annotation.Autowired;

    public class CDPlayer implements MediaPlayer { private CompactDisc cd;

    @Autowired public CDPlayer(CompactDisc cd) { this.cd = cd; }

    public void play() { cd.play(); }

    } ```

  2. 对强依赖使用构造器注入,而对可选性的依赖使用属性注入。对于 BlankDisc 来讲,唱片名称、艺术家以及磁道列表是强依赖

2.5 导入和混合配置

2.5.1 在JavaConfig中引用XML配置

假设 BlankDisc 定义在名为 cdconfig. xml 的文件中,该文件位于根类路径下,那么可以修改 SoundSystemConfig,让它使用 @ImportResource 注解,如 下所示

package soundsystem;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:")
public class SoundSystemConfig {

}
2.5.2 在XML中引用JavaConfig
<?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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
  <bean class="soundsystem.CDConfig" />
  <bean id="cdPlayer" class="soundsystem.CDPlayer"
        c:cd-ref="compactDisc" />    
</beans>

不管使用 JavaConfig 还是使用 XML 进行装配,我通常都会创建一个根配置(root configuration),也就是这里展现的这样,这个配置会将两个或更多的装配类或 XML 文件组合起来。

2.6 小结

第三章 高级装配

  • Spring profile
  • 条件化的bean声明
  • 自动装配与歧义性
  • bean的作用域
  • Spring表达式语言

3.1 环境与profile

  1. 在开发和测试以及生产环境的配置不尽相同。开发环境使用嵌入式数据库:

    java @Bean(destroyMethod = "shutdown") public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); }

    EmbeddedDatabaseBuilder 会搭建一个嵌入式的 Hypersonic 数据库,它的模式(schema)定义在 schema.sql 中,测试数据则是通过 test-data.sql 加载的。

    在生产环境来说,这种方法就比较糟糕(我猜是因为不够满足业务的需要的灵活性),所以采用如下的方式创建bean:

    java @Bean public DataSource dataSource() { JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); }

    通过 JNDI 获取 DataSource 能够让容器决定该如何创建这个 DataSource,甚至包括切换为容器管理的连接池。即便如此,JNDI 管理的 DataSource 更加适合于生产环境,对于简单的集成和开发测试环境来说,这会带来不必要的复杂性。在测试环境,可能是如下:

    ```java @Bean(destroyMethod = "close") public DataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setUrl("jdbc:h2:tcp://dbserver/~/test"); dataSource.setDriverClassName("org.h2.Driver"); dataSource.setUsername("sa"); dataSource.setPassword("password"); dataSource.setInitialSize(20); dataSource.setMaxActive(30);

    return dataSource; } ```

  2. 为不同的环境在不同的配置文件配置不同的bean,在构建阶段选择构建然后部署到应用。但是这种方法会让从QA阶段迁移到生产阶段时,重新构建可能引入新的bug。

3.1.1 配置profile bean
  1. Spring的方案也是类似上文,不过判断的阶段是在运行时。

  2. 将不同的bean定义在不同的profile,在运行时确保对应的profile处于active

    ```java package com.myapp;

    import javax.sql.DataSource;

    import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.jndi.JndiObjectFactoryBean;

    @Configuration public class DataSourceConfig {

    @Bean(destroyMethod = "shutdown") @Profile("dev") public DataSource embeddedDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); }

    @Bean @Profile("prod") public DataSource jndiDataSource() { JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); }

    } ```

  3. 也可以在XML中配置profile

    ```xml <?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:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <beans profile="dev"> <jdbc:embedded-database id="dataSource" type="H2"> <jdbc:script location="classpath:schema.sql" /> <jdbc:script location="classpath:test-data.sql" /> </jdbc:embedded-database> </beans>

    <beans profile="prod"> <jee:jndi-lookup id="dataSource" lazy-init="true" jndi-name="jdbc/myDatabase" resource-ref="true" proxy-interface="javax.sql.DataSource" /> </beans> </beans> ```

3.1.2 激活profile
  1. Spring 在确定哪个 profile 处于激活状态时,需要依赖两个独立的属性:spring.profiles.activespring.profiles.default

    • 如果设置了 spring.profiles.active 属性的话,那么它的值就会用来确定哪个 profile 是激活的。
    • 但如果没有设置 spring.profiles.active 属性的话,那 Spring 将会查找 spring.profiles.default 的值。
    • 如果 spring.profiles.activepring.profiles.default 均没有设置的话,那就没有激活的 profile,因此只会创建那些没有定义在 profile 中的 bean。
  2. 设置这俩个属性的方式:

    • 作为 DispatcherServlet 的初始化参数;
    • 作为 Web 应用的上下文参数;
    • 作为 JNDI 条目;
    • 作为环境变量;
    • 作为 JVM 的系统属性;
    • 在集成测试类上,使用 @ActiveProfiles 注解设置。
  3. 作者惯用的一种方式,在Servlet上下文中将default设为dev。方便于开发人员从版本控制软件获得源码直接使用。如果部署到QA或者生产环境,只需更改相关配置。

    ```xml <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_2_5.xsd" >

    <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring/root-context.xml</param-value> </context-param>

    <context-param> <param-name>spring.profiles.default</param-name> <param-name>dev</param-name> </context-param>

    <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener>

    <servlet> <servlet-name>appServlet</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>spring.profile.default</param-name> <param-value>dev</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>

    <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

    </web-app> ```

  4. 在进行测试的时候,我们希望加载与生产或者开发环境相同的配置,可以使用@ActiveProfiles

    java @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes={PersistenceTestConfig.class}) @ActiveProfile("dev") public class PersistenceTest { ... }

3.2 条件化的bean

  1. 你希望一个或多个 bean 只有在应用的类路径下包含特定的库时才创建。或者我们希望某个 bean 只有当另外某个特定的 bean 也声明了之后才会创建。我们还可能要求只有某个特定的环境变量设置之后,才会创建某个bean。

  2. 使用@Conditional注解可以做到。例如,假设有一个名为 MagicBean 的类,我们希望只有设置了 magic 环境属性的时候,Spring 才会实例化这个类。如果环境中没有这个属性,那么 MagicBean 将会被忽略。

    java @Bean @Conditional(MagicExistsCondition.class) public MagicBean magicBean() { return new MagicBean(); }

    @Conditional会通过Condition接口进行条件对比

    java public interface Condition { boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata); }

    设置给@Conditional的类需要实现Condition接口

    ```java package com.habuma.restfun;

    import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata;

    public class MagicExistsCondition implements Condition {

    @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); return env.containsProperty("magic"); }

    } ```

  3. ConditionContext是一个接口,大致如下所示:

    java public interface ConditionContext { BeandefinitionRegistry getRegistry(); // 借助 getRegistry() 返回的 BeanDefinitionRegistry 检查 bean 定义; ConfigurationListableBeanFactory getBeanFactory(); // 借助 getBeanFactory() 返回的 ConfigurableListableBeanFactory 检查 bean 是否存在,甚至探查 bean 的属性; Environment getEnvironment(); // 借助 getEnvironment() 返回的 Environment 检查环境变量是否存在以及它的值是什么; ResourceLoader getResourceLoader(); // 读取并探查 getResourceLoader() 返回的 ResourceLoader 所加载的资源; ClassLoader getClassLoader(); // 借助 getClassLoader() 返回的 ClassLoader 加载并检查类是否存在。 }

  4. AnnotatedTypeMetadata 能够让我们检查带有 @Bean 注解的方法上还有什么其它的注解。它也是一个接口。它如下所示:

    java public interface AnnotatedTypeMetadata { boolean isAnnotated(String annotationType); Map<String, Object> getAnnotationAttributes(String annotationType); Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString); MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType); MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString); }

  5. @Profile 注解进行了重构,基于 @Conditional 和 Condition 实现。

    java @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Documented @Conditional({ProfileCondition.class}) public @interface Profile { String[] value(); }

    @Profile 本身也使用了 @Conditional 注解,并且引用 ProfileCondition 作为 Condition 实现。如下所示,ProfileCondition 实现了 Condition 接口,并且在做出决策的过程中,ProfileCondition 通过 AnnotatedTypeMetadata 得到了用于 @Profile 注解的所有属性。借助该信息,它会明确地检查 value 属性,该属性包含了 bean 的 profile 名称。然后它根据通过 ConditionContext 得到的 Environment 来检查(借助 acceptsProfiles() 方法)该 profile 是否处于激活状态。

    ```java class ProfileCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment() != null) {
            MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if (attrs != null) {
                for (Object value : attrs.get("value")) {
                    if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                        return true;
                    }
                }
                return false;
            }
        }
        return true;
    }
    

    } ```

3.3 处理自动装配的歧义性

如何处理如下情况(不处理Spring会报错NoUniqueBeanDefinitionException)

@Component
public class Cake implements Dessert { ... }
@Component
public class Cookies implements Dessert { ... }
@Component
public class IceCream implements Dessert { ... }
3.3.1 标识首选的bean
  1. 使用@Primary,配合@Component将bean设置为首选。也可以设置在JavaConfig或则XML中。

    java @Component @Primary public class IceCream implements Dessert { ... }

  2. 但是如果另一个也设置了首选,那就又成了没有首选了。那就需要使用限定符

3.3.2 限定自动装配的bean
  1. @Qualifier 注解是使用限定符的主要方式

    java @Autowired @Qualifier("iceCream") public void setDessert(Dessert dessert) { this.dessert = dessert; }

  2. 没有指定限定符的类都会被设定一个每人限定符,与bean的ID一样。

  3. 存在一个问题:限定符和bean的名称紧耦合,改动类名会导致限定符失效。

创建自定义限定符

  1. 使用自定义限定符,与类名解耦。在bean声明的地方添加限定符:

    java @Component @Qualifier("cold") public class IceCream implements Dessert { ... }

    然后后在注入的地方引用:

    java @Autowired @Qualifier("cold") public void setDessert(Dessert dessert) { this.dessert = dessert; }

  2. 当使用自定义的 @Qualifier 值时,最佳实践是为 bean 选择特征性或描述性的术语,而不是使用随意的名字。在本例中,我将 IceCream bean 描述为“cold”bean。在注入的时候,可以将这个需求理解为“给我一个凉的甜点”,这其实就是描述的 IceCream。类似地,我可以将 Cake 描述为“soft”,将 Cookie 描述为“crispy”。

使用自定义的限定符注解

  1. 新的问题是,多个bean具有相同属性,都加入了@Qualifier("cold")。当我们再加一个@Qualifier()的时候Java 不允许在同一个条目上重复出现相同类型的多个注解。

    java @Autowired @Qualifier("cold") @Qualifier("creamy") // 这种方式会报错 public class IceCream implements Dessert { ... }

  2. 方法:自定义新的注解,将旧注解带进去:

    java @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Cold { }

3.4 bean的作用域

  1. 默认情况下bean是单例。初始化可GC成本比较小。
  2. Spring 定义了多种作用域,可以基于这些作用域创建 bean,包括:
    • 单例(Singleton):在整个应用中,只创建 bean 的一个实例。
    • 原型(Prototype):每次注入或者通过 Spring 应用上下文获取的时候,都会创建一个新的 bean 实例。
    • 会话(Session):在 Web 应用中,为每个会话创建一个 bean 实例。
    • 请求(Rquest):在 Web 应用中,为每个请求创建一个 bean 实例。
  3. 使用@Scope注解选择作用域。
3.4.1 使用会话和请求作用域

例如,在典型的电子商务应用中,可能会有一个 bean 代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。另一方面,如果购物车是原型作用域的,那么在应用中某一个地方往购物车中添加商品,在应用的另外一个地方可能就不可用了,因为在这里注入的是另外一个原型作用域的购物车。就购物车 bean 来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。

@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION,
       proxyMode=ScopedProxyMode.INTERFACES) // ScopedProxyMode.INTERFACES,这表明这个代理要实现 ShoppingCart 接口,并将调用委托给实现 bean。
public ShoppingCart cart() { ... }
  1. proxyMode这个属性解决了将会话或请求作用域的 bean注入到单例 bean 中所遇到的问题。假设我们要将 ShoppingCart bean 注入到单例 StoreService bean 的 Setter 方法中,如下所示:

    java @Component public class StoreService { @Autowired public void setShoppingCart(ShoppingCart shoppingCart) { this.shoppingCart = shoppingCart; } }

    StoreService 是一个单例的 bean,会在 Spring 应用上下文加载的时候创建。当它创建的时候,Spring 会试图将 ShoppingCart bean 注入到 setShoppingCart() 方法中。但是 ShoppingCart bean 是会话作用域的,此时并不存在。直到某个用户进入系统,创建了会话之后,才会出现 ShoppingCart 实例。

    Spring 并不会将实际的 ShoppingCart bean 注入到 StoreService 中, Spring 会注入一个到 ShoppingCart bean 的代理,如图 3.1 所示。这个代理会暴露与 ShoppingCart 相同的方法,所以 StoreService 会认为它就是一个购物车。但是,当 StoreService 调用 ShoppingCart 的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的 ShoppingCart bean。

    3.1 会话作用域

  2. shoppingCart 是一个具体的类的话, 也是可以的。但是Spring 就没有办法创建基于接口的代理了。此时,它必须使用 CGLib 来生成基于类的代理。所以,如果 bean 类型是具体类的话,我们必须要将 proxyMode 属性设置ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。

3.4.2 在XML中声明作用域代理

在XML中需要使用Spring aop中的scoped-proxy

<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
  <aop:scoped-proxy />
</bean>

3.5 运行时值注入

  1. bean装配另一面是将一个值注入到bean的属性或者构造器。有时候硬编码是可以的,有的时候我们可能会希望避免硬编码值,而是想让这些值在运行时再确定。
  2. Spring提供两种方式:属性占位符(Property placeholder)和Spring 表达式语言(SpEL)。
3.5.1 注入外部的值
  1. 在 Spring 中,处理外部值的最简单方式就是声明属性源并通过 Spring 的 Environment 来检索属性。

    ```java package com.soundsystem;

    import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment;

    @Configuration @PropertySource("classpath:/com/soundsystem/app.properties") public class ExpressiveConfig {

    @Autowired Environment env;

    @Bean public BlankDisc disc() { return new BlankDisc( env.getProperty("disc.title"), env.getProperty("disc.artist") ); } } ```

    在本例中,@PropertySource 引用了类路径中一个名为 app.properties 的文件。它大致会如下所示。这个属性文件会加载到 Spring 的 Environment 中,稍后可以从这里检索属性。同时,在 disc() 方法中,会创建一个新的 BlankDisc,它的构造器参数是从属性文件中获取的,而这是通过调用 getProperty() 实现的。

    properties disc.title=Sgt. Peppers Lonely Hearts Club disc.artisc=The Beatles

  2. 深入学习 Spring 的 Environment 当我们去了解 Environment 的时候会发现,上述程序所示的 getProperty() 方法并不是获取属性值的唯一方 法,getProperty() 方法有四个重载的变种形式:

    • String getProperty(String key)
    • String getProperty(String key, String defualtValue)
    • T getProperty(String key, Class<T> type)
    • T getProperty(String key, Class<T> type, T defaultValue)

    前两个方法返回String类型,第二个方法可以带一个默认值在get不到属性的时候。

  3. 后两种方法,例如,假设你想要获取的值所代表的含义是连接池中所维持的连接数量。如果我们从属性文件中得到 的是一个 String 类型的值,那么在使用之前还需要将其转换为 Integer 类型。但是,如果使用重载形式的 getProperty() 的话,就能非常便利地解决这个问题:

    java int connectionCount = env.getProperty("db.connection.count", Integer.class, 30);

  4. 除了属性相关的功能以外,Environment 还提供了一些方法来检查哪些 profile 处于激活状态:

    • String[] getActiveProfiles():返回激活 profile 名称的数组;
    • String[] getDefaultProfiles():返回默认 profile 名称的数组;
    • boolean acceptsProfiles(String... profiles):如果 environment 支持给定 profile 的话,就返回true。

解析属性占位符

在 Spring 装配中,占位符的形式为使用 ${ ... } 包装的属性名称。

3.5.2 使用Spring表达式语言进行装配

SpEL 表达式要放到 #{ ... } 之中, 它拥有很多特性,包括:

  • 使用 bean 的 ID 来引用 bean;
  • 调用方法和访问对象的属性;
  • 对值进行算术、关系和逻辑运算;
  • 正则表达式匹配;
  • 集合操作。

3.6 小结

  1. Spring profile解决了跨环境部署问题。
  2. profile bean是在运行时条件化创建bean的方式。
  3. 自动装配歧义性解决方法:首选bean和限定符。
  4. Spring 能够让 bean 以单例、原型、请求作用域或会话作用域的方式来创建以应对不同情况。在声明请求作用域或会话作用域的 bean 的时候,我们还学习了如何创建作用域代理,它分为基于类的代理和基于接口的代理的两种方式。
  5. Spring 表达式语言,它能够在运行时计算要注入到 bean 属性中的值

第四章 面向切面的Spring

本章内容:

  • 面向切面编程的基本原理
  • 通过 POJO 创建切面
  • 使用 @AspectJ 注解
  • 为 AspectJ 切面注入依赖

散布于应用中多处的功能被称为横切关注点(crosscutting concern)。它们从概念上应该是与业务逻辑分离的,这也是AOP要解决的问题。

4.1 什么是面向切面编程

Screenshot 2022-09-11_14-56-57-861

  1. 重用通用功能一般利用继承和委托。但是继承会导致脆弱的对象体系;委托可能需要对委托对象进行复杂的调用。
  2. 面向切面可以通过声明的方式定义这个功能何时何地应用。
4.1.1 定义AOP术语

在一个或多个连接点上,可以把切面的功能(通知)织入到程序的执行过程中

1

  1. 通知: 定义了切面的工作是什么以及何时使用。

    Spring 切面可以应用 5 种类型的通知:

    • 前置通知(Before):在目标方法被调用之前调用通知功能;
    • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
    • 返回通知(After-returning):在目标方法成功执行之后调用通知;
    • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
    • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
  2. 连接点:应用切面的时机。

  3. 切点:切点的定义会匹配通知所要织入(wave)的一个或多个连接点。(何处应用切面)

  4. 切面:通知和切点的组合。

  5. 引入:向现有的类添加新方法和属性。

  6. 织入(WeaVing):将切面应用到目标对象并创建新的代理对象的过程。

    • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
    • 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5 的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
    • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的
4.1.2 Spring对AOP的支持
  1. Spring 通知是 Java 编写的,Spring在运行时通知对象

    Screenshot 2022-09-11_14-59-48-536

  2. Spring 只支持方法级别的连接点

4.2 通过切点来选择连接点

4.2.1 编写切点
  1. 先定义一个主题
    package concert;
    
    public interface Performance {
      public void perform();
    }
  1. 假设我们想编写 Performance 的 perform() 方法触发的通知

    22

  2. 现在假设我们需要配置的切点仅匹配 concert 包。在此场景下,可以使用 within() 指示器来限制匹配

    33

4.2.2 在切点中选择bean

Spring 还引入了一个新的 bean() 指示器,它允许我们在切点表达式中使用 bean 的 ID 来标识 bean。bean() 使用 bean ID 或 bean 名称作为参数来限制切点只匹配特定的 bean。

execution(* concert.Performance.perform()) and bean('woodstock')

4.3 使用注解创建切面

4.3.1 定义切面
  1. 将观众定义为一个切面
    package concert;
    
    import org.aspect.lang.annotation.AfterReturning;
    import org.aspect.lang.annotation.AfterThrowing;
    import org.aspect.lang.annotation.Aspect;
    import org.aspect.lang.annotation.Before;
    
    @Aspect
    public class Audience {
    
      @Before("execution(** concert.Performance.perform(..))")
      public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
      }
      
      @Before("execution(** concert.Performance.perform(..))")
      public void takeSeats() {
        System.out.println("Taking seats");
      }
      
      @AfterReturning("execution(** concert.Performance.perform(..))")
      public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
      }
      
      @AfterThrowing("execution(** concert.Performance.perform(..))")
      public void demandRefund() {
        System.out.println("Demanding a refund");
      }
    }
  1. 可以进一步将切点简化,@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。
    package concert;
    
    import org.aspect.lang.annotation.AfterReturning;
    import org.aspect.lang.annotation.AfterThrowing;
    import org.aspect.lang.annotation.Aspect;
    import org.aspect.lang.annotation.Before;
    import org.aspect.lang.annotation.Pointcut;
    
    @Aspect
    public class Audience {
    
      @Pointcut("execution(** concert.Performance.perform(..))")
      public void performce() { }
    
      @Before("performce()")
      public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
      }
      
      @Before("performce()")
      public void takeSeats() {
        System.out.println("Taking seats");
      }
      
      @AfterReturning("performce()")
      public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
      }
      
      @AfterThrowing("performce()")
      public void demandRefund() {
        System.out.println("Demanding a refund");
      }
    }
Performance()本身只是一个标识,供 @Pointcut 注解依附。
  1. AspectJ 自动代理都会为使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。
4.3.2 创建环绕通知
    package concert;
    
    import org.aspect.lang.annotation.ProceedingJoinPoint;
    import org.aspect.lang.annotation.Around;
    import org.aspect.lang.annotation.Aspect;
    import org.aspect.lang.annotation.Pointcut;
    
    @Aspect
    public class Audience {
    
      @Pointcut("execution(** concert.Performance.perform(..))")
      public void performce() { }
    
      @Around("performce()")
      public void watchPerformance(ProceedingJoinPoint jp) {
        try {
          System.out.println("Silencing cell phones");
          System.out.println("Taking seats");
          jp.procee();
          System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable e) {
          System.out.println("Demanding a refund");
        }
      }
    }
它接受 **ProceedingJoinPoint** 作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将**控制权交给被通知**的方法时,它需要调用 ProceedingJoinPoint 的 proceed() 方法。
  1. 忘记调用proceed方法会阻塞服务,一般不是我们想要的。

  2. 你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

4.3.3 处理通知中的参数
  1. 假设你想记录每个磁道被播放的次数。

    1. 一种方法就是修改 playTrack() 方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,因此不应该属于 playTrack() 方法。看起来,这应该是切面要完成的任务。

    2. 另外的方法就是我们创建了 TrackCounter 类,它是通知 playTrack() 方法的一个切面。

        package soundsystem;
        
        import java.util.HashMap;
        import java.util.Map;
        import org.aspect.lang.annotation.Aspect;
        import org.aspect.lang.annotation.Before;
        import org.aspect.lang.annotation.Pointcut;
        
        @Aspect
        public class TrackCounter {
        
          private Map<Integer, Integer> trackCounts = new HashMap<>();
          
          @Pointcut("execution(* soundsystem.CompactDisc.playTrack(int) " +
                    "&& args(trackNumber)")
          public void trackPlayed(int trackNumber) { }
        
          @Before("trackPlayed(trackNumber)")
          public void countTrack(int trackNumber) {
            int currentCount = getPlayCount(trackNumber);
            trackCounts.put(trackNumber, currentCount + 1);
          }
          
          public int getPlayCount(int trackNumber) {
            return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
          }
        }
  1. 在Spring配置中将BlancDisc和TrackCounter定义为bean
    package soundsystem;
    
    import java.util.ArrayList;
    import java.util.List;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @EnableAspectJAutoProxy
    public class TrackCounterConfig {
    
      @Bean
      public CompactDisc sgtPeppers() {
        BlankDisc cd = new BlankDisc();
        cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band");
        cd.setArtist("The Beatles");
        List<String> tracks = new ArrayList<>();
        tracks.add("Sgt. Pepper's Lonely Hearts Club Band");
        tracks.add("With a Little Help from My Friends");
        tracks.add("Lucy in the Sky with Diamonds");
        tracks.add("Getting Better");
        tracks.add("Fixing a Hole");
        
        // ...other tracks omitted for brevity...
        cd.setTracks(tracks);
        return cd
      }
      
      @Bean
      public TrackCounter trackCounter() {
        return new TrackCounter();
      }
    }
  1. 最后,为了证明它能正常工作,你可以编写如下的简单测试。
    package soundsystem;
    
    import static org.junit.Assert.*;
    import org.junit.Assert;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
    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;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes=TrackCounterConfig.class)
    public class TrackCounterTest {
    
      @Rule
      public final StandardOutputStreamLog log = new StandardOutputStreamLog();
    
      @Autowired
      private CompactDisc cd;
      
      @Autowired
      private TrackCounter counter;
    
      @Test
      public void testTrackCounter() {
        cd.playTrack(1);
        cd.playTrack(2);
        cd.playTrack(3);
        cd.playTrack(3);
        cd.playTrack(3);
        cd.playTrack(3);
        cd.playTrack(7);
        cd.playTrack(7);
        
        assertEquals(1, counter.getPlayCount(1));
        assertEquals(1, counter.getPlayCount(2));
        assertEquals(4, counter.getPlayCount(3));
        assertEquals(0, counter.getPlayCount(4));
        
        assertEquals(0, counter.getPlayCount(5));
        assertEquals(0, counter.getPlayCount(6));
        assertEquals(2, counter.getPlayCount(7));
      }
    }
4.3.4 通过注解引入新功能

44

  1. 我们为示例中的所有的 Performance 实现引入下面的 Encoreable 接口:
    package concert;
    
    public interface Encoreable {
      void performEncore();
    }
  1. 我们现在假设你能够访问 Performance 的所有实现,并对其进行修改,让它们都实现 Encoreable 接口。但是,从设计的角度来看,这并不是最好的做法,并不是所有的 Performance 都是具有 Encoreable 特性的。另外一方面,有可能无法修改所有的 Performance 实现,当使用第三方实现并且没有源码的时候更是如此。

  2. 借助AOP可以实现非侵入性的改变。

    package concert;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.DeclareParents;
    
    @Aspect
    public class EncodeableIntroducer {
    
      @DeclareParents(value="concert.Performce+",
                      defaultImpl=DefaultEncoreable.class)
      public static Encoreable encoreable;
    }
@DeclareParents 注解由三部分组成:

- value 属性指定了哪种类型的 bean 要引入该接口。在本例中,也就是所有实现 Performance 的类型。(标记符后面的加号表示是 Performance 的所有子类型,而不是 Performance 本身。)
- defaultImpl 属性指定了为引入功能提供实现的类。在这里,我们指定的是 DefaultEncoreable 提供实现。
- @DeclareParents 注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是 Encoreable 接口。
  1. 在 Spring 应用中将 EncoreableIntroducer 声明为一个 bean:<bean class="concert.EncoreableIntroducer" />

4.4 在XML中声明切面

面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。如果你没有源码的话,或者不想将 AspectJ 注解放到你的代码之中,可以使用XML配置。

4.5 注入AspectJ切面

当 Spring AOP 不能满足需求时,我们必须转向更为强大的 AspectJ。

4.6 小结

略。

Thoughts? Leave a comment