spring

sping

一、概述

1.1 spring是什么?

Spring是分层的全栈轻量级开源框架。以IOC(控制反转)和AOP(面向切面编程)为内核。提供了展现层springmvc和持久层springjdbc以及业务层事务管理等众多的区企业级应用技术,还能整合开源世界众多的著名的第三方框架和类库,逐渐成为使用最多的java EE企业应用开源框架。

1.2 spring的两大核心

IOC(控制反转)和AOP(面向切面编程)

1.3 spring的优势

方便解耦
通过spring提供的IOC容器,可以将对象之间的依赖关系交由spring进行控制,避免硬编码所造成的程序过度耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。
AOP编程的支持
通过spring的AOP功能,封边进行面向切面编程,许多不容易用传统OOP实现的功能可以通过AOP轻松应付。
声明式事务的支持
可以将我们从单调烦闷的事务管理代码中解脱出来,通过生命方式灵活的进行事物的管理,提高开发效率和质量。
方便程序的测试
可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情。
方便集成各种优秀框架
spring可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、Quartz)的直接支持。
降低javaEE API 的使用难度
spring对javaEE API(如JDBC、JavaMail、远程调用等)进行了封装,使这些API的使用难度大为降低。
java源码是经典学习范例
spring源代码的设计,处处体现着大师对java设计模式灵活运用以及对java技术的高深造诣。

1.4 spring体系结构

二、基于XML的IOC配置

2.1 程序的耦合及解耦

耦合

指程序之间的依赖关系。包括类之间的依赖方法之间的依赖
比如:在注册驱动时使用

DriverManager.registerDriver(new com.mysql.jdbc.Driver());

这就会使程序耦合度较高。所以我们一般使用

Class.forName("com.mysql.jdbc.Driver()");

只用到了一个字符串,与驱动类没有依赖关系,就实现了解耦。

解耦

降低程序间的依赖关系。实际开发中应遵循编译期不依赖,运行时才依赖。

解耦的思路
1.使用反射来创建对象,二避免使用new关键字。
2.通过读取配置文件来获取要创建对象的全类名。

因为在controller层要用到service层的对象,在service层要用到dao层的对象。直接new的话,耦合度太高。直接使用反射的话,每次都要写很多重复代码。所以我们用到了**工厂模式。**即,定义一个专门用反射创建对象的类。

工厂模式的使用
先写properties配置文件,key和value,value是被创建对象的全限定名。
创建一个BeanFactory类,专门用来创建对象。
public class BeanFactory {

    //定义一个Properties对象
    private static Properties props;
  
    static {
        try {
            //实例化对象
            props = new Properties();
            //获取properties文件的流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(in);
        } catch (Exception e) {
            throw new ExceptionInInitializerError("初始化properties失败!");
        }
    }
      //写一个方法,用Bean的名称获取对象
    public static Object getBean(String beanName){

        Object bean = null;
        try {
            String beanPath = props.getProperty(beanName);
            bean = Class.forName(beanPath).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bean;
    }
}

这样只要在主程序中调用Beanfactory的静态方法就可以创建对象了。

IAccountService as = (IAccountService) BeanFactory.getBean("accountService");

但是有一个问题,那就是每次调用都会产生一个新的对象。但这似乎是没必要的,因此我们用到单例模式改进这个工厂。
单例模式的使用
: 让配置文件中对应的每个类只创建一次对象,每次主程序的调用只是用到已创建好的那个对象,而不会重复创建。

public class BeanFactory {

    //定义一个Properties对象
    private static Properties props;

    //定义一个容器,用来存放创建好的对象。注意规定泛型
    private static Map<String,Object> beans;

    //是用静态代码块为Properties对象赋值,获取流对象,加载配置文件
    //为了避免主程序每次调用getBean方法都创建同一个类的对象,那么我们采用单例模式
    //单例模式:在此静态方法中将配置文件中对应的所有类都创建各自的对象,然后存到一个map集合中。
    //          当主程序调用getBean方法时,直接返回集合中以创建好的对象。
    static {
        try {
            //实例化对象
            props = new Properties();
            //获取properties文件的流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(in);

            //获取容器实例。注意规定泛型
            beans = new HashMap<String,Object>();
            //枚举配置文件中所有的key
            Enumeration keys = props.keys();
            //遍历枚举
            while(keys.hasMoreElements()){
                //获取每个键的名字
                String key = keys.nextElement().toString();
                //根据key获取对应的value
                String beanPth = props.getProperty(key);
                //根据value(全限定名),使用反射创建对象
                Object value = Class.forName(beanPth).newInstance();
                //把创建好的对象和对应配置文件中的key存入容器中
                beans.put(key,value);
            }

        } catch (Exception e) {
            throw new ExceptionInInitializerError("初始化properties失败!");
        }
    }

    /**
     * 使用单例模式后改进的方法
     * 根据Bean的名称获取对象
     */
    public static Object getBean(String beanName){
        return beans.get(beanName);
    }
}

这样每次主程序调用getBean()方法时,都是返回容器(map集合)中已创建好的对象。但要注意: 使用单例模式时,被创建对象的类中不要定义成员变量,可以将变量定义在方法中。

2.2 IOC概念和spring中的IOC

2.2.1 概念

IOC就是用来解耦,削减类之间的依赖。
IOC内部实现了 读取配置文件、创建对象、存入map集合这些操作。我们使用IOC就是根据id从容器中取出对象。

2.2.2 如何使用

这里我们会提到核心容器的两个接口:ApplicationContext 和 BeanFactory

1、获取核心容器对象

有三种常用方式:分别用到了核心容器接口ApplicationContext的三个实现类。

三者的区别如下
ClassPathXmlApplicationContext: 可以加载类路径下的配置文件,如果配置文件不在类路径下,则不可以。
ApplicationContext ac = new ClassPathXmlApplicationContext(“bean.xml”);

FileSystemXmlApplicationContext: 可以加载磁盘中的配置文件。(需要有访问该配置文件的权限)

ApplicationContext ac = new FileSystemXmlApplicationContext("E:\\IdeaProjects\\day01_eesy_03spring\\src\\main\\resources\\bean.xml");

AnnotationConfigApplicationContext: 用于读取注解创建容器。

//配置类为 public class SpringConfiguration{}
ApplicationContext ac = new AnnotationConfigurationApplicationContext(SpringConfiguration.class)

参数中涉及到的类可以不用加@Configuration注解也可以被spring扫描到。

2、根据id获取Bean对象

获取Bean对象时需要类型转换,有两种方式:

  1. 强制转换
  2. 将目标类型的字节码对象 作为参数 写在方法的参数列表
    如:
IAccountService as = (IAccountService) ac.getBean("accountService");
IAccountDao ad = ac.getBean("accountDao",IAccountDao.class);
核心容器两个接口的区别
**ApplicationContext:**单例对象适用
它在构建核心容器时,创建对象采用的是立即加载的方式。即,只要成功读完配置文件,就马上创建配置文件中的配置对象。
**BeanFactory:**多例对象适用
它在构建核心容器时,创建对象采用的是延迟加载的方式。即,什么时候要根据id获取对象了,什么时候才真正创建。

2.2.3 spring对Bean的管理细节

三种创建Bean对象的方式
 <!--把对象的创建交给spring来管理-->

    <!--spring对Bean管理细节
        1.创建bean的三种方式
        2.bean对象的作用范围
        3.bean对象的生命周期
    -->

    <!--创建bean的三种方式:-->
    <!--第一种方式:使用默认构造函数的创建
            在spring的配置文件中使用bean标签,只配id和class属性,再无其他属性和标签时。
            采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则无法创建对象。

    <bean id="accountService" class="com.itcast.service.impl.AccountServiceImpl"></bean>
    -->

    <!--在实际开发中,我们可能会用到别人的类或者jar包中的类 中的方法返回的对象,我们无法修改它的源码。此时如何创建bean对象呢?-->
    <!--第二种方式:使用普通工厂中的方法创建对象(使用某个类中的方法创建对象),并存入spring容器中。
                    此时要用到属性factory-bean 和factory-method

    <bean id="instanceFactory" class="com.itcast.factory.InstanceFactory"></bean>
    <bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"></bean>
    -->


    <!--第三种方式:使用工厂中的静态方法创建对象,(使用某个类中的静态方法创建对象),并存入spring容器

    -->
    <bean id="accountService" class="com.itcast.factory.staticFactory" factory-method="getAccountService"></bean>
bean的作用范围

bean的作用范围调整:
bean标签的scope属性:
作用:用于指定bean的作用范围
取值:
singleton 单例 (默认值)
prototype 多例
request 作用于web应用的请求范围
session 作用于web应用的会话范围
global-session 作用于集群环境的会话范围(全局会话范围),当不是集群环境时,就是session

<--创建多例的bean对象-->
<bean id="accountService" class="com.itcast.service.impl.AccountServiceImpl" scope="prototype"></bean>
bean的生命周期

bean对象的生命周期

            单例对象:
                    出生:当容器创建时对象出生       **init-method**=“ ”
                    活着:只要容器还在,对象一直活着
                    死亡:容器销毁,对象消亡         **destory-method**=“”
                    总结:单例对象的生命周期和容器相同
<--创建多例的bean对象-->
<bean id="accountService" class="com.itcast.service.impl.AccountServiceImpl" init-method="" destory-method=""></bean>
        多例对象:
                出生:使用对象时,spring框架为我们创建对象
                活着:对象在使用过程中就一直活着
                死亡:当对象长时间不用,且没有别的对象引用时,有Java垃圾回收机器回收

2.3 依赖注入(Dependency Injection)

IOC的作用是降低了程序间的依赖关系。
但在程序开发中,controller层调用service层,service层调用dao层。所以程序中的依赖不可能消除。
那么我们把依赖关系的管理都交给spring来维护。
依赖关系就是在当前类需要用到其他类的对象。这些对象由spring为我们提供,我们只需要在配置文件中说明。

依赖注入:就是依赖关系的维护。

能注入的数据:有三类
  1. 基本数据类型和String
  2. 其他Bean类型(在配置文件中或者注解配置过的bean)
  3. 复杂类型/集合类型
注入的方式:有三种
  1. 使用构造函数注入
  2. 使用set方法注入
  3. 使用注解注入
  • 构造函数的注入
    使用的标签:constructor-arg
    标签出现的位置:bean标签的内部
    标签中的属性:
  • type :用于指定要注入的数据的数据类型,就是构造函数中某个参数的类型。(但若有两个以上的参数类型一致,就不能准却赋值了)
  • index:用于指定要注入的数据,给构造函数中指定索引位置的参数赋值,索引的位置是从0开始的。(需要知道每一个参数的索引和数据类型,使用起来很不方便)
  • name(最常用的):用于给构造函数中指定名称的参数赋值
  • value:用于提供基本类型和String类型的数据
  • ref:引用关联的bean对象。

   <bean id="accountService" class="com.itcast.service.impl.AccountServiceImpl" >
       <constructor-arg name="s" value="冉冉"></constructor-arg>
       <constructor-arg name="i" value="18"></constructor-arg>
       <constructor-arg name="t" ref="now"></constructor-arg>
   </bean>

    <!--配置一个日期对象-->
    <bean id="now" class="java.util.Date"></bean>

优势: 在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。对于一些数据固定的bean对象是方便的。
**弊端:**改变了bean对象实例化的方式,是我们在创建对象时,如果用不到这些数据,也必须提供。

  • set方法注入(常用)
    使用的标签:property
    标签出现的位置:bean标签的内部
    标签中的属性:
  • name(最常用的):用于给指定名称的参数赋值
  • value:用于提供基本类型和String类型的数据
  • ref:引用关联的bean对象。
<bean id="accountService" class="com.itcast.service.impl.AccountServiceImpl">
        <property name="strin" value="set方法注入"></property>
        <property name="inte" value="19"></property>
        <property name="tim" ref="now"></property>
    </bean>

**优势:**创建对象没有明确的限制,只给需要的提供属性
**弊端:**如果某个成员必须有值而没有提供,会导致创建对象失败

  • 注入复杂类型的数据
    用于List结构集合注入的标签:
    List 、 array 、 set
    用于给Map结构集合注入的标签:
    map、 props
    注意:结构相同,标签可以随意用。
<bean id="accountService3" class="com.itcast.service.impl.AccountServiceImpl3">
        <property name="myStrs" >
            <array>
                <value>数组一号元素</value>
                <value>数组二号元素</value>
                <value>数组三号元素</value>
            </array>
        </property>

        <property name="myList" >
            <array>
                <value>list一号元素</value>
                <value>list二号元素</value>
                <value>list三号元素</value>
            </array>
        </property>

        <property name="mySet" >
            <array>
                <value>set一号元素</value>
                <value>set二号元素</value>
                <value>set三号元素</value>
            </array>
        </property>

        <property name="myMap" >
            <map>
                <entry key="testA" value="map一号元素"></entry>
                <entry key="testB" value="map二号元素"></entry>
                <entry key="testC" value="map三号元素"></entry>
            </map>
        </property>

        <property name="myProps">
            <props>
                <prop key="testEEE">EEE</prop>
                <prop key="testFFF">FFF</prop>
            </props>
        </property>
    </bean>

三、基于注解的IOC和案例

注解配置和xml配置要实现的功能都是一样的,都是要降低程序间的耦合,只是配置的形式不一样。

3.1 spring中IOC的常用注解

3.1.1 用于创建对象的注解

这类注解的作用相当于xml配置文件中标签的实现功能。

@component:
  • 作用:用于把当前类对象存入spring容器中
  • 属性:value:用于指定当前bean的id。当我们不写时,它的默认值是当前类名,且首字母改小写。
注意: 只加上Component注解是不行的,还需要配置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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
        
   <!--告知spring在创建容器的时候要扫描的包,配置所要的标签不是在beans约束中的,而是一个名称为context的名称空间和约束-->
   <context:component-scan base-package:"此处输入groupId"></context:component-scan>

@Controller: 一般用在表现层
@Service: 一般用在业务层
@Repository: 一般用在持久层
以上三个注解他们的作用和属性与Component是一模一样的。这是spring为我们提供明确的三层使用的,使我们的三层对象更清晰。

3.1.2 用于注入数据的注解

这类注解的作用相当于xml配置文件中标签下的标签的实现功能。

@Autowired:
  • 作用:自动按照类型注入。1.只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。
    2.如果容器中一个类型匹配的变量都没有,那么注入失败。
    3.如果容器中有多个类型匹配的变量,就再加一个Qualifier注解。
  • 出现位置:可以是变量上,也可以是方法上。
  • 细节:在使用注解注入时,set方法就不是必须的了。
@Qualifier:
  • 作用:在按照类型注入(Autowired)的基础上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以。
  • 属性:value:用于指定注入的bean的id
@Resource:
作用:直接按照bean的id注入,它可以独立使用。
属性:name:用于指定bean的id。

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

@Value:
  • 作用:用于注入基本类型和String类型的数据
  • 属性:value:用于指定数据的值。它可以使用spring中SpEl(也就是spring的el表达式)。 SpEl的写法:${表达式}

3.1.3 用于改变作用范围的注解

这类注解的作用相当于在bean标签中使用scope属性实现的功能。

@Scope:
  • 作用:用于指定bean的作用范围。
  • 属性:value:指定范围的取值。常用取值:singleton,prototype

3.1.4 和生命周期相关的注解

这类注解的作用相当于在bean标签中使用init-method和destroy-method属性实现的功能。

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

3.1.5 spring中的新注解

@Configuration:
  • 作用:指定当前类是一个配置类,作用与resources目录下的xml文件是一样的。
@ComponentScan:
  • 作用:用于通过注解指定spring在创建容器时要扫描的包
  • 属性:value:它和basepackage的作用是一样的,都是用于指定spring在创建容器时要扫描的包。就相当于在xml文件中配置了:
<context:component-scan base-package:"此处输入groupId"></context:component-scan>
@Bean:
  • 作用:用于把当前方法的返回值作为bean对象存入spring的IOC容器中
  • 属性:name:用于指定bean的id。当不写时,默认是当前方法的名称。
@Import:
  • 作用:用于导入其他的配置类
  • 属性:value:用于指定其他的配置类的字节码。
    当我们使用@Import后,被该注解标记的就是父配置类,导入的就是子配置类。
@ PropertySource:
  • 作用:用于指定properties文件的位置
  • 属性:value:指定文件的名称和路径。 关键字:classpath 表示类路径下

3.2 案例使用xml方式和注解方式实现单表的CRUD操作,持久层技术选择dbutils

3.2.1 使用xml方式实现

第一步:新建工程后,在pom.xml文件中导入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
        <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.19</version>
        </dependency>
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.5</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

第二步:写service层接口和实现类,dao层接口实现类

/**
 * 业务层接口
 */
public interface IAccountService {
    /**
     * 查询所有
     */
    public List<Account> findAll();
}
/**
 * 业务层实现类
 */
public class AccountServiceImpl implements IAccountService{

    private IAccountDao accountDao;
    //set方法用于IOC的数据注入
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public List<Account> findAll() {
        return accountDao.findAll();
    }

}
/**
 * 持久层接口
 */
public interface IAccountDao {

    /**
     * 查询所有
     */
    public List<Account> findAll();
}
/**
 * 持久层实现类
 */
public class AccountDaoImpl implements IAccountDao {

    private QueryRunner runner;

    //用于Spring的IOC容器注入
    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    @Override
    public List<Account> findAll() {
        try {
            return runner.query("select * from account", new BeanListHandler<Account>(Account.class));
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}

第三步:在resource目录下面新建bean.xml文件,进行创建bean对象和注入数据的配置。
头部的信息的获取:点击进入网址,按Ctrl+F,输入xmlns,点击搜索,将找到的配置文件的头部信息复制。

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--配置service层对象-->
    <bean id="accountService" class="com.itcast.service.impl.AccountServiceImpl">
        <!--注入dao层对象-->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置dao层对象-->
    <bean id="accountDao" class="com.itcast.dao.impl.AccountDaoImpl">
        <!--注入QueryRunner对象-->
        <property name="runner" ref="queryRunner"></property>
    </bean>

    <!--配置QueryRunner对象
        默认是单例对象,但在面对多个对象的操作时,会出现线程问题。所以应使用多例对象。
        此时scope为prototype,即多例,这里的多例其实是connection多例,每次执行runner中某个方法时,都会进行一次新的连接。-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="dateSource"></constructor-arg>
    </bean>

    <!--配置数据源-->
    <bean id="dateSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--配置连接数据库的必备四大信息-->
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
</beans>

第四步、在test目录下写测试类

public class AccountServiceTest {
    public AccountServiceTest() {
    }

    @Test
    public void testFindAll() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        IAccountService as = (IAccountService)ac.getBean("accountService");
        List<Account> accounts = as.findAll();
        Iterator var4 = accounts.iterator();

        while(var4.hasNext()) {
            Account account = (Account)var4.next();
            System.out.println(account);
        }

    }

    @Test
    public void testFindById() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        IAccountService as = (IAccountService)ac.getBean("accountService");
        Account byId = as.findById(1);
        System.out.println(byId);
    }

    @Test
    public void testsave() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        IAccountService as = (IAccountService)ac.getBean("accountService");
        Account account1 = new Account();
        account1.setId(5);
        account1.setName("张大彪");
        account1.setMoney(800.0F);
        as.saveAccount(account1);
    }

    @Test
    public void testupdate() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        IAccountService as = (IAccountService)ac.getBean("accountService");
        Account account = as.findById(1);
        account.setMoney(500.0F);
        as.updateAccount(account);
    }

    @Test
    public void testDelete() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        IAccountService as = (IAccountService)ac.getBean("accountService");
        as.deleteAccount(6);
    }
}
关注点:
  1. QueryRunner对象也需要注入。默认是单例对象,但在面对多个对象的操作时,会出现线程问题。所以应使用多例对象。
  1. 在注入QueryRunner时,使用构造方法注入数据源。
  1. 注入数据源,并将连接数据库的信息注入进来。

3.2.2 使用注解和xml混合使用的方式实现

在纯xml配置的基础上进行改造:
  1. xml配置文件头部的信息的获取:点击进入网址,按Ctrl+F,输入xmlns:context,点击搜索,将找到的配置文件的头部信息复制。
  1. 用到注解@Service,@Repository后,上面3.1.1中的xml文件中配置service层bean对象和dao层的bean对象就可以去掉了。
  1. 用到注解@Autowired后,上面3.1.1中的配置文件中配置在service层注入dao层数据、在dao层注入QueryRunner对象的配置就可以去掉了。
  1. 但需要告知spring在创建容器时需要扫描的包,这样才可以把有@Service和@Repository标记的类对象创建到容器中。写法如下:
<context:component-scan base-package:"这里写包名"></context:component-scan>

但是QueryRunner类并不是我们自己写的,而是jar包中的,我们也无法通过在jar包中加上注解来把该类的对象创建到容器中。所以xml文件中依然要保留配置数据源,配置QueryRunner并在其中注入数据源这些内容。
所以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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

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

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!--配置数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--配置连接数据库的基本信息-->
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
</beans>

service 层加上注解后是这样的:

/**
 * 账户业务层实现类
 */
@Service("accountService")
public class AccountServiceImpl implements IAccountService{

    @Autowired
    private IAccountDao accountDao;
    @Override
    public List<Account> findAll() {
        return accountDao.findAll();
    }
  }

dao层加上注解后是这样的:

/**
 * 账户持久层实现类
 */
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao{

    @Autowired
    private QueryRunner runner;

    @Override
    public List<Account> findAll() {
        try {

            //注意BeanListHandler加泛型
            return runner.query("select * from account", new BeanListHandler<Account>(Account.class));
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
 }

3.3 改造基于注解的IOC案例,使用纯注解的方式实现。spring的一些新注解的使用。

上面3.2虽然用注解进行了改进,但是xml文件依然存在。而且Junit单元测试中重复代码很多(3.4来解决)。那么如何将xml文件彻底取缔,用纯注解的方式来实现呢?
第一步: 我们在conf包下创建一个配置类,给它加上@Configuration注解,用来取代xml文件。并加上@ComponentScan,告诉spring创建容器时要扫描的包。

@Configuration
@ComponentScan("要扫描的包名")
public class SpringConfiguration{
	
}

第二步: 经过3.2的改造,xml文件中还剩下的配置是:jar包中的QueryRunner类和ComboPoolDataSource类。
我们发现配置这两个类的时候采用的是构造方法创建Bean对象的方式,所以我们可以在java代码中写一个方法,把他们new出来并作为方法的返回值,这样就可以获取到该类的对象了。
但只获取到对象是不够的,我们还要将对象放入IOC容器中。这就需要用到注解@Bean。因为在xml文件中QueryRunner对象是多例的,所以添加@Scope

@Bean(name="runner")
@Scope("prototype")
public QueryRunner createQueryRunner(DataSource dataSource){
	return new QueryRunner(DataSource);
}

第三步: 上面两步已经完全取代了xml文件。但是我们之前获取容器对象时使用的是ClassPathXmlApplicationContext读取配置文件的方式。现在应该用:
AnnotationConfigApplicationContext(Class<?> annotatedClass);
参数就是 被@Configuration所标记类的字节码对象。

//配置类为 public class SpringConfiguration{}
ApplicationContext ac = new AnnotationConfigurationApplicationContext(SpringConfiguration.class)

有多个配置类时的注解使用

比如,上面的SpringConfiguration是主配置类,再新建一个配置类JDBCConfiguration,用来写数据源相关的配置。此时注解的使用有如下几种方式:

  1. 直接在获取容器对象时,将所有配置类的字节码对象都作为参数传进去。此时配置类上可以不用加@Configuration注解了。
ApplicationContext ac = new AnnotationConfigurationApplicationContext(SpringConfiguration.class,JDBCConfiguration.class;
  1. 所有的配置类上都用@Configuration标记;获取容器对象时只传主配置类的字节码对象;配置扫描包时,将所有配置类所在的包名设置进去。
@Configuration
@ComponentScan("com.itcast","config")
public class SpringConfiguration{
	
}
  1. (常用) 在主配置类上添加@Import注解,参数为子配置类的字节码对象;所有配置类上使用@Configuration;获取容器对象时参数为主配置类的字节码对象。

将数据源配置信息写进properties文件

我们在配置类JDBCConfiguration中进行了数据源信息(driver,jdbcURL,user,password)的配置,但缺点是写死了,修改维护很不方便。因此我们用properties配置文件的方式解决这个问题。
第一步: 在resources目录下创建文件JDBCConfig.properties,用键=值的形式将数据源信息写入文件。
第二步: 在主配置类上添加@PropertySource注解,参数为类路径和文件名。
第三步: 获取文件中的数据,在JDBCConfiguration配置类中定义String类型的成员变量,并在上面添加注解@Value("${配置文件中的key}"),参数为el表达式。

在实际开发中如何判断选择用xml文件方式还是注解方式。

通过上面用注解方式对xml文件方式的不断改造,到最后,我们发现依然很繁琐,并没有达到简化配置工作的目的。所以在实际开发中遵循的思路应该是:对于我们自己写的类用注解;对于jar包或者别人的类,用xml文件配置。

3.4 spring和Junit的整合

Spring整合Junit的配置:

  1. 导入Spring整合Junit的jar包(坐标)
<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
  1. 使用Junit提供的一个 @Runwith 注解把原有的main方法替换掉,换成Spring提供的 。
  2. 告知Spring的运行器,spring的IOC的创建是基于xml文件还是注解的,并且说明位置。添加 @ContextConfiguration() ,关键字:1.location:指定xml文件的位置,加上classpath关键字,表示在类路径下。2.classes:指定注解类所在的位置。
    注意: 当使用spring 5以上版本时,要求Junit的jar必须是4.12及以上.
/**
 * 使用Junit单元测试
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
//@ContextConfiguration(classes = SpringConfiguration.class) //纯注解的写法
public class AccountServiceTest {

    @Autowired
    IAccountService accountService;
    @Test
    public void testTransfer() {
        accountService.transfer("李云龙","丁伟",200f);
    }
}

四、AOP和基于XML及注解的AOP配置

4.1 完善案例并分析其中存在的问题

在案例中添加转账方法,实现转账功能。
在业务层实现类中添加

 /**
     * 转账
     * @param sourceName:转出账户
     * @param targetName:转入账户
     * @param money:转账金额
     */
    @Override
    public void transfer(String sourceName, String targetName, float money) {
        //1、根据name查出转出账户
        Account account1 = accountDao.findByName(sourceName);
        //2、根据name查出转入账户
        Account account2 = accountDao.findByName(targetName);
        //3、转出账户减钱
        account1.setMoney(account1.getMoney()-money);
        //4、转入账户加钱
        account2.setMoney(account2.getMoney()+money);
        //5、更新转出账户
        accountDao.updateAccount(account1);
        //6、更新转入账户
        accountDao.updateAccount(account2);

    }

    /**
     * 根据名字查询账户
     */
     Account findByName(String name){
        return accountDao.findByName(name);
    }

在dao层实现类中添加方法

/**
     * 根据name查询
     * @param name
     * @return 如果有唯一的结果就返回,如果没有就返回null,如果结果集超过一个,就报异常
     */
     @Override
    public Account findByName(String name) {

        try {
            List<Account> accounts = runner.query("select * from account where name = ?", new BeanListHandler<Account>(Account.class), name);
            if (accounts == null || accounts.size()==0){
                return null;
            }
            if (accounts.size() > 1){
                throw new RuntimeException("结果集不唯一,数据有问题");
            }
            return accounts.get(0);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

然后测试该方法,可以运行成功。

存在的问题:
当我们在转账方法中手动造一个异常,此时就会出现问题。运行的结果是,转出账户金额减少了,但转入账户金额却没有增加。
  //更新转出账户
        accountDao.updateAccount(account1);
        //手动添加异常
        int i= 1/0;
        //更新转入账户
        accountDao.updateAccount(account2);
问题原因:不满足事务的一致性。
在业务层转账方法的六个步骤中,第1、2、5、6步执行的时候都各自获取数据库连接(connection)并各自提交,所以才会导致上面的执行成功,修改了转出账户的信息,而下面的由于异常没有执行,也没有修改数据库的信息。所以导致了事务的不一致。
解决思路:
1、让转账方法只有一个connection。每个步骤都不会获取自己的connection,而公用一个,最后提交一次事务。
2、使用TreadLocal对象把connection和当前线程绑定,从而一个线程中只有一个能控制事务的对象
解决步骤:
  1. 在utils包下,新建一个管理连接的工具类。用于将线程和连接绑定,并获取连接。
/**
 * 管理connection的工具类。用于从数据源获取一个连接,并且实现和线程的绑定
 */
public class ConnectionUtils {

    private ThreadLocal<Connection> t1 = new ThreadLocal<Connection>();

    private DataSource ds;

    public void setDs(DataSource ds) {
        this.ds = ds;
    }

    /**
     * 获取当前线程上的连接
     * @return
     */
    public Connection getThreadConnection(){

        //从ThreadLocal上获取连接
        Connection conn = t1.get();

        try {
            //判断当前线程上是否有连接
            if (conn == null) {

                //如果为空,就从数据源获取一个新的连接
                conn = ds.getConnection();
                //将连接与线程绑定
                t1.set(conn);
            }

            return conn;
        }catch (Exception e){
            throw new RuntimeException(e);
        }

    }

    /**
     * 将线程与连接进行解绑
     */
    public void removeConnection(){
        t1.remove();
    }
}
  1. 在utils包下再新建一个事务管理的类。
/**
 * 和事务管理相关的类。
 */
public class TransactionManager {

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }


    /**
     * 开启事务
     */
    public void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 提交事务
     */
    public void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 回滚事务
     */
    public void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 释放连接
     */
    public void release(){
        try {
            //关闭连接
            connectionUtils.getThreadConnection().close();
            //解绑连接与线程
            connectionUtils.removeConnection();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
  1. 改造业务层代码和dao层代码
    业务层:每个方法都加上事务管理,这里只列举两个。
/**
 * 账户业务层实现类
 */
public class AccountServiceImpl implements IAccountService{

    private IAccountDao accountDao;
    private TransactionManager txManager;

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

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }
 @Override
    public List<Account> findAll() {
        try {
            //开启事务
            txManager.beginTransaction();
            //执行操作
            List<Account> all = accountDao.findAll();
            //提交事务
            txManager.commit();
            //返回结果
            return all;
        }catch (Exception e){
            txManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //释放连接
            txManager.release();
        }
    }
    
 @Override
public void transfer(String sourceName, String targetName, float money) {
        try {
            //1、开启事务
            txManager.beginTransaction();
            //2、执行操作
            //2.1根据name查出转出账户
            Account account1 = accountDao.findByName(sourceName);
            //2.2根据name查出转入账户
            Account account2 = accountDao.findByName(targetName);
            //2.3转出账户减钱
            account1.setMoney(account1.getMoney()-money);
            //2.4转入账户加钱
            account2.setMoney(account2.getMoney()+money);
            //2.5更新转出账户
            accountDao.updateAccount(account1);
            //手动添加异常
            int i= 1/0;
            //2.6更新转入账户
            accountDao.updateAccount(account2);
            //3、提交事务
            txManager.commit();
        }catch (Exception e){
            //4、回滚事务,抛出异常
            txManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //5、释放连接
            txManager.release();
        }
    }
}

dao层:不再用QueryRunner中注入的连接,而是用自己写的工具类ConnectionUtils中的连接,并给CRUD的方法添加参数。

/**
 * 持久层实现类
 */
public class AccountDaoImpl implements IAccountDao{

    QueryRunner runner;
    ConnectionUtils connectionUtils;

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    @Override
    public List<Account> findAll() {
        try {

            return runner.query(connectionUtils.getThreadConnection(),"select * from account", new BeanListHandler<Account>(Account.class));
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}
  1. 修改想xml文件
 <!--配置accountService-->
    <bean id="accountService" class="com.itcast.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
        <property name="txManager" ref="txManager"></property>
    </bean>
<!--配置accountDao-->
    <bean id="accountDao" class="com.itcast.dao.impl.AccountDaoImpl">
        <property name="runner" ref="runner"></property>
        <property name="connectionUtils" ref="connectionUtil"></property>
    </bean>
 <!--配置一个QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
    </bean>
<!--配置ConnectionUtils-->
    <bean id="connectionUtil" class="com.itcast.utils.ConnectionUtils">
        <property name="ds" ref="dataSource"></property>
    </bean>

    <!--配置TransactionManager-->
    <bean id="txManager" class="com.itcast.utils.TransactionManager">
        <property name="connectionUtils" ref="connectionUtil"></property>
    </bean>
<!--配置数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>

修改完运行程序,我们发现已经实现了事务一致性。
但是依然存在问题,那就是方法间的依赖关系太强,如果TransactionManager类中的某个方法名称发生改变,那么业务层将需要做大量的修改。这是我们在开发中所不希望的。
我们使用IOC是降低了类与类之间的依赖关系,那么接下来研究降低方法与方法之间的依赖关系。

4.2 代理的分析

我通过一个案例简单案例来学习动态代理。
生产厂家、代理商、消费者之间的活动。
在没有代理商的时候,生产厂家负责生产产品、销售产品和售后服务。有了代理商后,就将销售和售后的工作交给

动态代理:

  • 特点:字节码随用随创建,随用随加载
  • 作用:不修改源码的基础上对方法增强
  • 分类:
    • 基于接口的动态代理:
      • 涉及的类:Proxy
      • 提供者:jdk官方
      • 如何创建代理对象:
        使用Proxy类中的newProxyInstance方法
      • 创建代理对象的要求:
        被代理类最少实现一个接口,如果没有则不能使用
      • newProxyInstance()方法的参数:
        • Classloader:类加载器。它是用于加载代理对象字节码的。写被代理对象的类加载器。(固定写法)
        • Class[]:字节码数组。它是用于让代理对象和被代理对象拥有相同的方法。要代理谁,就写谁的(固定写法)
        • InvocationHandler:用于提供增强的代码。它是让我们写一些接口的实现类,通常情况下都是匿名内部类,但不是必须的,此接口的实现类都是谁用谁写。
          需要实现此接口中的invoke()方法。参数有:
          • proxy 代理对象的引用。
          • method 要执行的方法
          • args 执行方法所需要的参数
          • 返回值:和被代理对象方法具有相同的返回值
/**
 * 生产商接口
 */
public interface IProducer {

    void saleProduct(float money);

    void afterService(float money);
}

/**
 * 生产商实现类
 */
public class Producer implements IProducer{
    @Override
    public void saleProduct(float money) {
        System.out.println("销售产品,并拿到钱:"+money);
    }

    @Override
    public void afterService(float money) {
        System.out.println("售后服务,并拿到钱:"+money);
    }
}
/**
 * 模拟一个消费者
 */
public class Client {
    public static void main(String[] args) {

        //匿名内部类中调用外部对象时,外部对象必须是final修饰
        final IProducer producer = new Producer();

        /**
         * 基于接口的动态代理
         * 用到的类:Proxy
         * 提供者:jdk官方
         * 如何创建代理对象:
         *      使用Proxy中的方法newProxyInstance()
         * 创建代理对象的要求:
         *      被代理的对象至少实现了一个接口
         *
         *   newProxyInstance方法的参数:
         *   ClassLoader:类加载器。用于加载代理对象的字节码。和被代理对象使用相同的类加载器。固定写法。
         *   Class[]:字节码数组。 用于让代理对象和被代理对象拥有相同的方法。
         *   InvocationHandler:用于提供增强代码的。他是让我们写如何代理。我们一般都是写一个该接口的实现类。通常情况下都是匿名内部类。
         *                但不是必须的。此接口的实现是谁用谁写。
         */
        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {

            /**
             * 此方法的作用:执行被代理对象的任何接口方法都会经过该方法
             * 参数的含义:
             * @param proxy 代理对象的引用
             * @param method    当前执行的方法
             * @param args  当前执行方法所需要的参数
             * @return      和被代理对象方法的返回值相同
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object returnVale = null;
                Float money = (Float) args[0];
                if ("saleProduct".equals(method.getName())) {
                    returnVale = method.invoke(producer, money * 0.8f);
                }
                return returnVale;
            }
        });
        proxyProducer.saleProduct(10000f);
    }
}

但是,这种基于接口的动态代理有一个问题。那就是被代理的类必须实现至少一个接口,若没有实现就会出现代理异常的错误。

  • 基于类的动态代理:
    • 涉及的类:Enhancer
    • 提供者:第三方jar包。所以需要先到jar包依赖。 cglib
    • 如何创建代理对象?
      使用Enhancer的create方法
    • 创建代理对象的要求:被代理类不能是最终类
    • create方法的参数:
      Class:字节码。它是用于指定代理对象的字节码;
      Callback:用于提供增强的代码。通常情况下都是匿名内部类,我们一般用该接口的子接口实现类MethodInterceptor。
    • MethodInterceptor:
      • 作用:任何被代理对象的方法都会经过该方法
      • 参数:
        • Object proxy :代理对象的引用
        • Method method:当前执行的方法
        • Object[] args:当前方法所需要的参数
        • MethodProxy methodProxy:当前执行方法的代理对象
        • 返回值:与被代理对象执行方法中的返回值是一样的
/**
 * 模拟消费者
 */
public class Client {
    public static void main(String[] args) {

        final Producer producer = new Producer();

        /**
         * 基于类的动态代理
         *      涉及到的类:Enhancer
         *      提供者:第三方jar包
         *      如何创建代理对象?
         *          Enhancer类中的create方法
         *      创建代理对象的要求:
         *          被代理类不能是最终类
         *      create方法的参数:
         *          Class:字节码。被代理对象的字节码
         *          Callback(接口):用于提供增强的代码。让我们写如何代理,通常情况下写一个该接口的实现类,一般通过匿名内部类的方式来写,但不是必须的。
         *                          一般用该接口子接口的实现类MethodIntercept
         *
         */
        Producer proxyProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {

            /**
             * 方法的作用:任何被代理对象的方法执行都会经过该方法
             * @param proxy     代理对象的引用
             * @param method    当前执行的方法
             * @param args      当前执行的方法的参数
             * @param methodProxy   当前执行方法的代理对象
             * @return      返回值与被代理对象的方法的返回值相同
             * @throws Throwable
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                Object returnValue = null;
                Float money = (Float)args[0];
                if ("saleProduct".equals(method.getName())){
                     returnValue = method.invoke(producer, money*0.8f);
                }

                return returnValue;
            }
        });

        proxyProducer.saleProduct(12000f);
    }


}

用动态代理来改进account案例

前面的service层方法中存在大量的重复性代码(事务控制的代码),并且方法之间的依赖性很强。我们用动态代理的方式来解决这两个问题。
写一个创建service代理对象的类BeanFactory,使用get方法获取动态代理对象,将事务控制的代码写在InvocationHandler的匿名内部类中,这样service层中就不用写了。

/**
 * 用于创建Service的代理对象。此处用基于接口的动态代理
 */
public class BeanFactory {

    private IAccountService accountService;
    private TransactionManager txManager;

    public final void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    /**
     * 获取Service的代理对象
     * @return
     */
    public IAccountService getAccountService() {

        return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
            //写增强的代码
            //添加事务支持
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object returnValue = null;
                try {
                    //开启事务
                    txManager.beginTransaction();
                    //执行操作
                    returnValue = method.invoke(accountService, args);
                    //提交事务
                    txManager.commit();
                    //返回结果
                    return returnValue;
                }catch (Exception e){
                    txManager.rollback();
                    throw new RuntimeException(e);
                }finally {
                    //释放连接
                    txManager.release();
                }
            }
        });
    }
}

然后在改一下bean.xml配置文件。

  1. service中就不需要注入事务管理器了;
  2. 事务管理器注入到beanFactory中;
  3. 配置service的代理对象 时,由于在BeanFactory类中使用get方法获取的代理对象,所以使用factory-bean和factory-method来配置。
<!--配置BeanFactory-->
    <bean id="beanFactory" class="com.itcast.factory.BeanFactory">
        <!--注入service-->
        <property name="accountService" ref="accountService"></property>
        <!--注入事务管理器-->
        <property name="txManager" ref="txManager"></property>
    </bean>
    <!--配置代理的service-->
    <bean id="proxyAccount" factory-bean="beanFactory" factory-method="getAccountService"></bean>
</beans>

测试类中之前是使用@Autowired自动注入accountService,但此时IOC容器中多了一个代理对象,因为这两个类型相同,@Autowired只能按类型匹配唯一对象,所以增加一个@Qualifier,通过属性名来选取要注入的对象。

/**
 * 使用Junit单元测试
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {


    @Autowired
    @Qualifier("proxyAccount")
    IAccountService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer("李云龙","丁伟",200f);
    }
}

4.3 AOP

1. 概述

1.1 什么是AOP

AOP 全称是Aspect Oriented Programming,面向切面编程。通过预编译的方式和运行期动态代理实现程序功能的统一维护的一种技术。

1.2 AOP的作用及优势
作用:
在程序运行期间,不修改源码对已有方法进行增强
优势:
减少重复代码
提高开发效率
维护方便

2. AOP的相关术语

说明:spring中的AOP 是通过配置的方式实现的,而不需要写那么多的动态代理的代码。

  • Joinpoint(连接点):指被拦截到的方法。在我们案例的service层中所有的方法都是连接点。
  • Pointcut(切入点):指被增强的方法。(所有的切入点都是连接点,但连接点中被增强的那些方法才是切入点)
  • Advice(通知 / 增强):拦截到Joinpoint之后要做的事情就是通知。
    • 通知的类型分为:前置通知,后置通知,异常通知,最终通知,环绕通知。这几种通知是以切入点方法为基准的,以案例中的代码为例:
				// 整个invoke方法在执行就是环绕通知
 @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {	
                Object returnValue = null;
try {
                    //开启事务
                    txManager.beginTransaction();	//前置通知
                    //执行操作
                    returnValue = method.invoke(accountService, args);	//切入点方法调用
                    //提交事务
                    txManager.commit();		//后置通知
                    //返回结果
                    return returnValue;
                }catch (Exception e){
                    txManager.rollback();		//异常通知
                    throw new RuntimeException(e);
                }finally {
                    //释放连接
                    txManager.release();		//最终通知
                }
         }
  • Introduction(引介):
    引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态的添加一些方法或Field。
  • Target(目标对象):被代理对象
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。
  • Proxy(代理):代理对象
  • Aspect(切面):是切入点和通知(引介)的结合。也就是建立切入点方法和通知的相对关系。
学习AOP要明确的事情:
  1. 开发阶段:(我们要做的)
    • 编写核心业务代码;
    • 把公用代码抽取出来,制作成通知(开发阶段最后再做)
    • 在配置文件中,声明切入点与通知之间的关系,及切面(Aspect)
  1. 运行阶段(Spring框架来做的)
    • 监控切入点方法的执行
    • 使用代理机制,动态创建代理对象
    • 在代理对象的对应位置,将通知对应的功能织入,完成完整代码逻辑的运行。

3. spring基于xml的AOP

  1. 导入依赖坐标
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.7</version>
</dependency>

这是用来解析切入点表达式的。

  1. 编写service层代码
/**
 * 模拟业务层实现类
 */
public class AccountService implements IAccountService{

    /**
     * 模拟保存账户
     * 无返回值无参
     */
    @Override
    public void saveAccount() {
        System.out.println("执行了保存方法");
    }

    /**
     * 模拟删除账户
     * 无返回值有参
     * @param id
     */
    @Override
    public void deleteAccount(int id) {
        System.out.println("执行了删除方法");
    }

    /**
     * 模拟更新账户
     * 有返回值无参
     * @return
     */
    @Override
    public int updateAcount() {
        System.out.println("执行了更新");
        return 0;
    }
}
  1. 写一个提供公共代码的类
/**
 * 用于记录日志的工具类,他里面提供了公共的代码
 */
public class Logger {


    /**
     * 打印日志,并且在切入点方法之前执行
     */
    public void printLog(){
        System.out.println("Logger类中的printLog方法开始记录日志了");
    }
}
  1. 写配置文件
思路:
  • 先导约束
  • service层中方法在执行时需要被增强,所以配置service的bean对象
  • 日志类中的方法用于给service层中的方法做通知,所以配置日志类的bean对象
  • 配置aop的切面,配置通知类和通知方法,配置通知和切入点方法的关系
   <!--配置accountService-->
    <bean id="accountService" class="com.itcast.service.Impl.AccountService"></bean>

    <!--spring中基于xml的aop配置步骤
        1、把通知Bean也交给spring来管理
        2、使用aop:config标签表名开始aop的配置
        3、使用aop:aspect标签表明配置切面
                id属性:是给切面提供一个唯一标识
                ref属性:是指定通知类的bean的id
        4、在aop:aspect标签内部使用对应的标签来配置通知的类型
                我们现在的实例是让printLog方法在切入点方法执行之前,所以是前置通知
                aop:before:表示配置前置通知
                pointcut属性:用于指定切入点表达式,该表达式的含义是指对业务层中的那些方法增强

             切入点表达式的写法:
                    关键字:execution(表达式)
                    表达式:
                        访问修饰符   返回值 包名.包名.包名...类名.方法名(参数列表)
                    标准的表达式写法:
                        public void com.itcast.service.impl.AccountServiceImpl.saveAccount()

                        访问修饰符可以省略
                        返回值可以使用通配符*,表示任意返回值
                        包名可以使用..表示当前包及其子包
                        类名和方法名都可以使用* 来实现通配
                        参数列表:
                            可以直接写数据类型:
                                    基本数据类型直接写名称:如 int
                                    引用数据类型写 包名.类名 :java.lang.String

                    全通配的写法:
                        * *..*.*(..)
                    实际开发中切入点表达式的通产写法:
                        切到业务层实现类下所有的方法:
                            * com.itcast.service.impl.*.*(..)

    -->
    <!--配置logger-->
    <bean id="logger" class="com.itcast.utils.Logger"></bean>

    <!--配置aop-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置通知和切入点方法的关系-->
            <aop:before method="printLog" pointcut="execution( * com.itcast.service.Impl.*.*(..)))"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>
四种通知类型的配置:
前置通知、后置通知、异常通知、最终通知
<!--配置aop-->
    <aop:config>
    <!--为了简便,可以把切入点表达式单独配置。但要放在切面的前面,否则会报异常。-->
        <aop:pointcut id="pt1" expression="execution( * com.itcast.service.impl.*.*(..))"></aop:pointcut>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置通知和切入点方法的关系-->

            <!--前置通知,在切入点方法执行之前执行-->
            <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>

            <!--后置通知,在切入点方法执行之后-->
            <aop:after-returning method="afterReturnPrintLog" pointcut-ref="pt1"></aop:after-returning>

            <!--异常通知,在切入点方法产生异常时执行。他和后置通知只能执行一个-->
            <aop:after-throwing method="afterThrowPrintLog" pointcut-ref="pt1"></aop:after-throwing>

            <!--最终通知,无论切入点方法是否正常执行,他都会在其后面执行-->
            <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
            <!--<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>-->
        </aop:aspect>
    </aop:config>
环绕通知的配置:
  1. xml文件中的配置
<aop:config>
        <aop:pointcut id="pt1" expression="execution( * com.itcast.service.impl.*.*(..))"></aop:pointcut>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置通知和切入点方法的关系-->
            <!--配置环绕通知。详细注释看Logger类中-->
            <aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>
2. Logger类中的代码编写
/**
     * 环绕通知
     *
     *  问题:当我们配置了环绕通知后,切入点方法没有执行,而通知方法执行了
     *  分析:通过对比动态代理中的环绕通知代码,发现动态代理中有明确的切入点方法的调用,而此处却没有。
     *  解决:
     *      Spring框架为我们提供了一个接口:ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
     *      该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
     *
     *
     * spring环绕通知:
     *      它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
     */
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            //得到方法运行所需的参数
            Object[] args = pjp.getArgs();

            System.out.println("Logger类中的aroundPrintLog方法开始执行了记录日志.....前置");

            //明确调用业务层方法(切入点方法)
            rtValue = pjp.proceed(args);


            System.out.println("Logger类中的aroundPrintLog方法开始执行了记录日志.....后置");

            return rtValue;
        }catch (Throwable t){
            System.out.println("Logger类中的aroundPrintLog方法开始执行了记录日志.....异常");


            throw new RuntimeException(t);

        }finally {
            System.out.println("Logger类中的aroundPrintLog方法开始执行了记录日志.....最终");

        }

    }

4. spring基于注解的AOP

1、非纯注解
  1. 在xml文件中的配置
    • 导入IOC容器和AOP的约束
    • 配置spring创建容器时要扫描的包
    • 配置spring开启AOP注解的支持
<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <!--配置spring创建容器时要扫描的包-->
    <context:component-scan base-package="com.itcast"></context:component-scan>
    <!--配置spring开启AOP的注解支持-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
  1. 在通知类中配置
    • 给通知类加上注解@Component和@Aspect。
    @Component("logger")
    @Aspect //表示当前类是一个切面类
    public class Logger {
    
    1. 写一个方法,加上@Pointcut,并配置切面表达式
    //配置切面表达式
     @Pointcut("execution(* com.itcast.service.Impl.*.*(..))")  
    public void pt1(){}
    
    1. 给前四种通知加上各自的注解
    /**
     * 前置通知
     */
    @Before("pt1()")       //这个Before与Junit中的Before不同,写的时候要看是哪个包下的
    public void beforePrintLog(){
        System.out.println("前置通知:打印了日志");
    }
    
    /**
     * 后置通知
     */
    @AfterReturning("pt1()")
    public void afterReturnPrintLog(){
        System.out.println("后置通知:打印了日志");
    }
    
    /**
     * 异常通知
     */
    @AfterThrowing("pt1()")
    public void afterThrowPrintLog(){
        System.out.println("异常通知:打印了日志");
    }
    
    /**
     * 最终通知
     */
    @After("pt1()")
    public void afterPrintLog(){
        System.out.println("最终通知:打印了日志");
    }
    
    前四种通知与环绕通知是冲突的,不能同时使用。前四种通知的使用会出现通知顺序倒置的问题。这个是spring的问题,我们没有办法。所以我们可以使用环绕通知来避免这一问题。
    1. 在环绕通知方法上加注解@Around
     @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
    
2、纯注解

在上面非纯注解的基础上进行改造:
在配置类上加注解@Component和@EnableAspectJAutoProxy

@Configuration
@ComponentScan(basePackages = "com.itcast")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}

测试类中

五、JdbcTemplate以及spring的事务控制

JdbcTemplate概述

它是spring框架找你提供的一个对象,是对原始jdbc API对象的简单分装。spring框架为我们提供了很多的操作模板类。

1、JdbcTemplate的作用:

它就是用于和数据库交互的,实现对表的CRUD操作

2、如何创建该对象:

3、对象中常用的方法:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值