一、Spring概述
1.1 Spring概述
1、Spring是一个开源框架
2、Spring为简化企业级开发而生,使用Spring,JavaBean就可以实现很多以前要靠EJB才能实现的功能。同样的功能,在EJB中要通过繁琐的配置和复杂的代码才能够实现,而在Spring中却非常的优雅和简洁。
3、Spring是一个IOC(DI)和AOP容器框架。
4、Spring的优良特性
1)非侵入式:基于Spring开发的应用中的对象可以不依赖于Spring的API
2)依赖注入:DI——Dependency Injection,反转控制(IOC)最经典的实现。
3)面向切面编程:Aspect Oriented Programming——AOP
4)容器:Spring是一个容器,因为它包含并且管理应用对象的生命周期
5)组件化:Spring实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用XML和Java注解组合这些对象。
6)一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上Spring 自身也提供了表述层的SpringMVC和持久层的Spring JDBC)。
5、Spring模块
1.2 搭建Spring运行时环境
1、加入JAR包
1)Spring自身JAR包:spring-framework-4.0.0.RELEASE\libs目录下
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
2)commons-logging-1.1.1.jar
2、根据需要创建Spring配置文件
二、IOC容器和Bean的配置
2.1 IOC和DI
2.1.1 IOC(Inversion of Control):反转控制
在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。
反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。
2.1.2 DI(Dependency Injection):依赖注入
IOC的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入。相对于IOC而言,这种表述更直接。
2.1.3 IOC容器在Spring中的实现
1、在通过IOC容器读取Bean的实例之前,需要先将IOC容器本身实例化。
2、Spring提供了IOC容器的两种实现方式
1)BeanFactory:IOC容器的基本实现,是Spring内部的基础设施,是面向Spring本身的,不是提供给开发人员使用的。
2)ApplicationContext:BeanFactory的子接口,提供了更多高级特性。面向Spring的使用者,几乎所有场合都使用ApplicationContext而不是底层的BeanFactory。
2.1.4 ApplicationContext的主要实现类
1、ClassPathXmlApplicationContext:对应类路径下的XML格式的配置文件
2、FileSystemXmlApplicationContext:对应文件系统中的XML格式的配置文件
3、在初始化时就创建单例的bean,也可以通过配置的方式指定创建的Bean是多实例的。
2.1.5 ConfigurableApplicationContext
1、是ApplicationContext的子接口,包含一些扩展方法
2、refresh()和close()让ApplicationContext具有启动、关闭和刷新上下文的能力。
2.1.6 WebApplicationContext
专门为WEB应用而准备的,它允许从相对于WEB根目录的路径中完成初始化工作
2.2 通过类型获取bean
从IOC容器中获取bean时,除了通过id值获取,还可以通过bean的类型获取。但如果同一个类型的bean在XML文件中配置了多个,则获取时会抛出异常,所以同一个类型的bean在容器中必须是唯一的。
HelloWorld helloWorld = cxt.getBean(HelloWorld. class);
2.3 给bean的属性赋值
2.3.1 赋值的途经
1、通过bean的setXxx()方法赋值
2、通过bean的构造器赋值
<bean id="book" class="com.atguigu.spring.bean.Book" >
<constructor-arg value= "10010"/>
<constructor-arg value= "Book01"/>
<constructor-arg value= "Author01"/>
<constructor-arg value= "20.2"/>
</bean>
1)通过索引值指定参数位置
<bean id="book" class="com.atguigu.spring.bean.Book" >
<constructor-arg value= "10010" index ="0"/>
<constructor-arg value= "Book01" index ="1"/>
<constructor-arg value= "Author01" index ="2"/>
<constructor-arg value= "20.2" index ="3"/>
</bean>
2)通过类型不同区分重载的构造器
<bean id="book" class="com.atguigu.spring.bean.Book" >
<constructor-arg value= "10010" index ="0" type="java.lang.Integer" />
<constructor-arg value= "Book01" index ="1" type="java.lang.String" />
<constructor-arg value= "Author01" index ="2" type="java.lang.String" />
<constructor-arg value= "20.2" index ="3" type="java.lang.Double" />
</bean >
3、给bean的级联属性赋值
<bean id="action" class="com.atguigu.spring.ref.Action">
<property name="service" ref="service"/>
<!-- 设置级联属性(了解) -->
<property name="service.dao.dataSource" value="DBCP"/>
</bean>
4、p名称空间
为了简化XML文件的配置,越来越多的XML文件采用属性而非子元素配置信息。Spring从2.5版本开始引入了一个新的p命名空间,可以通过元素属性的方式配置Bean的属性。使用p命名空间后,基于XML的配置方式将进一步简化。
<bean
id="studentSuper"
class="com.atguigu.helloworld.bean.Student"
p:studentId="2002" p:stuName="Jerry2016" p:age="18" />
2.3.2 可以使用的值
1、字面量
1)可以使用字符串表示的值,可以通过value属性或value子节点的方式指定
2)基本数据类型及其封装类、String等类型都可以采取字面值注入的方式
3)若字面值中包含特殊字符,可以使用把字面值包裹起来
2、null值
<bean class="com.atguigu.spring.bean.Book" id="bookNull" >
<property name= "bookId" value ="2000"/>
<property name= "bookName">
<null/>
</property>
<property name= "author" value ="nullAuthor"/>
<property name= "price" value ="50"/>
</bean >
3、外部已声明的bean
<bean id="shop" class="com.atguigu.spring.bean.Shop" >
<property name= "book" ref ="book"/>
</bean >
4、内部bean
当bean实例仅仅给一个特定的属性使用时,可以将其声明为内部bean。内部bean声明直接包含在或元素里,不需要设置任何id或name属性,内部bean不能使用在任何其他地方
<bean id="shop2" class="com.atguigu.spring.bean.Shop" >
<property name= "book">
<bean class= "com.atguigu.spring.bean.Book" >
<property name= "bookId" value ="1000"/>
<property name= "bookName" value="innerBook" />
<property name= "author" value="innerAuthor" />
<property name= "price" value ="50"/>
</bean>
</property>
</bean >
2.3.3 集合属性
在Spring中可以通过一组内置的XML标签来配置集合属性,例如:,或。
1、数组和List
配置java.util.List类型的属性,需要指定标签,在标签里包含一些元素。这些标签可以通过指定简单的常量值,通过指定对其他Bean的引用。通过指定内置bean定义。通过指定空元素。甚至可以内嵌其他集合。
数组的定义和List一样,都使用元素,也可以使用。
配置java.util.Set需要使用标签,定义的方法与List一样。
<bean id="shop" class="com.atguigu.spring.bean.Shop" >
<property name= "categoryList">
<!-- 以字面量为值的List集合 -->
<list>
<value>历史</value >
<value>军事</value >
</list>
</property>
<property name= "bookList">
<!-- 以bean的引用为值的List集合 -->
<list>
<ref bean= "book01"/>
<ref bean= "book02"/>
</list>
</property>
</bean >
2、Map
Java.util.Map通过标签定义,标签里可以使用多个作为子标签。每个条目包含一个键和一个值。必须在标签里定义键。因为键和值的类型没有限制,所以可以自由地为它们指定、、或元素。可以将Map的键和值作为的属性定义:简单常量使用key和value来定义;bean引用通过key-ref和value-ref属性定义。
<bean id="cup" class="com.atguigu.spring.bean.Cup">
<property name="bookMap">
<map>
<entry>
<key>
<value>bookKey01</value>
</key>
<ref bean="book01"/>
</entry>
<entry>
<key>
<value>bookKey02</value>
</key>
<ref bean="book02"/>
</entry>
</map>
</property>
</bean>
3、Properties
使用定义java.util.Properties,该标签使用多个作为子标签。每个标签必须定义key属性
<bean class="com.atguigu.spring.bean.DataSource" id="dataSource">
<property name="properties">
<props>
<prop key="userName">root</prop>
<prop key="password">root</prop>
<prop key="url">jdbc:mysql:///test</prop>
<prop key="driverClass">com.mysql.jdbc.Driver</prop>
</props>
</property>
</bean>
4、集合类型的bean
如果只能将集合对象配置在某个bean内部,则这个集合的配置将不能重用。我们需要将集合bean的配置拿到外面,供其他bean引用。配置集合类型的bean需要引入util名称空间
<util:list id="bookList">
<ref bean="book01"/>
<ref bean="book02"/>
<ref bean="book03"/>
<ref bean="book04"/>
<ref bean="book05"/>
</util:list>
<util:list id="categoryList">
<value>编程</value>
<value>极客</value>
<value>相声</value>
<value>评书</value>
</util:list>
2.4 通过工厂创建bean
2.4.1 静态工厂
调用静态工厂方法创建bean是将对象创建的过程封装到静态方法中。当客户端需要对象时,只需要简单地调用静态方法,而不用关心创建对象的细节。
声明通过静态方法创建的bean需要在bean的class属性里指定静态工厂类的全类名,同时在factory-method属性里指定工厂方法的名称。最后使用元素为该方法传递方法参数。
配置工厂类:
/**
* User对象的工厂类
*/
public class UserFactory {
// 静态方法
public static User getUser1() {
return new User();
}
}
XML配置:
<!-- 使用静态工厂创建user -->
<bean id="user1" class="ioc.service.UserFactory" factory-method="getUser1"></bean>
class 指的是该工厂类的包路径,factory-method 指的是该工厂类创建Bean的静态方法。注意:这里一定要静态方法
测试:
@Test
public void testUser1(){
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
// 静态工厂创建
User user1 = (User) context.getBean("user1");
System.out.println("静态工厂创建:" + user1);
}
控制台输出结果:静态工厂创建:ioc.pojo.User@3b088d51
2.4.2 实例工厂
实例工厂方法:将对象的创建过程封装到另外一个对象实例的方法里。当客户端需要请求对象时,只需要简单的调用该实例方法而不需要关心对象的创建细节。
实现方式:①配置工厂类实例的bean;②在factory-method属性里指定该工厂方法的名称;③使用 construtor-arg 元素为工厂方法传递方法参数。
配置工厂类:
/**
* User对象的工厂类
*/
public class UserFactory {
//普通方法
public User getUser2() {
return new User();
}
}
XML配置:
<!-- 使用实例工厂创建 user -->
<bean id="userFactory" class="ioc.service.UserFactory"></bean>
<bean id="user2" factory-bean="userFactory" factory-method="getUser2"></bean>
测试:
@Test
public void testUser2(){
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
// 实例工厂创建
User user2 = (User) context.getBean("user2");
System.out.println("实例工厂创建:" + user2);
}
控制台输出结果:实例工厂创建:ioc.pojo.User@3b088d51
2.4.3 FactoryBean
Spring中有两种类型的bean,一种是普通bean,另一种是工厂bean,即FactoryBean。工厂bean跟普通bean不同,其返回的对象不是指定类的一个实例,其返回的是该工厂bean的getObject方法所返回的对象。工厂bean必须实现org.springframework.beans.factory.FactoryBean接口。
第一步 创建类,让这个类作为工厂 bean,实现接口 FactoryBean
第二步 实现接口里面的方法,在实现的方法中定义返回的 bean 类型
public class MyBean implements FactoryBean<Course> { //定义返回 bean @Override public Course getObject() throws Exception { Course course = new Course(); course.setCname("abc"); return course; } @Override public Class<?> getObjectType() { return null; } @Override public boolean isSingleton() { return false; }}
XML配置:
<bean id="myBean" class="com.atguigu.spring5.factorybean.MyBean"></bean>
测试:
@Testpublic void test3() { ApplicationContext context = new ClassPathXmlApplicationContext("bean3.xml"); Course course = context.getBean("myBean", Course.class); System.out.println(course);}
2.5 bean的高级配置
2.5.1 配置信息的继承
1、背景:查看下面两个Employee的配置,其中dept属性是重复的。
<bean id="dept" class="com.atguigu.parent.bean.Department"> <property name="deptId" value="100"/> <property name="deptName" value="IT"/></bean><bean id="emp01" class="com.atguigu.parent.bean.Employee"> <property name="empId" value="1001"/> <property name="empName" value="Tom"/> <property name="age" value="20"/> <!-- 重复的属性值 --> <property name="detp" ref="dept"/></bean><bean id="emp02" class="com.atguigu.parent.bean.Employee"> <property name="empId" value="1002"/> <property name="empName" value="Jerry"/> <property name="age" value="25"/> <!-- 重复的属性值 --> <property name="detp" ref="dept"/></bean>
2、配置信息的继承
<!-- 以emp01作为父bean,继承后可以省略公共属性值的配置 --><bean id="emp02" parent="emp01"> <property name="empId" value="1002"/> <property name="empName" value="Jerry"/> <property name="age" value="25"/></bean>
Spring允许继承bean的配置,被继承的bean称为父bean,继承这个父bean的bean称为子bean。子bean从父bean中继承配置,包括bean的属性配置,子bean也可以覆盖从父bean继承过来的配置。
3、补充说明
父bean可以作为配置模板,也可以作为bean实例。若只想把父bean作为模板,可以设置的abstract 属性为true,这样Spring将不会实例化这个bean。
如果一个bean的class属性没有指定,则必须是抽象bean,并不是元素里的所有属性都会被继承。比如:autowire,abstract等。也可以忽略父bean的class属性,让子bean指定自己的类,而共享相同的属性配置。但此时abstract必须设为true。
2.5.2 bean之间的依赖
有的时候创建一个bean的时候需要保证另外一个bean也被创建,这时我们称前面的bean对后面的bean有依赖。例如:要求创建Employee对象的时候必须创建Department。这里需要注意的是依赖关系不等于引用关系,Employee即使依赖Department也可以不引用它。
<bean id="emp03" class="com.atguigu.parent.bean.Employee" depends-on="dept"> <property name="empId" value="1003"/> <property name="empName" value="Kate"/> <property name="age" value="21"/></bean>
2.5.3 bean的作用域
在Spring中,可以在元素的scope属性里设置bean的作用域,以决定这个bean是单实例的还是多实例的。默认情况下,Spring只为每个在IOC容器里声明的bean创建唯一一个实例,整个IOC容器范围内都能共享该实例:所有后续的getBean()调用和bean引用都将返回这个唯一的bean实例。该作用域被称为singleton,它是所有bean的默认作用域。
当bean的作用域为单例时,Spring会在IOC容器对象创建时就创建bean的对象实例。而当bean的作用域为prototype时,IOC容器在获取bean的实例时创建bean的实例对象。
2.5.4 bean的生命周期
1、Spring IOC容器可以管理bean的生命周期,Spring允许在bean生命周期内特定的时间点执行指定的任务。
2、Spring IOC容器对bean的生命周期进行管理的过程:
1)通过构造器或工厂方法创建bean实例
2)为bean的属性设置值和对其他bean的引用
3)调用bean的初始化方法
4)bean可以使用了
5)当容器关闭时,调用bean的销毁方法
3、在配置bean时,通过init-method和destroy-method 属性为bean指定初始化和销毁方法
4、bean的后置处理器
1)bean后置处理器允许在调用初始化方法前后对bean进行额外的处理
2)bean后置处理器对IOC容器里的所有bean实例逐一处理,而非单一实例。其典型应用是:检查bean属性的正确性或根据特定的标准更改bean的属性。
3)bean后置处理器时需要实现接口:org.springframework.beans.factory.config.BeanPostProcessor。在初始化方法被调用前后,Spring将把每个bean实例分别传递给上述接口的以下两个方法:
● postProcessBeforeInitialization(Object, String):实例化、依赖注入完毕,在调用显示的初始化之前完成一些定制的初始化任务
● postProcessAfterInitialization(Object, String):实例化、依赖注入、初始化完毕时执行
5、添加bean后置处理器后bean的生命周期
1)通过构造器或工厂方法创建bean实例
2)为bean的属性设置值和对其他bean的引用
3)将bean实例传递给bean后置处理器的**postProcessBeforeInitialization()**方法
4)调用bean的初始化方法
5)将bean实例传递给bean后置处理器的**postProcessAfterInitialization()**方法
6)bean可以使用了
7)当容器关闭时调用bean的销毁方法
6、自定义后置处理器演示
1)自定义处理器
package com.dpb.processor;import org.springframework.beans.BeansException;import org.springframework.beans.factory.config.BeanPostProcessor;/** * 自定义BeanPostProcessor实现类 * BeanPostProcessor接口的作用是: * 我们可以通过该接口中的方法在bean实例化、配置以及其他初始化方法前后添加一些我们自己的逻辑 * */public class MyBeanPostProcessor implements BeanPostProcessor{ /** * 实例化、依赖注入完毕,在调用显示的初始化之前完成一些定制的初始化任务 * 注意:方法返回值不能为null * 如果返回null那么在后续初始化方法将报空指针异常或者通过getBean()方法获取不到bena实例对象 * 因为后置处理器从Spring IoC容器中取出bean实例对象没有再次放回IoC容器中 */ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("初始化 before--实例化的bean对象:"+bean+"\t"+beanName); // 可以根据beanName不同执行不同的处理操作 return bean; } /** * 实例化、依赖注入、初始化完毕时执行 * 注意:方法返回值不能为null * 如果返回null那么在后续初始化方法将报空指针异常或者通过getBean()方法获取不到bena实例对象 * 因为后置处理器从Spring IoC容器中取出bean实例对象没有再次放回IoC容器中 */ @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("初始化 after...实例化的bean对象:"+bean+"\t"+beanName); // 可以根据beanName不同执行不同的处理操作 return bean; }}
注意:接口中两个方法不能返回null,如果返回null那么在后续初始化方法将报空指针异常或者通过getBean()方法获取不到bena实例对象,因为后置处理器从Spring IoC容器中取出bean实例对象没有再次放回IoC容器中
2)pojo类
public class User { private int id; private String name; private String beanName; public User(){ System.out.println("User 被实例化"); } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { System.out.println("设置:"+name); this.name = name; } public String getBeanName() { return beanName; } public void setBeanName(String beanName) { this.beanName = beanName; } /** * 自定义的初始化方法 */ public void start(){ System.out.println("User 中自定义的初始化方法"); }}
3)配置文件注册
<?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 class="com.dpb.pojo.User" id="user" init-method="start"> <property name="name" value="波波烤鸭" /> </bean> <!-- 注册处理器 --> <bean class="com.dpb.processor.MyBeanPostProcessor"></bean></beans>
4)测试
@Testpublic void test() { ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); User user = ac.getBean(User.class); System.out.println(user);}
5)输出结果
User 被实例化 设置:波波烤鸭 初始化 before--实例化的bean对象:com.dpb.pojo.User@65e2dbf3 user User 中自定义的初始化方法 初始化 after...实例化的bean对象:com.dpb.pojo.User@65e2dbf3 user com.dpb.pojo.User@65e2dbf3
注意!!!
BeanFactory和ApplicationContext两个容器对待bean的后置处理器稍微有些不同。ApplicationContext容器会自动检测Spring配置文件中那些bean所对应的Java类实现了BeanPostProcessor接口,并自动把它们注册为后置处理器。在创建bean过程中调用它们,所以部署一个后置处理器跟普通的bean没有什么太大区别。
BeanFactory容器注册bean后置处理器时必须通过代码显示的注册,在IoC容器继承体系中的ConfigurableBeanFactory接口中定义了注册方法。测试代码如下:
@Testpublic void test2() { //ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("applicationContext.xml")); // 显示添加后置处理器 bf.addBeanPostProcessor(bf.getBean(MyBeanPostProcessor.class)); User user = bf.getBean(User.class); System.out.println(user);}
7、多个后置处理器
我们可以在Spring配置文件中添加多个BeanPostProcessor(后置处理器)接口实现类,在默认情况下Spring容器会根据后置处理器的定义顺序来依次调用。
在Spring机制中可以指定后置处理器调用顺序,通过让BeanPostProcessor接口实现类实现Ordered接口getOrder方法,该方法返回一整数,默认值为 0,优先级最高,值越大优先级越低。
public class MyBeanPostProcessor implements BeanPostProcessor,Ordered{ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("A before--实例化的bean对象:"+bean+"\t"+beanName); // 可以根据beanName不同执行不同的处理操作 return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("A after...实例化的bean对象:"+bean+"\t"+beanName); // 可以根据beanName不同执行不同的处理操作 return bean; } @Override public int getOrder() { // TODO Auto-generated method stub return 10; }}
public class MyBeanPostProcessor2 implements BeanPostProcessor,Ordered{ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("B before--实例化的bean对象:"+bean+"\t"+beanName); // 可以根据beanName不同执行不同的处理操作 return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("B after...实例化的bean对象:"+bean+"\t"+beanName); // 可以根据beanName不同执行不同的处理操作 return bean; } @Override public int getOrder() { // TODO Auto-generated method stub return 2; }}
测试输出结果:
User 被实例化 设置:波波烤鸭 B before--实例化的bean对象:com.dpb.pojo.User@7fac631b user A before--实例化的bean对象:com.dpb.pojo.User@7fac631b user User 中自定义的初始化方法 B after...实例化的bean对象:com.dpb.pojo.User@7fac631b user A after...实例化的bean对象:com.dpb.pojo.User@7fac631b user com.dpb.pojo.User@7fac631b
2.5.5 引用外部属性文件
当bean的配置信息逐渐增多时,查找和修改一些bean的配置信息就变得愈加困难。这时可以将一部分信息提取到bean配置文件的外部,以properties格式的属性文件保存起来,同时在bean的配置文件中引用properties属性文件中的内容,从而实现一部分属性值在发生变化时仅修改properties属性文件即可。这种技术多用于连接数据库的基本信息的配置。
1、直接配置
<!-- 直接配置 --><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="root"/> <property name="password" value="root"/> <property name="jdbcUrl" value="jdbc:mysql:///test"/> <property name="driverClass" value="com.mysql.jdbc.Driver"/></bean>
2、创建properties属性文件
prop.userName=rootprop.password=rootprop.url=jdbc:mysql:///testprop.driverClass=com.mysql.jdbc.Driver
3、引入context名称空间
4、指定properties属性文件的位置
<!-- 指定properties属性文件的位置 --><!-- classpath:xxx 表示属性文件位于类路径下 --><context:property-placeholder location="classpath:jdbc.properties"/>
5、从properties属性文件中引入属性值
<!-- 从properties属性文件中引入属性值 --><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${prop.userName}"/> <property name="password" value="${prop.password}"/> <property name="jdbcUrl" value="${prop.url}"/> <property name="driverClass" value="${prop.driverClass}"/></bean>
2.6 自动装配
2.6.1 自动装配的概念
1、手动装配:以value或ref的方式明确指定属性值都是手动装配。
2、自动装配:根据指定的装配规则,不需要明确指定,Spring自动将匹配的属性值注入bean中。
2.6.2 装配模式
1、根据类型自动装配:将类型匹配的bean作为属性注入到另一个bean中。若IOC容器中有多个与目标bean类型一致的bean,Spring将无法判定哪个bean最合适该属性,所以不能执行自动装配
2、根据名称自动装配:必须将目标bean的名称和属性名设置的完全相同
3、通过构造器自动装配:当bean中存在多个构造器时,此种自动装配方式将会很复杂。不推荐使用。
2.6.3 选用建议
相对于使用注解的方式实现的自动装配,在XML文档中进行的自动装配略显笨拙,在项目中更多的使用注解的方式实现。
2.6.4 演示自动装配过程
1、根据属性名称自动注入
<!--实现自动装配 bean 标签属性 autowire,配置自动装配 autowire 属性常用两个值: byName 根据属性名称注入,注入值 bean 的 id 值和类属性名称一样 byType 根据属性类型注入--><bean id="emp" class="com.atguigu.spring5.autowire.Emp" autowire="byName"> <!--<property name="dept" ref="dept"></property>--></bean> <bean id="dept" class="com.atguigu.spring5.autowire.Dept"></bean>
2、根据属性类型自动注入
<!--实现自动装配 bean 标签属性 autowire,配置自动装配 autowire 属性常用两个值: byName 根据属性名称注入,注入值 bean 的 id 值和类属性名称一样 byType 根据属性类型注入--><bean id="emp" class="com.atguigu.spring5.autowire.Emp" autowire="byType"> <!--<property name="dept" ref="dept"></property>--></bean> <bean id="dept" class="com.atguigu.spring5.autowire.Dept"></bean>
2.7 SpEL
2.7.1 简介
Spring Expression Language,Spring表达式语言,简称SpEL。支持运行时查询并可以操作对象图。
和JSP页面上的EL表达式、Struts2中用到的OGNL表达式一样,SpEL根据JavaBean风格的getXxx()、setXxx()方法定义的属性访问对象图,完全符合我们熟悉的操作习惯。
2.7.2 基本语法
SpEL使用**#{…}**作为定界符,所有在大框号中的字符都将被认为是SpEL表达式。
2.7.3 使用字面量
● 整数:
● 小数:
● 科学计数法:
● String类型的字面量可以使用单引号或者双引号作为字符串的定界符号
<property name=“name” value="#{'Chuck'}"/>
<property name='name' value='#{"Chuck"}'/>
● Boolean:
2.7.4 引用其他bean
<bean id="emp04" class="com.atguigu.parent.bean.Employee"> <property name="empId" value="1003"/> <property name="empName" value="Kate"/> <property name="age" value="21"/> <property name="detp" value="#{dept}"/></bean>
2.7.5 引用其他bean的属性值作为自己某个属性的值
<bean id="emp05" class="com.atguigu.parent.bean.Employee"> <property name="empId" value="1003"/> <property name="empName" value="Kate"/> <property name="age" value="21"/> <property name="deptName" value="#{dept.deptName}"/></bean>
2.7.6 调用非静态方法
<!-- 创建一个对象,在SpEL表达式中调用这个对象的方法 --><bean id="salaryGenerator" class="com.atguigu.spel.bean.SalaryGenerator"/><bean id="employee" class="com.atguigu.spel.bean.Employee"> <!-- 通过对象方法的返回值为属性赋值 --> <property name="salayOfYear" value="#{salaryGenerator.getSalaryOfYear(5000)}"/></bean>
2.7.7 调用静态方法
<bean id="employee" class="com.atguigu.spel.bean.Employee"> <!-- 在SpEL表达式中调用类的静态方法 --> <!--T()中为完整的类名--> <property name="circle" value="#{T(java.lang.Math).PI*20}"/></bean>
2.7.8 运算符
1、算术运算符:+、-、*、/、%、^
2、字符串连接:+
3、比较运算符:<、>、==、<=、>=、lt、gt、eq、le、ge
4、逻辑运算符:and, or, not, |
5、三目运算符:判断条件?判断结果为true时的取值:判断结果为false时的取值
6、正则表达式:matches
2.8 通过注解配置
2.8.1 概述
相对于XML方式而言,通过注解的方式配置bean更加简洁和优雅,而且和MVC组件化开发的理念十分契合,是开发中常用的使用方式。
2.8.2 使用注解标识组件
1、普通组件:@Component
标识一个受Spring IOC容器管理的组件
2、持久化层组件:@Respository
标识一个受Spring IOC容器管理的持久化层组件
3、业务逻辑层组件:@Service
标识一个受Spring IOC容器管理的业务逻辑层组件
4、表述层控制器组件:@Controller
标识一个受Spring IOC容器管理的表述层控制器组件
5、组件命名规则
1)默认情况:使用组件的简单类名首字母小写后得到的字符串作为bean的id
2)使用组件注解的value属性指定bean的id
@Respository("bookdaocxc")//设置bean的作用域为多实例@Scope(value = "prototype")public class BookDao() {}
注意:事实上Spring并没有能力识别一个组件到底是不是它所标记的类型,即使将@Respository注解用在一个表述层控制器组件上面也不会产生任何错误,所以@Respository、@Service、@Controller这几个注解仅仅是为了让开发人员自己明确当前的组件扮演的角色。
2.8.3 扫描组件
组件被上述注解标识后还需要通过Spring进行扫描才能够侦测到。
1、指定被扫描的package
<context:component-scan base-package="com.atguigu.component"/>
2、详细说明
1)base-package属性指定一个需要扫描的基类包,Spring容器将会扫描这个基类包及其子包中的所有类。
2)当需要扫描多个包时可以使用逗号分隔。
3)如果仅希望扫描特定的类而非基包下的所有类,可使用resource-pattern属性过滤特定的类,示例:
<context:component-scan base-package="com.atguigu.component" resource-pattern="autowire/*.class"/>
4)包含与排除
● context:include-filter子节点表示要包含的目标类
注意:通常需要与use-default-filters属性配合使用才能够达到“仅包含某些组件”这样的效果。即:通过将use-default-filters属性设置为false,禁用默认过滤器,然后扫描的就只是include-filter中的规则指定的组件了。
● context:exclude-filter子节点表示要排除在外的目标类
● component-scan下可以拥有若干个include-filter和exclude-filter子节点
● 过滤表达式
| 类别 | 示例 | 说明 |
|---|---|---|
| annotation | com.atguigu.XxxAnnotation | 过滤所有标注了XxxAnnotation的类。这个规则根据目标组件是否标注了指定类型的注解进行过滤。 |
| assignable | com.atguigu.BaseXxx | 过滤所有BaseXxx类的子类。这个规则根据目标组件是否是指定类型的子类的方式进行过滤。 |
| aspectj | com.atguigu.*Service+ | 所有类名是以Service结束的,或这样的类的子类。这个规则根据AspectJ表达式进行过滤。 |
| regex | com.atguigu.anno.* | 所有com.atguigu.anno包下的类。这个规则根据正则表达式匹配到的类名进行过滤。 |
| custom | com.atguigu.XxxTypeFilter | 使用XxxTypeFilter类通过编码的方式自定义过滤规则。该类必须实现org.springframework.core.type.filter.TypeFilter接口 |
<!--示例 1 use-default-filters="false" 表示现在不使用默认 filter,自己配置 filter context:include-filter,设置扫描哪些内容--><context:component-scan base-package="com.atguigu" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><!--示例 2 context:exclude-filter,设置哪些内容不进行扫描--><context:component-scan base-package="com.atguigu"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan>
3、JAR包
必须在原有JAR包组合的基础上再导入一个:spring-aop-4.0.0.RELEASE.jar
2.8.4 组件装配
1、需求
Controller组件中往往需要用到Service组件的实例,Service组件中往往需要用到Repository组件的实例。Spring可以通过注解的方式帮我们实现属性的装配。
2、实现依据
在指定要扫描的包时,context:component-scan 元素会自动注册一个bean的后置处理器:AutowiredAnnotationBeanPostProcessor的实例。该后置处理器可以自动装配标记了@Autowired、@Resource或@Inject注解的属性。
3、@Autowired注解
1)根据类型实现自动装配。
2)构造器、普通字段(即使是非public)、一切具有参数的方法都可以应用@Autowired注解
3)默认情况下,所有使用@Autowired注解的属性都需要被设置。当Spring找不到匹配的bean装配属性时,会抛出异常。
4)若某一属性允许不被设置,可以设置@Autowired注解的required属性为 false
5)默认情况下,当IOC容器里存在多个类型兼容的bean时,Spring会尝试匹配bean的id值是否与变量名相同,如果相同则进行装配。如果bean的id值不相同,通过类型的自动装配将无法工作。此时可以在@Qualifier注解里提供bean的名称。Spring甚至允许在方法的形参上标注@Qualifiter注解以指定注入bean的名称。
6)@Autowired注解也可以应用在数组类型的属性上,此时Spring将会把所有匹配的bean进行自动装配。
7)@Autowired注解也可以应用在集合属性上,此时Spring读取该集合的类型信息,然后自动装配所有与之兼容的bean。
8)@Autowired注解用在java.util.Map上时,若该Map的键值为String,那么 Spring将自动装配与值类型兼容的bean作为值,并以bean的id值作为键。
4、@Resource
@Resource注解要求提供一个bean名称的属性,若该属性为空,则自动采用标注处的变量或方法名作为bean的名称。
5、@Inject
@Inject和@Resource注解一样也是按类型注入匹配的bean,但没有reqired属性。
/** 基于注解方式实现属性注入 1、@Autowired:根据属性类型进行自动装配 第一步 把 service 和 dao 对象创建,在 service 和 dao 类添加创建对象注解 第二步 在 service 注入 dao 对象,在 service 类添加 dao 类型属性,在属性上面使用注解*/@Servicepublic class UserService { //定义 dao 类型属性 //不需要添加 set 方法 //添加注入属性注解 @Autowired private UserDao userDao; public void add() { System.out.println("service add......."); userDao.add(); }}/** 2、@Qualifier:根据名称进行注入 这个@Qualifier注解的使用,和上面@Autowired一起使用*///定义 dao 类型属性//不需要添加 set 方法//添加注入属性注解@Autowired //根据类型进行注入@Qualifier(value = "userDaoImpl1") //根据名称进行注入private UserDao userDao; /** 3、@Resource:可以根据类型注入,可以根据名称注入*///@Resource //根据类型进行注入@Resource(name = "userDaoImpl1") //根据名称进行注入private UserDao userDao; /** 4、@Value:注入普通类型属性*/@Value(value = "abc")private String name;//5、完全注解开发 //(1)创建配置类,替代 xml 配置文件@Configuration //作为配置类,替代 xml 配置文件@ComponentScan(basePackages = {"com.atguigu"})public class SpringConfig {}//(2)编写测试类@Testpublic void testService2() { //加载配置类 ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); UserService userService = context.getBean("userService", UserService.class); System.out.println(userService); userService.add();}
2.9 泛型依赖注入
2.9.1 简介
Spring 4.x中可以为子类注入子类对应的泛型类型的成员变量的引用
2.9.2 实现
1、组件基类
public class BaseRepository<T> { public void save() { System.out.println("Saved by BaseRepository"); }}public class BaseService<T> { @Autowired private BaseRepository<T> repository; public void add() { repository.save(); }}
2、组件实体类
@Repositorypublic class UserRepository extends BaseRepository<User>{ public void save() { System.out.println("Saved by UserRepository"); }}@Servicepublic class UserService extends BaseService<User>{}
3、模型实体类
public class User {}
4、测试
ApplicationContext ioc = new ClassPathXmlApplicationContext("di.xml");UserService us = (UserService) ioc.getBean("userService");us.add();//执行结果//Saved by UserRepository
2.10 整合多个配置文件
1、Spring允许通过将多个配置文件引入到一个文件中,进行配置文件的集成。这样在启动Spring容器时,仅需要指定这个合并好的配置文件就可以。
2、import元素的resource属性支持Spring的标准的路径资源
三、AOP概述
3.1 AOP概述
1、AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
2、AOP编程操作的主要对象是切面(aspect),而切面模块化横切关注点。
3、在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
4、AOP的好处:
1)每个事物逻辑位于一个位置,代码不分散,便于维护和升级
2)业务模块更简洁,只包含核心业务代码
3.2 AOP术语
3.2.1 横切关注点
从每个方法中抽取出来的同一类非核心业务。
3.2.2 切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。
3.2.3 通知(Advice)
切面必须要完成的各个具体工作
3.2.4 目标(Target)
被通知的对象
3.2.5 代理(Proxy)
向目标对象应用通知之后创建的代理对象
3.2.6 连接点(Joinpoint)
横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。在应用程序中可以使用横纵两个坐标来定位一个具体的连接点。
3.2.7 切入点(Pointcut):
定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件,AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
3.3 AspectJ
3.3.1 简介
AspectJ:Java社区里最完整最流行的AOP框架。在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。
3.3.2 在Spring中启用AspectJ注解支持
1、导入JAR包
1)aopalliance.jar
2)aspectj.weaver.jar
3)spring-aspects.jar
2、引入aop名称空间
3、配置
<aop:aspectj-autoproxy>:当Spring IOC容器侦测到bean配置文件中的<aop:aspectj-autoproxy>元素时,会自动为与AspectJ切面匹配的bean创建代理
3.3.3 用AspectJ注解声明切面
1、要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例。
2、当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。
3、在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知。
4、通知是标注有某种注解的简单的Java方法。
5、AspectJ支持5种类型的通知注解:
1)@Before:前置通知,在方法执行之前执行
2)@After:后置通知,在方法执行之后执行
3)@AfterReturning:返回通知,在方法返回结果之后执行
4)@AfterThrowing:异常通知,在方法抛出异常之后执行
5)@Around:环绕通知,围绕着方法执行
6、AOP 操作(AspectJ 注解)
//1、创建类,在类里面定义方法public class User { public void add() { System.out.println("add......."); }}//2、创建增强类(编写增强逻辑)//(1)在增强类里面,创建方法,让不同方法代表不同通知类型//增强的类public class UserProxy { public void before() {//前置通知 System.out.println("before......"); }}
<!--3、进行通知的配置(1)在 spring 配置文件中,开启注解扫描--><?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: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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 开启注解扫描 --> <context:component-scan base-package="com.atguigu.spring5.aopanno"></context:component-scan>
//(2)使用注解创建 User 和 UserProxy 对象//被增强的类@Componentpublic class User {}//增强的类@Componentpublic class UserProxy { }//(3)在增强类上面添加注解 @Aspect//增强的类@Component@Aspect //生成代理对象public class UserProxy {}
<!--(4)在 spring 配置文件中开启生成代理对象--><!-- 开启 Aspect 生成代理对象--><aop:aspectj-autoproxy></aop:aspectj-autoproxy>
//4、配置不同类型的通知//(1)在增强类的里面,在作为通知方法上面添加通知类型注解,使用切入点表达式配置//增强的类@Component@Aspect //生成代理对象public class UserProxy { //前置通知 //@Before 注解表示作为前置通知 @Before(value = "execution(* com.atguigu.spring5.aopanno.User.add(..))") public void before() { System.out.println("before........."); } //后置通知(返回通知) @AfterReturning(value = "execution(* com.atguigu.spring5.aopanno.User.add(..))") public void afterReturning() { System.out.println("afterReturning........."); } //最终通知 @After(value = "execution(* com.atguigu.spring5.aopanno.User.add(..))") public void after() { System.out.println("after........."); } //异常通知 @AfterThrowing(value = "execution(* com.atguigu.spring5.aopanno.User.add(..))") public void afterThrowing() { System.out.println("afterThrowing........."); } //环绕通知 @Around(value = "execution(* com.atguigu.spring5.aopanno.User.add(..))") public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("环绕之前........."); //被增强的方法执行 proceedingJoinPoint.proceed(); System.out.println("环绕之后........."); }}//5、相同的切入点抽取//相同切入点抽取@Pointcut(value = "execution(* com.atguigu.spring5.aopanno.User.add(..))")public void pointdemo() {}//前置通知//@Before 注解表示作为前置通知@Before(value = "pointdemo()")public void before() { System.out.println("before.........");}//6、有多个增强类对同一个方法进行增强,设置增强类优先级//(1)在增强类上面添加注解 @Order(数字类型值),数字类型值越小优先级越高@Component@Aspect@Order(1)public class PersonProxy {}//7、完全使用注解开发 (1)创建配置类,不需要创建 xml 配置文件 @Configuration@ComponentScan(basePackages = {"com.atguigu"})@EnableAspectJAutoProxy(proxyTargetClass = true)public class ConfigAop {}
7、AOP 操作(AspectJ 配置文件)
<!-- 1、创建两个类,增强类和被增强类,创建方法 2、在 spring 配置文件中创建两个类对象--><!--创建对象--> <bean id="book" class="com.atguigu.spring5.aopxml.Book"></bean> <bean id="bookProxy" class="com.atguigu.spring5.aopxml.BookProxy"></bean> <!--3、在 spring 配置文件中配置切入点--> <!--配置 aop 增强--><aop:config> <!--切入点--> <aop:pointcut id="p" expression="execution(* com.atguigu.spring5.aopxml.Book.buy(..))"/> <!--配置切面--> <aop:aspect ref="bookProxy"> <!--增强作用在具体的方法上--> <aop:before method="before" pointcut-ref="p"/> </aop:aspect></aop:config>
四、AOP细节
4.1 切入点表达式
1、作用
通过表达式的方式定位一个或多个具体的连接点。
2、语法细节
1)切入点表达式的语法格式
execution([权限修饰符] [返回值类型] [简单类名/全类名] 方法名([参数列表]))
2)举例说明
| 表达式 | 含义 |
|---|---|
| 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类型的方法 |
| execution (* .add(int,..)) || execution( *.sub(int,..)) | 任意类中第一个参数为int类型的add方法或sub方法 |
3)在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
4.2 当前连接点细节
4.2.1 概述
切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中。
4.2.2 JoinPoint
4.3 通知
4.3.1 概述
1、在具体的连接点上要执行的操作。
2、一个切面可以包括一个或者多个通知。
3、通知所使用的注解的值往往是切入点表达式。
4.3.2 前置通知
1、前置通知:在方法执行之前执行的通知
2、使用@Before注解
4.3.3 后置通知
1、后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候
l 使用@After注解
4.3.4 返回通知
1、返回通知:无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
2、使用@AfterReturning注解
3、在返回通知中访问连接点的返回值
1)在返回通知中,只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
2)必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值
3)原始的切点表达式需要出现在pointcut属性中
4.3.5 异常通知
1、异常通知:只在连接点抛出异常时才执行异常通知
2、将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
3、如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
4.3.6 环绕通知
1、环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
2、对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
3、在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
4、注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。
4.3.7 重用切入点定义
1、在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。
2、在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
3、切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
4、其他通知可以通过方法名称引入该切入点
4.3.8 指定切面的优先级
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
2、切面的优先级可以通过实现Ordered接口或利用@Order注解指定。
3、实现Ordered接口,getOrder()方法的返回值越小,优先级越高。
4、若使用@Order注解,序号出现在注解中
五、以XML方式配置切面
5.1 概述
除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
5.2 配置细节
在bean配置文件中,所有的Spring AOP配置都必须定义在<aop:config>元素内部。对于每个切面而言,都要创建一个aop:aspect元素来为具体的切面实现引用后端bean实例。
切面bean必须有一个标识符,供aop:aspect元素引用。
5.3 声明切入点
1、切入点使用<aop:pointcut>元素声明。
2、切入点必须定义在<aop:aspect>元素下,或者直接定义在<aop:config>元素下。
1)定义在<aop:aspect>元素下:只对当前切面有效
2)定义在<aop:config>元素下:对所有切面都有效
3、基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。
5.4 声明通知
1、在aop名称空间中,每种通知类型都对应一个特定的XML元素。
2、通知元素需要使用来引用切入点,或用直接嵌入切入点表达式。
3、method属性指定切面类中通知方法的名称
<?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: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/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd"> <!-- 基于注解的AOP步骤; 1、将目标类和切面类都加入到ioc容器中。@Component 2、告诉Spring哪个是切面类。@Aspect 3、在切面类中使用五个通知注解来配置切面中的这些通知方法都何时何地运行 4、开启基于注解的AOP功能 --> <!-- 开启基于注解的AOP功能;aop名称空间--> <!-- 基于配置的AOP--> <bean id="myMathCalculator" class="com.atguigu.impl.MyMathCalculator"></bean> <bean id="BValidateApsect" class="com.atguigu.utils.BValidateApsect"></bean> <bean id="logUtils" class="com.atguigu.utils.LogUtils"></bean> <!-- 需要AOP名称空间 --> <aop:config> <aop:pointcut expression="execution(* com.atguigu.impl.*.*(..))" id="globalPoint"/> <!-- 普通前置 ===== 目标方法 =====(环绕执行后置/返回)====s普通后置====普通返回--> <!-- 指定切面:@Aspect --> <aop:aspect ref="logUtils" order="1"> <!-- 当前切面能用的 --> <aop:around method="myAround" pointcut-ref="mypoint"/> <aop:pointcut expression="execution(* com.atguigu.impl.*.*(..))" id="mypoint"/> <!-- 配置哪个方法是前置通知;method指定方法名--> <aop:before method="logStart" pointcut="execution(* com.atguigu.impl.*.*(..))"/> <aop:after-returning method="logReturn" pointcut-ref="mypoint" returning="result"/> <aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="exception"/> <aop:after method="logEnd" pointcut-ref="mypoint"/> </aop:aspect> <aop:aspect ref="BValidateApsect" order="3"> <aop:before method="logStart" pointcut-ref="globalPoint"/> <aop:after-returning method="logReturn" pointcut-ref="globalPoint" returning="result"/> <aop:after-throwing method="logException" pointcut-ref="globalPoint" throwing="exception"/> <aop:after method="logEnd" pointcut-ref="globalPoint"/> </aop:aspect> <!-- 在切面类中使用五个通知注解来配置切面中的这些通知方法都何时何地运行 --> </aop:config> <!--注解:快速方便 配置:功能完善;重要的用配置,不重要的用注解; --></beans>
六、JdbcTemplate
6.1 概述
为了使JDBC更加易于使用,Spring在JDBC API上定义了一个抽象层,以此建立一个JDBC存取框架。
作为Spring JDBC框架的核心,JDBC模板的设计目的是为不同类型的JDBC操作提供模板方法,通过这种方式,可以在尽可能保留灵活性的情况下,将数据库存取的工作量降到最低。
可以将Spring的JdbcTemplate看作是一个小型的轻量级持久化层框架,和我们之前使用过的DBUtils风格非常接近。
6.2 环境准备
6.2.1 导入JAR包
1、IOC容器所需要的JAR包
commons-logging-1.1.1.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
2、JdbcTemplate所需要的JAR包
spring-jdbc-4.0.0.RELEASE.jar
spring-orm-4.0.0.RELEASE.jar
spring-tx-4.0.0.RELEASE.jar
3、数据库驱动和数据源
c3p0-0.9.1.2.jar
mysql-connector-java-5.1.7-bin.jar
6.2.2 创建连接数据库基本信息属性文件
user=rootpassword=rootjdbcUrl=jdbc:mysql:///query_datadriverClass=com.mysql.jdbc.DriverinitialPoolSize=30minPoolSize=10maxPoolSize=100acquireIncrement=5maxStatements=1000maxStatementsPerConnection=10
6.2.3 在Spring配置文件中配置相关的bean
1、数据源对象
<context:property-placeholder location="classpath:jdbc.properties"/><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${user}"/> <property name="password" value="${password}"/> <property name="jdbcUrl" value="${jdbcUrl}"/> <property name="driverClass" value="${driverClass}"/> <property name="initialPoolSize" value="${initialPoolSize}"/> <property name="minPoolSize" value="${minPoolSize}"/> <property name="maxPoolSize" value="${maxPoolSize}"/> <property name="acquireIncrement" value="${acquireIncrement}"/> <property name="maxStatements" value="${maxStatements}"/> <property name="maxStatementsPerConnection" value="${maxStatementsPerConnection}"/></bean>
2、JdbcTemplate对象
<bean id="template" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/></bean>
6.3 持久化操作
6.3.1 增删改
JdbcTemplate.update(String, Object...)
6.3.2 批量增删改
JdbcTemplate.batchUpdate(String, List<Object[]>)
Object[]封装了SQL语句每一次执行时所需要的参数
List集合封装了SQL语句多次执行时的所有参数
6.3.3 查询单行
JdbcTemplate.queryForObject(String, RowMapper, Object...)
6.3.4 查询多行
JdbcTemplate.query(String, RowMapper, Object...)
RowMapper对象依然可以使用BeanPropertyRowMapper
6.3.5 查询单一值
JdbcTemplate.queryForObject(String, Class, Object...)
6.4 使用具名参数的JdbcTemplate
6.4.1 关于具名参数
在Hibernate的HQL查询中我们体验过具名参数的使用,相对于基于位置的参数,具名参数具有更好的可维护性,在SQL语句中参数较多时可以考虑使用具名参数。
在Spring中可以通过NamedParameterJdbcTemplate类的对象使用带有具名参数的SQL语句。
6.4.2 通过IOC容器创建NamedParameterJdbcTemplate对象
<!-- 配置可以使用具名参数的JDBCTemplate类对象 --><bean id="namedTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate"> <!-- 没有无参构造器,必须传入数据源或JdbcTemplate对象 --> <constructor-arg ref="dataSource"/></bean>
6.4.3 具名参数在SQL语句中的格式
INSERT INTO depts (dept_name) VALUES (:deptName)
6.4.4 具名参数传入
1、通过Map对象传入
NamedParameterJdbcTemplate.update(String, Map<String, ?>)
Map的键是参数名,值是参数值
2、通过SqlParameterSource对象传入
String sql = "INSERT INTO depts (dept_name) VALUES (:deptName)";Department department = new Department(null, "YYY", null);SqlParameterSource sqlParameterSource = new BeanPropertySqlParameterSource(department);namedTemplate.update(sql, sqlParameterSource);
6.5 使用JdbcTemplate实现Dao
6.5.1 通过IOC容器自动注入
JdbcTemplate类是线程安全的,所以可以在IOC容器中声明它的单个实例,并将这个实例注入到所有的Dao实例中。
@Repositorypublic class EmployeeDao { @Autowired private JdbcTemplate jdbcTemplate; public Employee get(Integer id){ //… }}
6.5.2 扩展JdbcDaoSupport类
/** * 不推荐 */@Repositorypublic class DepartmentDao extends JdbcDaoSupport{ @Autowired public void setDataSource2(DataSource dataSource){ //父类提供的setDataSource()方法是final修饰的,不能通过覆盖的方式注入dataSource setDataSource(dataSource); } public Department get(Integer id){ String sql = "SELECT id, dept_name name FROM departments WHERE id = ?"; RowMapper<Department> rowMapper = new BeanPropertyRowMapper<>(Department.class); return getJdbcTemplate().queryForObject(sql, rowMapper, id); }}
七、声明式事务
7.1 事务概述
1、在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
2、事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
3、事务的四个关键属性(ACID)
1)原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
2)一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
3)隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
4)持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
7.2 Spring事务管理
7.2.1 编程式事务管理
1、使用原生的JDBC API进行事务管理
1)获取数据库连接Connection对象
2)取消事务的自动提交
3)执行操作
4)正常完成操作时手动提交事务
5)执行失败时回滚事务
6)关闭相关资源
2、评价
使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
7.2.2 声明式事务管理
大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
Spring既支持编程式事务管理,也支持声明式的事务管理。
7.2.3 Spring提供的事务管理器
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
事务管理器可以以普通的bean的形式声明在Spring IOC容器中。
7.2.4 事务管理器的主要实现
1、DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。
2、JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理
3、HibernateTransactionManager:用Hibernate框架存取数据库
7.3 测试数据准备
7.3.1 需求
7.3.2 数据库表
CREATE TABLE book ( isbn VARCHAR (50) PRIMARY KEY, book_name VARCHAR (100), price INT);CREATE TABLE book_stock ( isbn VARCHAR (50) PRIMARY KEY, stock INT, CHECK (stock > 0));CREATE TABLE account ( username VARCHAR (50) PRIMARY KEY, balance INT, CHECK (balance > 0));INSERT INTO account (`username`,`balance`) VALUES ('Tom',100000);INSERT INTO account (`username`,`balance`) VALUES ('Jerry',150000);INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-001','book01',100);INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-002','book02',200);INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-003','book03',300);INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-004','book04',400);INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-005','book05',500);INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-001',1000);INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-002',2000);INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-003',3000);INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-004',4000);INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-005',5000);
7.4 初步实现
7.4.1 配置文件
<!-- 配置事务管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean><!-- 启用事务注解 --><tx:annotation-driven transaction-manager="transactionManager"/>
7.4.2 在需要进行事务控制的方法上加注解
@Transactional
7.5 事务的传播行为
7.5.1 简介
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。
事务传播属性可以在@Transactional注解的propagation属性中定义。
7.5.2 测试
7.5.3 说明
1、REQUIRED传播行为
当bookService的purchase()方法被另一个事务方法checkout()调用时,它默认会在现有的事务内运行。这个默认的传播行为就是REQUIRED。因此在checkout()方法的开始和终止边界内只有一个事务。这个事务只在checkout()方法结束的时候被提交,结果用户一本书都买不了。
2、REQUIRES_NEW传播行为
表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它。
7.5.4 补充
在Spring 2.x事务通知中,可以像下面这样在<tx:method>元素中设定传播事务属性。
7.6 事务的隔离级别
7.6.1 数据库事务并发问题
假设现在有两个事务:Transaction01和Transaction02并发执行。
1、脏读
[1] Transaction01将某条记录的AGE值从20修改为30。
[2] Transaction02读取了Transaction01更新后的值:30。
[3] Transaction01回滚,AGE值恢复到了20。
[4] Transaction02读取到的30就是一个无效的值。
2、不可重复读
[1] Transaction01读取了AGE值为20。
[2] Transaction02将AGE值修改为30。
[3] Transaction01再次读取AGE值为30,和第一次读取不一致。
3、幻读
[1] Transaction01读取了STUDENT表中的一部分数据。
[2] Transaction02向STUDENT表中插入了新的行。
[3] Transaction01读取了STUDENT表时,多出了一些行。
7.6.2 隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
1、读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
2、读已提交:READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。
3、可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
4、串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
5、各个隔离级别解决并发问题的能力见下表
| 脏读 | 不可重复读 | 幻读 | |
|---|---|---|---|
| READ UNCOMMITTED | 有 | 有 | 有 |
| READ COMMITTED | 无 | 有 | 有 |
| REPEATABLE READ | 无 | 无 | 有 |
| SERIALIZABLE | 无 | 无 | 无 |
6、各种数据库产品对事务隔离级别的支持程度
| Oracle | MySQL | |
|---|---|---|
| READ UNCOMMITTED | × | √ |
| READ COMMITTED | √ | √ |
| REPEATABLE READ | × | √(默认) |
| SERIALIZABLE | √ | √ |
7.6.3 在Spring中指定事务隔离级别
1、注解
用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别
2、XML
在Spring 2.x事务通知中,可以在tx:method元素中指定隔离级别
7.7 触发事务回滚的异常
7.7.1 默认情况
捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。
7.7.2 设置途经
1、注解
@Transactional 注解
1)rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个
2)noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个
2、XML
在Spring 2.x事务通知中,可以在<tx:method>元素中指定回滚规则。如果有不止一种异常则用逗号分隔。
7.8 事务的超时和只读属性
7.8.1 简介
由于事务可以在行和表上获得锁,因此长事务会占用资源,并对整体性能产生影响。如果一个事务只读取数据但不做修改,数据库引擎可以对这个事务进行优化。
超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。
7.8.2 设置
1、注解
@Transaction注解
2、XML
在Spring 2.x事务通知中,超时和只读属性可以在<tx:method>元素中进行指定
7.9 基于XML文档的声明式事务配置
<!-- 配置事务切面 --><aop:config> <aop:pointcut expression="execution(* com.atguigu.tx.component.service.BookShopServiceImpl.purchase(..))" id="txPointCut"/> <!-- 将切入点表达式和事务属性配置关联到一起 --> <aop:advisor advice-ref="myTx" pointcut-ref="txPointCut"/></aop:config><!-- 配置基于XML的声明式事务 --><tx:advice id="myTx" transaction-manager="transactionManager"> <tx:attributes> <!-- 设置具体方法的事务属性 --> <tx:method name="find*" read-only="true"/> <tx:method name="get*" read-only="true"/> <tx:method name="purchase" isolation="READ_COMMITTED" no-rollback-for="java.lang.ArithmeticException,java.lang.NullPointerException" propagation="REQUIRES_NEW" read-only="false" timeout="10"/> </tx:attributes></tx:advice>