1. Spring的常用注解
1.1. xml配置的回顾
<bean id="" class=""
scope="" init-method="init" destroy-method="destroy">
<property>
name="" value="" | ref=""
</property>
</bean>
将以上出现的标签和属性划分为4类:
- 用于创建对象:与在xml配置bean标签的作用是一样的
- 用于注入数据:与xml配置中bean标签中的property标签的作用是一样的
- 用于改变作用范围:在bean标签中使用scope属性的作用一致
- 用于生命周期使用:在bean标签中使用init_method和destroy_method的作用一致
1.2. Component注解
@Component注解(String value() default “”)
细节:如果注解中有且只有一个属性要赋值时,且名称是 value,value 在赋值是可以不写。
作用:
把资源让 spring 来管理,相当于在 xml 中配置一个 bean。
属性:
value:指定 bean 的 id,如果不指定 value 属性,默认 bean 的 id 是当前类的类名,首字母为小写。
注意:
直接在某个实现类上加Component注解,是不会被Spring执行的,因为并没有告知Spring创建容器时需要扫描的包或者类是哪些,因此需要在xml中进行配置:
<context:component-scan base-package="com.huzhen"></context:component-scan>
注意是在beans标签内配置,而不是bean约束中进行配置,在context名称空间约束中指定package之后,spring会扫描该包下及其子包中的所有类和接口中的注解:
@Component("accountService")
@Scope("singleton")
public class AccountServiceImpl implements AccountService {
...
另外,如果没有在Component注解中执行value的值,也就是容器的key,那么会使用默认key,当前类的name,并且首字母是小写,在测试类中演示:
public static void main(String[] args) {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 如果注解中没有指定value,传递进去的参数是当前需要创建对象的类名(默认首字母小写 accountServiceImpl)
// AccountService serviceDao = ac.getBean("accountServiceImpl", AccountService.class);
AccountService serviceDao = ac.getBean("accountService", AccountService.class);
System.out.println(serviceDao);
1.2. Controller Service Repository注解
他们三个注解都是Component的衍生注解,他们的作用及属性都是一模一样的,只不过是提供了更加明确的语义化。Spring框架提供的明确的三层使用的注解,使得三层对象更加清晰。
使用范围:
@Controller:一般用于表现层的注解。
@Service:一般用于业务层的注解。
@Repository:一般用于持久层的注解。
注意:
如果每个层的实现类的注解都使用Component以及三个衍生注解中任何一个注解,都是没有问题的,只是为了区分开每层之间的实体类对象
1.3. 注入数据的注解
1.3.1. Autowired
作用:
自动按照类型注入,只要容器中有唯一一个bean对象类型和待注入的变量类型匹配,即可注入。当使用注解注入属性时,set 方法可以省略。它只能注入其他 bean 类型。当有多个类型匹配时,使用要注入的对象变量名称作为 bean 的 id,在 spring 容器查找,找到了也可以注入成功。找不到就报错。
出现位置:
可以作用在类,方法也可以在某个变量上
注意:
在使用注解注入时,set方法就不是必须的了。比如说下面,对于AccountServiceImpl类中的AccountDao对象,不需要使用set方法使用注解可以进行自动注入。
如果仅使用Component注解,而不使用注入数据的注解,在调用dao接口对象时依旧报错,此时的错误是空指针异常,表示下面的accounDao对象没有找到
@Component("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountInterface accountDao1;
public AccountServiceImpl(){
System.out.println("业务层的对象创建...");
}
@Override
public void saveAccount() {
System.out.println("Service业务层中的方法... ");
accountDao1.saveAccount();
}
public void init(){
System.out.println("对象initial...");
}
public void destroy(){
System.out.println("对象destroy...");
}
}
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 如果注解中没有指定value,传递进去的参数是当前需要创建对象的类名(默认首字母小写 accountServiceImpl)
// AccountService serviceDao = ac.getBean("accountServiceImpl", AccountService.class);
AccountService serviceDao = ac.getBean("accountService", AccountService.class);
System.out.println(serviceDao);
AccountInterface accountDao = ac.getBean("accountDao", AccountInterface.class);
System.out.println(accountDao);
// 如果不添加注入数据的注解,报空指针异常
serviceDao.saveAccount();
}
在AccountDao接口对象上配上注解Autowired之后:
那么Spring是如何去寻找并且注入数据的?具体可以看下面的图示:
自动按照类型注入:此时待注入的数据类型为IAccountDao,从容器中寻找是否存在改类型的对象,发现存在那么就可以将IAccountDao对象成功注入到accountDao变量中去。相反,如果容器没有一个bean类型与待注入的变量类型匹配,会报错
如果IoC容器中存在多个类型可以匹配到,首先在类型匹配到的对象范围内(上图中的红框),其次根据变量名匹配bean对象的id也就是map中的key,如果能匹配到,则注入成功,否则报错。
为了演示多个bean对象可以匹配到的情况,定义两个dao接口的实现类AccountDaoImpl1和AccountDaoImpl2
@Repository("accountDao1")
public class AccountDaoImpl implements AccountInterface {
@Override
public void saveAccount() {
System.out.println("First Saved Account...");
}
public AccountDaoImpl(){
System.out.println("持久层的对象创建...");
}
}
@Repository("accountDao2")
public class AccountDaoImpl2 implements AccountInterface {
@Override
public void saveAccount() {
System.out.println("Second Save Account...");
}
}
测试报错的情况,当dao下存在两个impl,一个命名为accountDao1,一个命名为accountDao2,而待注入的变量名为AccountDao,此时匹配不到对象,报错
当ServiceImpl中的成员变量修改为accountDao1或者accountDao2时,此时可以匹配成功:
而修改成员变量的方法并不是很友好,那么就引入下面一个注解@Qualifier
1.3.2. Qualifier
作用:
在按照类中注入的基础上再按照名称注入。在给类成员注入数据时不能单独使用,需要与Autowired注解一起使用;在给方法参数注入数据时可以单独使用。
属性:
value指定bean的id。
在ServiceImpl类的类成员变量AccountDao中增加Qualifier注解指明bean对象的id,即可注入成功,但不能去掉Autowired注解
@Autowired
@Qualifier("accountDao1")
private AccountInterface accountDao;
1.3.3. Resource
作用:
直接按照bean的id进行注入,可以独立使用
属性:
name用于指定bean对象的id。
注意:
Resource注解不是Spring提供的,是javax.annotation下的注解,需要在pom.xml中导入javax.annotation的依赖:
<!-- https://mvnrepository.com/artifact/javax.annotation/javax.annotation-api -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
@Resource(name = "accountDao1")
private AccountInterface accountDao;
使用Resource注解直接指定bean对象的id,可以直接进行注入
以上三个注解只能注入bean类型的数据,对于基本类型和String类型无法使用以上三种注解,另外,集合类型只能通过xml配置注入
1.3.4. Value
作用:
用于注入基本类型和String类型的数据
属性:
value指定数据的值,可以使用Spring中的SpEL(Spring中的EL表达式)
SpEL的写法:${表达式}
@Value("大司马")
private String name;
@Value("35")
private Integer age;
1.4. 改变作用范围的注解----Scope
作用:
指定bean对象的作用范围
属性:
value指定范围的取值,常用取值两个:singleton单例对象;prototype多例对象(默认为singleton)
1.5. 和生命周期相关的(了解)
1.5.1. PreDestroy
用于指定销毁方法
1.5.2. PostConstructor
用于指定初始化方法
对于Scope和生命周期相关的注解演示:
@Component("accountService")
@Scope("singleton")
public class AccountServiceImpl implements AccountService {
// @Autowired
// @Qualifier("accountDao1")
@Resource(name = "accountDao1")
private AccountInterface accountDao;
@Value("大司马")
private String name;
@Value("35")
private Integer age;
public AccountServiceImpl(){
System.out.println("业务层的对象创建...");
}
@Override
public void saveAccount() {
System.out.println("Service业务层中的方法... " + "," + name + "," + age);
accountDao.saveAccount();
}
@PostConstruct
public void init(){
System.out.println("对象initial...");
}
@PreDestroy
public void destroy(){
System.out.println("对象destroy...");
}
}
测试类中输出结果:
public static void main(String[] args) {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 如果注解中没有指定value,传递进去的参数是当前需要创建对象的类名(默认首字母小写 accountServiceImpl)
// AccountService serviceDao = ac.getBean("accountServiceImpl", AccountService.class);
AccountService serviceDao = ac.getBean("accountService", AccountService.class);
AccountService serviceDao2 = ac.getBean("accountService", AccountService.class);
System.out.println(serviceDao);
System.out.println(serviceDao == serviceDao2);
// 如果不添加注入数据的注解,报空指针异常
serviceDao.saveAccount();
ac.close(); // 调用子类的对象方法,并且只能是单例对象才能被Spring管理
}
- 由于是设置为singleton为单例对象,那么创建对象是立即加载,在读取配置文件结束就会立刻创建对象
- 然后执行初始化方法,打印该对象,由于这里是单例对象,所以这里创建的两次对象其实是同一个,返回true
- 最后执行dao接口的方法,销毁方法执行。
另外,对于Scope使用prototype属性,是不会执行销毁方法的,因为多例对象和容器不相关,输出结果:
不仅会输出两次对象的创建,而且也打印出false,证明创建的是多例对象。
2. 使用xml方式和注解方式实现单表的CRUD操作
需求:使用xml方式完成单表account的CRUD操作
Account表数据如下:
2.1.1. 导入依赖
由于不涉及Spring和Mybatis的整合,所以使用工具类DBUtils来帮助执行sql语句。因此导入的依赖除了Spring之外,还包括:
- MySQL
- DBUtils
- c3p0
- JUnit(用于单元测试)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.huzhen</groupId>
<artifactId>Spring_xml_Instance</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.14</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-dbutils/commons-dbutils -->
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2.2.2. Dao层,Service层以及实体类
- 账户的持久层接口:AccountDao
package com.huzhen.dao;
import com.huzhen.domain.Account;
import java.util.List;
/**
* 账户的持久层接口
*/
public interface AccountDao {
/**
* 查询所有账户信息
* @return
*/
List<Account> findAll();
/**
* 根据id查询账户信息
*/
Account findAccountById(Integer id);
/**
* 增加账户信息
*/
void saveAccount(Account account);
/**
* 更新账户信息
*/
void updateAccount(Account account);
/**
* 根据id删除账户信息
*/
void deleteAccount(Integer id);
}
- 持久层接口的实现类:AccountDaoImpl
package com.huzhen.dao.impl;
import com.huzhen.dao.AccountDao;
import com.huzhen.domain.Account;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.SQLException;
import java.util.List;
/**
* 账户的持久层接口实现类
*/
public class AccountDaoImpl implements AccountDao {
// 使用DBUtils工具类来帮助Dao层的开发,简化操作,不需要再使用JDBC那一套冗余的工作:
// 注册驱动,获取连接,获取代理对象,代理对象执行sql,释放资源
private QueryRunner runner;
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
public List<Account> findAll() {
try {
return runner.query("select * from account", new BeanListHandler<Account>(Account.class));
} catch (SQLException e) {
throw new RuntimeException();
}
}
public Account findAccountById(Integer id) {
try {
return runner.query("select * from account where id=?", new BeanHandler<Account>(Account.class), id);
} catch (SQLException e) {
throw new RuntimeException();
}
}
public void saveAccount(Account account) {
try {
runner.update("insert into account(name, revenue) values(?,?)", account.getName(), account.getRevenue());
} catch (SQLException e) {
throw new RuntimeException();
}
}
public void updateAccount(Account account) {
try {
runner.update("update account set name=?, revenue=? where id=?", account.getName(), account.getRevenue(), account.getId());
} catch (SQLException e) {
e.printStackTrace();
}
}
public void deleteAccount(Integer id) {
try {
runner.update("delete from account where id=?", id);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
这里使用DBUtils工具类来执行SQL语句,需要定义一个QueryRunner对象,并且对象该变量的set方法,用于使用xml配置来注入数据,使用runner对象的query方法来进行查询,update方法进行增删改操作。其中第一个参数为String类型的sql语句;如果存在单个返回值,使用BeanHandler类,指定泛型反射出一个Account对象;如果存在多个返回值,使用BeanListHandler,返回一个Account类型的集合;没有返回值则忽略,对于SQL语句中需要输入值的,使用占位符来解决,并且在参数列表中加入输入值,输入值的顺序与占位符的顺序保持一致。
- 业务层接口:AccountService
package com.huzhen.service;
import com.huzhen.domain.Account;
import java.util.List;
/**
* 账户的业务层接口
*/
public interface AccountService {
/**
* 查询所有账户信息
* @return
*/
List<Account> findAll();
/**
* 根据id查询账户信息
*/
Account findAccountById(Integer id);
/**
* 增加账户信息
*/
void saveAccount(Account account);
/**
* 更新账户信息
*/
void updateAccount(Account account);
/**
* 根据id删除账户信息
*/
void deleteAccount(Integer id);
}
- 业务层接口的实现类:AccountServiceImpl
package com.huzhen.service.impl;
import com.huzhen.dao.AccountDao;
import com.huzhen.domain.Account;
import com.huzhen.service.AccountService;
import java.util.List;
/**
* 账户的业务层接口的实现类
*/
public class AccountServiceImpl implements AccountService {
// 业务层调用持久层的对象
private AccountDao accountDao;
// 由于使用xml进行配置来注入数据,需添加set方法
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public List<Account> findAll() {
return accountDao.findAll();
}
public Account findAccountById(Integer id) {
return accountDao.findAccountById(id);
}
public void saveAccount(Account account) {
accountDao.saveAccount(account);
}
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
public void deleteAccount(Integer id) {
accountDao.deleteAccount(id);
}
}
业务层实现类中需要定义一个AccountDao接口的对象,并且增加其set方法,用来给accountDao对象注入数据,来执行accountDao对象中的方法。
- 实体类Account
package com.huzhen.domain;
import java.io.Serializable;
public class Account implements Serializable {
private Integer id;
private String name;
private Double revenue;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getRevenue() {
return revenue;
}
public void setRevenue(Double revenue) {
this.revenue = revenue;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", revenue=" + revenue +
'}';
}
}
2.3.3. 配置xml
四步走逐个配置:
- 配置service对象,并且在service对象中注入dao对象
- 配置dao对象,并且在dao对象中注入runner对象
- 配置runner对象,使用构造函数进行注入dataSource对象
- 配置dataSource对象,使用set方法注入数据
<?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">
<!--配置Service对象-->
<bean id="accountService" class="com.huzhen.service.impl.AccountServiceImpl">
<!--在Service中注入dao对象-->
<property name="accountDao" ref="accountDao"></property>
</bean>
<!--配置dao对象-->
<bean id="accountDao" class="com.huzhen.dao.impl.AccountDaoImpl">
<!--在Dao中注入queryRunner对象-->
<property name="runner" ref="runner"></property>
</bean>
<!--配置queryRunner对象 为了防止多个dao使用runner对象发生线程安全问题,因此配置成多例对象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!--注入数据源 使用构造函数注入-->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
<!--配置数据源 使用c3p0来连接数据库 使用set方法注入数据-->
<bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///springIoC?serverTimezone=UTC&useSSL=false"></property>
<property name="user" value="root"></property>
<property name="password" value="*****"></property>
</bean>
</beans>
这里在配置runner对象时,为了防止多个dao对象使用runner对象引发的线程安全问题,所以使用scope属性配置成多例对象prototype
2.3.4. 单元测试
三步走:
- 获取IoC容器
- 得到业务层对象
- 执行方法
mport java.util.List;
/**
* 使用JUnit单元测试 xml配置
* 步骤:
* 1. 获取容器
* 2. 得到业务层对象
* 3. 执行方法
*/
public class accountServiceTest {
AccountService accountService = null;
@Before
public void init(){
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
accountService = ac.getBean("accountService", AccountService.class);
}
@Test
public void testFindAll(){
List<Account> list = accountService.findAll();
for(Account account : list){
System.out.println(account);
}
}
@Test
public void testFindOne(){
Account account = accountService.findAccountById(20201001);
System.out.println(account);
}
@Test
public void testSave(){
Account account = new Account();
account.setName("小超梦");
account.setRevenue(517812.00);
accountService.saveAccount(account);
}
@Test
public void testUpdate(){
Account account = accountService.findAccountById(20201006);
account.setName("胖子");
account.setRevenue(8000.00);
accountService.updateAccount(account);
}
@Test
public void testDelete(){
accountService.deleteAccount(20201006);
}
}
举findAll和insert方法的结果进行展示:
3. 使用纯注解的方式实现
3.1.1 简单注解开发
- 账户的业务层实现类
/**
* 账户的业务层接口的实现类
*/
@Service("accountService")
public class AccountServiceImpl implements AccountService {
// 业务层调用持久层的对象 由于使用注解进行注入数据,set方法可以删除掉
@Autowired
private AccountDao accountDao;
// 针对于xml防方式注入数据,需要使用set方法注入数据
// public void setAccountDao(AccountDao accountDao) {
// this.accountDao = accountDao;
// }
由于是单表的CRUD操作,不需要指明对应的变量名,使用Autowired注解即可
- 账户的持久层实现类
/**
* 账户的持久层接口实现类
*/
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
// 使用DBUtils工具类来帮助Dao层的开发,简化操作,不需要再使用JDBC那一套冗余的工作:
// 注册驱动,获取连接,获取代理对象,代理对象执行sql,释放资源
@Autowired
private QueryRunner runner;
// public void setRunner(QueryRunner runner) {
// this.runner = runner;
// }
此时就可以将xml配置中的accountService和accountDao配置删除了,但是runner和数据源的配置以及Component-scan的配置不可删除。
单元测试类中的操作与xml配置时保持一致,不需要改变。
3.2.2. 使用Spring新注解进行开发
3.2.2.1. 简单注解存在的问题思考
基于注解的 IoC 配置已经完成,但是存在一个问题:这里依然离不开 spring 的 xml 配置文件,那么能不能不写这个 bean.xml,所有配置都用注解来实现呢?当然,需要注意的是,我们选择哪种配置的原则是简化开发和配置方便,而非追求某种技术,不是为了实现而实现。
之所以离不开注解,原因是存在xml有一句很重要的配置也就是:
<context:component-scan base-package="com.huzhen"/>
component-scan用来告Spring需要扫描注解的包在哪里? 然后Spring才能帮助我们去读取配置文件,创建容器时,扫描注解,依据注解创建对象,并存入容器中
另外,对于其他的bean对象(如存在jar包的某个类中反射出来的对象):数据源和DBUtils的配置
<!--配置queryRunner对象 为了防止多个dao使用runner对象发生线程安全问题,因此配置成多例对象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!--注入数据源 使用构造函数注入-->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
<!--配置数据源 使用c3p0来连接数据库 使用set方法注入数据-->
<bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///springIoC?serverTimezone=UTC&useSSL=false"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</bean>
如何去掉这两块配置,需要使用到另外两个Spring中的新注解:@Configuration和@ComponentScan
3.2.2.2. Configuration注解
作用:
指定当前类是一个配置类,当创建容器时会从该类上加载注解,获取容器时需要使用AnnotationApplicationContext(有@Configuration 注解的类.class);属性value用于指定配置类的字节码
属性:
value用于指定配置类的字节码
细节:
当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。但是存在多个配置类的情况下,并且在获取容器时在AnnotationConfigApplicationContext方法中只传入了某一个配置类的字节码,那么需要在其他配置类上写上注解Configuration,否则Spring将不能加载这些配置类。
3.2.2.3. ComponentScan注解
作用:
通过注解来指定创建容器时需要扫描的包,起到的作用与xml中:
<context:component-scan base-package="com.huzhen"/>
是一样的
属性:
- basePackages
- value
两者的作用一致,都是指定需要扫描的包名
@Configuration()
@ComponentScan("com.huzhen")
public class SpringConfiguration {
3.2.2.4. Bean注解
作用:
只能作用在方法上,用于将当前方法的返回值作为bean对象存到spring的IoC容器中。
属性:
name用于指定当前bean对象的id,当不指定name时,默认值为当前方法名作为key,返回的bean对象则为value
观察数据源获取runner对象的配置可以发现,其中的class传递进去的是QueryRunner的全限定类名,然后Spring通过反射创建一个runner对象存到IoC容器中,那么可以在配置中定义一个方法,使用构造函数进行创建runner对象,根据DataSource来获取一个runner对象。如下图:
此时两者得到的对象一样的,通过构造函数new出来的runner对象仅仅是一个独立的对象,而配置文件创建的对象是存在容器中的,因此需要使用@Bean注解来使得runner对象存入到容器中。
细节:
当使用注解去配置方法时,如果某个方法有输入参数,spring会去容器中查找是否存在可用的bean对象,查找的方式和Autowired注解是一样的。
在没有获取DataSource对象的方法时,这里的形参ds会报错,表明在IoC容器中不存在DataSource对象,所以我们必须再定义一个获取DataSource对象的方法
/**
* 创建数据源对象
* @return dataSource
*/
@Bean("ds")
public DataSource createDataSource(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass("com.mysql.cj.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql:///springIoC?serverTimezone=UTC&useSSL=false");
ds.setUser("root");
ds.setPassword("123456");
} catch (Exception e) {
throw new RuntimeException(e);
}
return ds;
}
此时不再需要xml配置文件中了,然后在单元测试类中需要读取这个配置类中的数据源和DBUtils的配置,使用ApplicationContext接口的另一个实现类AnnotationConfigApplicationContext,通过源码可以知道传入的参数为配置类SpringConfiguration的字节码
public class accountServiceTest {
AccountService accountService = null;
@Before
public void init(){
// ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
accountService = ac.getBean("accountService", AccountService.class);
}
@Test
public void testFindAll(){
List<Account> list = accountService.findAll();
for(Account account : list){
System.out.println(account);
}
}
3.2.2.5. Import
作用:
用于导入其他配置类,在引入其他配置类时,可以不用再写@Configuration 注解
属性:
value指定其他配置类的字节码,当使用Import注解之后,带有Import注解的类即为父配置类,而导入其他的均为子配置类
3.2.2.6. PropertySource
作用:
用于加载.properties 文件中的配置。例如在配置数据源时,可以把连接数据库的信息写到properties 配置文件中,就可以使用此注解指定 properties 配置文件的位置
属性:
value用于指定properties文件的路径。如果是在类路径下,需要在文件名之前加上classpath:
以上两个注解联合演示:
- jdbcConfig配置类
@SuppressWarnings("all")
public class JdbcConfig {
// 使用value注解注入数据
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 根据QueryRunner的构造函数创建一个runner对象
* 如果Bean注解中不指定name,则将方法名作为key,对象作为value保存到IoC容器中
* @param ds
* @return QueryRunner
*/
@Bean(name = "runner")
@Scope("prototype")
public QueryRunner createRunner(@Qualifier("ds1") DataSource ds){
return new QueryRunner(ds);
}
/**
* 创建数据源对象
* @return dataSource
*/
@Bean("ds1")
public DataSource createDataSource1(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
} catch (Exception e) {
throw new RuntimeException(e);
}
return ds;
}
/**
* 当匹配相同数据类型的bean对象,会根据bean的id去寻找,解决方法有:
* 1. 将输入参数的变量名修改成某一个bean对象的key
* 2. 对于Qualifier注解,对于类成员变量不能单独使用,但是对于方法可以直接使用,在形参之前使用Qualifier注解
* 并且传入对应的bean对象的key
*/
@Bean("ds2")
public DataSource createDataSource2(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
} catch (Exception e) {
throw new RuntimeException(e);
}
return ds;
}
}
- 主配置类SpringConfiguration
//@Configuration()
@ComponentScan("com.huzhen")
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {
}
- 由于在测试类中已经将主配置类的字节码读入,所以在SpringConfiguration类上的@Configuration可以省略
- 可以在获取容器时读取多个配置类,那么需要传入多个配置类的字节码,如下:
ApplicationContext ac = new AnnotationConfigApplicationContext(配置类1.class, 配置类2.class,...)
也可以在主配置类上使用@Import来指明其他配置类的字节码,就不再需要传入多个配置类的字节码,也不需要在其他子配置类上标注@Configuration
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql:///SpringIoC?serverTimezone=UTC&useSSL=false
jdbc.username=root
jdbc.password=******
- 使用外部配置文件来传入连接数据源的信息,在主配置类上使用==@PropertySource并指明properties的文件位置==,在子配置类中定义4个类成员变量,并且通过@Value注解来注入数据,注意此时需要使用SpEL表达式也就是==${}==来传递参数,其中的参数即为外部文件properties中的key
- 对于容器存在多个bean对象与某个方法的输入参数的数据类型匹配,那么此时根据该方法的形参名来进行匹配,如果存在,则匹配成功,否则报错。
- 可以将形参名修改成某个需要的bean对象的key
- 由于Qualifier注解是可以在方法和参数上单独使用的,因此可以使用@Qualifier注解来指明此时需要传入的bean对象具体是哪一个(Qualifier中传入的是bean对象的key)
4. Spring和Junit整合
4.1. 问题引入
在测试类中,每个测试方法都有以下两行代码:
ApplicationContext ac = **new** ClassPathXmlApplicationContext("bean.xml");
IAccountService as = ac.getBean("accountService",IAccountService.**class**);
这两行代码的作用是获取容器,如果不写的话,直接会提示空指针异常。所以又不能轻易删掉。即使使用Junit的Before注解将其定义到init函数中,依旧是不合理的,因为测试人员并不需要关心容器是如何获取以及如何获取对象的。
分析:
- 应用程序的入口是main方法,
- JUnit单元测试中没有main方法也能执行,因为JUnit继集成了一个main方法,该方法会判断当前测试类中哪些方法有==@Test注解==,JUnit会让带有JUnit注解的方法执行。
- JUnit不会管是否采用了什么框架,在执行测试时,JUint也就不会去读取配置文件或者配置类去获取容器,来创建对象
- 因此,当测试方法执行时,根本没有IoC容器,即使在类变量上写上Autowired注解,也无法实现注入
4.2. 解决方法
junit 给我们暴露了一个注解,可以让我们替换掉它的运行器。这时,需要依靠 spring 框架,因为它提供了一个运行器,可以读取配置文件(或注解)来创建容器。我们只需要告诉它配置文件在哪就行了。
- 导入Spring整合JUnit的jar包
- 使用JUnit提供的注解==@RunWith将原有的运行器(main)替换成Spring提供的SpringJUnit4ClassRunner==
阅读源码,发现RunWith接口传入的是一个字节码:
将SpringJUnit4ClassRunner运行器的字节码传入,因为是Spring提供的运行器,才能主动去获取容器
- 告知Spring的运行器,Spring的IoC创建是基于xml(告知xml的类路径)还是基于注解(告知配置类的字节码),使用==@ContextConfiguration==来指明。通过阅读ContextConfiguration的源码:
可以发现,如果基于xml配置的,使用locations来指定xml的位置:classpath:类路径
如果基于注解的,使用value来指定配置类的字节码:xxxx.class
另外,当使用Spring 5.x版本时,要求JUnit的jar包必须是4.12及以上,不然会报错
JUnit单元测试:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class accountServiceTest {
@Autowired
AccountService accountService = null;
@Test
public void testFindAll(){
List<Account> list = accountService.findAll();
for(Account account : list){
System.out.println(account);
}
}
@Test
public void testFindOne(){
Account account = accountService.findAccountById(20201001);
System.out.println(account);
}
@Test
public void testSave(){
Account account = new Account();
account.setName("我兄弟");
account.setRevenue(1000.00);
accountService.saveAccount(account);
}
@Test
public void testUpdate(){
Account account = accountService.findAccountById(20201006);
account.setName("胖子");
account.setRevenue(8000.00);
accountService.updateAccount(account);
}
@Test
public void testDelete(){
accountService.deleteAccount(20201006);
}
}
使用@RunWith来使用Spring提供的运行器,才能让Spring去获取容器;
使用@ContextConfiguration来告知Spring的配置文件,才能告知Spring去找xml配置文件还是去找配置类
最后只需要在AccountService接口实现类的对象上加上@Autowired,Spring才能完成自动注入数据
输出结果完全没有问题,合理完成spring和JUnit的整合。