Spring 学习笔记

124 阅读30分钟

一.Spring_HelloWorld

1.主要依赖包
  • commons-logging.jar
  • spring-beans.jar
  • spring-context.jar
  • spring-core.jar
  • spring-expression.jar
2.Spring容器的实现
容器实现的方式
  1. BeanFactory

    IOC容器的基本实现,是 Spring 框架的基础设施,面向 Spring 本身

  2. ApplicationContext

    提供了更多的高级特性,是BeanFactory的子接口。ApplicationContext 面向使用 Spring 框架的开发者,几乎所有的应用场合都直接使用 ApplicationContext 而非底层的 BeanFactory

容器接口的实现类

image-20200817105144304

依赖注入的方式
  1. 属性注入

    • 属性注入即通过 setter 方法注入Bean 的属性值或依赖的对象
    • 属性注入使用 元素, 使用 name 属性指定 Bean 的属性名称,value 属性或 子节点指定属性值
    • ==属性注入是实际应用中最常用的注入方式==
    <bean id="helloWorld" class="com.atguigu.spring.helloworld.HelloWorld">
    		<!-- 为属性赋值 name的值要与Bean类中setter方法的后缀一致 -->
    		<property name="user" value="Jerry"></property>
    </bean>
    
  2. 构造器注入

    • xml配置

      <!-- 若一个 bean 有多个构造器, 如何通过构造器来为 bean 的属性赋值 -->
      <!-- 可以根据 index 和 value 进行更加精确的定位. (了解) -->
      <bean id="car" class="com.atguigu.spring.helloworld.Car">
         <constructor-arg value="KUGA" index="1"></constructor-arg>
         <constructor-arg value="ChangAnFord" index="0"></constructor-arg>
         <constructor-arg value="250000" type="float"></constructor-arg>
      </bean>
      
    • bean中构造器

      	public Car(String company, String brand, float price) {
      		super();
      		this.company = company;
      		this.brand = brand;
      		this.price = price;
      	}
      
  3. 工厂方法注入(很少使用,不推荐)

注入参数详解
  1. null值的注入

    可以使用专用的 元素标签为 Bean 的字符串或其它对象类型的属性注入 null 值

    <bean id="dao2" class="com.atguigu.spring.ref.Dao">
    	<!-- 为 Dao 的 dataSource 属性赋值为 null, 若某一个 bean 的属性值不是 null, 使用时需要为其设置为 null(了解) -->
    		<property name="dataSource"><null/></property>
    </bean>
    
  2. 级联属性

    <bean id="person" class="">
        <constructor-arg ref="car"></constructor-arg>
        <property name="car.maxSpeed" value="260"></property>
    </bean>
    

    注意:为级联属性赋值,属性需要先初始化后才可以为级联属性赋值,否则有异常,和struts2不同

  3. 集合属性

    • xml中配置

      以list为例:

      <bean id="person" class="com.test.Person">
          <property name="" value="Mike"></property>
          <property name="cars">
              <list>
                  <ref bean="car"></ref>
                  <ref bean="car1"></ref>
              </list>
          </property>
      </bean>
      
    • 其他集合属性

      在 Spring中可以通过一组内置的 xml 标签(例如: , 或 ) 来配置集合属性

      • 配置 java.util.List 类型的属性, 需要指定 标签, 在标签里包含一些元素. 这些标签可以通过 指定简单的常量值, 通过 指定对其他 Bean 的引用. 通过**** 指定内置 Bean 定义. 通过 指定空元素. 甚至可以内嵌其他集合

      • 数组的定义和 List 一样, 都使用

      • 配置 java.util.Set 需要使用 标签, 定义元素的方法与 List 一样

      • Java.util.Map 通过 标签定义, 标签里可以使用多个 作为子标签. 每个条目包含一个键和一个值

        <bean id="newPerson" class="com.test.NewPerson">
            <property name="name" value="Rose"></property>
            <property name="cars">
                <map>
                    <entry key="AA" value-ref="car"></entry>
                    <entry key="BB" value-ref="car1"></entry>
                </map>
            </property>
        </bean>
        
        • 必须在 标签里定义键

        • 因为键和值的类型没有限制, 所以可以自由地为它们指定 , , 元素

        • 可以将 Map 的键和值作为 的属性定义: 简单常量使用 key 和 value 来定义; Bean 引用通过 key-ref 和 value-ref 属性定义

        • 使用 定义 java.util.Properties, 该标签使用多个 作为子标签. 每个 标签必须定义 key 属性

          <bean id="dataSource" class="com.test.DataSource">
              <property name="properties">
                  <props>
                      <prop key="user">root</prop>
                      <prop key="password">123456</prop>
                      <prop key="jdbcUrl">jdbc:mysql://test</prop>
                      <prop key="driverClass">com.mysql.jdbc.Driver</prop>
                  </props>
              </property>
          </bean>
          
  4. 使用utility scheme定义集合

    使用基本的集合标签定义集合时, 不能将集合作为独立的 Bean 定义,导致其他 Bean 无法引用该集合,所以无法在不同Bean之间共享集合,可以使用 util schema 里的集合标签定义独立的集合 Bean. 需要注意的是, 必须在 根元素里添加 util schema 定义

    <util:list id="cars">
        <ref bean="car"></ref>
        <ref bean="car1"></ref>
    </util:list>
    
    <bean id="person4" class="com.test.Person">
        <property name="name" value="Jack"></property>
        <property name="age" value="24"></property>
        <property name="cars" ref="cars"></property>
    </bean>
    
  5. 使用P命名空间

    为了简化 XML 文件的配置,越来越多的 XML 文件采用属性而非子元素配置信息。

    Spring 从 2.5 版本开始引入了一个新的 p 命名空间,可以通过 元素属性的方式配置 Bean 的属性。

    使用 p 命名空间后,基于 XML 的配置方式将进一步简化。

    <bean id="person5" class="com.test.Person" p:age="30" p:name="Queen" p:cars-ref="cars"></bean>
    
自动装配

概念介绍

  1. Spring IOC 容器可以自动装配 Bean. 需要做的仅仅是在 的 autowire 属性里指定自动装配的模式
  2. byType(根据类型自动装配): 若 IOC 容器中有多个与目标 Bean 类型一致的 Bean. 在这种情况下, Spring 将无法判定哪个 Bean 最合适该属性, 所以不能执行自动装配.
  3. byName(根据名称自动装配): 必须将目标 Bean 的名称和属性名设置的完全相同.
  4. constructor(通过构造器自动装配): 当 Bean 中存在多个构造器时, 此种自动装配方式将会很复杂. 不推荐使用

实现

<bean id="address" class="com.test.Address" p:city="BeiJing" p:street="HuilongGuan"></bean>

<bean id="car" class="com.test.Car" p:brand="Audi" p:price="300000"></bean>

<!-- 指定自动装配类型为 byName Person中的其他属性会根据属性名去找相同名字的bean(id属性对应的值)-->
<bean id="person" class="com.test.Person" p:name="Tom" autowire="byName"></bean>

自动装配缺点

  • 在 Bean 配置文件里设置 autowire 属性进行自动装配将会装配 Bean 的所有属性. 然而, 若只希望装配个别属性时, autowire 属性就不够灵活了
  • autowire 属性要么根据类型自动装配, 要么根据名称自动装配, 不能两者兼而有之
  • 一般情况下,在实际的项目中很少使用自动装配功能,因为和自动装配功能所带来的好处比起来,明确清晰的配置文档更有说服力一些
Spring Bean之间的关系
  1. 继承

    使用 bean 的 parent 属性指定继承哪个bean的配置

    <bean id="address" class="com.test.Address" p:city="BeiJing" p:street="WuDaoKou"></bean>
    
    <bean id="address1"  p:street="WuDaoKou" parent="address"></bean>
    
    • Spring 允许继承 bean 的配置, 被继承的 bean 称为父 bean. 继承这个父 Bean 的 Bean 称为子 Bean
    • 子 Bean 从父 Bean 中继承配置,包括 Bean 的属性配置
    • 子 Bean 也可以覆盖从父 Bean 继承过来的配置
    • 父 Bean 可以作为配置模板, 也可以作为 Bean 实例. 若只想把父 Bean作为模板,可以设置 的abstract属性为true, 这样 Spring 将不会实例化这个 Bean
    • 并不是元素里的所有属性都会被继承. 比如: autowire, abstract 等.
    • 也可以忽略父 Bean 的 class 属性, 让子 Bean 指定自己的类, 而共享相同的属性配置. 但此时 abstract必须设为 true
  2. 依赖

    • Spring 允许用户通过 depends-on 属性设定 Bean 前置依赖的Bean,前置依赖的 Bean 会在本 Bean 实例化之前创建好
    • 如果前置依赖于多个 Bean,则可以通过逗号,空格或的方式配置 Bean 的名称
    <bean id="address" class="com.test.Address" p:city="BeiJing" p:street="WuDaoKou"></bean>
    
    <bean id="car" class="com.test.Car" p:brand="Audi" p:price="address1"></bean>
    <bean id="person" class="com.test.Person" p:name="Tom" p:address-ref="address1" depends-on="car"></bean>
    
Spring bean的作用域
  1. 使用bean 的scope属性来配置bean的作用域
  2. singleton:默认值,容器初始化时创建bean实例,在整个容器的生命周期内只创建一个bean,单例的
  3. prototype:原型的,容器初始化时不创建bean的实例,而在每次请求时都创建一个新的bean实例,并返回

scope的取值

类别说明
singleton在SpringIOC容器中仅存在一个Bean实例,Bean以单实例的方式存在
prototype每次调用getBean()时都会返回一个新的实例
request每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境
session同一个Http Session 共享一个Bean,不同的Http Session 使用不同的 bean ,该作用域仅适用于WebApplicationContext环境

配置方法

<bean id="car" class="com.test.Car" scope="prototype">
    <property name="brand" value="Audi"></property>
    <property name="price" value="300000"></property>
</bean>
使用外部属性文件
  1. 在配置文件里配置 Bean 时, 有时需要在 Bean 的配置里混入系统部署的细节信息(例如: 文件路径, 数据源配置信息等). 而这些部署细节实际上需要和 Bean 配置相分离
  2. Spring 提供了一个 PropertyPlaceholderConfigurer 的 BeanFactory 后置处理器, 这个处理器允许用户将 Bean 配置的部分内容外移到属性文件中. 可以在 Bean 配置文件里使用形式为 ${var} 的变量, PropertyPlaceholderConfigurer 从属性文件里加载属性, 并使用这些属性来替换变量
  3. Spring 还允许在属性文件中使用 ${propName},以实现属性之间的相互引用。

属性文件配置,db.properties

user=root
password=1234
driverclass=com.mysql.jdbc.Driver
jdbcurl=jdbc:mysql:///test

xml文件配置

<!--导入属性文件-->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <!--使用外部化属性文件的属性-->
    <property name="user" value="${user}"></property>
    <property name="password" value="${password}"></property>
    <property name="driverClass" value="${driverclass}"></property>
    <property name="jdbcUrl" value="${jdbcurl}"></property>
</bean>

Java中获取数据源

ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-property.xml");

DataSource dataSource = ctx.getBean("dataSouce");
System.out.println(dataSource.getConnection());

扩展:

上面的com.mchange.v2.c3p0.ComboPooledDataSource是c3p0封装的jdbc对DataSource接口的实现。

DataSource 是javax.sql.DataSource是sun公司为java和数据库连接提供的一种接口规范,比如原生的jdbc连接。

3.Spring SpEL
  • Spring 表达式语言(简称SpEL):是一个支持运行时查询和操作对象图的强大的表达式语言。
  • 语法类似于 EL:SpEL 使用 #{…} 作为定界符,所有在大框号中的字符都将被认为是 SpEL
  • SpEL 为 bean 的属性进行动态赋值提供了便利
  • 通过 SpEL 可以实现:
    • 通过 bean 的 id 对 bean 进行引用
    • 调用方法以及引用对象中的属性
    • 计算表达式的值
    • 正则表达式的匹配
SpEL 字面量

字面量的表示

  • 整数:
  • 小数:
  • 科学计数法:
  • String可以使用单引号或者双引号作为字符串的定界符号: 或
  • Boolean:
SpEL 引用Bean、属性和方法
image-20200817163550947 image-20200817164059526
SpEL支持的运算符号
image-20200817163810198 image-20200817163904383
4.Bean的生命周期管理
  1. Spring IOC 容器可以管理 Bean 的生命周期, Spring 允许在 Bean 生命周期的特定点执行定制的任务
  2. Spring IOC 容器对 Bean 的生命周期进行管理的过程:
    • 通过构造器或工厂方法创建 Bean 实例
    • 为 Bean 的属性设置值和对其他 Bean 的引用
    • 调用 Bean 的初始化方法
    • Bean 可以使用了
    • 当容器关闭时, 调用 Bean 的销毁方法(可以用ctx.close()方法模拟)
  3. 在 Bean 的声明里设置 init-method 和 destroy-method 属性, 为 Bean 指定初始化和销毁方法
Bean后置处理器
  • Bean 后置处理器允许在调用初始化方法前后对 Bean 进行额外的处理

  • Bean 后置处理器对 IOC 容器里的所有 Bean 实例逐一处理, 而非单一实例. 其典型应用是: 检查 Bean 属性的正确性或根据特定的标准更改 Bean 的属性

  • 对Bean 后置处理器而言, 需要实现BeanPostProcessor接口. 在初始化方法被调用前后, Spring 将把每个 Bean 实例分别传递给上述接口的以下两个方法:

    • postProcessBeforeInitialization()
    • postProcessAfterInitialization()

    image-20200818155116946

    在xml文件中配置bean的后置处理器

    <bean class="com.test.MyBeanPostProcessor"></bean>
    
添加Bean后置处理器后Bean的生命周期
  • 通过构造器或工厂方法创建 Bean 实例
  • 为 Bean 的属性设置值和对其他 Bean 的引用
  • ==将 Bean 实例传递给 Bean 后置处理器的 postProcessBeforeInitialization 方法==
  • 调用 Bean 的初始化方法
  • ==将 Bean 实例传递给 Bean 后置处理器的 postProcessAfterInitialization方法==
  • Bean 可以使用了
  • 当容器关闭时, 调用 Bean 的销毁方法(可以用ctx.close()方法模拟)
5.通过工厂方法配置Bean
通过调用静态工厂方法创建Bean
  • 调用静态工厂方法创建 Bean是将对象创建的过程封装到静态方法中. 当客户端需要对象时, 只需要简单地调用静态方法, 而不关心创建对象的细节
  • 要声明通过静态方法创建的 Bean, 需要在 Bean 的 class 属性里指定拥有该工厂的方法的类, 同时在 factory-method 属性里指定工厂方法的名称,最后, 使用 元素为该方法传递方法参数
/**
*  静态工厂方法,不需要创建工厂实例本身
*/
public class StaticCarFactory{
    private static Map<String,Car> cars = new HashMap<String,car>();
    static{
        cars.put("audi",new Car("audi",300000));
        cars.put("audi",new Car("ford",400000));
    }
    //静态工厂方法
    public static Car getCar(String name){
        return cars.get(name);
    }
}
<!--通过静态工厂方法来配置bean ,不是配置工厂实例-->
<bean id="car1" class="com.test.StaticCarFactory"
      factory-method="getCar">
    <constructor-arg value="audi"></constructor-arg>
</bean>
//获取bean实例的方法
public class Main{
    public static void main(String[] args){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans-factory.xml");
        Car car1 = (Car)ctx.getBean("car1");
    }
}
通过调用实例工厂方法创建Bean
  • 实例工厂方法: 将对象的创建过程封装到另外一个对象实例的方法里. 当客户端需要请求对象时, 只需要简单的调用该实例方法而不需要关心对象的创建细节
  • 要声明通过实例工厂方法创建的 Bean
    • 在 bean 的 factory-bean 属性里指定拥有该工厂方法的 Bean
    • 在 factory-method 属性里指定该工厂方法的名称
    • 使用 construtor-arg 元素为工厂方法传递方法参数
/**
*  实例工厂方法,需要创建工厂实例本身,再调用工厂的实例方法来返回bean的实例
*/
public class InstanceCarFactory{
    private static Map<String,Car> cars = null;
    
    //工厂实例构造方法
    public InstanceCarFactory(){
        cars = new HashMap<String,Car>();
        cars.put("audi",new Car("audi",300000));
        cars.put("audi",new Car("ford",400000));
    }
    
    //工厂实例获取bean的方法
    public Car getCar(String name){
        return cars.get(name);
    }
}
<!--配置工厂的实例-->
<bean id="carFactory" class="com.test.InstanceCarFactory"></bean>

<!--通过实例工厂方法来配置bean ,不是配置工厂实例-->
<bean id="car2" factory-bean="carFactory"
      factory-method="getCar">
    <constructor-arg value="ford"></constructor-arg>
</bean>
//获取bean实例的方法
public class Main{
    public static void main(String[] args){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans-factory.xml");
        Car car2 = (Car)ctx.getBean("car2");
    }
}
静态工厂方法和实例工厂方法对比
静态工厂方法实例工厂方法
要不要配置工厂bean不要配置工厂bean要配置工厂bean
创建bean的过程中要不要生成工厂实例不用生成工厂实例要生成工厂实例
要不要工厂构造器不需要工厂构造器,需要生成的bean实例在静态代码块中生成需要工厂构造器,需要生成的bean实例在工厂的构造方法中生成
通过工厂获取bean的方式静态方法获取实例方法获取
6.通过FactoryBean配置bean
  • Spring 中有两种类型的 Bean, 一种是普通Bean, 另一种是工厂Bean, 即FactoryBean
  • 工厂 Bean 跟普通Bean不同, 其返回的对象不是指定类的一个实例, 其返回的是该工厂 Bean 的 getObject 方法所返回的对象
public class CarFacroryBean implements FactoryBean<Car> {
    private String brand;

    public void setBrand(String brand) {
        this.brand = brand;
    }

    @Override
    public Car getObject() throws Exception {
        return new Car(brand,500000);
    }

    @Override
    public Class<?> getObjectType() {
        return Car.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}
<!--通过FactoryBean来配置Bean的实例
class:指向的是FactoryBean的全类名
property:配置FactoryBean的属性

但实际返回的实例却是FactoryBean 的 getObject() 方法返回的对象
-->
<bean id="car" class="com.atguigu.spring.helloworld.CarFacroryBean">
    <property name="brand" value="BMW"></property>
</bean>
7.通过注解配置bean
在classpath中扫描组件
  • 组件扫描(component scanning): Spring 能够从 classpath 下自动扫描, 侦测和实例化具有特定注解的组件.

  • 特定组件包括:

    • @Component: 基本注解, 标识了一个受 Spring 管理的组件
    • @Respository: 标识持久层组件
    • @Service: 标识服务层(业务层)组件
    • @Controller: 标识表现层组件
  • 对于扫描到的组件, Spring 有默认的命名策略: 使用非限定类名, 第一个字母小写. 也可以在注解中通过 value 属性值标识组件的名称

  • 当在组件类上使用了特定的注解之后, 还需要在 Spring 的配置文件中声明 context:component-scan

    • base-package 属性指定一个需要扫描的基类包,Spring 容器将会扫描这个基类包里及其子包中的所有类.

    • 当需要扫描多个包时, 可以使用逗号分隔.

    • 如果仅希望扫描特定的类而非基包下的所有类,可使用 resource-pattern 属性过滤特定的类,示例:

      <context:component-scan base-package="com.atguigu.spring.beans" 
                              resource-pattern="autowire/*.class"/>
      
    • context:include-filter 子节点表示要包含的目标类

    • context:exclude-filter 子节点表示要排除在外的目标类

    • context:component-scan 下可以拥有若干个 context:include-filtercontext:exclude-filter 子节点

  • context:include-filtercontext:exclude-filter 子节点支持多种类型的过滤表达式:

    image-20200819113120520

  • context:component-scan 元素还会自动注册 AutowiredAnnotationBeanPostProcessor 实例, 该实例可以自动装配具有 @Autowired 和 @Resource 、@Inject注解的属性

使用@Autowired自动装配Bean
  • 构造器, 普通字段(即使是非 public), 一切具有参数的方法都可以应用@Authwired 注解
  • 默认情况下, 所有使用 @Authwired 注解的属性都需要被设置. 当 Spring 找不到匹配的 Bean 装配属性时, 会抛出异常, 若某一属性允许不被设置, 可以设置 @Authwired 注解的 required 属性为 false
  • 默认情况下, 当 IOC 容器里存在多个类型兼容的 Bean 时, 通过类型的自动装配将无法工作. 此时可以在 @Qualifier 注解里提供 Bean 的名称. Spring 允许对方法的入参标注 @Qualifiter 已指定注入 Bean 的名称
  • @Authwired 注解也可以应用在数组类型的属性上, 此时 Spring 将会把所有匹配的 Bean 进行自动装配.
  • @Authwired 注解也可以应用在集合属性上, 此时 Spring 读取该集合的类型信息, 然后自动装配所有与之兼容的 Bean.
  • @Authwired 注解用在 java.util.Map 上时, 若该 Map 的键值为 String, 那么 Spring 将自动装配与之 Map 值类型兼容的 Bean, 此时 Bean 的名称作为键值
8.泛型依赖注入

Spring 4.x 中可以为子类注入子类对应的泛型类型的成员变量的引用

image-20200819154223271

实现方式:

  1. 父类

    public class BaseService<T> {
    
    	@Autowired
    	private BaseDao<T> dao;
    	
    	public void addNew(T entity){
    		System.out.println("addNew by " + dao);
    		dao.save(entity);
    	}
    	
    }
    
    public class BaseDao<T> {
    
    	public void save(T entity){
    		System.out.println("Save:" + entity);
    	}
    	
    }
    
    
    
    
  2. 子类

    //若注解没有指定 bean 的 id, 则类名第一个字母小写即为 bean 的 id
    @Service
    public class UserService extends BaseService<User>{
    
    }
    
    @Repository
    public class UserDao extends BaseDao<User>{
    
    }
    
  3. 泛型类

    public class User {
    
    }
    
  4. 使用

    public class Main {
    	
    	public static void main(String[] args) {
    		
    		ApplicationContext ctx = new ClassPathXmlApplicationContext("beans-annotation.xml");
    		
    		UserService userService = (UserService) ctx.getBean("userService");
    		userService.addNew(new User());
    		
    		RoleService roleService = (RoleService) ctx.getBean("roleService");
    		roleService.addNew(new Role()); 
    	}
    	
    }
    
9.整合多个配置文件

Spring 允许通过 将多个配置文件引入到一个文件中,进行配置文件的集成。这样在启动 Spring 容器时,仅需要指定这个合并好的配置文件就可以。

import 元素的 resource 属性支持 Spring 的标准的路径资源

image-20200819154932137

10.Spring AOP基础
AOP前奏

为什么要用AOP?

通过案例分析:

image-20200819155618535

代码实现:

image-20200819160059317

这样实现的问题:

  1. 代码混乱:越来越多的非业务需求(日志和验证等)加入后, 原有的业务方法急剧膨胀. 每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点.
  2. 代码分散: 以日志需求为例, 只是为了满足这个单一需求, 就不得不在多个模块(方法)里多次重复相同的日志代码. 如果日志需求发生变化, 必须修改所有模块.

使用动态代理解决上述问题

代理设计模式的原理: 使用一个代理将对象包装起来, 然后用该代理对象取代原始对象. 任何对原始对象的调用都要通过代理. 代理对象决定是否以及何时将方法调用转到原始对象上

image-20200819162402602

使用动态代理的方式可以解决上述问题,但是实现逻辑比较复杂,实现方式略。

最佳的实现方式应该是使用Spring AOP来实现。

AOP简介
  • AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面向对象编程) 的补充.
  • AOP 的主要编程对象是切面(aspect), 而切面模块化横切关注点.
  • 在应用 AOP 编程时, 仍然需要定义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类. 这样一来横切关注点就被模块化到特殊的对象(切面)里.
  • AOP 的好处:
    • 每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级
    • 业务模块更简洁, 只包含核心业务代码.
image-20200819162815706
AOP术语
  • 切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
  • 通知(Advice): 切面必须要完成的工作
  • 目标(Target): 被通知的对象
  • 代理(Proxy): 向目标对象应用通知之后创建的对象
  • 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置
  • 切点(pointcut):每个类都拥有多个连接点:例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
启用AspectJ注解
  • 要在 Spring 应用中使用 AspectJ 注解, 必须在 classpath 下包含 AspectJ 类库: aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar
  • 将 aop Schema 添加到 根元素中.
  • 要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素 aop:aspectj-autoproxy
  • 当 Spring IOC 容器侦测到 Bean 配置文件中的 aop:aspectj-autoproxy 元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理.
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

	<!-- 自动扫描的包 -->
	<context:component-scan base-package="com.atguigu.spring.aop"></context:component-scan>

	<!-- 使 AspectJ 的注解起作用 -->
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>
//通过添加 @Aspect 注解声明一个 bean 是一个切面!
@Aspect
@Component
public class LoggingAspect {

	@Before("execution(public int com.atguigu.spring.aop.ArithmeticCalculator.*(int, int))")
	public void beforeMethod(JoinPoint joinPoint){
		String methodName = joinPoint.getSignature().getName();
		Object [] args = joinPoint.getArgs();
		
		System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
	}
	
	@After("execution(* com.atguigu.spring.aop.*.*(..))")
	public void afterMethod(JoinPoint joinPoint){
		String methodName = joinPoint.getSignature().getName();
		System.out.println("The method " + methodName + " ends");
	}
	
}
  • AspectJ 支持 5 种类型的通知注解:
    • @Before: 前置通知, 在方法执行之前执行
    • @After: 后置通知, 在方法执行之后执行
    • @AfterRunning: 返回通知, 在方法返回结果之后执行
    • @AfterThrowing: 异常通知, 在方法抛出异常之后
    • @Around: 环绕通知, 围绕着方法执行
前置通知
  • 前置通知:在方法执行之前执行的通知
  • 前置通知使用 @Before 注解, 并将切入点表达式的值作为注解值
image-20200819170745837
  • 最典型的==切入点表达式==时根据==方法的签名==来匹配各种方法:

    • execution * com.atguigu.spring.ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中声明的所有方法,第一个 * 代表任意修饰符及任意返回值. 第二个 * 代表任意方法, .. 匹配任意数量的参数. 若目标类与接口与该切面在同一个包中, 可以省略包名.
    • execution public * ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 接口的所有公有方法.
    • execution public double ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中返回 double 类型数值的方法
    • execution public double ArithmeticCalculator.*(double, ..): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数
    • execution public double ArithmeticCalculator.*(double, double): 匹配参数类型为 double, double 类型的方法.
  • 合并切入点表达式

    在 AspectJ 中, 切入点表达式可以通过操作符 &&, ||, ! 结合起来

    image-20200819171626310
  • 让通知访问当前连接点的细节

    image-20200819173231546
后置通知
  • 后置通知是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候, 下面的后置通知记录了方法的终止.
  • 一个切面可以包括一个或者多个通知.
image-20200819174354160
返回通知

无论连接点是正常返回还是抛出异常, 后置通知都会执行. 如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知.

image-20200819174854961

在返回通知中访问连接点的返回值:

  • 在返回通知中, 只要将 returning 属性添加到 @AfterReturning 注解中, 就可以访问连接点的返回值. 该属性的值即为用来传入返回值的参数名称.
  • 必须在通知方法的签名中添加一个同名参数. 在运行时, Spring AOP 会通过这个参数传递返回值.
  • 原始的切点表达式需要出现在 pointcut 属性中
image-20200819175020473
异常通知
  • 只在连接点抛出异常时才执行异常通知
  • 将 throwing 属性添加到 @AfterThrowing 注解中, 也可以访问连接点抛出的异常. Throwable 是所有错误和异常类的超类. 所以在异常通知方法可以捕获到任何错误和异常.
  • 如果只对某种特殊的异常类型感兴趣, 可以将参数声明为其他异常的参数类型. 然后通知就只在抛出这个类型及其子类的异常时才被执行.
image-20200819175632395
环绕通知
  • 环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点. 甚至可以控制是否执行连接点.
  • 对于环绕通知来说, 连接点的参数类型必须是 ProceedingJoinPoint . 它是 JoinPoint 的子接口, 允许控制何时执行, 是否执行连接点.
  • 在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法. 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行.
  • 注意: 环绕通知的方法需要返回目标方法执行之后的结果, 即调用 joinPoint.proceed(); 的返回值, 否则会出现空指针异常
image-20200819175903102
	/**
	 * 环绕通知需要携带 ProceedingJoinPoint 类型的参数. 
	 * 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
	 * 且环绕通知必须有返回值, 返回值即为目标方法的返回值
	 */
	@Around("execution(public int com.atguigu.spring.aop.ArithmeticCalculator.*(..))")
	public Object aroundMethod(ProceedingJoinPoint pjd){
		
		Object result = null;
		String methodName = pjd.getSignature().getName();
		
		try {
			//前置通知
			System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
			//执行目标方法并返回返回值
			result = pjd.proceed();
			//返回通知
			System.out.println("The method " + methodName + " ends with " + result);
		} catch (Throwable e) {
			//异常通知
			System.out.println("The method " + methodName + " occurs exception:" + e);
			throw new RuntimeException(e);
		}
		//后置通知
		System.out.println("The method " + methodName + " ends");
		
		return result;
	}
11.切面的优先级
  • 在同一个连接点上应用不止一个切面时, 除非明确指定, 否则它们的优先级是不确定的.
  • 切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定.
  • 实现 Ordered 接口, getOrder() 方法的返回值越小, 优先级越高.
  • 若使用 @Order 注解, 序号出现在注解中
@Order(2)
@Aspect
@Component
public class LoggingAspect {
}
12.重用切入点表达式
  • 在编写 AspectJ 切面时, 可以直接在通知注解中书写切入点表达式. 但同一个切点表达式可能会在多个通知中重复出现.
  • 在 AspectJ 切面中, 可以通过 @Pointcut 注解将一个切入点声明成简单的方法. 切入点的方法体通常是空的, 因为将切入点定义与应用程序逻辑混在一起是不合理的.
  • 切入点方法的访问控制符同时也控制着这个切入点的可见性. 如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中. 在这种情况下, 它们必须被声明为 public. 在引入这个切入点时, 必须将类名也包括在内. 如果类没有与这个切面放在同一个包中, 还必须包含包名.
  • 其他通知可以通过方法名称引入该切入点
	/**
	 * 定义一个方法, 用于声明切入点表达式. 一般地, 该方法中再不需要添入其他的代码. 
	 * 使用 @Pointcut 来声明切入点表达式. 
	 * 后面的其他通知直接使用方法名来引用当前的切入点表达式. 
	 */
	@Pointcut("execution(public int com.atguigu.spring.aop.ArithmeticCalculator.*(..))")
	public void declareJointPointExpression(){}
	
	/**
	 * 在 com.atguigu.spring.aop.ArithmeticCalculator 接口的每一个实现类的每一个方法开始之前执行一段代码
	 */
	@Before("declareJointPointExpression()")
	public void beforeMethod(JoinPoint joinPoint){
		String methodName = joinPoint.getSignature().getName();
		Object [] args = joinPoint.getArgs();
		
		System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
	}
@Order(1)
@Aspect
@Component
public class VlidationAspect {
    //如果这个类与申明切入点的方法所在的类在同一个包下,则下面的包名可以省略
	@Before("com.atguigu.spring.aop.LoggingAspect.declareJointPointExpression()")
	public void validateArgs(JoinPoint joinPoint){
		System.out.println("-->validate:" + Arrays.asList(joinPoint.getArgs()));
	}
	
}
13.基于配置文件的方式配置AOP
  • 除了使用 AspectJ 注解声明切面, Spring 也支持在 Bean 配置文件中声明切面. 这种声明是通过 aop schema 中的 XML 元素完成的.

  • 正常情况下, 基于注解的声明要优先于基于 XML 的声明. 通过 AspectJ 注解, 切面可以与 AspectJ 兼容, 而基于 XML 的配置则是 Spring 专有的. 由于 AspectJ 得到越来越多的 AOP 框架支持, 所以以注解风格编写的切面将会有更多重用的机会.

  • 当使用 XML 声明切面时, 需要在 根元素中导入 aop Schema

  • 在 Bean 配置文件中, 所有的 Spring AOP 配置都必须定义在 aop:config 元素内部. 对于每个切面而言, 都要创建一个 aop:aspect 元素来为具体的切面实现引用后端 Bean 实例.

  • 切面 Bean 必须有一个标示符, 供 aop:aspect 元素引用

<?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"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

	<!-- 配置 bean -->
	<bean id="arithmeticCalculator" 
		class="com.atguigu.spring.aop.xml.ArithmeticCalculatorImpl"></bean>

	<!-- 配置切面的 bean. -->
	<bean id="loggingAspect"
		class="com.atguigu.spring.aop.xml.LoggingAspect"></bean>

	<bean id="vlidationAspect"
		class="com.atguigu.spring.aop.xml.VlidationAspect"></bean>

	<!-- 配置 AOP -->
	<aop:config>
		<!-- 配置切点表达式 -->
		<aop:pointcut expression="execution(* com.atguigu.spring.aop.xml.ArithmeticCalculator.*(int, int))" 
			id="pointcut"/>
		<!-- 配置切面及通知 -->
		<aop:aspect ref="loggingAspect" order="2">
			<aop:before method="beforeMethod" pointcut-ref="pointcut"/>
			<aop:after method="afterMethod" pointcut-ref="pointcut"/>
			<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
			<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
			<!--  
			<aop:around method="aroundMethod" pointcut-ref="pointcut"/>
			-->
		</aop:aspect>	
		<aop:aspect ref="vlidationAspect" order="1">
			<aop:before method="validateArgs" pointcut-ref="pointcut"/>
		</aop:aspect>
	</aop:config>

</beans>

14.Spring使用JdbcTemplate
  • 为了使 JDBC 更加易于使用, Spring 在 JDBC API 上定义了一个抽象层, 以此建立一个 JDBC 存取框架.
  • 作为 Spring JDBC 框架的核心, JDBC 模板的设计目的是为不同类型的 JDBC 操作提供模板方法. 每个模板方法都能控制整个过程, 并允许覆盖过程中的特定任务. 通过这种方式, 可以在尽可能保留灵活性的情况下, 将数据库存取的工作量降到最低.
JdbcTemplate使用方法
  1. 在applicationContext.xml文件中导入资源文件并配置数据源

    	<!-- 导入资源文件 -->
    	<context:property-placeholder location="classpath:db.properties"/>
    	
    	<!-- 配置 C3P0 数据源 -->
    	<bean id="dataSource"
    		class="com.mchange.v2.c3p0.ComboPooledDataSource">
    		<property name="user" value="${jdbc.user}"></property>
    		<property name="password" value="${jdbc.password}"></property>
    		<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
    		<property name="driverClass" value="${jdbc.driverClass}"></property>
    
    		<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
    		<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
    	</bean>
    

    配置db.properties数据源

    jdbc.user=root
    jdbc.password=1230
    jdbc.driverClass=com.mysql.jdbc.Driver
    jdbc.jdbcUrl=jdbc:mysql:///spring
    
    jdbc.initPoolSize=5
    jdbc.maxPoolSize=10
    
  2. 在applicationContext.xml文件中配置Spring的JdbcTemplate

    配置jdbcTemplate Bean对象,并将第1步中创建的dataSource引入jdbcTemplate

    	<!-- 配置 Spirng 的 JdbcTemplate -->
    	<bean id="jdbcTemplate" 
    		class="org.springframework.jdbc.core.JdbcTemplate">
    		<property name="dataSource" ref="dataSource"></property>
    	</bean>
    
  3. 使用jdbcTemplate执行增删改查操作

    	private ApplicationContext ctx = null;
    	private JdbcTemplate jdbcTemplate;
    	
    	{
    		ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    		jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
    	}
    
    
    
       /**
    	 * 查到实体类的集合
    	 * 注意调用的不是 queryForList 方法
    	 */
    	@Test
    	public void testQueryForList(){
    		String sql = "SELECT id, last_name lastName, email FROM employees WHERE id > ?";
    		RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
    		List<Employee> employees = jdbcTemplate.query(sql, rowMapper,5);
    		
    		System.out.println(employees);
    	}
    	
    	/**
    	 * 从数据库中获取一条记录, 实际得到对应的一个对象
    	 * 注意不是调用 queryForObject(String sql, Class<Employee> requiredType, Object... args) 方法!
    	 * 而需要调用 queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args)
    	 * 1. 其中的 RowMapper 指定如何去映射结果集的行, 常用的实现类为 BeanPropertyRowMapper
    	 * 2. 使用 SQL 中列的别名完成列名和类的属性名的映射. 例如 last_name lastName
    	 * 3. 不支持级联属性. JdbcTemplate 到底是一个 JDBC 的小工具, 而不是 ORM 框架
    	 */
    	@Test
    	public void testQueryForObject(){
    		String sql = "SELECT id, last_name lastName, email, dept_id as \"department.id\" FROM employees WHERE id = ?";
    		RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
    		Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1);
    		
    		System.out.println(employee);
    	}
    	
    	/**
    	 * 执行批量更新: 批量的 INSERT, UPDATE, DELETE
    	 * 最后一个参数是 Object[] 的 List 类型: 因为修改一条记录需要一个 Object 的数组, 那么多条不就需要多个 Object 的数组吗
    	 */
    	@Test
    	public void testBatchUpdate(){
    		String sql = "INSERT INTO employees(last_name, email, dept_id) VALUES(?,?,?)";
    		
    		List<Object[]> batchArgs = new ArrayList<>();
    		
    		batchArgs.add(new Object[]{"AA", "aa@atguigu.com", 1});
    		batchArgs.add(new Object[]{"BB", "bb@atguigu.com", 2});
    		batchArgs.add(new Object[]{"CC", "cc@atguigu.com", 3});
    		batchArgs.add(new Object[]{"DD", "dd@atguigu.com", 3});
    		batchArgs.add(new Object[]{"EE", "ee@atguigu.com", 2});
    		
    		jdbcTemplate.batchUpdate(sql, batchArgs);
    	}
    	
    	/**
    	 * 执行 INSERT, UPDATE, DELETE
    	 */
    	@Test
    	public void testUpdate(){
    		String sql = "UPDATE employees SET last_name = ? WHERE id = ?";
    		jdbcTemplate.update(sql, "Jack", 5);
    	}
    
NamedParameterJdbcTemplate具名参数模板的使用
  • 在经典的 JDBC 用法中, SQL 参数是用占位符 ? 表示,并且受到位置的限制. 定位参数的问题在于, 一旦参数的顺序发生变化, 就必须改变参数绑定.
  • 在 Spring JDBC 框架中, 绑定 SQL 参数的另一种选择是使用具名参数(named parameter).
  • 具名参数: SQL 按名称(以冒号开头)而不是按位置进行指定. 具名参数更易于维护, 也提升了可读性. 具名参数由框架类在运行时用占位符取代
  • 具名参数只在 NamedParameterJdbcTemplate 中得到支持

这里只说明具名参数模板不一样的地方:

  1. 配置 namedParameterJdbcTemplate Bean对象

    	<!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 -->
    	<bean id="namedParameterJdbcTemplate"
    		class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
    		<constructor-arg ref="dataSource"></constructor-arg>	
    	</bean>
    
    
  2. 执行增删改查操作

    	private ApplicationContext ctx = null;
    	private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    	
    	{
    		ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    		namedParameterJdbcTemplate = ctx.getBean(NamedParameterJdbcTemplate.class);
    	}
    
    
    
        /**
    	 * 使用具名参数时, 可以使用 update(String sql, SqlParameterSource paramSource) 方法进行更新操作
    	 * 1. SQL 语句中的参数名和类的属性一致!
    	 * 2. 使用 SqlParameterSource 的 BeanPropertySqlParameterSource 实现类作为参数. 
    	 */
    	@Test
    	public void testNamedParameterJdbcTemplate2(){
    		String sql = "INSERT INTO employees(last_name, email, dept_id) "
    				+ "VALUES(:lastName,:email,:dpetId)";
    		
    		Employee employee = new Employee();
    		employee.setLastName("XYZ");
    		employee.setEmail("xyz@sina.com");
    		employee.setDpetId(3);
    		
    		SqlParameterSource paramSource = new BeanPropertySqlParameterSource(employee);
    		namedParameterJdbcTemplate.update(sql, paramSource);
    	}
    	
    	/**
    	 * 可以为参数起名字. 
    	 * 1. 好处: 若有多个参数, 则不用再去对应位置, 直接对应参数名, 便于维护
    	 * 2. 缺点: 较为麻烦. 
    	 */
    	@Test
    	public void testNamedParameterJdbcTemplate(){
    		String sql = "INSERT INTO employees(last_name, email, dept_id) VALUES(:ln,:email,:deptid)";
    		
    		Map<String, Object> paramMap = new HashMap<>();
    		paramMap.put("ln", "FF");
    		paramMap.put("email", "ff@atguigu.com");
    		paramMap.put("deptid", 2);
    		
    		namedParameterJdbcTemplate.update(sql, paramMap);
    	}
    
15.Spring 事务管理
  • 作为企业级应用程序框架, Spring 在不同的事务管理 API 之上定义了一个抽象层. 而应用程序开发人员不必了解底层的事务管理 API, 就可以使用 Spring 的事务管理机制.
  • Spring 既支持编程式事务管理, 也支持声明式的事务管理. 编程式事务管理: 将事务管理代码嵌入到业务方法中来控制事务的提交和回滚. 在编程式管理事务时, 必须在每个事务操作中包含额外的事务管理代码.
  • 声明式事务管理: 大多数情况下比编程式事务管理更好用. 它将事务管理代码从业务方法中分离出来, 以声明的方式来实现事务管理. 事务管理作为一种横切关注点, 可以通过 AOP 方法模块化. Spring 通过 Spring AOP 框架支持声明式事务管理.
声明式事务

声明式事务开发步骤

  1. 配置事务管理器并启用事务注解

    	<!-- 配置事务管理器 -->
    	<bean id="transactionManager" 
    		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    		<property name="dataSource" ref="dataSource"></property>
    	</bean>
    	
    	<!-- 启用事务注解 -->
    	<tx:annotation-driven transaction-manager="transactionManager"/>
    
  2. 在对应的方法上添加事务注解

    @Transactional
    public void purchase(String username,String isbn){
        
    }
    
事务传播属性及行为

当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播. 例如: 方法可能继续在现有事务中运行, 也可能开启一个新事务, 并在自己的事务中运行. 事务的传播行为可以由传播属性指定. Spring 定义了 7 种类传播行为.

image-20200821162244519
并发事务所导致的问题
  • 当同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题
  • 并发事务所导致的问题可以分为下面三种类型:
    • 脏读: 对于两个事物 T1, T2, T1 读取了已经被 T2 更新但 还没有被提交的字段. 之后, 若 T2 回滚, T1读取的内容就是临时且无效的.
    • 不可重复读:对于两个事物 T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段. 之后, T1再次读取同一个字段, 值就不同了.
    • 幻读:对于两个事物 T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行. 之后, 如果 T1 再次读取同一个表, 就会多出几行.
事务的隔离级别
  • 从理论上来说, 事务应该彼此完全隔离, 以避免并发事务所导致的问题. 然而, 那样会对性能产生极大的影响, 因为事务必须按顺序运行.
  • 在实际开发中, 为了提升性能, 事务会以较低的隔离级别运行.
  • 事务的隔离级别可以通过隔离事务属性指定

Spring支持的事务隔离级别:

image-20200821165341762
  • 事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持.
  • Oracle 支持的 2 种事务隔离级别:READ_COMMITED , SERIALIZABLE
  • Mysql 支持 4 中事务隔离级别.
回滚事务属性

使用如下的属性可以指定回滚规则

noRollbackFor={UserAccountException.class},//指定的异常不进行回滚
rollbackFor={}//指定的异常需要回滚
超时和只读属性
  • 由于事务可以在行和表上获得锁, 因此长事务会占用资源, 并对整体性能产生影响.
  • 如果一个事物只读取数据但不做修改, 数据库引擎可以对这个事务进行优化.
  • 超时事务属性: 事务在强制回滚之前可以保持多久. 这样可以防止长期运行的事务占用资源.
  • 只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务.
使用范例
	//1.使用 propagation 指定事务的传播行为, 即当前的事务方法被另外一个事务方法调用时
	//如何使用事务, 默认取值为 REQUIRED, 即使用调用方法的事务
	//REQUIRES_NEW: 事务自己的事务, 调用的事务方法的事务被挂起. 
	//2.使用 isolation 指定事务的隔离级别, 最常用的取值为 READ_COMMITTED
	//3.默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚. 也可以通过对应的
	//属性进行设置. 通常情况下去默认值即可. 
	//4.使用 readOnly 指定事务是否为只读. 表示这个事务只读取数据但不更新数据, 
	//这样可以帮助数据库引擎优化事务. 若真的事一个只读取数据库值的方法, 应设置 readOnly=true
	//5.使用 timeout 指定强制回滚之前事务可以占用的时间.  
//	@Transactional(propagation=Propagation.REQUIRES_NEW,
//			isolation=Isolation.READ_COMMITTED,
//			noRollbackFor={UserAccountException.class})
	@Transactional(propagation=Propagation.REQUIRES_NEW,
			isolation=Isolation.READ_COMMITTED,
			readOnly=false,
			timeout=3)
	@Override
	public void purchase(String username, String isbn) {
        
    }
使用xml文件方式配置事务
	<!-- 1. 配置事务管理器 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<!-- 2. 配置事务属性 -->
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<!-- 根据方法名指定事务的属性 -->
			<tx:method name="purchase" propagation="REQUIRES_NEW"/>
			<tx:method name="get*" read-only="true"/>
			<tx:method name="find*" read-only="true"/>
			<tx:method name="*"/>
		</tx:attributes>
	</tx:advice>
	
	<!-- 3. 配置事务切入点, 以及把事务切入点和事务属性关联起来 -->
	<aop:config>
		<aop:pointcut expression="execution(* com.atguigu.spring.tx.xml.service.*.*(..))" 
			id="txPointCut"/>
		<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>	
	</aop:config>
16.Spring 整合Hibernate
Spring 整合 Hibernate 整合什么
  1. 让IOC 容器来管理 Hibernate 的 SessionFactory
  2. 让 Hibernate 使用上 Spring 的声明式事务
整合步骤
  1. 加入Hibernate

    • 添加jar

      hibernate-commons-annotations-4.0.2.Final.jar
      hibernate-core-4.2.4.Final.jar
      hibernate-jpa-2.0-api-1.0.1.Final.jar
      
    • 添加Hibernate的配置文件hibernate.cfg.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE hibernate-configuration PUBLIC
      		"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
      		"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
      <hibernate-configuration>
          <session-factory>
          
          	<!-- 配置 hibernate 的基本属性 -->
          	<!-- 1. 数据源需配置到 IOC 容器中, 所以在此处不再需要配置数据源 -->
          	<!-- 2. 关联的 .hbm.xml 也在 IOC 容器配置 SessionFactory 实例时在进行配置 -->
          	<!-- 3. 配置 hibernate 的基本属性: 方言, SQL 显示及格式化, 生成数据表的策略以及二级缓存等. -->
          	<property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
      
      		<property name="hibernate.show_sql">true</property>
      		<property name="hibernate.format_sql">true</property>
      		
      		<property name="hibernate.hbm2ddl.auto">update</property>
      		
      		<!-- 配置 hibernate 二级缓存相关的属性. -->
      		    	
          </session-factory>
      </hibernate-configuration>
      
      
    • 编写持久化类对应的 .hbm.xml 文件

      以Account.hbm.xml为例:

      <?xml version="1.0"?>
      <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
      "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
      
      <hibernate-mapping>
          <class name="com.atguigu.spring.hibernate.entities.Account" table="SH_ACCOUNT">
              
              <id name="id" type="java.lang.Integer">
                  <column name="ID" />
                  <generator class="native" />
              </id>
              
              <property name="username" type="java.lang.String">
                  <column name="USERNAME" />
              </property>
              
              <property name="balance" type="int">
                  <column name="BALANCE" />
              </property>
              
          </class>
      </hibernate-mapping>
      
      
  2. 加入Spring

    • 添加jar

      spring-aop-4.0.0.RELEASE.jar
      spring-aspects-4.0.0.RELEASE.jar
      spring-beans-4.0.0.RELEASE.jar
      spring-context-4.0.0.RELEASE.jar
      spring-core-4.0.0.RELEASE.jar
      spring-expression-4.0.0.RELEASE.jar
      spring-jdbc-4.0.0.RELEASE.jar
      spring-orm-4.0.0.RELEASE.jar
      spring-tx-4.0.0.RELEASE.jar
      spring-web-4.0.0.RELEASE.jar
      spring-webmvc-4.0.0.RELEASE.jar
      
    • 添加Spring的配置文件applicationContext.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"
      	xmlns:tx="http://www.springframework.org/schema/tx"
      	xmlns:aop="http://www.springframework.org/schema/aop"
      	xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
      		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
      		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
      	
      	<!-- 配置自动扫描的包 -->
      	<context:component-scan base-package="com.atguigu.spring.hibernate"></context:component-scan>
      	
      	<!-- 配置数据源 -->
      	<!-- 导入资源文件 -->
      	<context:property-placeholder location="classpath:db.properties"/>
      	
      	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
      		<property name="user" value="${jdbc.user}"></property>
      		<property name="password" value="${jdbc.password}"></property>
      		<property name="driverClass" value="${jdbc.driverClass}"></property>
      		<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
      
      		<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
      		<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
      	</bean>
      	
      	<!-- 配置 Hibernate 的 SessionFactory 实例: 通过 Spring 提供的 LocalSessionFactoryBean 进行配置 -->
      	<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
      		<!-- 配置数据源属性 -->
      		<property name="dataSource" ref="dataSource"></property>
      		<!-- 配置 hibernate 配置文件的位置及名称 -->
      		<!--  
      		<property name="configLocation" value="classpath:hibernate.cfg.xml"></property>
      		-->
      		<!-- 使用 hibernateProperties 属相来配置 Hibernate 原生的属性 -->
      		<property name="hibernateProperties">
      			<props>
      				<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
      				<prop key="hibernate.show_sql">true</prop>
      				<prop key="hibernate.format_sql">true</prop>
      				<prop key="hibernate.hbm2ddl.auto">update</prop>
      			</props>
      		</property>
      		<!-- 配置 hibernate 映射文件的位置及名称, 可以使用通配符 -->
      		<property name="mappingLocations" 
      			value="classpath:com/atguigu/spring/hibernate/entities/*.hbm.xml"></property>
      	</bean>
      
      	<!-- 配置 Spring 的声明式事务 -->
      	<!-- 1. 配置事务管理器 -->
      	<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
      		<property name="sessionFactory" ref="sessionFactory"></property>
      	</bean>
      
      	<!-- 2. 配置事务属性, 需要事务管理器 -->
      	<tx:advice id="txAdvice" transaction-manager="transactionManager">
      		<tx:attributes>
      			<tx:method name="get*" read-only="true"/>
      			<tx:method name="purchase" propagation="REQUIRES_NEW"/>
      			<tx:method name="*"/>
      		</tx:attributes>
      	</tx:advice>
      
      	<!-- 3. 配置事务切点, 并把切点和事务属性关联起来 -->
      	<aop:config>
      		<aop:pointcut expression="execution(* com.atguigu.spring.hibernate.service.*.*(..))" 
      			id="txPointcut"/>
      		<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
      	</aop:config>
      
      </beans>
      
      
    • 配置数据源db.properties

      jdbc.user=root
      jdbc.password=1230
      jdbc.driverClass=com.mysql.jdbc.Driver
      jdbc.jdbcUrl=jdbc:mysql:///spring7
      
      jdbc.initPoolSize=5
      jdbc.maxPoolSize=10
      
  3. 整合

    主要是Dao中的区别

    package com.atguigu.spring.hibernate.dao.impl;
    
    import org.hibernate.Query;
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Repository;
    
    import com.atguigu.spring.hibernate.dao.BookShopDao;
    import com.atguigu.spring.hibernate.exceptions.BookStockException;
    import com.atguigu.spring.hibernate.exceptions.UserAccountException;
    
    @Repository
    public class BookShopDaoImpl implements BookShopDao {
    	
    	@Autowired
    	private SessionFactory sessionFactory;
    	
    	//不推荐使用 HibernateTemplate 和 HibernateDaoSupport
    	//因为这样会导致 Dao 和 Spring 的 API 进行耦合
    	//可以移植性变差
    //	private HibernateTemplate hibernateTemplate;
    	
    	//获取和当前线程绑定的 Session. 
    	private Session getSession(){
    		return sessionFactory.getCurrentSession();
    	}
    
    	@Override
    	public int findBookPriceByIsbn(String isbn) {
    		String hql = "SELECT b.price FROM Book b WHERE b.isbn = ?";
    		Query query = getSession().createQuery(hql).setString(0, isbn);
    		return (Integer)query.uniqueResult();
    	}
    
    	@Override
    	public void updateBookStock(String isbn) {
    		//验证书的库存是否充足. 
    		String hql2 = "SELECT b.stock FROM Book b WHERE b.isbn = ?";
    		int stock = (int) getSession().createQuery(hql2).setString(0, isbn).uniqueResult();
    		if(stock == 0){
    			throw new BookStockException("库存不足!");
    		}
    		
    		String hql = "UPDATE Book b SET b.stock = b.stock - 1 WHERE b.isbn = ?";
    		getSession().createQuery(hql).setString(0, isbn).executeUpdate();
    	}
    
    	@Override
    	public void updateUserAccount(String username, int price) {
    		//验证余额是否足够
    		String hql2 = "SELECT a.balance FROM Account a WHERE a.username = ?";
    		int balance = (int) getSession().createQuery(hql2).setString(0, username).uniqueResult();
    		if(balance < price){
    			throw new UserAccountException("余额不足!");
    		}
    		
    		String hql = "UPDATE Account a SET a.balance = a.balance - ? WHERE a.username = ?";
    		getSession().createQuery(hql).setInteger(0, price).setString(1, username).executeUpdate();
    	}
    
    }
    
    
  4. Spring hibernate 事务的流程

    • 在方法开始之前
      • 获取 Session
      • 把 Session 和当前线程绑定, 这样就可以在 Dao 中使用 SessionFactory 的 getCurrentSession() 方法来获取 Session 了
    • 若方法正常结束, 即没有出现异常, 则
      • 提交事务
      • 使和当前线程绑定的 Session 解除绑定
      • 关闭 Session
    • 若方法出现异常, 则
      • 回滚事务
      • 使和当前线程绑定的 Session 解除绑定
      • 关闭 Session

17.Spring 在Web应用中的使用
  1. 加入额外的jar包

    spring-web-4.0.0.RELEASE.jar
    spring-webmvc-4.0.0.RELEASE.jar
    
  2. Spring的配置文件,没有什么不同

  3. 如何创建IOC容器

    • 非 WEB 应用在 main 方法中直接创建

    • WEB应用应该在 WEB 应用被服务器加载时就创建 IOC 容器

      在 ServletContextListener.contextInitialized(ServletContextEvent sce) 方法中创建 IOC 容器.

    • 在 WEB 应用的其他组件中如何来访问 IOC 容器

      在 ServletContextListener.contextInitialized(ServletContextEvent sce) 方法中创建 IOC 容器后, 可以把其放在 ServletContext(即 application 域)的一个属性中

    • 实际上, Spring 配置文件的名字和位置应该也是可配置的! 将其配置到当前 WEB 应用的初始化参数中较为合适.

  4. 具体代码编写步骤

    • 在web.xml中注册ServletContextListener监听器,并配置Spring配置文件的位置

        <context-param>
          <param-name>configLocation</param-name>
          <param-value>applicationContext.xml</param-value>
        </context-param>
      
        <listener>
          <listener-class>com.atguigu.spring.struts2.listeners.SpringServletContextListener</listener-class>
        </listener>
      
    • 编写监听器实现类,在实现类的contextInitialized方法中创建IOC容器

      public class SpringServletContextListener implements ServletContextListener {
      
          /**
           * Default constructor. 
           */
          public SpringServletContextListener() {
              // TODO Auto-generated constructor stub
          }
      
      	/**
           * @see ServletContextListener#contextInitialized(ServletContextEvent)
           */
          public void contextInitialized(ServletContextEvent arg0) {
          	//1. 获取 Spring 配置文件的名称. 
          	ServletContext servletContext = arg0.getServletContext();
          	String config = servletContext.getInitParameter("configLocation");
          	
          	//1. 创建 IOC 容器
          	ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
          	
          	//2. 把 IOC 容器放在 ServletContext 的一个属性中. 
          	servletContext.setAttribute("ApplicationContext", ctx);
          }
      
      	/**
           * @see ServletContextListener#contextDestroyed(ServletContextEvent)
           */
          public void contextDestroyed(ServletContextEvent arg0) {
              // TODO Auto-generated method stub
          }
      	
      }
      
    • 在applicationContext.xml中配置bean

      <?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">
      
      	<bean id="person"
      		class="com.atguigu.spring.struts2.beans.Person">
      		<property name="username" value="atguigu"></property>
      	</bean>
      	
      </beans>
      
    • 其他组件中获取IOC容器及其中的Bean

      public class TestServlet extends HttpServlet {
      	private static final long serialVersionUID = 1L;
      
      	/**
      	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
      	 */
      	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      		//1. 从 application 域对象中得到 IOC 容器的引用
      		ServletContext servletContext = getServletContext();
      		ApplicationContext ctx = (ApplicationContext) servletContext.getAttribute("ApplicationContext");
      		
      		//2. 从 IOC 容器中得到需要的 bean
      		Person person = ctx.getBean(Person.class);
      		person.hello();
      	}
      
      }
      
  5. 使用Spring自己的ServletContextListner

    在上面的基础上修改web.xml文件就行了

    	<!-- 配置 Spring 配置文件的名称和位置 -->
    	<context-param>
    		<param-name>contextConfigLocation</param-name>
    		<param-value>classpath:applicationContext.xml</param-value>
    	</context-param>
    	
    	<!-- 启动 IOC 容器的 ServletContextListener -->
    	<listener>
    		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    	</listener>
    

18.Spring 整合 Struts2
  1. 整合目标

    让IOC容器来管理 Struts2 的Action

  2. 如何整合

    • 正常加入Struts2

      • 在web.xml文件中配置Struts2的Filter

        	<!-- 配置 Struts2 的 Filter -->
        	<filter>
                <filter-name>struts2</filter-name>
                <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
            </filter>
        
            <filter-mapping>
                <filter-name>struts2</filter-name>
                <url-pattern>/*</url-pattern>
            </filter-mapping>
        
        
      • 引入相关jar包

        struts2-core-2.3.15.3.jar
        struts2-spring-plugin-2.3.15.3.jar
        
    • 在 Spring 的 IOC 容器中配置 Struts2 的 Action

      注意: 在 IOC 容器中配置 Struts2 的 Action 时, 需要配置 scope 属性, 其值必须为 prototype

      <bean id="personAction" 
      	class="com.atguigu.spring.struts2.actions.PersonAction"
      	scope="prototype">
      	<property name="personService" ref="personService"></property>	
      </bean>
      
    • 配置 Struts2 的配置文件struts.xml: action 节点的 class 属性需要指向 IOC 容器中该 bean 的 id

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE struts PUBLIC
      	"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
      	"http://struts.apache.org/dtds/struts-2.3.dtd">
      
      <struts>
      
          <constant name="struts.enable.DynamicMethodInvocation" value="false" />
          <constant name="struts.devMode" value="true" />
      
          <package name="default" namespace="/" extends="struts-default">
      		
      		<!--  
      		Spring 整合 Struts2 时, 在 Struts2 中配置的 Spring 的 Action 的 class 需要指向 IOC 容器中该 bean               的 id
      		-->
      		<action name="person-save" class="personAction">
      			<result>/success.jsp</result>
      		</action>
      		
          </package>
      
      </struts>
      
      
    • 加入 struts2-spring-plugin-2.3.15.3.jar

      如果在第一步引入jar包时已经添加了该jar包,忽略此步骤

  3. 整合原理

    通过添加 struts2-spring-plugin-2.3.15.3.jar 以后, Struts2 会先从 IOC 容器中获取 Action 的实例.

    if (appContext.containsBean(beanName)) {
        o = appContext.getBean(beanName);
    } else {
        Class beanClazz = getClassInstance(beanName);
        o = buildBean(beanClazz, extraContext);
    }