Spring-初识
--------------------------------------------------------------------------------------
1、获取bean实例的三种方式
1.1 id 属性
1.1.1 jar
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.3.18.RELEASE</spring.version>
<lombok.version>1.16.18</lombok.version>
<junit.version>4.11</junit.version>
</properties>
<dependencies>
<!-- spring-beans begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-beans end -->
<!-- spring-core begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-core end -->
<!-- spring-context begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-context end -->
<!-- spring-expression begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-expression end -->
</dependencies>
1.1.2 application.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 基于spring的核心配置文件方式,手动配置一个bean,放到spring的容器中 -->
<bean id="userOne" class="com.kgc.spring.bean.User">
<property name="nickName" value="kh96_spring_one"></property>
</bean>
</beans>
<bean>标签:定义一个实例对象,会自动被创建 并 交给spring容器进行管理:
-
id 属性:被spring容器进行管理的实例的 唯一标识,整个容器中,只能是唯一的(不可重复);
-
class属性:指定创建当前实例对象的类型(全类名),spring底层是使用的 反射机制 ,根据指定的目标类型,创建目标实例(必须有 空参构造)
-
<property>子标签:给当前创建对象的属性值
- name:指定对象的属性名
- value: 给属性赋值
1.1.3 测试
@Test
public void testSpringPrimer(){
//1.创建spring的核心容器对象(引用上下文对象),解析spring的核心配置文件,将文件中的bean标签进行实例化(创建对象并赋值)
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//2. 写法一: 从容器中获取实例对象,根据核心配置文件中,配置的bean标签的id属性值
// 不足:强制转化,容易出现转换错误
User userOne = (User) context.getBean("userOne");
userOne.sayHello();
//对比以前创建对象的写法(对象必须手动new创建,并手动赋值)
User userOld = new User();
userOld.setNickName("kh96_old");
userOld.sayHello();
}
1.1.4 总结
通过id属性,获取实例对象后需要强制转换,容易出现强制转化错误;
1.2 class 属性
1.2.1 测试
//写法二:从容器中获取实例对象,根据核心配置文件中,配置的bean标签的class属性指定的目标类型
User userTwo = context.getBean(User.class);
userTwo.sayHello();
1.2.2 总结
不足:如果存在同类型的多个实例,会报异常;
1.2.2.1 application.xml
当用多个同类型的实例:
...
<bean id="userOne" class="com.kgc.spring.bean.User">
<property name="nickName" value="kh96_spring_one"></property>
</bean>
<!-- Configuration problem: Bean name 'userOne' is already used in this <beans> element -->
<bean id="userTwo" class="com.kgc.spring.bean.User">
<property name="nickName" value="kh96_spring_two"></property>
</bean>
...
1.2.2.2 测试
User userTwo = context.getBean(User.class);
会发生异常:
NoUniqueBeanDefinitionException: No qualifying bean of type 'com.kgc.spring.bean.User' available: expected single matching bean but found 2: userOne,userTwo
主要原因是由于只用class属性去获取实例,但是有多个相同类型的实例,所以无法确定你要获取的是哪一个;
1.3 id 属性 和 class 属性 (推荐)
1.3.1 测试
//写法三:从容器中获取实例对象,根据配置文件中,配置的bean标签的id属性值和class属性指定的目标类型
User userTwoThree = context.getBean("userTwo", User.class);
1.3.2 总结
能够确定唯一实例,推荐使用;
1.4 id 不能重复注意点
配置文件中的id必须是唯一的;
如果id不唯一:两个id一样的实例
...
<bean id="userOne" class="com.kgc.spring.bean.User">
<property name="nickName" value="kh96_spring_one"></property>
</bean>
<bean id="userOne" class="com.kgc.spring.bean.User">
<property name="nickName" value="kh96_spring_one"></property>
</bean>
...
报错:
BeanDefinitionParsingException: Configuration problem: Bean name 'userOne' is already used in this <beans> element
提示 id 为 userOne 的实例已经存在;
1.5 实例化时机
当初始化spring的容器对象时,会将核心配置文件中所有的bean实例化,不是使用哪个,创建哪个;
2、DI
IOC(控制反转是一种思想),DI是IOC的一种实现方式;
2.1 set 方式注入
2.1.1 实体
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
//用户昵称
//普通参数
private String nickName;
//私家车
//实体参数
private Car car;
//喜欢的书
//数组参数
private String[] books;
//爱好
//list集合参数
private List<String> hobbies;
//喜欢的游戏
//set 集合
private Set<String> games;
//卡包
//map参数
private Map<String,String> cards;
//在校信息
//properties参数
private Properties info;
//是否有女朋友 (主要是演示赋值为null,其他类型都可以)
//空参数注入
private Boolean wife;
}
2.1.2 参数 注入
...
<!-- 不同类型参数的set注入 -->
<bean id="user01" class="com.kgc.spring.bean.User">
<!-- 1.普通参数 注入 -->
<property name="nickName" value="小气鬼"></property>
<!-- 2.实体参数 注入 -->
<!-- 2.1 实体参数 注入方式一 外部引入 ref -->
<!-- <property name="car" ref="carOne"></property>-->
<property name="car">
<!-- 2.2 实体参数注入方式二 内部注入 <bean> -->
<bean class="com.kgc.spring.bean.Car">
<property name="brand" value="Bmw325"></property>
<property name="factory" value="华晨"></property>
<property name="price" value="300000"></property>
</bean>
</property>
<!-- 3.数组参数 注入 -->
<property name="books">
<array>
<value>红楼梦</value>
<value>水浒传</value>
<value>三国演义</value>
<value>西游记</value>
</array>
</property>
<!-- 4.list集合参数 -->
<property name="hobbies" >
<list>
<value>跑步</value>
<value>尤克里里</value>
<value>敲代码</value>
</list>
</property>
<!-- 5.set 集合参数 注入 -->
<property name="games">
<set>
<value>唱歌</value>
<value>跳舞</value>
<value>喝酒</value>
</set>
</property>
<!-- 6.map参数 -->
<property name="cards">
<map>
<entry key="身份证" value="1212121212121212"></entry>
<entry key="银行卡" value="1111111111111111"></entry>
</map>
</property>
<!-- 7.properties参数 -->
<property name="info">
<props>
<prop key="学号">123456</prop>
<prop key="性别">男</prop>
<prop key="姓名">化羽</prop>
</props>
</property>
<!-- 8.空参数注入 -->
<property name="wife">
<null></null>
</property>
</bean>
<bean id="carOne" class="com.kgc.spring.bean.Car">
<property name="brand" value="Bmw325"></property>
<property name="factory" value="华晨"></property>
<property name="price" value="300000"></property>
</bean>
...
2.1.3 测试
@Test
public void testPramsDI(){
User user01 = context.getBean("user01", User.class);
//输入对象详情
System.out.println(user01);
}
输出结果:
User(
nickName=小气鬼,
car=Car(brand=Bmw325, factory=华晨, price=300000.0),
books=[红楼梦, 水浒传, 三国演义, 西游记],
hobbies=[跑步, 尤克里里, 敲代码],
games=[唱歌, 跳舞, 喝酒],
cards={身份证=1212121212121212, 银行卡=1111111111111111},
info={学号=1913001072, 性别=男, 姓名=化羽},
wife=null
)
2.2 Constructor 构造器注入
2.2.1 按照默认顺序 注入
2.2.1.1 参数注入
<!--
实体构造方法:Car(String brand, String factory, Double price)
-->
<!-- 构造器注入:通过构造方法,默认是按照构造方法的参数定义顺序赋值 -->
<bean id="carTwo" class="com.kgc.spring.bean.Car">
<constructor-arg value="AudiA4"></constructor-arg>
<constructor-arg value="一汽"></constructor-arg>
<constructor-arg value="320000"></constructor-arg>
</bean>
2.2.1.2 测试
@Test
public void testSpringDIConstructor(){
Car carTwo = context.getBean("carTwo", Car.class);
//输出对象详情
System.out.println(carTwo);
}
2.2.2 根据 参数的下标 和 类型 注入
2.2.2.1 参数注入
<!-- 根据构造器中参数的下标 和 类型 赋值 -->
<bean id="carThree" class="com.kgc.spring.bean.Car">
<constructor-arg index="0" value="BenzC200"></constructor-arg>
<constructor-arg index="1" value="北京"></constructor-arg>
<constructor-arg index="2" type="java.lang.Double" value="350000"></constructor-arg>
<!--
如果参数列表中,该类型的参数只用一个,也可以只指定参数类型
<constructor-arg type="java.lang.Double" value="350000"></constructor-arg>
-->
</bean>
2.2.2.2 测试
@Test
public void testSpringDIConstructor2(){
Car carTwo = context.getBean("carThree", Car.class);
//输入对象详情
System.out.println(carTwo);
}
2.3 自定义 实体工厂bean
自定义实体工厂bean ,必须实现FactoryBean接口;
普通bean 与 工厂bean 的区别:
- 普通 bean:在配置文件中定义 bean 类型 就是 返回类型
- 工厂 bean:在配置文件定义 bean 类型 和 返回类型 不一样
2.3.1 返回bean
@Data
@ToString
public class Car {
/*
品牌
*/
private String brand;
/*
厂商
*/
private String factory;
/*
价格
*/
private Double price;
}
2.3.2 实体工厂 bean
- 在spring容器初始化时,创建当前工厂bean的实例对象,但是真实返回的不是当前类的实例对象,而是当前类的实例对象返回的目标实例对象(自定义);
- 作用:可以在程序中,实现自定义创建实例(还可以增加业务逻辑处理),放入容器;
- 存在的原因:spring框架对外开放了一个入口,可以让其他的开发人员参与到spring底层创建bean的实例过程中去,给整合其他框架使用的,比如mybatis;
public class CarFactoryBean implements FactoryBean<Car> {
@Override
public Car getObject() throws Exception {
System.out.println("通过CarFactoryBean的实例对象的getObject方法:返回一个自定义的car的实例");
return new Car("Byd唐","南京",2500000.0);
}
@Override
public Class<?> getObjectType() {
//指定给Object方法返回的目标类型
return Class.class;
}
@Override
public boolean isSingleton() {
//是否单例
return false;
}
}
2.3.3 工厂bean容器中添加自定义实例对象
...
<!-- 使用工厂bean容器,添加自定义实例对象 -->
<bean id="carFactoryBean" class="com.kgc.spring.bean.CarFactoryBean"></bean>
...
2.3.4 测试
@Test
public void testSpringFactoryBean(){
//从容器中获取工厂bean的实例对象
Car car1 = context.getBean("carFactoryBean", Car.class);
Car car2 = context.getBean("carFactoryBean", Car.class);
//输出对象详情
System.out.println(car 1);
//在容器中添加的实例是 CarFactoryBean 类型,返回的是
//Car(brand=Byd唐, factory=南京, price=2500000.0)
System.out.println("car1 == car2 : " + ( car1 == car2));
//false 说明工厂bean是多例的
}
3、scope 作用域
- singleton 单例 (默认) 容器初始化之前创建;
- prototype 多例 (手动设置) 使用到的时候才创建;
3.1 singleton 单例
这里区别于,整个程序运行期间,有且只有唯一的实例(单例模式-懒汉和饿汉);
容器中bean的单例,不是指当前类的实例在容器中,只有唯一的实例,而是当创建bean的实例时,此实例是单例(容器内唯一),但是同一个类的实例,容器中可以创建多个,每个都是单例的;
3.1.1 配置bean
<!-- scope="singleton" 不写默认也是单例的 -->
<bean id="busTwo" class="com.kgc.spring.bean.Bus" scope="singleton">
<property name="brand" value="金龙2"></property>
<property name="factory" value="厦门2"></property>
<property name="price" value="1200000"></property>
</bean>
3.1.2 测试
@Test
public void testScopeSingleton(){
//从容器中,获取Bus的实例对象
Bus busThree = context.getBean("busTwo", Bus.class);
Bus busFour = context.getBean("busTwo", Bus.class);
System.out.println(busThree);
System.out.println(busFour);
System.out.println("singleton busThree == busFour:"+(busThree == busFour));
//true
}
3.2 prototype 多例
多例:prototype,不是spring容器中的默认作用,需要单独指定;
spring容器创建时,不会自动创建指定作用域为多例的bean的实例,而是每次通过getBean方法,获取bean的实例,才会创建bean的实例;
3.2.1 配置bean
<!-- scope="prototype" 多例需要手动设置 -->
<bean id="busThree" class="com.kgc.spring.bean.Bus" scope="prototype">
<property name="brand" value="中通3"></property>
<property name="factory" value="山东3"></property>
<property name="price" value="1200000"></property>
</bean>
3.2.2 测试
@Test
public void testScopePrototype(){
Bus busOne = context.getBean("busThree", Bus.class);
Bus busTwo = context.getBean("busThree", Bus.class);
//输入详情
System.out.println(busOne);
System.out.println(busTwo);
System.out.println("prototype busOne == busTwo:"+(busOne == busTwo));
//false
}
-----------------------------------------------------------------------------------------
1、生命周期
Spring容器的 bean的生命周期;
1.1 默认生命周期
1.1.1 生命周期
- 调用构造方法,创建实例对象;
- set方法,给实例对象赋值;
- init 初始化方法 初始化对象;(手写并配置到bean上init-method="")
- 使用容器中的bean对象;
- destroy 销毁方法 销毁对象 (手写并配置到bean上destroy-method="")
1.1.2 bean 实体类
Truck
@Data
@ToString
public class Truck {
//品牌
private String brand;
//厂商
private String factory;
//价格
private Double price;
public Truck() {
//空参构造方法,观察bean什么时候实例化
System.out.println("------ 1.调用构造方法,创建实例对象 ------\n");
}
public void setBrand(String brand) {
//任意一个set方法,观察bean什么时候注入参数
System.out.println("------ 2.set方法,给实例对象赋值 ------");
this.brand = brand;
}
public void initTruck(){
//init初始化方法,观察bean什么时候初始化
//需要再配置bean的时候,配置init初始化方法
System.out.println("------ 3.Truck init 初始化方法 初始化对象 ------\n");
this.brand = "大运";
}
public void destroyTruck(){
//destory方法,观察bean什么时候销毁
//需要再配置bean的时候,配置destory销毁方法
System.out.println("------ 5.Truck destroy 销毁方法 销毁对象 ------\n");
}
//这里方法上标注的序号是测试后得来的;
}
1.1.3 bean 配置
spring-lifecycle.xml
<!-- spring容器中bean的生命周期 默认生命周期 -->
<bean id="truck" class="com.kgc.spring.lifecycle.Truck" init-method="initTruck" destroy-method="destroyTruck">
<property name="brand" value="江淮"></property>
<property name="factory" value="安徽"></property>
<property name="price" value="200000"></property>
</bean>
1.1.4 测试
public class TestSpringLifeCycle {
//定义全局容器对象,如果需要关闭容器对象,
//必须使用ApplicationContext的子接口 ConfigurableApplicationContext
//ApplicationContext接口主要各种属性的get方法;
//ConfigurableApplicationContext重在对各种属性的配置;
// private ApplicationContext context;
private ConfigurableApplicationContext context;
@Before
public void initApplicationContext(){
context = new ClassPathXmlApplicationContext("spring-lifecycle.xml");
}
//测试spring 容器的bean的生命周期,默认和加了处理器两种场景
@Test
public void testSpringBeanLifeCycle(){
//从容器中,获取Truck的是实例对象
Truck truck = context.getBean("truck", Truck.class);
//使用对象
System.out.println("------ 4.使用容器中的bean对象"+truck +" ------");
//关闭容器
context.close();
}
}
输出结果:
//可以得出 spring中bean的 默认生命周期
------ 1.调用构造方法,创建实例对象 ------
------ 2.set方法,给实例对象赋值 ------
------ 3.Truck init 初始化方法 初始化对象 ------
------ 4.使用容器中的bean对象Truck(brand=大运, factory=安徽, price=200000.0) ------
------ 5.Truck destroy 销毁方法 销毁对象 ------
1.1.5 ApplicationContext 和 ConfigurableApplicationContext
参考博客: ApplicationContext和ConfigurableApplicationContext解析
-
ApplicationContext接口主要 各种属性的get方法;
-
ConfigurableApplicationContext重在对 各种 属性的配置;
1.2 增加后置处理器
1.2.1 生命周期
1.调用构造方法,创建实例对象;
2.set方法,给实例对象赋值;
3-1.后置处理的 before 方法;
3.初始化方法 初始化对象;
3+1.后置处理器的的 after 方法;
4.使用容器中的bean对象;
5.destroy 销毁方法 销毁对象;
1.2.2 后置处理器
-
要求:必须实现 BeanPostProcessor 接口
-
自定义 bean 的 后置处理器,对容器中所有的bean统一处理(生效),
-
要生效的话,必须将此处理器放到容器中(配置到spring的核心配置文件中,增加处理器的实例配置);
注意:
当前案例,只对容器中的一个实例处理;
MyBeanPostProcessor
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
//在容器中bean的实例对象调用 初始化方法 前 自动调用(init方法可以没有,不影响)
//模拟处理容器中的bean,接下来的写法,仅限于当前的用法案例(容器中就 只有一个 卡车实例)
Truck truck = (Truck)bean;
System.out.println("++++++ 容器中的卡车对象 "+truck+"++++++");
System.out.println("++++++ 3-1,后置处理的 before 方法 ++++++");
truck.setBrand("后置处理的before方法");
System.out.println("++++++ 处理后的 卡车对象 "+truck+" ++++++\n");
return truck;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//bean 初始化方法 执行 后,调用此方法处理bean
Truck truck = (Truck)bean;
System.out.println("++++++ 初始化容器中的卡车对象 "+truck+"++++++");
System.out.println("++++++ 3+1,后置处理器的的 after 方法 ++++++");
truck.setBrand("after");
System.out.println("++++++ 初始化后 处理后的 卡车对象 "+truck+" ++++++\n");
return truck;
}
}
1.2.3 bean 配置
在配置文件中配置 MyBeanPostProcessor;
<!-- 配置后置处理器的实例,自动放入容器中,可以自动生效 (容器中所有的实例生效) -->
<bean class="com.kgc.spring.lifecycle.MyBeanPostProcessor"></bean>
1.2.4 测试
跟默认生命周期的测试代码一致;
输出结果:
------ 1.调用构造方法,创建实例对象 ------
------ 2.set方法,给实例对象赋值 ------
++++++ 容器中的卡车对象 Truck(brand=江淮, factory=安徽, price=200000.0)++++++
++++++ 3-1,后置处理的 before 方法 ++++++ ------ 2.set方法,给实例对象赋值 ------
++++++ 处理后的 卡车对象 Truck(brand=后置处理的before方法, factory=安徽, price=200000.0) ++++++
------ 3.Truck init 初始化方法 初始化对象 ------
++++++ 初始化容器中的卡车对象 Truck(brand=大运, factory=安徽, price=200000.0)++++++
++++++ 3+1,后置处理器的的 after 方法 ++++++ ------ 2.set方法,给实例对象赋值 ------
++++++ 初始化后 处理后的 卡车对象 Truck(brand=after, factory=安徽, price=200000.0) ++++++
------ 4.使用容器中的bean对象Truck(brand=after, factory=安徽,
------ 5.Truck destroy 销毁方法 销毁对象 ------
1.2.3 BeanPostProcesso
参考博客:BeanPostProcessor简介
BeanPostProcessor官方定义为工厂钩子,我们也俗称后置处理器。它允许自定义修改新的bean实例,例如检查标记接口或用代理包装它们。应用程序上下文可以在其bean定义中自动检测BeanPostProcessor bean,并将它们应用于随后创建的任何bean。
BeanPostProcessor 的 前置处理 和 后置处理;
![image-20220826150821502](Spring-02 生命周期 + 自动装配(xml) +自动装配(注解).assets/image-20220826150821502.png)
2、自动装配(xml)
2.1 bean 实体类
Person
@Data
public class Person {
//昵称
private String nickName;
//车子
private Car car;
//房子
private House house;
}
Car
@Data
public class Car {
//品牌
private String brand;
//厂商
private String factory;
//价格
private Double price;
}
House
@Data
public class House {
//户型
private String type;
//面积
private double area;
//价格
private Integer price;
}
2.2 bean 配置 (byType)
autowire="byType":根据属性 的 类型自动装配;
spring-auto.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- spring的自动装配方式,基于xml配置文件方式,掌握 -->
<!-- 容器中实例化一个容器的Car对象 -->
<bean id="car" class="com.kgc.spring.auto.Car">
<property name="brand" value="Bnw520"></property>
<property name="factory" value="华晨"></property>
<property name="price" value="450000"></property>
</bean>
<!-- 容器中实例化一个容器的House对象 -->
<bean id="house" class="com.kgc.spring.auto.House">
<property name="type" value="三室一厅"></property>
<property name="area" value="96"></property>
<property name="price" value="2800000"></property>
</bean>
<!-- 根据类型自动装配 -->
<bean id="person" class="com.kgc.spring.auto.Person" autowire="byType">
<property name="nickName" value="huayu"></property>
</bean>
</beans>
2.3 测试
public class TestSpringAutoUserXml {
private ApplicationContext context;
@Before
public void initApplicationContext(){
context = new ClassPathXmlApplicationContext("spring-auto.xml");
}
@Test
public void testSpringAuto(){
Person person = context.getBean("person", Person.class);
//使用对象
System.out.println("容器中的person对象:"+person);
}
}
输出结果:
容器中的person对象:Person(
nickName=huayu,
car=Car(brand=Bnw520, factory=华晨, price=450000.0),
house=House(type=三室一厅, area=96.0, price=2800000)
)
2.4 bean 配置 (多个同类型bean)
bean 配置:
其他不变,多增加一个Car类型的实例bean;
<bean id="car" class="com.kgc.spring.auto.Car">
<property name="brand" value="Bnw520"></property>
<property name="factory" value="华晨"></property>
<property name="price" value="450000"></property>
</bean>
<bean id="carNew" class="com.kgc.spring.auto.Car">
<property name="brand" value="AudiA6"></property>
<property name="factory" value="一汽"></property>
<property name="price" value="450000"></property>
</bean>
测试,报错信息:
No qualifying bean of type 'com.kgc.spring.auto.Car' available: expected single matching bean but found 2: car,carNew
总结
:autowire="byType" 当有多个相同类型的bean时,无法确定要装配的 bean;
2.5 bean 配置(byName)
其他配置信息不变,设置 autowire="byName" ,根据 属性 的 名字 自动装配;
<bean id="person" class="com.kgc.spring.auto.Person" autowire="byName">
<property name="nickName" value="hauyu"></property>
</bean>
测试输出结果:
容器中的person对象:Person(
nickName=huayu,
car=Car(brand=Bnw520, factory=华晨, price=450000.0),
house=House(type=三室一厅, area=96.0, price=2800000)
)
总结
:
- byType:根据类型自动装配:
- 根据实体属性的 类型,到容器中,根据 bean类型 进行唯一匹配,如果可以匹配到对应类型的bean的实例,就会执行自动装配, 如果不能唯一匹配(同类型的bean有多个),会报错;
- byName: 根据名称自动装配:
- 根据属性的 属性名,到容器中,根据 bean的id 属性值,进行唯一匹配,如果能够成功匹配,执行自动装配, 如果匹配不到,不执行自动装配,实体属性为null;
3、自动装配 (注解)
3.1 注解
- @Component 普通组件注解;
- @Repository 持久层注解
- @Service 业务层注解
- @Controller 控制层注解
3.3.1 注解的原理
默认情况下:spring自动将分层组件(@Controller,@Service,@Repository,@component)标识的类(不是接口),自动创建实例对象放入容器中,使用bean的标识id值为 对应类名首字母小写 就相当于,帮我们手动添加了配置 :
<bean id="分层注解标识类的类名首字母小写" class="分层注解标识类的全类名"> ... <bean>
3.1.2 自定义id 属性
如果不想使用默认的类名首字母小写,我们可以使用注解的value属性执行一个自定义的id值;
比如:@Service(value="自定义的id值"),注解只有value属性,可以省略value执行,简化为@Service("自定义的id值")
3.1.3 分层组件的目的
分层组件的目的,就仅仅是为了方便开发人员明确当前注解所在的类所对应的角色,在使用上,建议使用,按照官方定义的使用,防止模糊不清;在springMVC框架中@Controller有特殊含义;
3.2 配置文件
spring创建容器对象时,如果解析到 component-scan 组件扫描配置,会将base-package指定的基包(父包)及其子包所有增加了分层组件的类,自动创建实例,放进容器中;
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
">
<!-- 组件扫描:注解标识的组件,必须通过组件扫描配置,才可以添加到spring的容器中-->
<context:component-scan base-package="com.kgc.spring.acnocation" > </context:component-scan>
</beans>
3.3 默认id属性 测试
3.3.1 实体
@Component
public class User {
//用户名
@Value("huayu") //@Value() 自动装配参数
private String userName;
//用户密码
@Value("123")
private String userPwd;
}
3.3.2 测试
@Test
public void testSpringAutoUserAnnotation(){
User user = context.getBean("user", User.class);
System.out.println(user);
//User(userName=huayu, userPwd=123)
}
3.4 自定义id属性 测试
3.4.1 实体(自定义id属性)
@Component(value = "myUser")
public class User {
//用户名
private String userName;
//用户密码
private String userPwd;
}
3.4.2 测试
@Test
public void testSpringAutoUserAnnotation(){
//User user = context.getBean("user", User.class);
//自定义id后,默认id不能使用: No bean named 'user' available
//必须使用自定义id
User user = context.getBean("myUser", User.class);
System.out.println(user);
//User(userName=huayu, userPwd=123)
}
3.5 自动装配
3.5.1 @Autowired
- 组件自动装配,可以实现实体属性类型的自动装配,自动到spring的容器中,根据当前属性的类型或者名称进行注入,如果容器中能匹配到,就直接将实例对象注入到当前实体属性上,无序手动指定;
- @Autowired自动装配原理:首先会根据byType方式,进行自动装配,
- 如果不能唯一匹配(存在同类型多个实例对象),会再次尝试使用byName方式,根据当前实体属性名,到容器中进行匹配(容器中bean的id值),如果能唯一匹配,直接执行自动装配,
- 默认情况下,@Autowired注解标识的实体属性,必须被装配
- 如果装配失败,就直接抛出异常;
- 如果不需要校验必须被装配(项目启动,如果装配失败,项目是起不来);
- 通过指定required = false,去除必须执行自动装配的校验(即便容器中找不到装配的实例,也不会抛出异常);
- 如果自动装配,容器中存在多个同类型的bean对象,可以使用注解@Qualifier("容器中同类型多个bean的某个id值"),实现指定到容器中,找对应的bean实例对象,进行自动装配;
- 底层是如何做的:在指定要扫描的包时,<context:component-scan> 元素会自动注册一个bean的后置处理器:AutowiredAnnotationBeanPostProcessor的实例。该后置处理器可以自动装配标记了@Autowired、@Resource或@Inject注解的属性。
3.5.2 实体
People
@Data
@Component("myPeople")
public class People {
//昵称
@Value("huayu")
private String name;
//玩具
@Autowired
private Toy toy;
}
Toy接口
public interface Toy {
//得到玩具
public void getToy();
}
ToyImpl1
@Component("toy1")
public class ToyImpl1 implements Toy {
@Value("玩具车")
private String toyName;
@Override
public void getToy() {
System.out.println(this.toyName);
}
}
3.5.3 测试
注意
:可以通过接口类型获取实现类(推荐使用);
@Test
public void testAutowired (){
People people = context.getBean("myPeople", People.class);
people.getToy().getToy(); //玩具车
}
3.5.4 存在多个相同类型的bean
当存在多个相同类型的bean,不能唯一匹配,会自动装配错误;
在写一个Toy实现类,ToyImpl2
@Component("toy2")
public class ToyImpl2 implements Toy {
@Value("尤克里里")
private String toyName;
@Override
public void getToy() {
System.out.println(this.toyName);
}
}
3.5.4.1 测试
报错信息(项目无法启动):
No qualifying bean of type 'com.kgc.spring.acnocation.bean.Toy' available: expected single matching bean but found 2: toy1,toy2
主要信息:类型无法唯一匹配;
3.5.4.2 required = false 允许不装配
People
@Data
@Component("myPeople")
public class People {
//昵称
@Value("huayu")
private String name;
//玩具
@Autowired (required = false)
private Toy toy;
}
项目可以启动但是还是报错(一般项目中不会有两个相同类型的实现类;)
3.5.4.3 @Quailfy
People
@Data
@Component("myPeople")
public class People {
//昵称
@Value("huayu")
private String name;
//玩具
@Autowired
@Qualifier("toy2") //指定bean的id值
private Toy toy;
}
3.5.4.4 测试
@Test
public void testAutowired (){
People people = context.getBean("myPeople", People.class);
System.out.println(people.getToy());
//com.kgc.spring.acnocation.bean.ToyImpl2@15d9bc04
people.getToy().getToy();
//尤克里里
}
3.6 指定扫描 排除扫描
3.6.1 指定扫描
include-filter
指定扫描(包含扫描):
- 只会扫描指定的类或者某类组件(使用分组扫描),加入到容器中;
- 但是必须配合父标签的user-default-filter使用,默认值是true,就是全部扫描;
- 指定扫描,如果要生效必须改为false;
- 指定扫描某类组件,type="annotation" expression="某类组件注解的全类名";
- 指定扫描某个类,type="assignable" expression="某个类的全类名";
3.6.1.1 指定扫描某类组件
type="annotation"
- org.springframework.stereotype.Component
- org.springframework.stereotype.Repository
- org.springframework.stereotype.Service
- org.springframework.stereotype.Controller
<!-- 指定扫描 @Component 组件 -->
<context:component-scan base-package="com.kgc.spring.acnocation" use-default-filters="false" >
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>
3.6.1.2 指定扫描某个类
type="assignable"
<!-- 指定扫描 ToyImpl1 -->
<context:component-scan base-package="com.kgc.spring.acnocation" use-default-filters="false" >
<context:include-filter type="assignable" expression="com.kgc.spring.acnocation.bean.ToyImpl1"/>
</context:component-scan>
3.6.2 排除扫描
exclude-filter
- 排除扫描(剔除扫描):排除指定的类或某类组件,不加入到容器中,处理排除外的其他组件,仍然会被添加到容器中;
- 不需要配合父标签,use-default-filters="true" 因为,默认就是在全部扫描的基础上剔除;
- 排除扫描某类组件,type="annotation" expression="某类组件注解的全类名";
- 排除扫描某个类,type="assignable" expression="某个类的全类名";
3.6.2.1 排除扫描某类组件
type="annotation"
<!-- use-default-filters="true" 可写可不写 -->
<!-- 排除扫描 @Component组件 -->
<context:component-scan base-package="com.kgc.spring.acnocation" use-default-filters="true" >
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>
3.6.2.2 排除扫描个类
type="assignable"
<!-- 排除扫描 ToyImpl1 -->
<context:component-scan base-package="com.kgc.spring.acnocation" >
<context:exclude-filter type="assignable" expression="com.kgc.spring.acnocation.bean.ToyImpl1"/>
</context:component-scan>
----------------------------------------------------------------------------------------------
1、名词理解
- 切面(Aspect):
- 含有前置通知,后置通知,返回通知,异常抛出通知,环绕通知等方法的类;
- 通知(Advice):
- 对原方法进行添加处理(如日志等)的方法;
- 切入点(PointCute):
- 通知需要在哪些方法上执行的表达式;(可以唯一匹配或模糊匹配);
- 连接点(JoinPoint):
- 与切入点匹配的具体执行的方法;
- 目标(Target):
- 原业务类(主要 是核心代码);
- 代理(Proxy):
- 生成的代理类(包含原业务类的 核心代码 和 通知里面的代码);
2、前置通知
2.1 jar
<properties>
<spring.version>4.3.18.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring-beans begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-beans end -->
<!-- spring-core begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-core end -->
<!-- spring-context begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-context end -->
<!-- spring-expression begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-expression end -->
<!-- spring-aspects begin -->
<!-- maven项目中,使用aop的AspectJ框架,只需要增加此依赖,自动添加依赖aspectjweaver(包含了aspectjrt)-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-aspects end -->
</dependencies>
2.2 切入点
通知需要在哪些方法上执行的表达式;(可以唯一匹配或模糊匹配);
2.2.1 唯一匹配
execution(public int com.kgc.spring.aspectj.ArithmeticCalculator.add(int ,int ))
execution(修饰符 返回值类型 方法全类名)
2.2.2 模糊匹配
execution(* com.kgc.spring.aspectj.*.*(..)
通用切入点表达式含义:
-
第一个*:代表任意的修饰符,任意的返回值类型;
-
第二个*:代表任意的类;
-
第三个*:代表任意的方法;
-
. . :代表任意的类型和个数的形参;
2.2.3 可重用切入点表达式
其他地方直接应用此方法即可;
//重用切入点表达式
@Pointcut( "execution(* com.kgc.spring.aspectj.*.*(..))")
public void joinPointcut(){}
//同一个类中引用
@Before("joinPointcut()")
@After("joinPointcut()")
//其他类中引用(方法全类名)
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
2.3 JoinPoint 和 ProceedingJoinPoint
2.3.1 JoinPoint 对象
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。
常用api:
方法名 | 功能 |
---|---|
Signature getSignature(); | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |
2.3.2 ProceedingJoinPoint对象
ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中 添加了 两个方法.
方法名 | 功能 |
---|---|
Object proceed() throws Throwable | 执行目标方法 |
Object proceed(Object[] var1) throws Throwable | 传入的新的参数去执行目标方法 |
2.4 @Before
2.4.1 接口
ArithmeticCalculator
public interface ArithmeticCalculator {
//加
int add(int m,int n);
//减
int sub(int m,int n);
//乘
int nul(int m,int n);
//除
int div(int m,int n);
}
2.4.2 实现类
ArithmeticCalculatorImpl
@Service("arithmeticCalculator")
//起别名,方便单元测试,根据别名,从容器中获取
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int m, int n) {
return m + n;
}
@Override
public int sub(int m, int n) {
return m - n;
}
@Override
public int nul(int m, int n) {
return m*n;
}
@Override
public int div(int m, int n) {
System.out.println("====== 执行 div 方法 ======");
return m/n;
}
}
2.4.3 @Before 前置通知
在目标方法执行前,自动执行此方法(通过代理实现);
@Component //声明为一个普通的组件,放入spring的容器中,才可以生效
@Aspect //声明当前类是 一个切面
public class LogAspect {
//重用切入点表达式
@Pointcut( "execution(* com.kgc.spring.aspectj.*.*(..))")
public void joinPointcut(){}
//前置通知 @Before
@Before("joinPointcut()")
public void logBeforeMethod(JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspect "+methodName+" 方法,入参:"+ Arrays.toString(methodParams) +" ------");
}
}
2.5 配置文件
spring-aop.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: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.kgc.spring.aspectj"></context:component-scan>
<!-- 基于注解方式实现Aspect切面 -->
<!-- 作用:当spring的容器检测到此配置项,会自动将Aspect切面匹配的目标对象,放入容器,默认使用的是jdk的动态代理 -->
<aop:aspectj-autoproxy ></aop:aspectj-autoproxy>
</beans>
2.6测试
public void testSpringAopAspectj(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
class com.sun.proxy.$Proxy15
------ LogAspect div 方法,入参:[20, 10] ------
====== 执行 div 方法 ======
****** 通过单元测试,计算结果:2 ******
3、后置通知
3.1 @After
目标方法发执行之后,自动执行;
特点:
- 后置通知无法获取目标方法的返回值;
- 它的执行跟目标方法是否抛出异常无关,不影响此方法的执行;
@After("joinPointcut()")
public void logAfterMethod(JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspect "+methodName+" 方法执行结束 ------");
}
3.2 测试
3.2.1 无异常
@Test
public void testSpringAopAspectj(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
====== 执行 div 方法 ======
------ LogAspect div 方法执行结束 ------
****** 通过单元测试,计算结果:2 ******
3.2.2 有异常
@Test
public void testSpringAopAspectj(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 0);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
====== 执行 div 方法 ======
------ LogAspect div 方法执行结束 ------ //有异常也会执行后置通知
java.lang.ArithmeticException: / by zero
4、返回通知
4.1 @AfterReturning
- 目标方法返回结果后自动执行,可以获取目标方法的返回值;
- 但是要求@AfterReturning必须增加属性returning,指定一个参数名;
- 且此参数名必须跟通知方法的一个形参名一致,用于接收返回值;
@AfterReturning(value = "joinPointcut()",returning = "result")
public void afterReturningMethod(JoinPoint joinPoint,Object result){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("------ LogAspect "+methodName+" 方法,执行结果:"+ result +" ------");
}
4.2 测试
测试结果
====== 执行 div 方法 ======
------ LogAspect div 方法,返回结果:2 ------
****** 通过单元测试,计算结果:2 ******
5、异常抛出通知
5.1 @AfterThrowing
- 异常抛出通知 @AfterThrowing ,在目标方法抛出异常后,可以获取目标方法发生异常后抛出的异常信息;
- 但是要求 @AfterThrowing 必须增加属性 throwing,指定一个参数名;
- 且此参数名必须跟通知方法的一个形参名一致,用于接收异常;
@AfterThrowing(value = "joinPointcut()",throwing = "ex")
public void logAfterThrowingMethod(JoinPoint joinPoint,Exception ex){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("------ LogAspect "+methodName+" 方法,执行异常信息:"+ ex.getMessage() +" ------");
}
5.2 测试
@Test
public void testSpringAopAspectj(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 0);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
====== 执行 div 方法 ======
------ LogAspect div 方法,执行异常信息:/ by zero ------
java.lang.ArithmeticException: / by zero
6、环绕通知
6.1 @Around
- 环绕通知 @Around,可以看作是上面四种通知的结合体,一般不建议跟单个的通知共用(防止冲突失效);
- 作用:可以让开发人员在环绕通知的处理方法中根据不同也业务逻辑,决定是否发起对目标方法的调用;
@Around(value = "joinPointcut()")
public Object logAroundMethod(ProceedingJoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//定义获取目标方法的返回值变量
Object result = null;
try{
//实现前置通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,入参:"+ Arrays.toString(joinPoint.getArgs()) +" ------");
//手动调用原目标方法(业务中决定,是否对核心方法方法发起调用)
result = joinPoint.proceed();
}catch (Throwable tx){
//实现异常抛出通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,执行异常信息:"+ tx.getMessage() +" ------");
}finally {
//实现后置通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,执行结束 ------");
}
//实现返回通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,执行结果:"+ result +" ------");
return result;
}
6.2 测试
6.2.1 测试结果,无异常
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 10);
class com.sun.proxy.$Proxy13
------ LogAspect div 方法 Around通知,入参:[20, 10] ------
====== 执行 div 方法 ======
------ LogAspect div 方法 Around通知,执行结束 ------
------ LogAspect div 方法 Around通知,返回结果:2 ------
****** 通过单元测试,计算结果:2 ******
6.2.2 测试结果,有异常
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 0);
class com.sun.proxy.$Proxy13
------ LogAspect div 方法 Around通知,入参:[20, 0] ------
====== 执行 div 方法 ======
------ LogAspect div 方法 Around通知,执行异常信息:/ by zero ------
------ LogAspect div 方法 Around通知,执行结束 ------
------ LogAspect div 方法 Around通知,返回结果:null ------
6.2.3 测试结果 不调用 原方法
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 0);
//(业务中决定,是否对核心方法发起调用)
//不调用核心方法
//result = joinPoint.proceed();
------ LogAspect div 方法 Around通知,入参:[20, 10] ------
------ LogAspect div 方法 Around通知,执行结束 ------
------ LogAspect div 方法 Around通知,返回结果:null ------
7、切入点优先级
当有多个前置通知时,我们想自定义前置通知顺序:使用@Order(int)
指定切面优先级,一般都是int型整数,值越小,优先级越高**(默认值 2^31 - 1 最低优先级);
7.1 多个前置通知
logBeforeMethod
@Before("joinPointcut()")
public void logBeforeMethod(JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspectBeforeMethod "+methodName+" 方法,入参:"+ Arrays.toString(methodParams) +" ------");
}
verifyBeforeMethod
@Component
@Aspect
public class VerifyParamAspect {
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
public void verifyBeforeMethod( JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ verifyBeforeMethod "+methodName+" 方法,入参:"+ Arrays.toString(methodParams) +" ------");
}
}
7.2 测试(默认)
@Test
public void testVerifyParamAspect(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
------ LogAspectBeforeMethod div 方法,入参:[20, 10] ------ //LogAspectBeforeMethod 先执行
------ verifyBeforeMethod div 方法,入参:[20, 10] ------
====== 执行 div 方法 ======
****** 通过单元测试,计算结果:2 ******
7.3 测试(自定义优先级)
@Component
@Aspect
@Order(1) //指定切面优先级,一般都是int型整数,值越小,优先级越高(默认值 2^31 - 1)
public class VerifyParamAspect {
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
public void verifyBeforeMethod( JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ verifyBeforeMethod "+methodName+" 方法,入参:"+ Arrays.toString(methodParams) +" ------");
}
}
测试结果
------ verifyBeforeMethod div 方法,入参:[20, 10] ------ //优先级高的切面中的verifyBeforeMethod,先执行
------ LogAspectBeforeMethod div 方法,入参:[20, 10] ------
====== 执行 div 方法 ======
****** 通过单元测试,计算结果:2 ******
-------------------------------------------------------------------------------------------------
Spring-04 声明式事务
1、事务的定义
事务就是由一组逻辑上紧密关联的多个工作单元(数据库操作)而合并成一个整体,这些操作要么都执行,要么都不执行。
2、事务的特性:ACID
1)原子性A :原子即不可再分,表现:一个事务涉及的多个操作在业务逻辑上缺一不可,保证同一个事务中的操作要不都提交,要不都不提交;
2)一致性C :数据的一致性,一个事务中,不管涉及到多少个操作,都必须保证数据提交的正确性(一致);即:如果在事务数据处理中,有一个或者几个操作失败,必须回退所有的数据操作,恢复到事务处理之前的统一状态;
3)隔离性I :程序运行过程中,事务是并发执行的,要求每个事务之间都是隔离的,互不干扰;
4)持久性D :事务处理结束,要将数据进行持久操作,即永久保存。
3、事务的分类:
1)编程式事务-使用jdbc原生的事务处理,可以将事务处理写在业务逻辑代码中,违背aop原则,不推荐;
2)声明式事务-使用事务注解 @Transactional,可以声明在方法上,也可以声明在类上;
-
**优先级**: * <mark>声明在**类上**,会对**当前类内的所有方式生效**(所有方法都有事务处理);</mark> * <mark>声明在**方法上**,只会**对当前方法生效**,当类上和方法上同时存在,**方法的优先级高于类**(有些方法,对声明式事务做特殊属性配置);</mark>
4、事务的属性:
4.1 事务的传播行为:propagation属性
事务的传播行为:propagation 属性指定;
当一个带事务的方法被另一个带事务的方法调用时(事务嵌套),当前事务如何处理:
-
propagation = Propagation.REQUIRED :
- 默认值,使用调用者的事务(全程就一个事务,如果有事务嵌套,以外部事务为主);
-
propagation = Propagation.REQUIRES_NEW :
- 将调用者的事务直接挂起,自己重开新的事务处理,结束提交事务,失败回滚;(当事务嵌套时,内层事务,会重新开启新事务的处理,不受外部事务的管理);
4.2 事务的隔离级别:isolation属性
事务的隔离级别:isolation属性指定隔离级别,只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。
1、读未提交 : 读取其它事务未提交的数据,了解,基本不会使用;
2、读已提交 : oracle的默认事务隔离级别,同一个事务处理中,只能读取其它事务提交后的数据(也就是说事务提交之前对其余事务不可见);
3、可重复读 : mysql默认事务隔离级别,同一个事务处理中,多次读取同一数据是都是一样的,不受其它事务影响;
4、串行化 : 可以避免上面所有并发问题,但是执行效率最低,数据一致性最高;
4.3 事务的指定回滚和不会滚
事务的指定回滚和不会滚:Spring在默认的情况下,是对所有的运行时异常会执行事务回滚
1、 rollbackFor : 指定回滚异常,只有产生了指定的异常类型,才会回滚事务;
2、 noRollbackFor : 指定不会滚异常,产生了指定的异常类型,也不会回滚事务;
4.4 事务的超时时长-了解
1、timeout,指定事务出现异常,没有及时回滚,单位是秒,防止事务超时,占用资源;
4.5 事务的只读-了解
1、readOnly=false,默认,可读可写‘;
2、readOnly=true,代表该事务处理,理论上只允许读取,不能修改(只是通知spring,并不是一个强制选项)
目的就是:提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。
但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。
5、 环境搭建
5.1主要 jar包
<spring.version>4.3.18.RELEASE</spring.version>
<mysql.version>5.1.47</mysql.version>
<c3p0.version>0.9.5.2</c3p0.version>
<!-- transaction begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!--c3p0数据源 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>${c3p0.version}</version>
</dependency>
<!-- 最主要的是 spring-tx-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- transaction end -->
<!-- mysql begin -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- mysql end -->
5.2 配置文件
<?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"
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/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 组件扫描-->
<context:component-scan base-package="com.kgc.spring"></context:component-scan>
<!-- spring框架读取外部配置文件-->
<!-- 方式一-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<!-- 指定配置文件的位置,classpath:是类路径,只有spring可识别 -->
<property name="location" value="classpath:jdbc.properties"></property>
</bean>
<!-- c3p0 数据库配置,可以管理数据库连接,还可以自动重连 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${driver}"></property>
<property name="jdbcUrl" value="${url}"></property>
<property name="user" value="${username}"></property>
<property name="password" value="${password}"></property>
</bean>
<!-- Spring框架对JDBC进行封装,我们使用JdbcTemplate可以方便实现对数据库的增删改查操作。 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 数据源事务管理器 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--
配置声明式事务注解扫描,扫描所有添加的声明式事务注解,交给事务管理器进行统一管理;
名称空间是tx结尾,才可以生效;
transaction-manager属性是指定当前自定义的事务管理器;
如果事务管理器的id值是transactionManager,可以省略此属性的指定
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>
6、测试
5.1 购买一辆车(没有事务嵌套)
TES 购买一辆 AudiQ5;
模拟购买一辆车,主要流程:(1,2,3 整体是一个事务)
1、据买家购买汽车编号,获取汽车详情;
2、扣汽车的库存
3、扣买家的余额
5.1.2 主要业务代码
5.1.2.1 扣用户余额业务
BuyerServiceImpl
如果买家余额不足,直接返回;
@Service
public class BuyerServiceImpl implements BuyerService {
@Autowired
private BuyerDao buyerDao;
@Override
public void subBuyerMoneyByName(String buyerName, Car car) {
// 根据买家姓名,查询买家详情
Buyer buyer = buyerDao.selectBuyerByName(buyerName);
// 判断买家余额是否充足,如果不足,不能继续扣减金额
if(buyer.getMoney() < car.getPrice()){
System.out.println(String.format("****** 买家:%s,余额不足! ------", buyerName));
return; //直接return
}
// 余额充足,执行扣减余额
int row = buyerDao.updateBuyerMoneyById(buyer.getId(), car.getPrice());
System.out.println(String.format("****** 买家:%s,余额扣减成功,影响行数:%s ------", buyerName, row));
}
}
5.1.2.2 扣库存业务
CarsStockServiceImpl
如果库存不足,直接返回;
@Service
public class CarsStockServiceImpl implements CarsStockService {
@Autowired
private CarsStockDao carsStockDao;
@Override
public void subCarsStockBuyId(Car car) {
//根据汽车编号,插叙汽车详情
CarsStock carsStock = carsStockDao.selectCarsStockByCid(car.getId());
//判断库存是否充足,如果不足,不能购买
if(carsStock.getStock() <= 0){
System.out.println("汽车"+car.getName()+"库存不足");
return; //直接return
}
//库存足够,执行库存减少
int row = carsStockDao.updateCarStockById(car.getId());
System.out.println("------ 汽车"+car.getName()+"库存扣减成功" + row +" ------");
}
}
5.1.2.3 用户买车业务
根据 卖家名字,和汽车编号买车;
BuyCarServiceImpl
@Service("buyCarService") //方便从容器中获取对象
public class BuyCarServiceImpl implements BuyCarService {
@Autowired
private CarDao carDao;
@Autowired
private CarsStockService carStockService;
@Autowired
private BuyerService buyerService;
//根据 卖家名字,和汽车编号买车
@Override
public void buyCar(String buyerName, Integer carId) {
System.out.println(String.format("------ 买家:%s,购买汽车编号:%s 开始 ------",buyerName,carId));
// 根据买家购买汽车编号,获取汽车详情
Car car = carDao.selectCarById(carId);
// 扣买家的余额
buyerService.subBuyerMoneyByName(buyerName, car);
// 扣汽车的库存
carStockService.subCarsStockBuyId(car);
System.out.println(String.format("------ 买家:%s,购买汽车编号:%s 结束 ------",buyerName,carId));
}
}
5.1.3 测试(没有添加事务处理)
5.1.3.1 测试前的数据
- 汽车价格
- 用户余额
- 库存
根据观察,发现用户TES的余额不够买AudiQ5;
5.1.3.2 测试
//没有添加事务处理
@Test
public void testSpringUnUsedTx(){
//获取买车的业务实现对象
BuyCarService buyCarService = context.getBean("buyCarService", BuyCarService.class);
//调用买车的业务方法
buyCarService.buyCar("TES",1);
}
运行结果:
5.1.3.3 测试后的数据
- 用户余额
- 库存
5.1.4 测试 (加上@Transactional 注解添加事务处理)
5.1.4.1 方法上加上@Transactional 注解
@Transactional
public void buyCar(String buyerName, Integer carId) {
...
}
5.1.4.1 测试
恢复初始数据后测试;
5.1.5 测试 (增加异常抛出)
余额不足,没有异常直接return,不能触发事务;
需要抛出自定义异常才会触发事务处理;
5.1.5.1 自定义异常类
BuyCarException
public class BuyCarException extends RuntimeException {
//生成所有的构造方法
public BuyCarException() {
}
public BuyCarException(String message) {
super(message);
}
public BuyCarException(String message, Throwable cause) {
super(message, cause);
}
public BuyCarException(Throwable cause) {
super(cause);
}
public BuyCarException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
5.1.5.2 抛出异常
当余额或库存不足的时候,抛出自定义异常;
BuyerServiceImpl
@Service
public class BuyerServiceImpl implements BuyerService {
@Autowired
private BuyerDao buyerDao;
@Override
public void subBuyerMoneyByName(String buyerName, Car car) {
Buyer buyer = buyerDao.selectBuyerByName(buyerName);
if(buyer.getMoney() < car.getPrice()){
System.out.println(String.format("****** 买家:%s,余额不足! ------", buyerName));
//return; //没有异常直接return,不能触发事务
//余额不足抛出自定义异常
//*****余额充足,执行扣减余额*****
throw new BuyCarException(String.format("****** 买家:%s,余额不足! ------", buyerName));
}
int row = buyerDao.updateBuyerMoneyById(buyer.getId(), car.getPrice());
System.out.println(String.format("****** 买家:%s,余额扣减成功,影响行数:%s ------", buyerName, row));
}
}
CarsStockServiceImpl
@Service
public class CarsStockServiceImpl implements CarsStockService {
@Autowired
private CarsStockDao carsStockDao;
@Override
public void subCarsStockBuyId(Car car) {
CarsStock carsStock = carsStockDao.selectCarsStockByCid(car.getId());
if(carsStock.getStock() <= 0){
System.out.println("汽车"+car.getName()+"库存不足");
//return; //没有异常直接return,不能触发事务
//*****库存不足,执行扣减余额*****
throw new BuyCarException("汽车"+car.getName()+"库存不足");
}
int row = carsStockDao.updateCarStockById(car.getId());
System.out.println("------ 汽车"+car.getName()+"库存扣减成功" + row +" ------");
}
}
5.1.5.3 测试
恢复初始数据后测试;
5.1.5.4 测试 (余额充足)
5.1.5.4.1 测试前的数据
- 用户余额
- 库存
5.1.5.4.2测试
5.1.5.4.3 测试后的数据
- 用户余额
- 库存
5.2购买两辆车(有事务嵌套) **对过程理解还有问题
JDG 购买一辆 AudiQ5 和一辆 BmwX3
模拟购物车一次购买两辆车,主要流程:(1,2 整体是一个事务)
1、买第一辆车(1.1,1.2,1.3 整体是一个事务 默认情况内部事务不生效)
1.1 据买家购买汽车编号,获取汽车详情;
1.2扣汽车的库存
1.3扣买家的余额
2、买第二辆车(1.1,1.2,1.3 整体是一个事务 默认情况内部事务不生效)
1.1 据买家购买汽车编号,获取汽车详情;
1.2扣汽车的库存
1.3扣买家的余额
5.2.1 主要业务代码
模拟购物车一次购买两辆车;
多次调用购买一辆汽车业务;
BuyCarCartServiceImpl
@Service("BuyCarCartService" )
public class BuyCarCartServiceImpl implements BuyCarCartService {
@Autowired
private BuyCarService buyCarService;
@Override
@Transactional //购物车外层事务注解,buyCarService接口方法中也有事务注解
public void buyCarCart(String buyerName, List<Integer> carIds) {
//模拟购物车垢面多辆车,方便演示事务传播行为,一辆一辆购买(单独调用买车接口)
// carIds.forEach(carId -> buyCarService.buyCar(buyerName,carId));
for (int i = 0; i < carIds.size(); i++) {
//异常处理,防止买第一辆车出现异常后,无法购买第二辆车
try{
buyCarService.buyCar(buyerName,carIds.get(i));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
5.2.1 propagation = Propagation.REQUIRED
默认传播特性,以外部事务为主;propagation = Propagation.REQUIRED 可以不写;
5.2.1.1 测试前的数据
- 汽车价格
- 用户余额
- 库存
5.2.1.2测试
//测试事务存在 事务嵌套 的传播行为
//购物车结算
@Test
public void testSpring(){
BuyCarCartService buyCarCartService = context.getBean("BuyCarCartService", BuyCarCartService.class);
//调用购物车买车的业务方法
buyCarCartService.buyCarCart("JDG", Arrays.asList(1,2));
}
测试结果:
5.2.1.3测试后的数据
- 用户余额
- 库存
5.2.1.4 总结
通过查看数据库的数据发现,数据没有改变,说明事务并事务已经回滚,也就是说默认传播特性,以外部事务为主;
5.2.3 propagation = Propagation.REQUIRES_NEW
propagation = Propagation.REQUIRES_NEW的传播特性,内部事务会自己重开新的事务处理;
5.2.3.1 内部事务注解添加属性参数
buyCar方法
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyCar(String buyerName, Integer carId) {
...
}
5.2.3.2 测试
恢复数据再测试;
5.2.3.3测试后的数据
- 用户余额
- 库存
5.2.3.4 总结
通过查看数据库的数据发现,数据发生改变,说明内部事务重新开起新的事务处理,不受外部事务的管理;
--------------------------------------------------------------------------------------------