# Spring 其它注解和杂项

# @Lazy 注解

使用 @Lazy 注解的典型场景就是解决循环依赖问题。特别是构造注入,@Lazy 是弥补构造注入的『缺点』的关键。

当你对注入的 JavaBean 使用 @Lazy 注解时,Spring 注入的并非是这个单例对象,而是它的一个代理。当你(在未来)第一次使用这个 Bean 时,这个代理对象才会去 IoC 容器中找这个真正的 Bean 。

# Spring 的 @Import 注解

在使用 maven 多模块的概念去构建项目时,我们的各个 @Bean 会分散在各个子模块中。

当然,我们可以仍在入口模块(web)中通过配置去配置各个模块必须创建的单例 Bean ,不过更好的方式是:将各个模块的配置也分散在各个模块中,由各个模块自己负责,最后让入口模块引入各个模块的配置即可。这样的话,责任更加分明。

  • 一个独立的配置类:ConfigA.java

    @ComponentScan("com.example.commandpattern.config.a")
    public class ConfigA {
    
        @Bean
        public String demo() {
            return "hello world";
        }
    }
    
  • 另一个独立的配置类:ConfigB.java

    @ComponentScan("com.example.commandpattern.config.b")
    public class ConfigB {
    
        @Bean
        public LocalDate localDate() {
            return LocalDate.now();
        }
    }
    
  • 主配置类:MainConfig.java

    // 主配置类引入各个独立配置类
    @Import({ConfigA.class, ConfigB.class})
    public class MainConfig {
    }
    
  • 引入,使用:

    // 只需要将主配之类交给 Spring IoC 容器即可。
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
        System.out.println(context.getBean(String.class));
        System.out.println(context.getBean(LocalDate.class));
        System.out.println(context.getBean(StudentDao.class));
        System.out.println(context.getBean(StudentService.class));
    }
    

# 用于单元测试中的注解

Spring 中有一些注解,主要的使用场景是用于单元测试中。

# Spring 的 @Sql 注解

@Sql 注解主要用于 JUnit 测试代码中,结合 @Transactional 和 @Rollback 它可以执行测试代码之前,先执行指定的 SQL 脚本或 SQL 语句,用以构造数据库测试环境。

# 核心注解 @Sql

@Sql 注解可以执行 SQL 脚本,也可以执行 SQL 语句。它既可以加上类上面,也可以加在方法上面。

默认情况下,方法上的 @Sql 注解会覆盖类上的 @Sql 注解。但可以通过 @SqlMergeMode 注解来修改此默认行为。

@Sql 有下面的属性:

属性 说明
config 与注解 @SqlConfig 作用一样,用来配置“注释前缀”、“分隔符”等。
executionPhase 决定 SQL 脚本或语句什么时候会执行,默认是 BEFORE_TEST_METHOD 。
statements 配置要一起执行的 SQL 语句。
scripts 配置 SQL 脚本路径。
value scripts 的别名,它不能和 scripts 同时配置,但 statements 可以。

例如:

@Sql({ "/drop_schema.sql", "/create_schema.sql" })
@Sql(scripts = "/insert_data1.sql", statements = "insert into student(id, name) values (100, 'Shiva')")
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
public class SqlTest {
	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Test
	public void fetchRows1() {
		List<Map<String, Object>> students = jdbcTemplate.queryForList("SELECT * FROM student");
		assertEquals(3, students.size());
	}

	@Sql("/insert_more_data1.sql")
	@Test
	public void fetchRows2() {
		List<Map<String, Object>> students = jdbcTemplate.queryForList("SELECT * FROM student");
		assertEquals(5, students.size());
	}
} 
  • drop_schema.sql :

    drop table if exists student; 
    
  • create_schema.sql :

    CREATE TABLE student (
        id INT NOT NULL,
        name VARCHAR(50) NOT NULL,
        PRIMARY KEY(id)
    ); 
    
  • insert_data1.sql :

    insert into student(id, name) values (101, 'Mohan');
    insert into student(id, name) values (102, 'Krishna'); 
    
  • insert_more_data1.sql :

    insert into student(id, name) values (103, 'Indra');
    insert into student(id, name) values (104, 'Chandra'); 
    

# 相关注解 @SqlConfig(了解)

@SqlConfig 用于配置如何去解释 @Sql 注解中指定的 Sql 脚本。

@SqlConfig 可以用于类上,也可以用于方法上。

@Sql 注解也有一个 config 属性,作用与 @SqlConfig 相同,不同的是作用域只在对应的 @Sql 注解范围。它的优先级也大于类注解的 @SqlConfig 。

属性 说明
blockCommentStartDelimiter 多行注释开始字符,默认是 /*
blockCommentEndDelimiter 多行注释结束字符,默认是 */
commentPrefix 单行注释前缀,默认是
commentPrefixes 指定多个单行注释前缀,默认是 ["–"]
dataSource 指定脚本执行的数据库的名字,只有在多个数据源时需要指定
encoding 指定 sql 脚本文件的字符编码。
errorMode 配置错误模式,默认是 SqlConfig.ErrorMode 的 DEFAULT
separator 配置脚本语句分隔符,默认是 \n
transactionManager 指定 transactionManager bean,只有有多个 transactionManager 时需要指定
transactionMode 指定脚本执行的事务模式,默认是 SqlConfig.TransactionMode 的 DEFAULT

例子:

@SqlConfig(commentPrefix = "#")
@Sql({ "/drop_schema.sql", "/create_schema.sql" })
@Sql(scripts = { "/insert_data2.sql" })
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
public class SqlConfigTest {
	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Test
	public void fetchRows1() {
		List<Map<String, Object>> students = jdbcTemplate.queryForList("SELECT * FROM student");
		assertEquals(2, students.size());
	}

	@Sql(scripts = "/insert_more_data2.sql", config= @SqlConfig(commentPrefix = "~"))
	@Test
	public void fetchRows2() {
		List<Map<String, Object>> students = jdbcTemplate.queryForList("SELECT * FROM student");
		assertEquals(4, students.size());
	}
}

insert_data2.sql :

-- Insert initial data
insert into student(id, name) values (101, 'Mohan');
insert into student(id, name) values (102, 'Krishna'); 

# 相关注解 @SqlMergeMode(了解)

@SqlMergeMode 可以加在类上,也可以加在方法上。用于指示方法上的 @Sql 和类上 @Sql 注解配置是否合并。方法上的 @SqlMergeMode 注解优先级更高。默认值是 SqlMergeMode.MergeMode 的 OVERRIDE 。

@SqlMergeMode(MergeMode.MERGE)
@Sql({ 
	"/drop_schema.sql", 
	"/create_schema.sql", 
	"/insert_data1.sql" 
})
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
public class SqlMergeModeTest {
	
	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Sql(statements = "insert into student(id, name) values (100, 'Shiva')")	
	@Test
	public void fetchRows1() {
		List<Map<String, Object>> students = jdbcTemplate.queryForList("SELECT * FROM student");
		assertEquals(3, students.size());
	}

	@SqlMergeMode(MergeMode.OVERRIDE)	
	@Sql("/insert_more_data1.sql")
	@Test
	public void fetchRows2() {
		List<Map<String, Object>> students = jdbcTemplate.queryForList("SELECT * FROM student");
		assertEquals(5, students.size());
	}
} 

# @AutoConfigureWebMvc 和 @AutoConfigureMockMvc

  • @AutoConfigureWebMvc

    主要用于 JUnit 测试代码中,特别是测试 Web 层的测试代码。它用于构建『仅包含』Web 层(及相关)的 Java Bean 的 Spring IoC 容器。

    简单来说,对比与 @SpringBootTest,@AutoConfigureWebMvc 构造出来的 Spring IoC 容器只是 @SpringBootTest 构造出来的 IoC 容器的子集。

    在这种情况下,你如果去 @Autowired 一个 Service 或 Dao,你会发现 Junit 代码执行报错,因为,这里的 Spring IoC 容器中没有 Service 和 Dao 的单例对象。

    它启用与 Web 层相关的所有自动配置,并且仅启用 Web 层。这是整体自动配置的子集。

  • @AutoConfigureMockMvc

    @AutoConfigureMockMvc 注解用于测试 Web 层 Controller 的 JUnit 代码中。它会在 Spring IoC 容器中自动创建、配置一个 MockMvc 单例对象。而后,我们可以在测试代码中 @Autowired 它,并发起对 @Controller 的 HTTP 请求测试。

    需要注意的是,正常情况下,Spring IoC 容器中是没有 MockMvc 单例对象的,你必须使用 @AutoconfigureMockMvc 才会导致 Spring IoC 容器创建、维护它。

  • @WebMvcTest

    它是 @AutoConfigureWebMvc 和 @AutoConfigureMockMvc 的组合,一个顶俩。

另外,

# @MockBean 和 @SpyBean

@MockBean 注解会代理 bean 的所有方法,对于未 mock 的方法调用均是返回 null:

@MockBean
private UsersService usersService;

@Test
public void createUsersTest() {

    /* 
     * @MockBean 注解会代理 bean 的所有方法,对于未 mock 的方法调用均是返回 null,
     * 这里的意思是针对调用 createUsers 方法的任意入参,均返回指定的结果
     */
    given(usersService.createUsers(any(), any())).willReturn(users);

    ...
}

@SpyBean 可达到部分 mock 的效果,未被 mock 的方法会被真实调用

@SpyBean
private UsersService usersService;

@Test
public void createUsersTest() {
    Users users = new Users();
    users.setUsername("jufeng98");

    /* @SpyBean可达到部分mock的效果,仅当 doReturn("").when(service).doSomething() 时,doSomething方法才被mock,
     * 其他的方法仍被真实调用。
     * 未发生实际调用
     */
    doReturn(users).when(usersService).createUsers(any(), any());

    ...
}

# 特殊情况下的 Bean 注入

# 非托管对象中获取托管对象

有时你需要在非托管对象中获取 Spring 的 ApplicationContext

# 方案一:通用方案

@Slf4j
@Component
public class ApplicationContextHolder {

    private static final ApplicationContext APPLICATION_CONTEXT;

    private final ApplicationContext context;

    public ApplicationContextHolder(ApplicationContext applicationContext) {
        application = applicationContext;
        APPLICATION_CONTEXT = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return APPLICATION_CONTEXT;
    }
}

使用时调用:

ApplicationContextRegister.getApplicationContext().getBean(Xxx.class);

# 方案二:Spring 可用,Spring Boot 不可用

在 Spring 项目中可用,在 Spring Boot 项目中不可用。

还有一种方案可以实现同样效果,直接调用 Spring 提供的工具类:

ContextLoader.getCurrentWebApplicationContext().getBean(Xxx.class);

经测试发现,在 Spring Boot 项目中该方案无效。有人跟踪源码分析,因为 Spring Boot 的内嵌 Tomcat 和真实 Tomcat 还是有一定的区别,从而导致 Spring Boot 中该方案无法起到一起效果。

# 单例 Bean 中注入多例 Bean

保证每次获取都是新的多例 Bean 。

在 Spring 中 如果需要一个对象为多例,需要使用 @Scope 注解,指明作用于为 SCOPE_PROTOTYPE 即可。

当我们在一个单例的 Bean A 中注入多例 Bean B 时,由于 Spring 在初始化过程中加载 A 的时候已经将 B 注入到 A 中,所以直接当做成员变量时,只会获取一个实例。

我们可以通过以下两种优雅的方法解决:

  • 使用 Spring 的 ObjectFactory

  • 使用 @Looup 注解

# 方案一:Spring 的 ObjectFactory

为你的单例对象注入一个 Spring 提供的 ObjectFactory ,毫无疑问,ObjectFacotry 也是一个单例对象。

@Component
public class SingleBean {

    @Autowired
    ObjectFactory<PrototypeBean> factory;

    public void print(String name) {
        System.out.println("single service is " + this);
        factory.getObject().test(name);
    }
}

但是,在单例对象中,你只要通过 ObjectFactory(的封装)ObjectFactory.getObject() 方法去获得多例对象,每次它返回给你的都是一个『新』的对象。

# 方案二:@Lookup 注解

我们可以使用 Spring 的 @Lookup 注解。该注解主要为单例 Bean 实现一个 cglib 代理类,并通过 BeanFacoty.getBean() 来获取对象。

@Lookup 注解是一个作用在方法上的注解,被其标注的方法会被 Spring 通过 cglib 实现的代理类重写,然后根据其返回值的类型,容器调用 BeanFactory 的 getBean() 方法来返回一个 bean 。

@Component
public class SingleBean {

  public void printClass() {
    System.out.println("This is SingleBean: " + this);
    getPrototypeBean().xxx();
  }
 
  /**
   * 方法的存在,以及方法的返回值是关键。
   * 该方法会被 Spring 重写:Spring 会来保证在『别处』你调用这个方法时,每次都返回一个新的 PrototypeBean 对象给你。
   */
  @Lookup
  public PrototypeBean getPrototypeBean() {
    return null;
  }
}