# 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;
}
}