刚刚两个问题
第一个问题,测试类重复代码
第二个问题,xml和注解配置,xml文件都无法脱离,只能用在我们的类上,比如QueryRunner这个类,dbutil.jar包下的类,它是无法加上注解的.
既然想去掉bean.xml配置文件,那么我们需要一个相同功能的注解出现.
创建一个配置类,他的作用和bean.xml 是一样的
1.spring的新注解-Configuration和ComponentScan
@Configuration
@ComponentScan(value = "com.itheima")
public class SpringConfiguaration {
}
- @Configuration
作用:指定该类是一个配置类 - @ComponentScan
作用:用于通过注解指定spring在创建容器时要扫描的包
属性:value:它的作用和basepackage的作用是一样的,都是用于指定容器创建时要扫描的包.
使用它相当于在xml中配置了
点进去可以看到用value属性和basePackages属性,value使用的别名是basePackages,basePackages的别名是value,所以写哪个都可以,如果注解的属性有且只有一个值的时候,不写也可以,basePackages的路径必须是类路径,另外可以看到属性的值是string类型,所以其实完整的写法应该是@ComponentScan(basePackages={"com.itheima"})
@Configuration
@ComponentScan(basePackages={"com.itheima"})
@Configuration
@ComponentScan(basePackages="com.itheima")
public class SpringConfiguaration {
2.spring的新注解-Bean
接下来我们需要考虑如何去掉在bean.xml中这剩下的部分
如果细心的话可以看到 红框标注的这一部分,这表示着可以用构造方法来创造对象,new Instance,不同的是一个需要参数,一个不需要
接下来我们在SpringConfiguation里继续配置,创建QueryRunner对象和DataSource对象
/**
* 创建一个QueryRunner对象
* @return
*/ public QueryRunner createQueryrunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
/**
* 创建一个QueryRunner对象
* @return
*/ public QueryRunner createQueryrunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
SpringConfiguation中createQueryrunner方法与xml中的配置作用是一样的吗???
不是一样的
在xml中,spring容器创建了runner对象并存放于Spring容器中,但SpringConfiguation 只是创建了对象,但没有存放于容器中,于是相应的,我们需要用到一个能把对象存放spring容器的注解
- @Bean
作用:用于把当前方法的返回值作为bean对象存入Ioc容器中
属性:用于指定当前bean的id,当不写时,默认值是当前方法的名称.
细节:当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象,查找的方式和AutoWried注解的作用是一样的
/**
* 创建一个QueryRunner对象
* @return
*/
@Bean(name = "runner")
public QueryRunner createQueryrunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
再创建一个createDataSource方法,得到DataSource对象
/**
* 创建一个DataSource对象
* @return
*/
@Bean( name = "DataSource")
public DataSource createDataSource(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/eesy");
ds.setUser("root");
ds.setPassword("password");
return ds;
} catch (Exception e) {
throw new RuntimeException();
}
}
3.AnnotationConfigApplicationContext的使用
- AnnotationConfigApplicationContext
用注解取代beam.xml,获取spring核心容器的时候,ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml") ;很显然我们不需要再读取xml中的配置,所以这个写法不再有效
//获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml") ;
//获取容器
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguaration.class);
要获取核心容器需要用到AnotationConfigApplication来获取核心容器.
4.spring的新注解-Import
至此,bean.xml需要配置的东西都用同功能的注解实现了.
如果把@Configuation 这个注解从SpringConfiguation类上注释掉,再运行testFindAll方法发现程序运行并不受影响,仍然可以成功.
这是因为 当配置类作为AnotationConfigApplicationContext对象创建参数时,该注解可以不写
但这不是绝对的,如果再建立一个config类,比如JDBCConfiguation ,此时把SpringConfiguation类置空
package jdbcconfig;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* 数据库相关配置类
*/
@Configuration
public class JDBCConfiguation {
/**
* 创建一个QueryRunner对象
* @return
*/
@Bean(name = "runner")
public QueryRunner createQueryrunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
/**
* 创建一个DataSource对象
* @return
*/
@Bean( name = "DataSource")
public DataSource createDataSource(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/eesy");
ds.setUser("root");
ds.setPassword("password");
return ds;
} catch (Exception e) {
throw new RuntimeException();
}
}
}
这个时候,JDBCConfiguation并没有打上Configuation的注解,其他类不变,包括Test类,运行程序是会报错的.
这个类虽然写了注解,但是要扫描的包并没有扫描JDBCConfiguation的包,那么我们在SpringConfiguation加上JDBCConfiguation的包
此时再运行仍然报错,错误仍然为
这也就是说QueryRunner并没有被找到.虽然包扫描了,但是这个包并没有注解加载.ComponetScan在扫描包时,首先它得认为是个配置类,才会对里面的注解去扫描.所以我们需要使用Configuation这个注解.
加上Configuation这个注解,程序又能继续运行了
这也说明了 Configuation不写的情况,只有当配置类作为AnotationConfigApplicationContext对象创建参数时,该注解可以不写
所以在AnotationConfigApplicationContext再加上JDBCConfiguation.class时,Configuation可以不写.
- @Import
作用:用于导入其他配置类
属性:value,用于指定其他配置类的字节码
当我们使用Import的注解后,有Import注解的类为父配置类,而导入的都是子配置类
如果只想要SpringConfiguation作为主配置类,可以使用@Import注解,可以看到Idea有提示输入.class的value,选择JDBCConfiguation作为Import的配置类.
(运行时junit很慢,几次找不到com.mysql.jdbc.Driver,发现properties里用了引号,更正以后仍然加载不成功,才发现是数据库没启动)
5.spring的新注解-PropertySource
再想想代码里还可以有什么可以改造的地方,这里写死的地方可以再改造一下,把这些写死的内容单独写出来
在resources文件下,创建一个新的 JdbcConfig.properties,来放有关jdbc的配置
jdbc.driver = com.mysql.jdbc.Driver
jdbc.url =jdbc:mysql://localhost:3306/eesy
jdbc.user = root
jdbc.password = password
怎么去读取这些配置呢?可以在JDBCConfiguation里创建几个变量,用spring的El表达式和@Value注解
/**
* 数据库相关配置类
*
*/
public class JDBCConfiguation {
@Value("${jdbc.driver}")
private String jdbcDriver;
@Value("${jdbc.url}")
private String jdbcUrl;
@Value("${jdbc.user}")
private String jdbcUser;
@Value("${jdbc.password}")
private String jdbcPassword;
/**
* 创建一个QueryRunner对象
* @return
*/
@Bean(name = "runner")
public QueryRunner createQueryrunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
/**
* 创建一个DataSource对象
* @return
*/
@Bean( name = "DataSource")
public DataSource createDataSource(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass(jdbcDriver);
ds.setJdbcUrl(jdbcUrl);
ds.setUser(jdbcUser);
ds.setPassword(jdbcPassword);
return ds;
} catch (Exception e) {
throw new RuntimeException();
}
}
}
怎样读取文件呢?就需要@PropertySource这个注解了,在SpringConfiguation类中添加@PropertySource注解
@Configuration
@ComponentScan(basePackages={"com.itheima"})
@Import(JDBCConfiguation.class)
@PropertySource("classpath:JdbcConfig.properties")
public class SpringConfiguaration {
}
在这段内容改造完成以后,纯注解的方式和不是纯注解的方式相比,并没有轻松很多,反而更费事.
实际开发中,注解和xml如何去选择,纯xml可以配,但是里面有些复杂性,但纯注解的配置,也很复杂,从这两点来说,选择有注解和有xml的方式更合适,如果这个类是已经写好的,存在于jar包中,用xml比较方便,如果是自己写的,用注解比较方便,用哪种方式更方便,就用哪种配置.
6.Qualifier注解的另一种用法
如果容器中有多个同类型的数据源呢?
我们再在JDBCConguation里创建一个数据源ds2
/**
* 创建一个QueryRunner对象
* @return
*/
@Bean(name = "runner")
@Scope("prototype")
public QueryRunner createQueryrunner( DataSource dataSource){
return new QueryRunner(dataSource);
}
/**
* 创建一个DataSource对象
* @return
*/
@Bean( name = "dataSource")
public DataSource createDataSource(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass(jdbcDriver);
ds.setJdbcUrl(jdbcUrl);
ds.setUser(jdbcUser);
ds.setPassword(jdbcPassword);
return ds;
} catch (Exception e) {
throw new RuntimeException();
}
}
@Bean( name = "ds2")
public DataSource createDataSource2(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/eesy02");
ds.setUser("root");
ds.setPassword("password");
return ds;
} catch (Exception e) {
throw new RuntimeException();
}
}
再在数据里创建一个数据库eesy02
mysql> create database eesy02;
Query OK, 1 row affected (0.02 sec)
mysql> use eesy02
Database changed
mysql> create table account(
-> id int primary key auto_increment,
-> name varchar(40),
-> money float
-> )character set utf8 collate utf8_general_ci;
Query OK, 0 rows affected, 2 warnings (0.04 sec)
mysql>
mysql> insert into account(name,money) values('aaa',1000);
Query OK, 1 row affected (0.01 sec)
mysql> insert into account(name,money) values('bbb',1000);
Query OK, 1 row affected (0.00 sec)
mysql> insert into account(name,money) values('ccc',1000);
Query OK, 1 row affected (0.00 sec)
此时我们运行testFindAll方法,还是能够执行成功,这是因为spring在选择数据源的时候,有两个相同类型的数据源,它首先根据bean的id去选择,而此时有个数据源的bean id就为 dataSource,用形参作为bean的id查找到了
可以从运行结果看到它选择了eesy这个数据库
["Account{id=1,name='aaa,money=1000.0}, "Account{id=4,name='ccc,money=1000.0}, "Account{id=5,name='bbb,money=1000.0}, "Account{id=8,name='ddd,money=3000.0}]
此时把bean id 为dataSource改为bean id 为 ds1,形参dataSource作为bean的id已经不能从从两个同类型数据源里找到相匹配的了
运行testFindAll会发现运行报错,报错的原因是 No qualifying bean of type 'javax.sql.DataSource' available expected single matching bean but found 2,这就是以前讲到@Autowired时遇到的错误
要解决这个问题就要用到@Qualifier注解,曾经讲过它不能脱离@Autowired单独使用,这个不能单独使用是指不能再类成员上单独使用,但是可以在方法和变量上使用,
这也就是说这里其实是暗藏了一个@Autowired的功能,一开始先按照类型注入,没有类型匹配或者有多个类型匹配,并且形参无法在多个匹配的类型中找到符合名称的id时就会报错,在这种情况下,@Qualifier注解就起到了作用.
在实际开发中,确实存在一个对象有多个实现类的情况,如果发现在参数上出现了一个@Qualifier注解,不要太惊讶,这是允许存在的
7.spring整合junit问题分析
一开始我们就说了,测试类存在大量重复字段.
我们当然可以通过把 这两行抽出来分方式来精简代码,把变量写在方法外,再用init方法再test方法执行之前为它们赋值.
(老师在讲这一段的时候,提到测试工程师只想通过定义好了accountService对象就开始测试的前提,所以不需要我们写的那个init方法,姑且把它理解为如何用spring的方式精简这段代码吧)
在删掉init方法后,为IAccountService 打上@Autowired的注解,理论上Spring会自动按类型注入,但运行起来仍然为空指针异常,也就是说没有按照类型自动注入
为什么会这样呢?
- 应用程序的入口是main方法
- Junit单元测试中 没有main方法也能执行 这是因为Junit集成了一个main方法 该方法就会判断当前测试类中哪些方法*有@Test注解 如果有@Test注解的时候,Junit就会让有@Test注解的方法执行
- Junit不会管我们是否采用Spring框架,所以也不会为我们读取配置文件/配置类,创建spring核心容器
由以上3点可知,当测试方法执行时,没有ioc容器,就算写了@Autowired注解,也无法实现注入
为什么出现了空指针异常?就是当我们执行的时候,只是让方法调用了一下,并没有创建容器,读取配置文件,所以无法注入,所以初始值是null,造成了空指针异常.
解决的思路:让它在执行的时候创建容器
spring整合junit
第一步,导入整合spring整合junit的jar包spring-text
第二步,使用Junit提供的一个注解把原有的main方法替换了,替换成spring提供的
第三步,告诉spring的运行器,spring和ioc是基于xml还是注解,并且说明位置
在pom.xml加入spring-test的依赖
org.springframework
spring-test
5.1.5.RELEASE
test
@Runwith注解
可以看到在Junit的核心runner的包下由个注解@Runwith,它就是
要替换Runner的运行器,运行器也就是带有main方法的类,接下来在Test方法上打上@Runwith注解,SpringJunitClassRunner 它继承了Junit的Runner类
/**
* 使用Junit单元测试
*/
@RunWith(SpringJUnit4ClassRunner.class)
public class AccountServiceTest {
@Autowired
private IAccountService accountService;
这个类是Spring提供的,它一定会为我们创建容器,前提是它得知道是注解还是xml的配置.
@ContextConfiguation
location:指定xml文件的位置,加上classpath关键字,表示在类路径下
classes:指定注解类所在的位置