Spring学习笔记第二天,Spring基于xml的IOC和注解的IOC以及IOC的案例


经过了第一天的学习,我们对spring框架应该有了一定的了解。对于使用xml配置的方式也应该会使用了。今天我们就来学习spring基于注解的IOC以及IOC的案例。

1 Spring中IOC的常用注解

在我们上一天的学习中我们使用的是xml对ioc进行配置,配置的方式如下

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl" scope="" init-method="" destroy-method="">
    <property name="" value="" ref=""></property>
</bean>

今天学习的注解配置IOC主要有四种注解

1. 用于创建对象的注解
他们的作用和在xml中配置文件中编写一个 标签实现的功能是一样的。
2. 用于注入数据的注解
他们的作用就和在xml配置文件中 标签中写一个标签的作用是一样的。
3. 用户改变作用范围的注解

	他们的作用就和在bean标签中使用scope属性实现的功能是一样的

4. 和生命周期相关的注解

	他们的作用就和在bean标签中使用 init-method 标签和 destroy-method 标签的作用是一样的

下面我们就来分别演示一下各种注解的使用,我们在原来的工程基础上新建一个Module命名为day02_01anno_ioc,然后我们把上一天里创建的Module名为 day01_03spring里src/main/里的所有内容拷贝到我们新建的Module的src/main里。我们新建的Module结构如下
在这里插入图片描述

1.1 用于创建对象的注解

他们的作用和在xml中配置文件中编写一个 标签实现的功能是一样的。

Component:当我们要创建的对象不属于三层架构时,我们就可以使用这个注解。
	作用:用于把当前类对象存入到spring容器中
	属性:value:用于指定bean的id,。当我们不写时,它的默认值时当前类名,且首字母改小写

Controller:一般用在表现层
Service:一般用于业务层
Repository:一般用于持久层

以上三个注解Controller、Service、Repository他们的作用和属性与Component是一模一样的。他们三个是Spring框架为我们提供明确的三层架构使用的注解,使我们的三层对象更加清晰

然后我们来修改一下 AccountServiceImpl.java

/*
 * 账户的业务层实现类
 * 用于创建对象的注解
 *      他们的作用和在xml中配置文件中编写一个 <bean></bean> 标签实现的功能是一样的
 *      @Component
 *          作用:用于把当前类对象存入到spring容器中
 *          属性:
 *              value:用于指定bean的id,。当我们不写时,它的默认值时当前类名,且首字母改小写
 * 用于注入数据的
 *      他们的作用就和在xml配置文件中 <bean>标签中写一个<property>标签的作用是一样的
 * 用户改变作用范围的
 *      他们的作用就和在bean标签中使用scope属性实现的功能是一样的
 * 和生命周期相关的
 *      他们的作用就和在bean标签中使用 init-method 标签和 destroy-method 标签的作用是一样的
 */
@Component
public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao = new AccountDaoImpl();
    public AccountServiceImpl() {
        System.out.println("对象被创建了...");
    }
    public void saveAccount() {
        accountDao.saveAccount();
    }
}

现在还不能实现数据的注入,还需要修改bean.xml,因为告知spring在创建容器时要扫描的包,配置所需要的标签不是在beans的约束中,而是一个名为context名称空间和约束中。首先我们登录spring的官网,找到spring官方说明文档的网页,然后找到对应spring框架版本的说明文档,我这里使用的5.0.2 的版本该版本的说明文档地址为 https://docs.spring.io/spring/docs/5.0.2.RELEASE/spring-framework-reference/core.html#spring-core 打开该网页,然后Ctrl + F 搜索 “xmlns:context” 找到含有 “context” 的命名空间和约束,如下
在这里插入图片描述
将其复制到我们的bean.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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <!--告知spring在创建容器时要扫描的包,配置所需要的标签不是在beans的约束中,而是一个名为context 名称空间和约束中。-->
	<!-- 告知spring在创建容器时要扫描的包 -->
    <context:component-scan base-package="com.itheima"></context:component-scan>
</beans>

然后我们在修改一下Client.java里的main方法,

public class Client {
    public static void main(String[] args) {
        //1. 获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//        ApplicationContext ac = new AnnotationConfigApplicationContext();

        //2.根据id获取bean对象
        IAccountService as = (IAccountService) ac.getBean("accountServiceImpl");
        System.out.println(as);
//        System.out.println(as);
    }
}

注意我们在 AccountServiceImpl 类上使用的 Component 注解且使用的是默认值,所以我们在获取对象的时候要使用这个类的名称并且把首字母改成小写accountServiceImpl,下面我们来运行一下main方法,控制台输出如下,说明数据注入成功。
在这里插入图片描述
下面我们来修改一下 AccountServiceImpl .java,我们不使用 Component 注解的默认值,

//@Component(value = "accountService")
@Component("accountService") //当注解中我们只用一个value属性,那这个value是可以不写的,如果同时有两个活以上的属性赋值就必须要写
public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;

    public AccountServiceImpl() {
        System.out.println("对象被创建了...");
    }

    public void saveAccount() {
        accountDao.saveAccount();
    }
}

注意:
当注解中我们只使用一个value属性,那这个value是可以不写的,如果同时有两个或者以上的属性赋值就必须要写。
然后这个Client.java里的main方法也要改一下,因为这个 AccountServiceImpl 类上的 Component 注解不在使用默认值了,这个Client.java修改如下

public class Client {

    public static void main(String[] args) {
        //1. 获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//        ApplicationContext ac = new AnnotationConfigApplicationContext();
        //2.根据id获取bean对象
//        IAccountService as = (IAccountService) ac.getBean("accountServiceImpl");
        IAccountService as = (IAccountService) ac.getBean("accountService");
        System.out.println(as);
    }
}

运行一下main方法,控制台输出如下,说明数据注入成功。
在这里插入图片描述
因为 Controller、Service、Repository这个三个注解和Component注解的作用和属性是一模一样的,所以,这个四个注解可以相互替换。比如我们先=现在把 AccountServiceImpl的注解换成 Controller 注解。其他的不用修改。

//@Controller(value = "accountService")
@Controller("accountService")
public class AccountServiceImpl implements IAccountService {
	......
}

然后我们来运行一下Client.java里的main方法,控制台输出如下,可以看出这样也是可以正常运行的。
在这里插入图片描述
注意 虽然这四个注解的作用个属性都是一样的,但是为了代码的已读我们在写代码的时候最好还是根据三层架构来使用不同的注解
现在我们把 AccountServiceImpl 的注解改成 Service 注解,AccountDaoImpl 的注解改成 Repository。然后修改一下Client.java里的main方法。

public class Client {
    public static void main(String[] args) {
        //1. 获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//        ApplicationContext ac = new AnnotationConfigApplicationContext();
        //2.根据id获取bean对象
//        IAccountService as = (IAccountService) ac.getBean("accountServiceImpl");
        IAccountService as = (IAccountService) ac.getBean("accountService");
        System.out.println(as);

        IAccountDao adao = ac.getBean("accountDao", IAccountDao.class);
        System.out.println(adao);
    }
}

运行一下main,控制台输出如下
在这里插入图片描述

1.2 用于注入数据的注解

他们的作用就和在xml配置文件中 标签中写一个标签的作用是一样的。

Autowired:
	作用:
		自动按照类型注入.只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。
		如果ioc容器中没有任何bean的类型和要注入的变量类型匹配,则报错。
		如果ioc容器中有多个类型匹配时:
			spring会把变量名当做id去容器中查找有没有对应类型并且id为变量名的对象,如果找到,就直接注入,否则就报错。
		出现位置:
			可以是成员变量上,也可以是方法上
		细节:
    		使用注解注入时,set方法就不是必须的了。
Qualifier:
	作用:在按照类型注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以(稍后讲)
	属性:
    	value:用于指定注入bean的id。
Resource
	作用:直接按照bean的id注入。它可以独立使用
	属性:
    	name:用于指定bean的id

以上三个注解Autowired、Qualifier、Resource都只能注入其他bean类型的数据,而基本数据类型和String类型无法使用上述注解实现。另外,集合类型的注入只能通过xml来实现

1.2.1 Autowired

下面就来演示一下,我们首先修改一下 AccountServiceImpl.java

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired // 自动按照类型注入.只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。
    private IAccountDao accountDao = null;
    public AccountServiceImpl() {
        System.out.println("对象被创建了...");
    }
    public void saveAccount() {
        accountDao.saveAccount();
    }
}

然后修改一下Client.java

public class Client {
    public static void main(String[] args) {
        //1. 获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//        ApplicationContext ac = new AnnotationConfigApplicationContext();
        //2.根据id获取bean对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        as.saveAccount();
    }
}

运行一下main方法,控制台输出如下,我们发现 dao里的saveAccount方法被正常调用了,
在这里插入图片描述
在AccountServiceImpl里是怎么给accountDao进行注入的呢,这里我们使用的 Autolifier 注解,框架首先在容器中查找 IAccountDao 这个类型的bean对象,如果容器中只有一个就直接注入成功。如果容器中有多个 IAccountDao 类型的数据呢?我在com.itheima.dao.impl包里在创建一个 AccountDaoImpl2.java。

@Repository("accountDao2")
public class AccountDaoImpl2 implements IAccountDao {
    public void saveAccount() {
        System.out.println("保存了账户222");
    }
}

然后我们把 AccountDaoImpl 的注解的属性改成 accountDao1

@Repository("accountDao1")
public class AccountDaoImpl implements IAccountDao {
    public void saveAccount() {
        System.out.println("保存了账户111");
    }
}

我们在次运行main方法,发现就会报错了,因为现在ioc容器中有多个 IAccountDao 这个类型的对象时Spring框架不知道要使用是哪一个进行数据注入,但是我们把变量 accountDao 的名字改成accountDao1之后重新运行main方法,控制台输出如下,可以看出,数据注入成功了,但是为什么注入的是 AccountDaoImpl 这个类型的对象呢?
在这里插入图片描述
我们尝试把变量名改成accountDao2之后再一次运行main方法,控制台输出如下,
在这里插入图片描述
从这上面的现象可以看出ioc容器中有多个 IAccountDao 这个类型的对象时,Spring框架会在用变量名做为id来查询IAccountDao 这个类型的对象进行数据注入。

1.2.2 Qualifier

如果不想根据变量名称来进行对属性的数据注入怎么办呢?这时我们可以使用 Qualifier 注解,Qualifier注解的作用是在按照类型注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用要结合Autowired一起使用。但是在给方法参数注入时可以(稍后讲留个悬念)。属性 value:用于指定注入bean的id。
修改一下AccountServiceImpl.java

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired
    @Qualifier("accountDao1") // 这个就是要注入bean对象的id
    private IAccountDao accountDao = null;
    public AccountServiceImpl() {
        System.out.println("对象被创建了...");
    }
    public void saveAccount() {
        accountDao.saveAccount();
    }
}

这样在数据注入的时候就会按照 Qualifier 注解的value属性值来进行数据注入。

1.2.3 Resource

上面的两种对属性的数据注入都有点麻烦,有没有在简单的办法呢?答案是有的,就是使用 Resource 注解。Resource 注解的作用:直接按照bean的id注入。它可以独立使用属性:name:用于指定bean的id。修改一下AccountServiceImpl.java

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    // @Autowired
    // @Qualifier("accountDao1")
	@Resource(name = "accountDao1") // name 用于指定bean的id
    private IAccountDao accountDao = null;
    public AccountServiceImpl() {
        System.out.println("对象被创建了...");
    }
    public void saveAccount() {
        accountDao.saveAccount();
    }
}

然后我们运行一下main方法,从控制台可以看出这样也是可以的。

以上三个注解都只能注入其他bean类型的数据,而基本数据类型和String类型无法使用上述注解实现。另外,集合类型的注入只能通过xml来实现。

1.3 用户改变作用范围的

他们的作用就和使用xml配置中的bean标签中使用scope属性实现的功能是一样的

Scope
    作用:指定bean的作用范围
    属性:value:指定范围的取值,常用取值:singleton(对应单例)默认值、prototype(对应多例)

下面我们来演示一下,我们修改一下 AccountServiceImpl。java

@Service("accountService")
@Scope("prototype")// singleton 是默认值
public class AccountServiceImpl implements IAccountService {
    // @Autowired
    // @Qualifier("accountDao1")
    @Resource(name = "accountDao1")
    private IAccountDao accountDao2 = null;

    public AccountServiceImpl() {
        System.out.println("对象被创建了...");
    }
    public void saveAccount() {
        accountDao2.saveAccount();
    }
}

然后在修改一下Client.java里的main方法

public class Client {
    public static void main(String[] args) {
        //1. 获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        IAccountService as2 = ac.getBean("accountService", IAccountService.class);

        System.out.println(as == as2);
    }
}

运行一下main方法,控制台输出如下,说明 as和as2是两个对象。
在这里插入图片描述

1.4 和生命周期相关的(了解)

他们的作用就和使用xml配置中在bean标签中使用 init-method 标签和 destroy-method 标签的作用是一样的。

PreDestroy
    作用:用于指定销毁方法
PostConstruct
    作用:用于指定初始化方法

下面我们来演示一下,修改一下 AccountServiceImpl.java

@Service("accountService")
@Scope("singleton") // 这里使用的是单例,因为当使用多例时,spring不会主动去销毁这个对象,是有jvm的gc来管理
public class AccountServiceImpl implements IAccountService {
    // @Autowired
    // @Qualifier("accountDao1")
    @Resource(name = "accountDao1")
    private IAccountDao accountDao2 = null;
    public AccountServiceImpl() {
        System.out.println("对象被创建了...");
    }
    @PostConstruct
    public void init() {
        System.out.println("AccountServiceImpl init ...");
    }
    @PreDestroy
    public void destroy() {
        System.out.println("AccountServiceImpl destroy ...");
    }
    public void saveAccount() {
        accountDao2.saveAccount();
    }
}

然后在修改一下Client.java里的main方法

public class Client {
    public static void main(String[] args) {
        //1. 获取核心容器对象
        // ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");

        //2.根据id获取bean对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        as.saveAccount();

        ac.close();
    }
}

然后运行mian方法,控制台输出如下。
在这里插入图片描述
注意在main方法里,变量 ac 的类型,不能是 ApplicationContext,因为后面的调用 ac 的 close()没有定义在接口 ApplicationContext 中是定义在子类ClassPathXmlApplicationContext中,父类引用对象只能调用父类方法。所以这里把ac的类型改成了 ClassPathXmlApplicationContext 。

2 案例使用xml方式和注解的方式实现单表的CRUD操作

2.1 使用xml方式实现单表的CRUD操作

经过上面的学习,现在我们使用spring框架来做一个小案例,使用junit来测试 AccountService 里的方法。我们重新创建一个Module命名为day02_02account_xml_ioc。在这个案例中我们使用xml配置的方式来配置ioc。
Module建成之后的结构
在这里插入图片描述
首先修改pom.xml文件

<?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.itheima</groupId>
    <artifactId>day02_02account_xml_ioc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <!--使用dbutils来进行持久化-->
        <dependency>
            <groupId>commons-dbutils</groupId>
            <artifactId>commons-dbutils</artifactId>
            <version>1.4</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <!--jdbc使用c3p0-->
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

创建domain类Account.java

public class Account implements Serializable {
    private Integer id;
    private String name;
    private Float money;

    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 Float getMoney() {
        return money;
    }
    public void setMoney(Float money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

创建dao接口IAccountDao.java

/**
 * 账户的持久层接口
 */
public interface IAccountDao {
    /**
     * 查询所有的Account
     * @return
     */
    List<Account> findAllAccount();
    /**
     * 根据id查询Account
     * @param accountId
     * @return
     */
    Account findAccountById(Integer accountId);
    /**
     * 保存Account
     * @param account
     */
    void saveAccount(Account account);
    /**
     * 更新Account
     * @param account
     */
    void updateAccount(Account account);
    /**
     * 删除
     * @param accountId
     */
    void deleteAccount(Integer accountId);
}

然后创建dao的实现类AccountDaoImpl.java

/**
 * 账户的持久层实现类
 */
public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;
    public void setRunner(QueryRunner runner) { //用于数据的注入
        this.runner = runner;
    }
    
    public List<Account> findAllAccount() {
        try {
            return runner.query("select * from account;", new BeanListHandler<Account>(Account.class));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    public Account findAccountById(Integer accountId) {
        try {
            return runner.query("select * from account where id = ?;", new BeanHandler<Account>(Account.class), accountId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void saveAccount(Account account) {
        try {
            runner.update("insert into account (name, money) values(?,?)", account.getName(), account.getMoney());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void updateAccount(Account account) {
        try {
            runner.update("update account set name = ?, money = ? where id = ?;", account.getName(), account.getMoney(), account.getId());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void deleteAccount(Integer accountId) {
        try {
            runner.update("delete from account where id = ?;", accountId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

创建 Service接口IAccountService.java

/**
 * 账户业务层接口
 */
public interface IAccountService {
    /**
     * 查询所有的Account
     * @return
     */
    List<Account> findAllAccount();
    /**
     * 根据id查询Account
     * @param accountId
     * @return
     */
    Account findAccountById(Integer accountId);
    /**
     * 保存Account
     * @param account
     */
    void saveAccount(Account account);
    /**
     * 更新Account
     * @param account
     */
    void updateAccount(Account account);
    /**
     * 删除
     * @param accountId
     */
    void deleteAccount(Integer accountId);
}

创建service接口的实现类AccountServiceImpl.java

/**
 * 账户的业务层实现类
 */
public class AccountServiceImpl implements IAccountService {

    private IAccountDao accountDao;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAllAccount() {
        return this.accountDao.findAllAccount();
    }

    public Account findAccountById(Integer accountId) {
        return this.accountDao.findAccountById(accountId);
    }

    public void saveAccount(Account account) {
        this.accountDao.saveAccount(account);
    }

    public void updateAccount(Account account) {
        this.accountDao.updateAccount(account);
    }

    public void deleteAccount(Integer accountId) {
        this.accountDao.deleteAccount(accountId);
    }
}

然后创建spring的配置文件bean.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置Service -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <!-- 使用set方法注入 dao对象-->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!-- 使用set方法注入 QueryRunner -->
        <property name="runner" ref="runner"></property>
    </bean>

    <!--配置 QueryRunner 对象,因为 bean 对象 默认是单例对象,所以有可能发生线程安全问题。所以让scope设置为prototype-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!--配置数据源,使用c3p0的数据库连接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入链接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
</beans>

最后创建测试类 AccountServiceTest.java

/**
 * 使用Junit单元测试,测试我们的配置
 */
public class AccountServiceTest {
    // private ApplicationContext ac;
    private ClassPathXmlApplicationContext ac;
    IAccountService as;

    @Before
    public void init() {
        //1. 获取核心容器对象
        ac = new ClassPathXmlApplicationContext("bean.xml");
        as = ac.getBean("accountService", IAccountService.class);
    }

    @After
    public void destroy() {
        ac.close();
    }

    @Test
    public void testFindAll() {
        List<Account> accounts = as.findAllAccount();
        for (Account account : accounts) {
            System.out.println(account);
        }
    }

    @Test
    public void testFindOne() {
        Account account = as.findAccountById(1);
        System.out.println(account);
    }

    @Test
    public void testSave() {
        Account account = new Account();
        account.setName("test");
        account.setMoney(4000f);
        as.saveAccount(account);
    }

    @Test
    public void testUpdate() {
        Account account = as.findAccountById(4);
        account.setName("test_update");
        as.updateAccount(account);
    }

    @Test
    public void testDelete() {
        as.deleteAccount(4);
    }
}

这些测试方法这里就不一个个运行了,感兴趣的同学可以自行运行一下。经过这个案例的练习对第一天的学习的内容怎么使用应该有了大致的了解。

2.2 使用注解方式实现单表的CRUD操作

我们重新创建一个Module命名为day02_03account_anno_ioc,然后修改pom.xml文件,文件内容参考上一个Module里的pom.xml。这个Module和上一个Module的差别不大我们就复制一下上一个Module的文件,我们把上一个Module里src/main里的所有的文件都拷贝到,我们新建的这个Module里src/main里。下面我们做一下修改。
修改AccountDaoImpl.java

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired //ioc容器中只有一个QueryRunner 对象,所以使用这个注解就行
    private QueryRunner runner;
	//删除掉 setRunner 方法
	...
}

修改AccountServiceImpl.java

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired// 因为ioc容器中只有一个 IAccountDao 对象,所以这里使用只这个注解也可以
	// @Resource(name = "accountDao") //使用这个注解也可以哦
    private IAccountDao accountDao;

}

然后修改bean.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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 告知spring在创建容器时要扫描的包 -->
    <context:component-scan base-package="com.itheima"></context:component-scan>

	<!-- 下面的这些内容暂时不能删除,因为这些都是引用的其他包里的内容,暂时没法使用注解进行配置 -->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="ds"></constructor-arg>
    </bean>

    <bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="user" value="root"/>
        <property name="password" value="123456"/>
    </bean>
</beans>

然后我们运行各自的测试方法,通过查看控制台的输出,可以发现,可以正常运行。但是刚刚也有注意的问题就是在bean.xml中使用其他包里的内容无法使用的注解的问题所以暂时这个bean.xml配置文件还不能删除。下面就来解决它。

3 改造基于注解的IOC案例,使用纯注解的方式实现

首先我们再创建一个新的Moduel命名为day02_04account_ioc_withoutXml,由于这个Module和上一个Moduel的结构变化不大,我们还是直接拷贝上一个Module(day02_03account_anno_ioc)里src/main里的所有文件复制到我们新建的Module的src/main里,然后修改pom.xml文件参考上一个Module里的。最后这个Module的结构如下
在这里插入图片描述
首先我们的目的是删除掉bean.xml后项目仍然可以正常运行,怎么办呢?下面spring的新注解就出场了。

spring中的新注解
Configuration
	作用:指定当前类是一个配置类
	细节:当配置类做为 AnnotationConfigurationContext对象创建的参数时,该注解可以不写。
ComponentScan
	作用:用于通过注解指定spring在创建容器时要扫描的包
	属性:
		value:它和 basePackages 的作用是一样的,都是用于指定创建容器时要扫描的包我们使用此注解就等同于在xml中配置了
                 <context:component-scan base-package="com.itheima"></context:component-scan>
Bean
	作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
	属性:用于指定bean的id。当不写时,默认值就是当前方法的名称
	细节:当使用注解配置方法方法时,如果方法有参数,spring框架会去容器中查找有没有可用的Bean对象,查找的方式和Autowired注解的方式是一样的。
Import
	作用:用于导入其他的配置类
	属性:
		value:用于指定其他配置类的字节码。
           	   当我们使用Import的注解之后,有Import修饰的类就是主配置类或者叫父配置类,其他的配置类叫配置类。
PropertySource
	作用:用于指定properties文件的位置
	属性:
		value:指定properties配置文件的名称和路径, 关键字:classPath,表示类路径下。不写 classpath: 也可以但是jdbcConfig.properties要在resources中不能有二级目录

创建SpringConfiguration.java,这个类就相当于我们使用xml配置时的bean.xml。

/*
 * 该类是一个配置类,它的作用就和bean.xml的作用是一样的
 * spring中的新注解
 * Configuration
 *      作用:指定当前类是一个配置类
 *      细节:当配置类做为 AnnotationConfigurationContext对象创建的参数时,该注解可以不写。
 * ComponentScan
 *      作用:用于通过注解指定spring在创建容器时要扫描的包
 *      属性:
 *          value:它和 basePackages 的作用是一样的,都是用于指定创建容器时要扫描的包
 *                 我们使用此注解就等同于在xml中配置了
 *                      <context:component-scan base-package="com.itheima"></context:component-scan>
 * Bean
 *      作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
 *      属性:用于指定bean的id。当不写时,默认值就是当前方法的名称
 *      细节:当使用注解配置方法方法时,如果方法有参数,spring框架会去容器中查找有没有可用的Bean对象,
 *           查找的方式和Autowired注解的方式是一样的。
 *
 * Import
 *      作用:用于导入其他的配置类
 *      属性:
 *          value:用于指定其他配置类的字节码。
 *                当我们使用Import的注解之后,有Import修饰的类就是主配置类或者叫父配置类,其他的配置类叫配置类。
 */
@Configuration
//@ComponentScan(value = "com.itheima")
//@ComponentScan(basePackages = "com.itheima")
@ComponentScan("com.itheima")
public class SpringConfiguration {
    /**
     * 用于创建一个 QueryRunner 对象
     * @param dataSource
     * @return
     */
    @Bean(name = "runner")
    public QueryRunner createQueryRunner(DataSource dataSource) {
        return new QueryRunner(dataSource);
    }

    /**
     * 创建数据源对象
     * @return
     */
    @Bean(name = "dataSource")
    public DataSource createDataSource() {
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource("");
            ds.setDriverClass("com.mysql.jdbc.Driver");
            ds.setJdbcUrl("jdbc:mysql://localhost:3306/spring");
            ds.setUser("root");
            ds.setPassword("123456");
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

然后我们修改一下 AccountServiceTest.java。注意 ApplicationContext的子类 AnnotationConfigApplicationContext 的一个构造参数为一个 Class 类型,但是这个Class的类要用 Configuration 注解修饰

/**
 * 使用Junit单元测试,测试我们的配置
 */
public class AccountServiceTest {
    // private ApplicationContext ac;
    // private ClassPathXmlApplicationContext ac;
    private AnnotationConfigApplicationContext ac;
    IAccountService as;

    @Before
    public void init() {
        //1. 获取核心容器对象
//        ac = new ClassPathXmlApplicationContext("bean.xml");
        ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); // 参数是用 Configuration 注解修饰的类的 class
        as = ac.getBean("accountService", IAccountService.class);
    }
    ...

然后就可以删除bean.xml了,删除之后我们运行一下测试类 AccountServiceTest 里的测试方法是可以正常运行的。
还有一个问题需要注意,就是ioc容器里的bean对象在不配置Scope的情况下都是单例,我们在练习使用xml配置的时候说过了,runner是单例的可能会有线程安全问题。下面我们来测试下runner这个对象是不是单例的。在测试文件夹里的com.itheima.test包下新建一个测试类 QueryRunnerTest.java

public class QueryRunnerTest {
    @Test
    public void testQueryRunner() {
        //1.获取ioc容器
        ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        //2.获取QueryRunner对象
        QueryRunner runner1 = ac.getBean("runner", QueryRunner.class);
        QueryRunner runner2 = ac.getBean("runner", QueryRunner.class);
        System.out.println(runner1 == runner2);
    }
}

我们运行下这个测试方法控制台输出为 true,说明这是单例的,我们怎么解决呢,你是否还记得 Scope这个注解,下面我们就修改一下SpringConfiguration.java

@Configuration
@ComponentScan("com.itheima")
public class SpringConfiguration {
    /**
     * 用于创建一个 QueryRunner 对象
     * @param dataSource
     * @return
     */
    @Bean(name = "runner")
    @Scope("prototype") // 让runner对象多例
    public QueryRunner createQueryRunner(DataSource dataSource) {
        return new QueryRunner(dataSource);
    }
    ...
}

然后我们在运行一下QueryRunnerTest 里的测试方法testQueryRunner,控制台输出就是false了。
下面说一下 新注解的细节问题。
首先我们尝试一下把 SpringConfiguration 这个配置类的 Configuration 注解删除掉,然后运行一下 AccountServiceTest测试类中的测试方法,发现仍然可以正常运行,为什么呢?答案在这里

/**
 * 使用Junit单元测试,测试我们的配置
 */
public class AccountServiceTest {
	...
    @Before
    public void init() {
        //1. 获取核心容器对象
        // ac = new ClassPathXmlApplicationContext("bean.xml");
        // 参数是用 Configuration 注解修饰(也可以不用这个注解修饰)的类的 class
        ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); 
        as = ac.getBean("accountService", IAccountService.class);
    }
    ...

我们在 new AnnotationConfigApplicationContext 这个对象的时候传递的参数,已经把配置文件类告诉了Spring框架。所以SpringConfiguration 这个配置类的 Configuration 注解删除掉

3.1 Spring中一些新注解的使用

3.1.1 Import

下面我新创建一个JDBC的配置类名为JdbcConfig,然后把 SpringConfiguration 这个配置类里的内容移动到JDBC配置类中,

/**
 * 和Spring连接数据库相关的配置类
 */
@Configuration
public class JdbcConfig {
    /**
     * 用于创建一个 QueryRunner 对象
     * @param dataSource
     * @return
     */
    @Bean(name = "runner")
	@Scope("prototype")
    public QueryRunner createQueryRunner(DataSource dataSource) {
        return new QueryRunner(dataSource);
    }
    /**
     * 创建数据源对象
     * @return
     */
    @Bean(name = "dataSource")
    public DataSource createDataSource() {
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource("");
            ds.setDriverClass("com.mysql.jdbc.Driver");
            ds.setJdbcUrl("jdbc:mysql://localhost:3306/spring");
            ds.setUser("root");
            ds.setPassword("123456");
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

SpringConfiguration 这个配置类当做Spring的主配置类。
我们怎么把JDBC配置类加载呢?有三种方法
方法一:就是使用 ComponentScan 注解,在扫描包时不但要扫描 “com.itheima” 这个包,还要扫描 “config”这个包。下面我们修改一下 SpringConfiguration.java

@Configuration
//@ComponentScan(value = "com.itheima")
//@ComponentScan(basePackages = "com.itheima")
@ComponentScan({"com.itheima", "config"})// 扫描包,只能扫描到代有注解的类
public class SpringConfiguration {
}

然后运行一下 AccountServiceTest 测试类中的测试方法,发现仍然可以正常运行,说明这样是有效果的。但是这里有一个地方需要注意的在上面的这段代码注释中已有说明, @ComponentScan({"com.itheima", "config"}) //扫描包,只能扫描到带有注解的类所以JdbcConfig 类必须要有Configuration 注解修饰。但是我就想删除 JdbcConfig 这个类的 Configuration 注解怎么办呢?
方法二:我们在 new AnnotationConfigApplicationContext 这个对象的时候把JdbcConfig.class也作为参数传递过去,这样JdbcConfig就可以不用 Configuration 修饰了。这样也不用在SpringConfiguration这个类中配置扫描"config"这个包了。

public class AccountServiceTest {
	...
    @Before
    public void init() {
        //1. 获取核心容器对象
        // 这样写叫并列配置类
        ac = new AnnotationConfigApplicationContext(SpringConfiguration.class, JdbcConfig.class);
        as = ac.getBean("accountService", IAccountService.class);
    }
    ...

方法三:使用新注解 Import 来达到和 ac = new AnnotationConfigApplicationContext(SpringConfiguration.class, JdbcConfig.class);一样的效果。修改一下SpringConfiguration.java

@Configuration
//@ComponentScan(value = "com.itheima")
//@ComponentScan(basePackages = "com.itheima")
//@ComponentScan({"com.itheima", "config"})// 扫描包,只能扫描到代有注解的类里
@ComponentScan("com.itheima")// 扫描包,只能扫描到代有注解的类里
@Import(JdbcConfig.class) //这样 JdbcConfig 类就可以不用 Configuration 注解修饰
public class SpringConfiguration {
}

Import 注解实现了把其他的细小的配置类导入到主配置类中,这样在其他的配置类中就不用再使用 Configuration 注解修饰了。当我们使用Import的注解之后,有Import修饰的类就是主配置类或者叫父配置类,其他的配置类叫配置类。

3.1.2 PropertySource

上面的案例中,我们把jdbc的配置都直接写在了JdbcConfig这个类中是写死的。怎么解决这个问题呢?首先在resources里创建一个jdbc.properties配置文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring
jdbc.username=root
jdbc.password=123456

然后修改一下JdbcConfig.java

/**
 * 和Spring连接数据库相关的配置类
 */
//@Configuration
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
    ...
    /**
     * 创建数据源对象
     * @return
     */
    @Bean(name = "dataSource")
    public DataSource createDataSource() {
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource("");
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

怎么加载jdbc.properties文件呢?这就要使用到新的注解 PropertySource

PropertySource
 作用:用于指定properties文件的位置
 属性:
     value:指定properties配置文件的名称和路径,关键字:classPath,表示类路径下。不写 classpath: 也可以但是jdbcConfig.properties要在resources中不能有二级目录

修改一下SpringConfiguration.java

@Configuration
//@ComponentScan(value = "com.itheima")
//@ComponentScan(basePackages = "com.itheima")
//@ComponentScan({"com.itheima", "config"})// 扫描包,只能扫描到代有注解的类里
@ComponentScan("com.itheima")// 扫描包,只能扫描到代有注解的类里
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties") // 加载jdbc配置文件
//不写 classpath: 也可以但是jdbcConfig.properties要在resources中不能有二级目录
//@PropertySource("jdbcConfig.properties") // 加载jdbc配置文件
public class SpringConfiguration {
}

3.2 1.2.2 Qualifier

还记得我们上面 1.2.2 Qualifier 留的悬念吗?Qualifier 注解在给类成员注入时不能单独使用要结合Autowired一起使用。但是在给方法参数注入时可以。我们修改一下JdbcConfig.java

//@Configuration
public class JdbcConfig {
    @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 对象
     * @param dataSource
     * @return
     */
    @Bean(name = "runner")
    @Scope("prototype")
    //@Qualifier 在给方法参数修饰时可以单独使用,参数 value 表示bean的id
    public QueryRunner createQueryRunner(@Qualifier("dataSource") DataSource dataSource) {
        return new QueryRunner(dataSource);
    }

    /**
     * 创建数据源对象
     * @return
     */
    @Bean(name = "dataSource")
    public DataSource createDataSource() {
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource("");
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Bean(name = "ds1")
    public DataSource createDataSource1() {
    System.out.println("使用的是 ds1");
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource("");
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

大家应该注意到createQueryRunner函数的参数 dataSource 前面有一个注解 Qualifier 注解,我们前面说过当我们不使用这个注解时,默认要注入的bean的id为 参数的名字,当我们Qualifier的参数改成 ds1时,然后运行测试方法,根据控制台输出可以看出使用的是第二个方法。
在这里插入图片描述

4 Spring和Junit整合

  1. 应用程序的入口
    main方法

  2. junit单元测试中,没有main方法也能执行
    junit集成了一个main方法
    该方法会判断当前测试类中哪些方法有@Test注解
    junit就让有Test注解的方法执行

  3. junit不会管我们是否采用spring框架
    在执行测试方法时,junit根本不知道我们是不是用了spring框架
    所以也就不会为我们读取配置文件/配置类创建spring核心容器

  4. 有以上三点可知
    当测试方法时,没有ioc容器,就算写了Autowired注解,也无法注解
    下面就来解决上面的问题。

     Spring整合junit的配置
     	1.导入spring整合junit的jar(坐标)
     	2.使用junit提供的一个注解把原有的main方法替换掉,替换成spring提供的
     		@Runwith
     	3.告知spring的运行器,spring的ioc创建是基于xml的还是基于注解的,并且说明位置
     		@ContextConfiguration
              locations:指定xml文件的位置,加上classpath关键字,表示在类路径下
              classes:指定注解类所在的位置
    

    当我们使用spring 5.x版本的时候,要求junit的jar包必须是4.12及以上
    首先导入有关jar包,修改pom.xml文件

<?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.itheima</groupId>
    <artifactId>day02_04account_anno_ioc_withoutXml</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>jar</packaging>
    <dependencies>
	    ...
        <dependency> <!--导入spring的测试包-->
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        
    </dependencies>
</project>

然后修改一下AccountServiceTest.java

/*
 * 使用Junit单元测试,测试我们的配置
 * Spring整合junit的配置
 *      1.导入spring整合junit的jar(坐标)
 *      2.使用junit提供的一个注解把原有的main方法替换掉,替换成spring提供的
 *          @Runwith
 *      3.告知spring的运行器,spring的ioc创建是基于xml的还是基于注解的,并且说明位置
 *          @ContextConfiguration
 *              locations:指定xml文件的位置,加上classpath关键字,表示在类路径下
 *              classes:指定注解类所在的位置,比如 "classpath:bean.xml"
 *  当我们使用spring 5.x版本的时候,要求junit的jar包必须是4.12及以上
 */
@RunWith(SpringJUnit4ClassRunner.class) // 使用junit提供的一个注解把原有的main方法替换掉,替换成spring提供的
@ContextConfiguration(classes = SpringConfiguration.class) //加载spring主配置类
public class AccountServiceTest {
    // private ApplicationContext ac;
    // private AnnotationConfigApplicationContext ac;
    @Autowired //自动注入数据
    IAccountService as;

    //@Before
    //public void init() {
    //    //1. 获取核心容器对象
    //    ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); // 参数是用 Configuration 注解修饰的类的 class
    //    as = ac.getBean("accountService", IAccountService.class);
    //}

    //@After
    //public void destroy() {
    //	ac.close();
    //}

    @Test
    public void testFindAll() {
        List<Account> accounts = as.findAllAccount();
        for (Account account : accounts) {
            System.out.println(account);
        }
    }
    ...
}

然后我们运行一下testFindAll测试方法,控制台输出如下,说明as已经成功注入数据啦。
在这里插入图片描述

参考文档
Spring 5.0.2 的说明文档

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页