0.学习准备
参考书籍:《Spring实战》(第四版)
内容概述:
- Spring profile
- 条件化的Bean声明
- 自动化装配与歧义性
- bean的作用域
1.学习中出现的一些新概念
- 嵌入式数据库:
这种数据库嵌入到了应用程序进程中,消除了与客户机服务器配置相关的开销。嵌入式数据库实际上是轻量级的,在运行时,它们需要较少的内存。它们是使用精简代码编写的,对于嵌入式设备,其速度更快,效果更理想。如SQLite,Empress,OpenBaseLite等。 JNDI
:Java API命名与目录接口。
jdni是一种将对象和名字绑定的技术,容器产生对象并都和唯一的名字绑定,这样外部程序就用JNDI技术通过名字来获取对象,跟反射一样。(网上大佬)
jdbc是java去找数据库驱动,jndi是通过你的服务器配置(如Tomcat)的配置文件context来找数据库驱动~QA
:质量检测。javax
:java extension,java扩展api接口。而java.XXX则是标准的API接口。
2.环境迁移时的问题(嵌入式数据库)
1)环境迁移的场景
- 将应用程序从一个环境迁移到另一个环境,很多时候即便是迁移成功了也无法正常工作,因为数据库配置,加密算法以及与外部系统集成都是会改变的因素。
- 假设需要使用的是Hypersonic嵌入式数据库:
需要使用到javax.sql.DataSource这个接口,但是我用的java(8)已经自带了,所以就不用导jar包了。其实作用和驱动包相同。
需要使用到Spring框架中的EmbeddedDatabaseBuilder方法,该方法用于搭建一个Hypersonic数据库,使用该类的build()方法会返回一个连接实例。(具体使用见后面) - 关于Javax.sql.DataSource的用法可以参考以下博客:
https://blog.csdn.net/guohuiJI/article/details/72458016
2)三种不同的DataSource类:
- 获取嵌入式数据库的DataSource。
JavaConfig配置类代码如下:
Hypersonic数据库创建的数据库的模式定义在schema.sql中,测试数据则是通过test-data.sql加载的。package chap3; import javax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; @Configuration public class SqlConfig { @Bean public DataSource dataSource(){ return new EmbeddedDatabaseBuilder() .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); } }
在开发环境中运行集成测试或者启动应用进行手动测试的时候,这样创建DataSource是非常有用的。每次启动它的时候都能让数据库处于一个给定(开启服务)的状态。 - 创建JNDI管理的DataSource。
尽管只使用EmbeddedDatabaseBuilder创建的DataSource非常实用与开发环境,但是对于环境却并不是那么好用。所以一般使用JNDI容器获取的DataSource,但是对于简单的开发测试环境JNDI会带来不必要的复杂性:
通过JNDI来获取DataSource能让容器(context)决定通过什么方式获取DataSource。(详细的JNDI配置数据源参考另一篇博客)@Bean public DataSource dataSource(){ //使用jndi从容器获取 JndiObjectFactoryBean jndiObjectFactoryBean=new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/kenshine"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject();
这里的JndiObjectFactoryBean仅仅只是通过JNDI名来产生一个DataSource的对象而已。 - 创建自配置DBCP连接池的DataSource。
在QA环境中可以将DataSource配置为Commons DBCP连接池:@Bean 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; }
- 这三个方法分别使用了三种不同的方式来创建DataSource对象。
在不同的环境下所要求的Bean是不同的,所以必须要有一种方式来配置DataSource,来为每一种环境选择合适的配置。 - 可以在单独的配置类或者xml文件中配置每个bean,然后在构建阶段确定要将哪个配置编译到可部署的应用中。但是有一个很大的缺点,就是要为每种环境重新构建应用。(一般是通过Maven Profile确定的)
- Spring提供的解决方案不需要重新构建–Spring profile。
2.Profile Bean的配置
其实和使用maven的profile确定环境差不多,只是不是在构建时确定,而是在运行时确定要加载哪一个bean。
1)JavaConf配置profileBean:
- 使用@profile注解:
@Configuration public class SqlConfig { @Bean(destroyMethod="shutdown") @Profile("dev") public DataSource dataSource(){ return new EmbeddedDatabaseBuilder() .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); } @Bean @Profile("pro") public DataSource dataSource2(){ //使用jndi从容器获取 JndiObjectFactoryBean jndiObjectFactoryBean=new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/kenshine"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); } @Bean @Profile("qa") public DataSource dataSource3(){ BasicDataSource dataSource=new BasicDataSource(); dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/springtest?useUnicode=true&characterEncoding=utf-8"); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUsername("root"); dataSource.setPassword("zjx1754294529"); dataSource.setInitialSize(20); return dataSource; } }
@Bean(destoryMethod="shutdown")
的含义:
在该bean销毁前去执行shutdown方法。
相应的initMethod就是初始化Bean之前执行的方法。- @Profile()注解:
指定某个bean属于哪一个profile,只有当对应的环境激活(active)时才会加载这个bean。
而profile默认是关闭的(未激活的)。 - 可以直接在方法上使用@profile注解,只有Spring3.2以后支持,Spring3.1及以前需要使用类级别的@profile注解,为每个方法单独配置一个配置类再引用到主配置类。如:
其余两个环境也相同配置。不太方便。@Configuration @Profile("dev") public class EmbeddedConfig { @Bean(destroyMethod="shutdown") private DataSource dataSource(){ return new EmbeddedDatabaseBuilder() .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); } }
- 没有指定@profile的Bean始终都会被创建。
2)XML配置profileBean:
- 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:p="http://www.springframework.org/schema/p" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <beans profile="dev"> <jdbc:embedded-database id="dataSource"> <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" jndi-name="jdbc/kenshine" resource-ref="true" proxy-interface="javax.sql.DataSource"> </jee:jndi-lookup> </beans> <beans profile="qa"> <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" p:url="jdbc:mysql://127.0.0.1:3306/springtest" p:driverClassName="com.mysql.jdbc.Driver" p:username="root" p:password="zjx1754294529" p:initialSize="20" > </bean> </beans> </beans>
- 使用嵌套的Beans标签分别配置了三种不同profile下的bean。
这三个Bean的id都是相同的,在创建时会根据激活的profile来创建。
也可以不使用嵌套Beans标签,直接在xsi:schemaLocation属性后添加profile属性并配置,但是这样会产生非常多不必要的代码。
3)关于两种配置方式的总结:
- javaConfig配置profile:
- XML配置profile:
3.激活Profile及使用
1)Spring激活Profile的原理:
- Spring确定哪个profile处于激活状态,需要两个独立的属性:
spring.profiles.active属性和spring.profiles.default属性。 - 如果active属性指定了激活的profile则激活active指定的profile,
如果active属性未指定激活的profile则激活default指定的profile,
都未指定则只加载那些没有指定profile的Bean。 - spring.profiles.active属性和spring.profiles.default属性都是复数的,意味着可以一次性激活多个(彼此不相关)的profile。
2)激活profile的方式:
有很多种方式能够激活profile(设置那俩属性)。
作为Spring MVC DispactcherServlet的初始化参数
作为Web应用上下文参数
作为JNDI条目
作为环境变量
作为JVM的系统属性
在集成测试类上,使用@ActiveProfiles注解设置
Servlet2.5及以下在web.xml中配置:
<?xml version="1.0" encoding="UTF-8"?> <web -app version="2.5" ...> <!--为上下文设置默认的profile--> <context-param> <param-name>spring.profile.default</param-name> <param-value>dev</param-value> </context-param> ... <servlet> ... <!--为Serlvet设置默认的profile--> <init-param> <param-name>spring-profiles.default</param-name> <param-value>dev</param-value> </init-prama> ... <web-app>
这样配置后默认的都是使用的dev开发环境,而实际使用时只需要设置相对应的spring.profile.active的代码就可以了。
- Servlet3.0以上可以使用如下方式激活:
package chap3; import javax.servlet.ServletContext; import javax.servlet.ServletException; import org.springframework.web.WebApplicationInitializer; public class WebInit implements WebApplicationInitializer{ @Override public void onStartup(ServletContext container) throws ServletException { container.setInitParameter("spring.profile.default","dev"); } }
- JVM增加参数spring.profiles.active设置
在JVM中增加参数spring.profiles.active设置,如果我们想设置spring.profiles.active为dev,使用Dspring.profiles.active=”dev” 。
此种方式需要修改tomcat的JVM配置,通用性不高。 在测试时激活:
@ActiveProfiles()
注解@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations="classpath:DataSourceConfig.xml") @ActiveProfiles("pro") public class TestActiveProfile { @Autowired private HelloService hs; @Test public void testProfile() throws Exception { String value = hs.sayHello(); System.out.println(value); } }
4.@Conditional条件化的Bean
1)简单使用:
- @Conditional的配置更加灵活,可以实现在某一个Bean创建之后才创建这个Bean这种效果。
- Java配置类的写法:
@Configuration public class ConditionConfig { @Bean @Conditional(GoodMusicCondition.class) //一个实现了Conditon接口的类 public GoodMusic goodMusic(){ return new GoodMusic(); } }
- GoodMusicCondition类的写法:
检测上下文中是否已经创建了名为softMusic的Bean,如果有则装配GoodMusic的Bean,没有则不装配。public class GoodMusicCondition implements Condition{ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { //各项检测机制,返回true或false代表是否生成Bean Environment env=context.getEnvironment(); return env.containsProperty("softMusic"); } }
还可以在这里面写更复杂的逻辑。
2)关于Condition接口的介绍:
- Condition接口:
可以看到matches方法是由ConditionContext对象与AnnotatedTypeMetadata对象来操作的,这俩其实都是接口public interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
- ConditionContext接口:
public interface ConditionContext { BeanDefinitionRegistry getRegistry(); //检查bean的定义 ConfigurableListableBeanFactory getBeanFactory(); //检查bean是否存在并探查bean的属性 Environment getEnvironment(); //环境变量是否存在以及值是什么 ResourceLoader getResourceLoader(); //返回ResourceLoader加载的资源 ClassLoader getClassLoader(); //返回ClassLoader加载并检查类是否存在 }
AnnotatedTypeMetadata接口:
可以检查带有@Bean的注解方法上还有什么其他注解。import org.springframework.util.MultiValueMap; public interface AnnotatedTypeMetadata { boolean isAnnotated(String annotationName); Map<String, Object> getAnnotationAttributes(String annotationName); Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString); MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName); MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString); }
3)@Profile的实现:
- profile的代码:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(ProfileCondition.class) public @interface Profile { String[] value(); }
- 其余注解以后研究源码再说,先看ProfileCondition类:
使用getAllAnnotationAttributes获取@profile注解的所有属性,检查value属性并获取profile名称,最后通过acceptsProfiles方法来判断该profile是否激活。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; } }
5.处理自动装配时的歧义性
1)歧义性产生情况及解决方法概述:
- 歧义产生的简单示意图:
- 产生歧义的代码示例:
Movie代码:
MoviePlayer类代码:@Component public class Movie1 implements Movie{ @Override public void play() { System.out.println("playing movie1"); } } @Component public class Movie1 implements Movie{ //和上面差不多 } @Component public class Movie1 implements Movie{ //和上面差不多 }
MoviePlayerConfig配置类:@Component public class MoviePlayer implements MediaPlayer{ @Autowired private Movie movie; @Override public void play() { movie.play(); } }
@Configuration @ComponentScan(basePackageClasses=MoviePlayer.class) public class MoviePlayerConfig { }
- 测试类:
测试结果:@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=MoviePlayerConfig.class) public class MoviePlayerTest { @Autowired private MoviePlayer moviePlayer; @Test public void play(){ moviePlayer.play(); } }
报错:No qualifying bean of type 'chap3.Movie' available: expected single matching bean but found 3: movie1,movie2,movie3
- 有两种解决方案:
标识首选的bean。
使用限定符缩小范围。
2)使用primary注解或者属性标识首选的bean:
- 配置方式一:
在希望成为首选Bean的@Compnent下使用@primary注解:@Component @Primary public class Movie1 implements Movie{ @Override public void play() { // TODO Auto-generated method stub System.out.println("playing movie1"); } }
- 配置方式二:
显式配置javaConfig配置类并且在@Bean注解下使用@primary注解指定:@Configuration public class MovieConfig { @Bean @Primary public Movie1 movie1(){ return new Movie1(); } @Bean public Movie2 movie2(){ return new Movie2(); } @Bean public Movie3 movie3(){ return new Movie3(); } }
- 配置方式三:显式的XML配置
...省略配置头 <bean id="movie1" class="chap3.Movie1" primary="true"></bean> <bean id="movie2" class="chap3.Movie2"></bean> <bean id="movie3" class="chap3.Movie3"></bean> ...省略配置尾
- 以上任意一种配置的测试结果都是这样:
- 但是如果有两个以上的primary还是会错,所以推荐使用限定符修饰。
3)限定自动装配的Bean:
- 使用自带的注解:
在MoviePlayer中使用@Qualifier注解:
就是按照上下文中的ID来获取,ID默认是@Bean方法名的第一个字母小写,所以这里使用movie1。@Autowired @Qualifier("movie1") public Movie movie;
但是这样有个缺陷:
如果方法名字修改后如将Movie1修改为其他的,那么久无法获取到bean了。 - 创建自定义的限定符(id)
可以在Movie1中这样修改(使用自定义的ID):
也可以在显示声明Bean时这样使用:@Component @Qualifier("movie1") public class Movie1 implements Movie{ @Override public void play() { // TODO Auto-generated method stub System.out.println("playing movie1"); } }
或者:@Bean @Qualifier("movie1") public Movie1 movie1(){ return new Movie1(); }
但是这种方式有时候还是有问题,比如不知道是否有重复的ID。@Bean(name="movie1") public Movie1 movie1(){ return new Movie1(); }
这是就得使用多个注解修饰了。
4)自定义限定注解的使用:
- 如果说有两个bean的@Qualifier修饰相同,那么取的时候仍然会报错,一般会想到再加一个@Qualifier注解进行修饰,然后取的时候也再加一个@Qualifier注解如:
但是Java8规定只有使用了@Repeatable注解的注解才能重复,而很遗憾@Qualifier注解并没有:@Autowired @Qualifier("movie1") @Qualifier("comedy") public Movie movie;
所以使用两个@Qualifier会出错。所以需要自定义一种注解(比如@Comedy)。@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Qualifier { String value() default ""; }
- 自定义注解@comedy的创建:
代码如下:@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Comedy { }
- 修改Movie2代码修改如下:
修改MoviePlayer代码如下:@Component @Qualifier("movie2") @Comedy public class Movie2 implements Movie { @Override public void play() { // TODO Auto-generated method stub System.out.println("playing movie2"); } }
测试结果:@Component public class MoviePlayer implements MediaPlayer{ @Autowired @Qualifier("movie2") @Comedy public Movie movie; @Override public void play() { movie.play(); } }
- 如果将某一个@Comedy标签去除,不管去掉啥标签,只要是不完全匹配就会报错说找不到对应的Bean。
6.Bean的作用域
1)作用域Bean简介及简单创建:
- 在默认情况下,Spring上下文中的所有Bean都是以单例的情况存在的(单例bean),不管获取多少次上下文中的Bean,获取到的Bean都是同一个实例。
大多数情况下这种单例的方案很好,但是有时有特殊需求时就不那么好用了。 - Spring自定义了很多种作用域,可以基于这些作用域创建Bean:
- 单例(Singleton):整个应用中只能创建一个Bean实例。
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候都会创建一个新的Bean。
- 会话(Session):WEB应用中,为每个会话创建一个bean实例。
- 请求(Request):WEB应用中,为每个请求创建一个bean实例。
- 使用@Scope注解或者属性可以指定作用域生成相对的Bean。
方法一:自动装配置时生成原型Bean
方法二:JavaConfig配置类配置@Scope@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //也可以直接使用("prototype") public class Movie4 implements Movie{ @Override public void play() { System.out.println("playing movie4"); } }
方法三:config.xml文件中使用scope属性配置... @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public Movie4 movie4(){ return new Movie4(); } ...
... <bean id="movie4" class="chap3.Movie4" scope="prototype"></bean> ...
2)使用会话和请求作用域:
- 如果是一个购物车的Bean,单例Bean则所有用户都使用一个Bean,而原型Bean则在应用的其他地方就不可用了。用SessionBean就非常合适。
在WEB应用中,会话级和请求级的Bean非常实用。 - 购物车类Cart如下:
接口如下:@Component @Scope(value=org.springframework.web.context.WebApplicationContext.SCOPE_SESSION,proxyMode=ScopedProxyMode.INTERFACES) public class Cart implements ShoppingCart{ public String name="Cart1"; @Override public void getName() { System.out.println(name); } }
@Scope注解还有个方法proxyMode,解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。public interface ShoppingCart { void getName(); }
创建一个单例Bean类StoreService:
@Component public class StoreService { @Autowired public ShoppingCart shoppingCart; public void Service(){ shoppingCart.getName(); } }
测试类如下:
@RunWith(SpringJUnit4ClassRunner.class) @ComponentScan public class CartTest { @Autowired private StoreService storeService; @Test public void testCart(){ storeService.Service(); } }
- StoreService是一个单例类,会在Spring应用上下文加载的时候创建。
创建时会试图将ShoppingCartBean注入单例类中,但是会话级的bean此时不存在,只有用户进入了才会产生实例。 - 多个用户会有多个ShoppingCart,但是我们只希望注入当前会话对应的哪一个。
Spring并不会将实际的ShoppingCart注入StoreService,而是注入一个ShoppingCart Bean的代理。 - 关于proxyMode的值:
- 如果注入的是接口而不是类(最好的状态),则使用ScopedProxyMode.INTERFACES。
表明该代理要实现这个接口并调用委托给实现类。 - 如果注入的是具体类而非接口,则proxyMode值使用ScopedProxyMode.TARGET_CLASS。使用的是CGLib动态代理。
- 如果注入的是接口而不是类(最好的状态),则使用ScopedProxyMode.INTERFACES。
- 示意图:
3)在XML中声明作用域代理:
- 使用aop命名空间的scoped-proxy元素(需要):
<bean id="cart" class="chap3.ShoppingCart" scope="session"> <aop:scoped-proxy/> </bean>
- 默认是基于具体类的代理,如果需要生成基于接口的代理可以这么写:
<bean id="cart" class="chap3.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false"/> </bean>
最后更新: 2018年05月04日 13:42
原始链接: https://zjxkenshine.github.io/2018/04/21/Spring学习(三):高级装配1/