Spring基础知识总结
SpringFramewrok架构图
一、IOC和DI
https://www.iteye.com/blog/jinnianshilongnian-1413846
1、IOC
基本概念:
IOC—Inversion of Control 即“控制反转”,不是什么技术,而是一种设计思想。
在传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IOC是有专门一个容器来创建这些对象,即由IOC容器来控制对象的创建而不再显式地使用new;
**主动式(**什么资源都自己创建):
public class UserService{
//我们在service想要使用dao中的对象,需要手动创建
UserDao userDao=new UserDao();
public User getUser(){
return userService.selectUser();
}
}
即 UserService依赖于 UserDao
被动式(由我们容器创建):
public class UserService{
//我们只需要声明要使用的对象即可,IOC容器会帮我们创建和管理
UserDao userDao;
public User getUser(){
return userService.selectUser();
}
}
即 UserService依赖于 UserDao
- 谁控制谁,控制什么:
- 传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
- 为何是反转,哪些方面反转了:
- 有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
2、DI
基本概念:
DI—Dependency Injection,即“依赖注入”:即是IOC的实现方式。是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,
**谁依赖于谁:**当然是应用程序依赖于IoC容器;
**为什么需要依赖:**应用程序需要IoC容器来提供对象需要的外部资源;
**谁注入谁:**很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
**注入了什么:**就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
容器指定那个组件(类)运行的时候,需要另外一个类(组件);容器通过反射的方式,将容器中准备好的UserDao对象注入到UserService中声明的UserDao属性中。
3、简单IOC实例
@Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor public class Person { private int id; private String name; private int age; private String gender; public Person() { System.out.println("person初始化..."); } }
<?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"> <!--注册一个Person对象。Spring会自动创建这个Person对象--> <!--一个bean可以注册一个组件(类)--> <!--id 唯一标识--> <!--class 类的全路径--> <bean id="person1" class="org.lc.entity.Person"> <!--property为属性赋值--> <!--name属性名 value属性值--> <property name="id" value="1001"/> <property name="name" value="张三"/> <property name="age" value="12"/> <property name="gender" value="male"/> </bean> <bean id="person2" class="org.lc.entity.Person"> <property name="id" value="1002"/> </bean> </beans>
1)读取Bean的xml获取Bean实例
public class AppTest { //从容器中拿到这个组件 @Test public void Test1(){ //从绝对路径拿到配置文件 // ApplicationContext ico1 = new FileSystemXmlApplicationContext(""); //从类路径(src)下拿到配置文件 classpath ApplicationContext ioc = new ClassPathXmlApplicationContext("ioc.xml"); Person person1 = (Person) ioc.getBean("person1"); Person person11 = (Person) ioc.getBean("person1"); Person person2 = (Person) ioc.getBean("person2"); //true System.out.println(person1==person11); } }
注意事项:
ApplicationContext IOC接口
当创建
ApplicationContext
IOC容器创建完毕时即创建所有的对象. 无需等到getBean时再创建ApplicationContext ioc = new ClassPathXmlApplicationContext("ioc.xml");
当配置一个bean,我们的IOC就位我们初始化创建一个实例对象
获取同一个bean多次时,获取的都是同一对象,即我们ICO创建bean对象是单实例的
找不到我们指定的bean时,会报错
org.springframework.beans.factory.NoSuchBeanDefinitionException:
所有为对象属性的设置值都是通过set方法实现的
2)处理类型相同的多个Bean
当我们通过类对象获取bean时,当配置了多个相同的bean类型,那么会报错
Person bean = ioc.getBean("person1",Person.class);
orgspringframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.lc.entity.Person' available: expected single matching bean but found 2: person1,person2
指定要获取bean的id名称和类型
Person bean = ioc.getBean("person1",Person.class);
3)通过Bean中的有参构造器赋值
<!--调用有参构造区进行赋值-->
<!--person类中必须要有该下面完整的构造器-->
<!-- public Person(int id, String name, int age, String gender) {}-->
<!--通过有参构造器赋值,无需调用set方法-->
<bean id="person3" class="org.lc.entity.Person">
<constructor-arg name="id" value="10"></constructor-arg>
<constructor-arg name="name" value="lc"></constructor-arg>
<constructor-arg name="age" value="11"></constructor-arg>
<constructor-arg name="gender" value="male"></constructor-arg>
</bean>
不写name属性的构造器赋值方法:
<!--当未指定name属性时,则必须按照构造器的属性顺序指定属性值,顺序不能颠倒-->
<bean id="person4" class="org.lc.entity.Person">
<constructor-arg value="10"></constructor-arg>
<constructor-arg value="lc"></constructor-arg>
<constructor-arg value="11"></constructor-arg>
<constructor-arg value="male"></constructor-arg>
</bean>
如果赋值顺序颠倒,则可以指定index属性来指定顺序。索引从0开始
<bean id="person4" class="org.lc.entity.Person">
<constructor-arg value="10"></constructor-arg>
<constructor-arg value="lc"></constructor-arg>
<constructor-arg value="male" index="3"></constructor-arg>
<constructor-arg value="11" index="2"></constructor-arg>
</bean>
还可以通过type来指定当前构造参数的类型
4)通过p标签赋值属性
内部还是通过set方法赋值
xmlns:p="http://www.springframework.org/schema/p"
<bean id="person4" class="org.lc.entity.Person" p:id="12" p:gender="female" p:name="张三" p:age="12"></bean>
5)通过Bean赋值属性为null
其实在xml中写的所有内容都是字符串,赋值给指定属性时类型会自动判断转换。
<!--若不为bean的属性赋值 则基本类型为默认值,引用类型默认为null-->
<!--若要显示为某个属性赋值为null. 不能写成<property name="id" value="null">否则还是赋值的为字符串null-->
<bean id="person1" class="org.lc.entity.Person">
<property name="id">
<null/>
</property>
</bean>
6)通过ref引用其他外部Bean对象
@Setter @Getter @ToString public class Role { private int id; private String name; private String nameZh; }
@Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class Car { private int id; private String nameCar; private double moneyCar; }
@Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor public class Person { private int id; private String name; private int age; private String gender; private Car car; private List<Role> roleList; private Properties properties; private Map<String,Object> maps; }
<bean id="car1" class="org.lc.entity.Car">
<property name="id" value="111"></property>
<property name="nameCar" value="宝马"></property>
<property name="moneyCar" value="3600000"></property>
</bean>
<bean id="person1" class="org.lc.entity.Person">
<!--将该属性 引用ref外部的bean-->
<property name="car" ref="car1"></property>
</bean>
Car car = (Car) ioc.getBean("car1");
Car car1 = ((Person) ioc.getBean("person1")).getCar();
//Car(id=111, nameCar=宝马, moneyCar=3600000.0)
System.out.println(car1.toString());
car.setNameCar("奔驰");
//Car(id=111, nameCar=奔驰, moneyCar=3600000.0)
System.out.println(car1.toString());
//true
System.out.println(car==car1);
- 通过bean声明的对象和ref引用的对象为同一对象。他们是同一个实例,修改一个对象的属性那么其他应引用该对象的属性也会改变。
7)创建使用内部Bean对象
- 创建内部的bean对象 与外部声明的bean不是同一对象
- 内部bean不能通过id获取到
<bean id="car1" class="org.lc.entity.Car">
<property name="id" value="111"></property>
<property name="nameCar" value="宝马"></property>
<property name="moneyCar" value="3600000"></property>
</bean>
<bean id="person1" class="org.lc.entity.Person">
<property name="car">
<!--创建内部的bean对象 与外部声明的bean不是同一对象-->
<bean class="org.lc.entity.Car">
<property name="id" value="222"></property>
<property name="nameCar" value="自行车"></property>
</bean>
</property>
</bean>
Car car = (Car) ioc.getBean("car1");
Car car1 = ((Person) ioc.getBean("person1")).getCar();
//Car(id=111, nameCar=宝马, moneyCar=3600000.0)
System.out.println(car.toString());
//Car(id=222, nameCar=自行车, moneyCar=0.0)
System.out.println(car1.toString());
8)为List类型赋值
<bean id="role1" class="org.lc.entity.Role">
<property name="id" value="1"></property>
<property name="name" value="admin"></property>
<property name="nameZh" value="管理员"></property>
</bean>
<bean id="person2" class="org.lc.entity.Person">
<property name="roleList">
<!--为list赋值 相当于 new ArrayList<Role>-->
<array>
<!--创建内部的bean并赋值-->
<bean class="org.lc.entity.Role" p:id="2" p:name="test" p:nameZh="测试人员"></bean>
<!--引用外部的bean-->
<ref bean="role1"></ref>
</array>
</property>
</bean>
Person person2 = (Person) ioc.getBean("person2");
List<Role> roleList = person2.getRoleList();
//[Role(id=2, name=test, nameZh=测试人员), Role(id=1, name=admin, nameZh=管理员)]
System.out.println(roleList);
9)为map赋值
<bean id="role1" class="org.lc.entity.Role">
<property name="id" value="1"></property>
<property name="name" value="admin"></property>
<property name="nameZh" value="管理员"></property>
</bean>
<bean id="person3" class="org.lc.entity.Person">
<property name="maps">
<!--为map赋值 相当于new LinkedHashMap<String,Object>-->
<map>
<!--一个entry代表一个健值对-->
<entry key="key01" value="张三"></entry>
<entry key="key02" value="18"></entry>
<!--将value值引用外部bean-->
<entry key="key03" value-ref="role1"></entry>
<!--将value值创建内部bean-->
<entry key="key04">
<bean class="org.lc.entity.Role" p:id="3333" p:name="root" p:nameZh="超级管理员"></bean>
</entry>
</map>
</property>
</bean>
Person person3 = (Person) ioc.getBean("person3");
Map<String, Object> maps = person3.getMaps();
//{key01=张三, key02=18, key03=Role(id=1, name=admin, nameZh=管理员), key04=Role(id=3333, name=root, nameZh=超级管理员)}
System.out.println(maps);
10)为Properties赋值
<bean id="person4" class="org.lc.entity.Person">
<property name="properties">
<!--为properties属性赋值 相当于properties =new Properties() 所有的key value都是字符串类型 -->
<props>
<!--k=v都是字符串 值直接写在标签内-->
<prop key="username">root</prop>
<prop key="password">123456</prop>
</props>
</property>
</bean>
Person person3 = (Person) ioc.getBean("person4");
Properties properties = person3.getProperties();
// root
System.out.println(properties.getProperty("username"));
//123456
System.out.println(properties.getProperty("password"));
11)使用util创建一个可以引用的集合
xmlns:utils="http://www.springframework.org/schema/util"
<!--相当于创建一个new LinkedHashMap 可以通过id 全局使用-->
<utils:map id="maptutil">
<!--一个entry代表一个健值对-->
<entry key="key01" value="张三"></entry>
<entry key="key02" value="18"></entry>
<!--将value值引用外部bean-->
<entry key="key03" value-ref="role1"></entry>
<!--将value值创建内部bean-->
<entry key="key04">
<bean class="org.lc.entity.Role" p:id="3333" p:name="root" p:nameZh="超级管理员"></bean>
</entry>
</utils:map>
<!--相当于创建一个list集合-->
<utils:list id="utillist">
<!--放入一个list-->
<list></list>
<!--放入一个普通的value值-->
<value>1</value>
</utils:list>
12)级联属性赋值
- 当引用外部的bean并修改其中的属性时,对应外部的bean的属性也会改变。
<bean id="car1" class="org.lc.entity.Car">
<property name="id" value="111"></property>
<property name="nameCar" value="宝马"></property>
<property name="moneyCar" value="3600000"></property>
</bean>
<bean id="person5" class="org.lc.entity.Person">
<!--引用外部bean-->
<property name="car" ref="car1"></property>
<!--引用外部bean后 并修改bean中的属性-->
<property name="car.nameCar" value="汗血宝马"></property>
</bean>
13)通过parent对指定Bean的继承
<bean id="person6" class="org.lc.entity.Person">
<property name="id" value="1002"></property>
<property name="name" value="张三"></property>
<property name="age" value="12"></property>
<property name="gender" value="male"></property>
</bean>
<!--通过parent继承其他的bean-->
<bean id="person7" parent="person6" class="org.lc.entity.Person">
<!--将自己的属性重写 其他的属性继承-->
<property name="name" value="李四"></property>
</bean>
Person person7 = (Person) ioc.getBean("person7");
//Person(id=1002, name=李四, age=12, gender=male, car=null, roleList=null, properties=null, maps=null)
System.out.println(person7);
14)通过abstract将Bean声明为抽象的
- 该声明为abstract的bean只能被继承,不能通过id获取到该bean实例对象
<bean id="person6" class="org.lc.entity.Person" abstract="true">
<property name="id" value="1002"></property>
<property name="name" value="张三"></property>
<property name="age" value="12"></property>
<property name="gender" value="male"></property>
</bean>
15)Bean之间的依赖创建顺序
- 默认根据bean声明的先后顺序来创建bean
- 若指定
depends-on
则先依次创建depends-on
中的beanrole1,car1
再创建person1
<!--若根据bean声明的先后顺序来创建bean -->
<bean id="person1" class="org.lc.entity.Person" depends-on="role1,car1"></bean>
<bean id="car1" class="org.lc.entity.Car"></bean>
<bean id="role1" class="org.lc.entity.Role"></bean>
4、Bean的作用域(默认为单例)
- prototype 多实例的
- 容器启动默认去创建多实例的bean
- 每次获取的时候才去创建这个bean
- 每次获取都会创建一个新的对象
- singleton 单实例的 (默认)
- 在容器启动完成之前就已经创建好对象,保存在容器中了
- 任何获取到的该bean都是同一个对象
- request 在web环境下,同一次请求创建一个Bean实例
- session 在web环境下,同一次会话创建一个Bean实例
单例(默认):
<bean id="person1" class="org.lc.entity.Person"></bean>
Person person1 = (Person) ioc.getBean("person1");
Person person2= (Person) ioc.getBean("person1");
//true
System.out.println(person1==person2);
多例:
<bean id="person1" class="org.lc.entity.Person" scope="prototype"></bean>
Person person1 = (Person) ioc.getBean("person1");
Person person2= (Person) ioc.getBean("person1");
//false
System.out.println(person1==person2);
5、静态工厂和实例工厂创建Bean
工厂模式:工厂帮我们创建对象,有一个专门帮我们创建对象的类,这个类就是工厂
@Getter
@Setter
@ToString
public class Drink {
private int id;
private String name;
private int capacity;
}
1)静态工厂
工厂本身不需要创建对象;通过工厂中的静态方法调用创建对象
实例对象=工厂类.工厂静态方法()
//静态工厂
public class DrinkStaticFactory {
public static Drink getDrink(String name){
Drink drink=new Drink();
drink.setId(1);
drink.setCapacity(1000);
drink.setName(name);
return drink;
}
}
Drink d1 = DrinkStaticFactory.getDrink("可口可乐");
//Drink(id=1, name=可口可乐, capacity=1000)
System.out.println(d1);
静态工厂获取bean
<!--通过静态工厂创建需要的对象, 注意这里并不会创建工厂对象-->
<!--class:工厂的类路径-->
<!--factory-method: 指定方法名,是哪个工厂方法创建对象-->
<bean id="drink1" class="org.lc.factory.DrinkStaticFactory" factory-method="getDrink">
<!--指定该工厂方法传入的参数-->
<!--name:为参数名-->
<!--value 为参数值-->
<constructor-arg name="name" value="coco"></constructor-arg>
</bean>
Drink drink1 = (Drink) ioc.getBean("drink1");
//Drink(id=1, name=coco, capacity=1000)
System.out.println(drink1);
2)实例工厂
工厂本身需要创建对象。
工厂类 工厂对象=new 工厂类();
实例对象=工厂对象.工厂实例方法();
public class DrinkInstanceFactory {
public Drink getDrink(String name) {
Drink drink=new Drink();
drink.setId(1);
drink.setCapacity(1000);
drink.setName(name);
return drink;
}
}
DrinkInstanceFactory drinkInstanceFactory=new DrinkInstanceFactory();
Drink d1 = drinkInstanceFactory.getDrink("百世可乐");
//Drink(id=1, name=百世可乐, capacity=1000)
System.out.println(d1);
实例工厂获取bean
<!--创建工厂bean对象-->
<bean id="drinkInstanceFactory" class="org.lc.factory.DrinkInstanceFactory"></bean>
<!--在要创建的bean中指定实例工厂的bean-->
<!--factory-bean:工厂bean的id -->
<!--factory-method: 指定工厂的方法名,工厂实例方法的名称-->
<bean id="drink2" class="org.lc.entity.Drink" factory-bean="drinkInstanceFactory" factory-method="getDrink">
<!--为该工厂方法传入需要的参数-->
<!--name 参数名-->
<!--value 参数值-->
<constructor-arg name="name" value="星巴克"></constructor-arg>
</bean>
Drink drink =(Drink)ioc.getBean("drink2");
//Drink(id=1, name=星巴克, capacity=1000)
System.out.println(drink);
6、实现FactoryBean工厂获取所需的Bean
public interface FactoryBean<T>
为spring的一个接口,只要是实现了这个接口的,都认为是一个工厂- 创建该工厂对象,通过getBean方法获取实现
FactoryBean<T>
中声明的所需要的类型对象 - IOC容器启动的时候会创建我们实现FactoryBean的工厂对象,但是并不会创建该需要的Role创建该实例对象(无论是单例还是多例)。只有获取的时候才会创建实例对象
public class MyFactoryImp implements FactoryBean<Role> {
//返回该对象
@Override
public Role getObject() throws Exception {
Role role =new Role();
role.setId(1);
role.setName("superuser");
role.setNameZh("超级用户");
return role;
}
//返回该实例Clss
@Override
public Class<?> getObjectType() {
return Role.class;
}
//指定返回的对象是否为单例
//true 指定为单例
//false 指定为多例
@Override
public boolean isSingleton() {
return true;
}
}
<!--public interface FactoryBean<T>为spring的一个接口,只要是实现了这个接口的,都认为是一个工厂-->
<!--IOC容器启动的时候并不会 创建该实例对象。只有获取的时候才会创建实例对象-->
<bean id="myFactoryImpl" class="org.lc.factory.MyFactoryImp"></bean>
Role role =(Role)ioc.getBean("myFactoryImpl");
Role role1 =(Role)ioc.getBean("myFactoryImpl");
//true
System.out.println(role==role1);
//Role(id=1, name=superuser, nameZh=超级用户)
System.out.println(role);
7、Bean的生命周期
@Setter
@Getter
@ToString
public class Role {
private int id;
private String name;
private String nameZh;
//构造器
public Role() {
System.out.println("创建...role");
}
//初始化方法
public void initRole() {
System.out.println("初始化role...");
}
//销毁方法
public void destoryRole() {
System.out.println("销毁role...");
}
}
singleton
):
1)单例(默认- 单例的对象bean的执行顺序: (容器启动)构造器---> (容器启动)初始化方法(init-method指定的方法)---> (容器关闭)销毁方法 (destroy-method指定的方法)
- 容器启动时调用 构造器 和 初始化方法, 关闭容器调用close()后调用销毁方法
<!--指定beans的初始化和销毁方法-->
<!--init-method:指定该bean中的初始化方法,在对象创建时调用-->
<!--destroy-method: 指定该bean的销毁方法。在容器销毁时调用-->
<bean id="role1" class="org.lc.entity.Role" init-method="initRole" destroy-method="destoryRole"></bean>
ConfigurableApplicationContext ioc = new ClassPathXmlApplicationContext("ioc3.xml");
System.out.println("容器要关闭了...");
//ioc的关闭容器的方法
ioc.close();
创建...role
初始化role...
容器要关闭了...
销毁role...
prototype
):
2)多例(- 多例的对象在获取该对象时才会创建该Bean,即获取对象时调用 (获取bean时)构造方法---> (获取bean时)初始化方法 。销毁方法使用完该bean后自动销毁,容器无需干预。
- 对象的销毁不会在容器close()时销毁,该对象使用完自动销毁
<!--指定beans的初始化和销毁方法-->
<!--init-method:指定该bean中的初始化方法,在对象创建时调用-->
<!--destroy-method: 多例时,该实例不会销毁,自动销毁。-->
<bean id="role1" class="org.lc.entity.Role" init-method="initRole" destroy-method="destoryRole" scope="prototype"></bean>
ConfigurableApplicationContext ioc = new ClassPathXmlApplicationContext("ioc3.xml");
Role role1 = (Role) ioc.getBean("role1");
System.out.println("容器要关闭了...");
//ioc的关闭容器的方法
ioc.close();
创建...role
初始化role...
容器要关闭了...
3)bean的前置后置处理器
在bean创建的时候,对实例对象进行的操作。
单例在容器的启动的时候创建bean
多例在获取该bean的时候创建
- 自定义类实现
BeanPostProcessor
接口。并将该自定义MyProcesserBean
处理器注册在bean.xml文件中 - 所有的bean实例都会在创建时使用该前置和后置处理器。
- 调用bean构造器方法--> 调用bean的前置处理器(postProcessBeforeInitialization) -->调用bean指定的初始化方法-->调用的bean的后置处理器(postProcessAfterInitialization)
- 若bean没有指定初始化方法则也可以正常使用该处理器
@Setter
@Getter
@ToString
public class Role {
private int id;
private String name;
private String nameZh;
//构造器
public Role() {
System.out.println("创建...role");
}
//初始化方法
public void initRole() {
System.out.println("初始化role...");
}
//销毁方法
public void destoryRole() {
System.out.println("销毁role...");
}
}
public class MyProcesserBean implements BeanPostProcessor {
/**
* bean调用初始化方法之前的处理器
* @param bean bean的构造方法创建的对象
* @param beanName bean在xml中的id
* @return 返回该对象
* @throws BeansException
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("bean初始化之前的处理器:beanname" + beanName);
System.out.println("----"+bean);
return bean;
}
/**
* bean调用初始化方法完成后的处理器
* @param bean bean的构造方法创建的对象
* @param beanName bean在xml中的id
* @return 返回该对象
* @throws BeansException
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("bean初始化之前的处理器:beanname" + beanName);
System.out.println("----"+bean);
return bean;
}
}
<!--为该bean指定 初始化执行的方法 和 销毁时执行的方法-->
<bean id="role1" class="org.lc.entity.Role" init-method="initRole" destroy-method="destoryRole" ></bean>
<!--配置bean的初始化处理器-->
<bean id="myProcessBean" class="org.lc.factory.MyProcesserBean"></bean>
Role role1 = (Role) ioc.getBean("role1");
System.out.println("获取bean===="+role1);
//调用bean的构造器时
创建...role
//调用bean的前置处理器 (在bean的指定的初始化方法之前调用)
bean初始化之前的处理器:beannamerole1
----Role(id=0, name=null, nameZh=null)
//调用bean的初始化方法
初始化role...
//调用bean的后置处理器 (在bean的指定的初始化方法之后调用)
bean初始化之前的处理器:beannamerole1
----Role(id=0, name=null, nameZh=null)
//获取该bean实例
获取bean====Role(id=0, name=null, nameZh=null)
8、Spring管理连接池
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.18</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
1、通过xml配置连接池
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
2、引用外部文件抽取连接配置
classpath: 固定写法,加载类路径下的配置文件
<context:property-placeholder location="classpath:jdbc.properties" />
- 注意 在配置
<property name="username" value="${jdbc.username}"></property>
的value时,不能写成value="${username}" 因为username为spring中的一个内部属性(当前用户)。在前面加前缀即可
jdbc.properties
jdbc.username=root jdbc.password=123456 jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false jdbc.dirverClass=com.mysql.jdbc.Driver
<!--加载外部的properties配置文件--> <!--classpath: 固定写法,加载类路径下的配置文件--> <context:property-placeholder location="classpath:jdbc.properties" /> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> <property name="url" value="${jdbc.url}"></property> <property name="driverClassName" value="${jdbc.dirverClass}"></property> </bean>
测试:
ApplicationContext ioc = new ClassPathXmlApplicationContext("application.xml");
@Test
public void Test1() throws SQLException {
//通过名称bean的id获取
// DruidDataSource druidDataSource = (DruidDataSource) ioc.getBean("druidDataSource");
//通过bean的类型获取(若存在多个类型相同的bena则会报错)
DruidDataSource druidDataSource = ioc.getBean(DruidDataSource.class);
DruidPooledConnection connection = druidDataSource.getConnection();
//com.mysql.jdbc.JDBC4Connection@6b67034
System.out.println(connection);
}
9、基于xml的自动装配
@Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class Car { private int id; private String nameCar; private double moneyCar; }
@Setter @Getter @ToString public class Role { private int id; private String name; private String nameZh; }
@Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class Person { private int id; private String name; private int age; private String gender; private Car car; private List<Role> roleList; public Person(Car car) { this.car = car; } }
1)手动装配
<bean id="car1" class="org.lc.entity.Car">
<property name="nameCar" value="奥迪双钻"/>
</bean>
<!--手动赋值-->
<bean id="person1" class="org.lc.entity.Person">
<!--通过property的ref引用bean为属性赋值-->
<property name="car" ref="car1"></property>
</bean>
2)自动装配
- autowire="no/default" 不自动装配 不自动为属性赋值
- autowire="byName" 通过属性名作为id去容器中找这个组件,找到则赋值,找不到赋值为null private Car car; 通过car名称作为id去找 相当于 getBean("car");
- autowire="byType" 通过类型去容器中寻找这个组件,找到一个则为其赋值,找到多个则报错。找不到则赋值为null pricate Car car; 通过Car类型寻找 相当于getBean(Car.class);
- autowire="constructor" 通过Person中的构造器进行寻找 public Person(Car car) {} 先按照构造器参数的上的Car类型进行寻找,找到则装配,找不到则返回null 如果按照类型找到了多个,则按照构造器car参数名称进行寻找,找到则装配,找不到赋值为null 不会报错
<bean id="car" class="org.lc.entity.Car">
<property name="nameCar" value="奥迪双钻"/>
</bean>
<bean id="car2" class="org.lc.entity.Car">
<property name="nameCar" value="宝马"/>
</bean>
<!--为Person中的自定义属性自动装配-->
<!--通过autowire属性为对象中的所有属性(这里的属性必须为自定义的属性)自动装配-->
<!--autowire:
autowire="no/default" 不自动装配 不自动为属性赋值
autowire="byName" 通过属性名作为id去容器中找这个组件,找到则赋值,找不到赋值为null
private Car car; 通过car名称作为id去找 相当于 getBean("car");
autowire="byType" 通过类型去容器中寻找这个组件,找到一个则为其赋值,找到多个则报错。找不到则赋值为null
pricate Car car; 通过Car类型寻找 相当于getBean(Car.class);
autowire="constructor" 通过Person中的构造器进行寻找
public Person(Car car) {}
先按照构造器参数的上的Car类型进行寻找,找到则装配,找不到则返回null
如果按照类型找到了多个,则按照构造器car参数名称进行寻找,找到则装配,找不到赋值为null
不会报错
-->
<bean id="person1" class="org.lc.entity.Person" autowire="byName">
</bean>
装配list集合
<bean id="person1" class="org.lc.entity.Person" autowire="byType">
</bean>
<!--下面的三个Role对象都会装配到 List<Role> roleList; 集合中-->
<bean id="role1" class="org.lc.entity.Role">
<property name="nameZh" value="测试1"></property>
</bean>
<bean id="role2" class="org.lc.entity.Role">
<property name="nameZh" value="测试2"></property>
</bean>
<bean id="role3" class="org.lc.entity.Role">
<property name="nameZh" value="测试3"></property>
</bean>
10、SPEL表达式(#表达式)
Spring Expression Language (spring表达式语言)
<bean id="car1" class="org.lc.entity.Car">
<property name="id" value="111"/>
<property name="nameCar" value="丰田"/>
<property name="moneyCar" value="100000"/>
</bean>
<bean id="person2" class="org.lc.entity.Person">
<!--计算值-->
<property name="id" value="#{2*10*10}"></property>
<!--引用其他bean的属性值-->
<property name="name" value="#{car1.nameCar}"></property>
<!--引用其他bean 和ref属性值一样-->
<property name="car" value="#{car1}"></property>
<!--调用静态方法和非静态方法-->
<!--语法规则:
#{T(静态类全类名).静态方法名(参数)}
-->
<property name="gender" value="#{T(java.util.UUID).randomUUID().toString().substring(0,5)}"></property>
<!--调用非静态方法-->
<property name="age" value="#{car1.getId()}"></property>
</bean>
Person person1 = (Person) ioc.getBean("person2");
//Person(id=200, name=丰田, age=111, gender=e57b0, car=Car(id=111, nameCar=丰田, moneyCar=100000.0), roleList=null)
System.out.println(person1);
11、通过注解创建Dao,Service,Controller
@Controller 一般加到控制器类上
@Service 一般加到服务层上service
@Repository 一般应用到持久层Dao
@Component 不属于以上三层时加该注解
上面所有的注解效果都是一样的,只是方便标识和查看
- 默认被以上组件标记的bean声明都是单例类型
- bean的id名默认为类名首字母小写。 即
UserController
的id名userController
@Controller public class UserController { }
@Repository public class UserDao { }
@Service public class UserService { }
1)扫描指定所有带注解的包
<!--扫描所有声明带注解到容器中的包--> <!--base-package 指定扫描基础包路径--> <context:component-scan base-package="org.lc"></context:component-scan>
UserController userController1 = (UserController) ioc.getBean("userController");
UserController userController2 = (UserController) ioc.getBean("userController");
//true
System.out.println(userController1==userController2);
2)为指定组件指定id名称
若注解中只有一个value属性则可省略
@Controller(value = "controller1")
public class UserController {
}
UserController userController1 = (UserController) ioc.getBean("controller1");
3)指定bean的作用域(单例,多例)
@Controller(value = "controller1")
@Scope(value = "prototype")
public class UserController {
}
UserController userController1 = (UserController) ioc.getBean("controller1");
UserController userController2 = (UserController) ioc.getBean("controller1");
//false
System.out.println(userController1==userController2);
context:exclude-filter
4)排除扫描不包含的组件 <!--扫描声明为到容器中的包-->
<!--base-package 指定扫描基础包路径-->
<!--默认全部扫描-->
<context:component-scan base-package="org.lc">
<!--扫描时排除一些不要的组件-->
<!--type="assignable" 指定排除规则,排除某个具体的类,按照类排除
expression="" 该类所在的全类名
type="annotation" 指定排除规则,按照注解排除指定类
expression="" 该注解的全类名
-->
<context:exclude-filter type="assignable" expression="org.lc.dao.UserDao"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
context:include-filter
5)扫描包含指定的组件 <!--扫描声明为到容器中的包-->
<!--base-package 指定扫描基础包路径-->
<!--默认全部扫描-->
<!--注意:这里默认扫描全部,我们要禁用use-default-filters默认的扫描规则,否则还是全部扫描进来了-->
<context:component-scan base-package="org.lc" use-default-filters="false">
<!-- 值包含标有@Controller注解的类-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
12、@Autowired自动装配原理
首先按照注入的类型(UserService)去IOC容器中查找对应的组件,若找到则装配,找不到抛异常
ioc.getBean(UserService.class);
若找到多个,则按照注入的属性名称(userService)作为bean的id去容器中查找,找到则装配,找不到,抛异常
@Autowired private UserService userService;
@Service //默认bean的id userService public class UserService { public void getUser() { System.out.println("执行查找用户..UserService"); } }
@Service ////默认bean的id userServiceExc public class UserServiceExc extends UserService{ @Override public void getUser() { System.out.println("执行查找用户..UserServiceExc"); } }
@Controller public class UserController { //注入类型 //默认按照类型到IOC容器中查找,若找到多个,则以属性名userService作为id去容器中查找指定id的bean。若没找到则抛出异常 @Autowired private UserService userService; public void getUser() { userService.getUser(); } }
UserController userController = (UserController) ioc.getBean("userController");
//执行查找用户..UserService
userController.getUser();
1)配合@Qualifier使用
若使用@Autowired找到多个类型,注入时不想通过默认属性名作为id寻找,那么直接可以使用@Qualifier按照其指定的名称去容器中找知道的bean
@Controller
public class UserController {
@Qualifier("userServiceExc")
@Autowired
//若找到多个类型,不想用属性名称作为id查找,则可以直接使用@Qualifier注解中指定的名称查找
private UserService userService;
public void getUser() {
userService.getUser();
}
}
//执行查找用户..UserServiceExc
UserController userController = (UserController) ioc.getBean(UserController.class);
userController.getUser();
@Autowired(required = false)
2)指定若指定@Qualifier还找不到,则将该属性装配为null。
@Controller
public class UserController {
@Qualifier("userServiceExc111")
@Autowired(required = false)
private UserService userService;
public void getUser() {
//UserService:null
System.out.println("UserService:"+userService);
}
}
3)在方法参数上使用@Autowired和@Qualifier
@Controller
public class UserController {
//容器启动时 会运行此方法
//自动为为 UserDao和UserService进行装配
//用法和在属性上一致
@Autowired
public void getUser(UserDao userDao,@Qualifier("userServiceExc")UserService userService) {
//userDao:org.lc.dao.UserDao@56a6d5a6
System.out.println("userDao:"+userDao);
//UserService:org.lc.service.UserServiceExc@18ce0030
System.out.println("UserService:"+userService);
}
}
13、@Resource自动装配与@Autowired的区别
1)@Autowired
用来装配bean, 可作用于字段上, 也可以作用于setter方法上.
是Spring的注解. 即只能在spring框架中使用
默认情况下要求对象必须存在, 它要求依赖对象必须存在. 若允许null值, 可以设置它的required为false.
默认按照类型byType进行装配注入. 如果想按照名称进行装配的话, 需要与Qualifer注解搭配使用
2)@Resource
是J2EE的注解. 扩展性更强,可以用在其他框架上。 所有不能应用@Autowired中的required属性和@Primary注解
- name和type都不指定,即按照默认按照名称来装配注入,若找不到则按照bean的类型来找,若找到多个bean类型则抛出异常
- 它有两个属性是比较重要的:
- name: Spring将name的属性值解析为bean的名称, 使用byName的自动注入策略, 该name必须存在,否则编译的时候就会提示错误,启动则抛异常
- type: Spring将type的属性值解析为bean的类型, 使用byType的自动注入策略,若找到多个类型,则按照名称属性取找,找不到则抛出异常。
- 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
//默认按照属性名称redisTemplate做为bean的id去容器中找,找到多个抛异常
@Resource
private RedisTemplate redisTemplate;
//通过type属性注入,则先按照类型找,若找到多个则按照属性名称找 找不到抛异常
@Resource(type = RedisTemplate.class)
private RedisTemplate redisTemplate;
//通过name,type属性注入 同时按照类型和名称找 找不到抛异常
@Resource(name = "RedisTemplate",type = RedisTemplate.class)
private RedisTemplate redisTemplate;
14、Spring的单元测试
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
//classpath: 使用指定的xml文件
@ContextConfiguration(locations = "classpath:application.xml")
//使用junit4测试
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringTest {
@Autowired
private UserController userController;
@Test
public void Test(){
System.out.println(userController);
}
}
15、泛型依赖注入
Dao:
@Repository
public abstract class BaseDao<T> {
public abstract void save();
}
@Repository
public class BookDao extends BaseDao<Book>{
@Override
public void save() {
System.out.println("BookDao...保存图书");
}
}
@Repository
public class UserDao extends BaseDao<User>{
@Override
public void save() {
System.out.println("UserDao...保存用户");
}
}
Service:
//这里并不用注册为bean对象 因为我们的继承该BaseService的对象已经注册为bean
// 虽然不能够使用private BaseDao<T> baseDao;属性和public void save() {}方法,但是能够继承此方法和属性。
public class BaseService<T> {
//泛型依赖注入,注入一个组件的时候,他的泛型也是参考标准
//即指定的T为User时 会在容器中寻找 BaseDao<User>类型的对象
//即指定的T为Book时 会在容器中寻找 BaseDao<Book>类型的对象
@Autowired
private BaseDao<T> baseDao;
public void save() {
System.out.println("自动注入的dao:"+baseDao);
baseDao.save();
}
}
@Service
public class BookService extends BaseService<Book>{}
@Service
public class UserService extends BaseService<User> {}
测试:
@Autowired
private UserService userService;
@Autowired
private BookService bookService;
@Test
public void Test(){
userService.save();
bookService.save();
}
自动注入的dao:org.lc.dao.UserDao@3c19aaa5
UserDao...保存用户
自动注入的dao:org.lc.dao.BookDao@3349e9bb
BookDao...保存图书
16、BeanFactory和ApplicationContext的区别
ApplicationContext是BeanFactory的子接口
BeanFactory:bean工厂接口;负责创建bean实例;容器里面保存的所有单实例bean起始是一个map(concurrentHashMap);也是spring最底层的接口
ApplicationContext:是容器接口;更多的负责容器功能的实现;可以基于BeanFactory创建好的对象之上完成强大的容器,容器可以从map中获取这个bean并进行AOP,DI
BeanFactory是最底层的接口,ApplicationContext留给程序员使用的IOC容器接口;
Spring中最大的模式就是工厂模式
- 通过在xml里面配置bean来让BeanFactory帮用户创建bean
<bean class="" id=""></bean>
工厂模式
优点:
- 减少耦合 例如:当前客户端
依赖
着这个new
出来的对象 - 客户端不需要在负责对象的创建,明确了各个类的职责
- 如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可
- 不会影响已有的代码,后期维护容易,增强系统的扩展性
确定:
- 工作量大,每增加一个类都要创建一个工厂
- 减少耦合 例如:当前客户端
BeanFactory
:延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory
来说会占用更少的内存,程序启动速度更快。ApplicationContext
:容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory
仅提供了最基本的依赖注入支持,ApplicationContext
扩展了BeanFactory
,除了有BeanFactory
的功能还有额外更多功能,所以一般开发人员使用ApplicationContext
会更多。
17、Bean的生命周期(初始化流程)
简单流程 https://www.jb51.net/article/154487.htm
- Bean 容器找到配置文件中 Spring Bean 的定义。
- Bean 容器利用 Java Reflection API 创建一个Bean的实例。
- 如果涉及到一些属性值 利用
set()
方法设置一些属性值。 - 如果 Bean 实现了
BeanNameAware
接口,调用setBeanName()
方法,传入Bean的名字。 - 如果 Bean 实现了
BeanClassLoaderAware
接口,调用setBeanClassLoader()
方法,传入ClassLoader
对象的实例。 - 与上面的类似,如果实现了其他
*.Aware
接口,就调用相应的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessBeforeInitialization()
方法 - 如果Bean实现了
InitializingBean
接口,执行afterPropertiesSet()
方法。- 如果 Bean 在配置文件中的定义包 含 init-method 属性,执行指定的方法。
- 如果有和加载这个 Bean的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法 - 当要销毁 Bean 的时候,如果 Bean 实现了
DisposableBean
接口,执行destroy()
方法。 - 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。
二、AOP
1、基本概念
AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待
在不改变原有的逻辑的基础上,增加一些额外的功能。代理也是这个功能,读写分离也能用aop来做。
将某段代码动态的切入到指定方法的指定位置进行运行的这种编程方式,称为面向切面编程
切 :指的是横切逻辑,原有业务逻辑代码不动,只能操作横切逻辑代码,所以面向横切逻辑
面 :横切逻辑代码往往要影响的是很多个方法,每个方法如同一个点,多个点构成一个面。这里有一个面的概念
AOP与OOP区别?
AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。
OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
2、使用场景
例如日志记录,在不改变源代码的情况下动态的加入到指定方法的指定位置
1)基于接口的jdk动态代理实现
目标对象需要实现一个接口对象,我们的代理对象无需实现接口对象,但是在底层仍然是通过反射来实现代理对象实现相应的接口
接口对象
public interface Caculate {
int add(int a, int b);
int substract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
}
目标对象
public class CaculateImpl implements Caculate {
@Override
public int add(int a, int b) {
//int c=1/0;
return a+b;
}
@Override
public int substract(int a, int b) {
return a-b;
}
@Override
public int multiply(int a, int b) {
return a-b;
}
@Override
public int divide(int a, int b) {
return a/b;
}
}
获取代理对象
public class ProxyInstance {
private Object object;
public ProxyInstance(Object object) {
this.object = object;
}
//获取代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), (proxy, method, args) -> {
Object invoke=null;
try {
System.out.println("执行:【" + method.getName() + "】方法之前的记录");
invoke = method.invoke(object, args);
System.out.println("执行:【" + method.getName() + "】方法之后的记录,方法的参数为:【" + Arrays.toString(args) + "】,方法的结果为【" + invoke + "】");
} catch (Exception e) {
System.out.println("方法抛出的异常为:【"+e.getCause()+"】");
}finally {
System.out.println("总要执行的记录....");
}
return invoke;
});
}
}
public class Test {
public static void main(String[] args) {
ProxyInstance proxyInstance = new ProxyInstance(new CaculateImpl());
//获取代理对象时,实际还是在底层为我们的目标对象创建了一个代理对象实现相同的接口。
//所以这里我们需要使用接口对象带接收返回值
Caculate instance = ((Caculate) proxyInstance.getProxyInstance());
int add = instance.add(1, 2);
System.out.println(add);
}
}
执行:【add】方法之前的记录
执行:【add】方法之后的记录,方法的参数为:【[1, 2]】,方法的结果为【3】
总要执行的记录....
3
3、专业术语解释
(1)横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
(2)Aspect(切面):通常是一个类,里面可以定义切入点和通知
(3)JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在 Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
(4)**Advice(通知)😗*AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)
(5)Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
(6)weave(织入):将切面应用到目标对象并导致代理对象创建的过程
(7)introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
(8)AOP代理(AOP Proxy):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类
(9)目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO
4、AOP简单配置流程
配置流程:
1)将目标类和切面类(封装了通知方法,切入点方法(切点表达式))加入到IOC容器中
2)指定切面类加上**@Aspect**注解
3)告诉Spring切面中的通知(执行方法)在什么时候运行
@Before 在目标方法执行之前运行
@After 在目标方法结束(无论目标方法是否成功完成)之后运行
@AfterReturning 在目标方法正常完成后之后运行
@AfterThrowing 在目标方法抛出异常之后运行
@Around 环绕通知
相当于:
Object invoke=null; try { //@Before invoke = method.invoke(object, args); //@AfterReturning } catch (Exception e) { //@AfterThrowing }finally { //@After } return invoke;
4)在通知上加入切点表达式
@Before("execution(访问修饰符 返回类型 类的全路径名.方法名(参数类型1,参数类型2...))")
// 在org.lc.aop.CaculateImpl类下的所有方法为pulibc修饰符,返回类型为int,参数为两个int类型的所有方法加上前置通知 @Before("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
5)开启基于注解的AOP注解
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
测试:
1)目标对象实现了接口(jdk动态代理)
public interface Caculate {
int add(int a, int b);
int substract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
}
@Service
public class CaculateImpl implements Caculate {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int substract(int a, int b) {
return a-b;
}
@Override
public int multiply(int a, int b) {
return a-b;
}
@Override
public int divide(int a, int b) {
return a/b;
}
}
//切面类
@Component
@Aspect
public class LogUtils {
@Before("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
public static void startLog() {
System.out.println("执行:【" + "method.getName()" + "】方法之前的记录");
}
@AfterReturning("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
public static void returnLog() {
System.out.println("执行:【" + "method.getName()" + "】方法之后的记录,方法的参数为:【" +" Arrays.toString(args)" + "】,方法的结果为【" + "invoke "+ "】");
}
@AfterThrowing("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
public static void exceptionLog() {
System.out.println("方法抛出的异常为:【"+"e.getCause()"+"】");
}
@After("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
public static void finallyLog() {
System.out.println("总要执行的记录....");
}
}
<!--扫描org.lc所有包-->
<context:component-scan base-package="org.lc"/>
<!--开启基于注解的AOP注解-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
//注入接口
@Autowired
private Caculate caculate;
@Test
public void Test(){
caculate.add(1, 2);
//class com.sun.proxy.$Proxy25
System.out.println(caculate.getClass());
}
执行:【method.getName()】方法之前的记录
总要执行的记录....
执行:【method.getName()】方法之后的记录,方法的参数为:【 Arrays.toString(args)】,方法的结果为【invoke 】
- 若目标对象实现了接口,则使用的是jdk动态代理来实现AOP
- 通过注入接口对象来使用,通过
caculate.getClass()
方法我们可以发现,AOP底层还是为我们创建了一个代理对象来实现相同的接口的。
2)目标对象没有实现接口(cglib代理)
- 若我们的目标对象没有实现接口,则使用的是cglib代理,默认为我们的目标类创建了一个内部类作为子类来实现代理-
- 通过注入本类使用。
- 通过实现
MethodInterceptor
接口中的intercept
方法完成拦截并加入横切逻辑
完成的
只对CaculateImpl
类做修改:
@Service
public class CaculateImpl{
public int add(int a, int b) {
return a+b;
}
public int substract(int a, int b) {
return a-b;
}
public int multiply(int a, int b) {
return a-b;
}
public int divide(int a, int b) {
return a/b;
}
}
@Autowired
private CaculateImpl caculate;
@Test
public void Test(){
caculate.add(1, 2);
//class org.lc.aop.CaculateImpl$$EnhancerBySpringCGLIB$$92c177be
System.out.println(caculate.getClass());
}
5、切点表达式详解
@Before("execution(访问修饰符 返回类型 类的全路径名.方法名(参数类型1,参数类型2...))")
1)匹配一个或多个字符
@Before("execution(public int org.lc.aop.CaculateIm*.*(int,int))")
2)匹配任意一个参数,第一个是int类型,第二个任意参数类型(匹配两个参数)
@Before("execution(public int org.lc.aop.CaculateIm*.*(int,*))")
3)匹配任意多个参数,任意参数类型
@Before("execution(public int org.lc.aop.CaculateIm*.*(..))")
4)匹配一层路径
即 aop和CaculateIm之间最多有一层路径
@Before("execution(public int org.lc.aop.*.CaculateIm*.*(int,*))")
5)匹配多层路径
即 aop和CaculateIm之间可以有任意层路径
@Before("execution(public int org.lc.aop..CaculateIm*.*(int,*))")
6)访问修饰符的位置不能写 * ,权限位置不写就行。 public关键字可选
7)最模糊的匹配(最好不要这样写)
任意包下的任意方法的任意参数的任意返回值
@Before("execution(* *.*(..))")
8)满足多个表达式&&
execution(public int org.lc.aop.CaculateIm*.*(..))&&execution(public int org.lc.aop.CaculateIm*.*(int,int))
8)满足一个表达式 ||
execution(public int org.lc.aop.CaculateIm*.*(int,double))||execution(public int org.lc.aop.CaculateIm*.*(int,int))
9)不满足表达式
!execution(public int org.lc.aop.CaculateIm*.*(..))
JoinPoint
/返回值/异常对象
6、AOP中的JoinPoint
必须放在参数列表的第一位
//切面类
@Component
@Aspect
public class LogUtils {
/**
* 前置通知
* @param joinPoint 连接点,封装了目标方法的信息
*/
@Before("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
public static void startLog(JoinPoint joinPoint) {
//获取方法的参数列表
Object[] args = joinPoint.getArgs();
//获取方法对象
Signature signature = joinPoint.getSignature();
//获取方法的名称
signature.getName();
//获取方法的访问修饰符
signature.getModifiers();
System.out.println("执行:【" + signature.getName() + "】方法之前的记录,方法的参数列表为【"+Arrays.toString(args)+"】");
}
/**
* 返回通知(方法正常返回)
* @param joinPoint 连接点,封装了目标方法的信息
* @param object 指定返回值 需要在注解中使用 returning = "参数名称",告诉spring要使用什么参数作为返回值
* 注意:如果指定返回类型为 double,方法只能返回double的类型才会执行此 返回通知
* 所以我们在声明返回类型时,一般声明为较大范围的类型
*/
@AfterReturning(value = "execution(public int org.lc.aop.CaculateImpl.*(int,int))",returning = "object")
public static void returnLog(JoinPoint joinPoint,Object object) {
System.out.println("执行:【" + joinPoint.getSignature().getName() + "】方法之后的记录,方法的结果为【" + object + "】");
}
/**
* 异常通知 方法抛出异常执行
* @param joinPoint
* @param e 方法抛出的异常类型 需要在注解中使用 throwing = "参数名称" ,告诉spring要使用什么参数作为返回值
* 注意:如果指定异常为 NullPointerException的异常,方法只能抛出的异常为NullPointerException的异常才会执行此 异常通知
* 所以我们在声明异常时,一般声明为较大范围的类型
*/
@AfterThrowing(value = "execution(public int org.lc.aop.CaculateImpl.*(int,int))",throwing = "e")
public static void exceptionLog(JoinPoint joinPoint,Exception e) {
System.out.println("【"+joinPoint.getSignature().getName()+"】方法抛出的异常为:【"+e+"】");
}
/**
* 后置通知 无论方法是否正常返回都执行
* @param joinPoint
*/
@After("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
public static void finallyLog(JoinPoint joinPoint) {
System.out.println("方法【"+joinPoint.getSignature().getName()+"】总要执行的记录....");
}
}
7、提取切点表达式实现重用
- 给定一个空的无参无返的方法
- 在方法上加入@Pointcut注解并加上重用的切点表达式
//切面类
@Component
@Aspect
public class LogUtils {
/**
* 1、给定一个空的无参无返的方法
* 2、在方法上加入@Pointcut注解并加上重用的切点表达式
*/
@Pointcut("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
public void myCutPointExpress(){
}
@Before("myCutPointExpress()")
public static void startLog(JoinPoint joinPoint) {
}
}
8、@Around环绕通知的使用
环绕通知集成了 前置通知,后置通知,返回通知,异常通知 为一体的通知
- @Around内部就是一个 代理方法 相当于
invoke.method(obj,args)
所以我们环绕通知的实现原理就是帮我们的切面类创建了一个代理对象,代理对象调用 invoke.method(obj,args)
来决定目标方法的执行时间和前后要处理的通知。而多层的切面,就相当于代理类调用的嵌套
//切面类
@Component
@Aspect
public class LogUtils {
/**
* 1、给定一个空的无参无返的方法
* 2、在方法上加入@Pointcut注解并加上重用的切点表达式
*/
@Pointcut("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
public void myCutPointExpress(){
}
/**
* 环绕通知
* interface ProceedingJoinPoint extends JoinPoint 可以使用JoinPoint中的方法
* @return
*/
@Around("myCutPointExpress()")
public Object aroundAdvice(ProceedingJoinPoint pjp){
//获取当前执行方法的参数
Object[] args = pjp.getArgs();
//获取当前执行的方法
Signature signature = pjp.getSignature();
Object proceed=null;
try {
//@Before
System.out.println("【前置通知】+【"+signature.getName()+"】方法执行之前的通知,方法的参数为【"+Arrays.toString(args)+"】");
//相当于利用反射调用目标方法,相当于 invoke.method(obj,args)
//帮我们执行目标方法
proceed = pjp.proceed(args);
//@AfterReturing
System.out.println("【返回通知】+【"+signature.getName()+"】方法返回之后的通知,方法结果为【"+proceed+"】");
}catch (Throwable throwable) {
//@AfterThrowing
System.out.println("【异常通知】+【"+signature.getName()+"】方法执行异常的通知,异常原因为【"+throwable.getCause()+"】");
}finally {
//@After
System.out.println("【后置通知】+【"+signature.getName()+"】方法最终执行的通知");
}
//将方法执行的结果返回出去 供外部接收使用
return proceed;
}
}
9、普通通知和环绕通知共存的执行顺序
环绕通知总是优先于普通通知执行的,因为环绕通知控制了目标方法的执行时机,只有环绕通知内部执行了pjp.proceed(args);
目标回调方法,普通通知才能运行
普通通知的执行顺序
- 【普通前置】 --> 目标方法执行 -->【普通后置】-->【普通返回通知】/【异常通知】
环绕通知的执行顺序
【环绕前置】 --> 目标方法执行 -->【环绕返回/异常】-->【环绕后置】
环绕通知和普通通知执行顺序
【环绕前置】--> 【普通前置】 -->目标方法执行 --> 【环绕正常返回/异常】--> 【环绕后置】--->【普通后置】--> 【普通返回/异常】
当我们目标方法出现异常时,环绕通知首先捕获该异常,则会消耗此异常,导致普通异常通知无法接受到,那么我们可以让环绕异常抛出此异常对象来让普通异常通知也能接收到该异常
@Around("myCutPointExpress()")
public Object aroundAdvice(ProceedingJoinPoint pjp){
//获取当前执行方法的参数
Object[] args = pjp.getArgs();
//获取当前执行的方法
Signature signature = pjp.getSignature();
Object proceed=null;
try {
System.out.println("【前置通知】+【"+signature.getName()+"】方法执行之前的通知,方法的参数为【"+Arrays.toString(args)+"】");
//相当于利用反射调用目标方法,相当于 invoke.method(obj,args)
proceed = pjp.proceed(args);
System.out.println("【返回通知】+【"+signature.getName()+"】方法返回之后的通知,方法结果为【"+proceed+"】");
}catch (Exception e) {
System.out.println("【异常通知】+【"+signature.getName()+"】方法执行异常的通知,异常原因为【"+e+"】");
//抛出该异常让 普通通知能够接收
throw new RuntimeException(e);
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
System.out.println("【后置通知】+【"+signature.getName()+"】方法最终执行的通知");
}
return proceed;
}
10、多切面AOP
1)默认按照切面文件的开头字母顺序决定谁先切入方法,先切入的后返回
2)通过**@Order**注解指定文件的加载顺序,值越小越优先加载
3)切面类中的环绕通知并不会影响其他切面类
若切面L优先于切面A先执行,并且只有切面A中有环绕通知:则执行顺序如下
【环绕L前置通知】+【add】方法执行之前的通知,方法的参数为【[1, 2]】
【前置通知L】执行:【add】方法之前的记录,方法的参数列表为【[1, 2]】
【前置通知A】执行:【add】方法之前的记录,方法的参数列表为【[1, 2]】
【后置通知A】方法【add】总要执行的记录....
【返回通知A】执行:【add】方法之后的记录,方法的结果为【3】
【环绕L返回通知】+【add】方法返回之后的通知,方法结果为【3】
【环绕L后置通知】+【add】方法最终执行的通知
【后置通知L】方法【add】总要执行的记录....
【返回通知L】执行:【add】方法之后的记录,方法的结果为【3】
11、AOP应用场景
Authentication 权限Caching 缓存Context passing 内容传递Error handling 错误处理Lazy loading 懒加载Debugging 调试logging, tracing, profiling and monitoring 记录跟踪 优化 校准Performance optimization 性能优化Persistence 持久化Resource pooling 资源池Synchronization 同步Transactions 事务
12、基于XML配置AOP
目标对象
public class CaculateImpl{
public int add(int a, int b) {
return a+b;
}
public int substract(int a, int b) {
return a-b;
}
public int multiply(int a, int b) {
return a-b;
}
public int divide(int a, int b) {
return a/b;
}
}
切面类
public class BLogUtils {
public static void startLog(JoinPoint joinPoint) {
//获取方法的参数列表
Object[] args = joinPoint.getArgs();
//获取方法对象
Signature signature = joinPoint.getSignature();
System.out.println("【前置通知】执行:【" + signature.getName() + "】方法之前的记录,方法的参数列表为【" + Arrays.toString(args) + "】");
}
public static void returnLog(JoinPoint joinPoint, Object object) {
System.out.println("【返回通知】执行:【" + joinPoint.getSignature().getName() + "】方法之后的记录,方法的结果为【" + object + "】");
}
public static void exceptionLog(JoinPoint joinPoint, Exception e) {
System.out.println("【异常通知】【" + joinPoint.getSignature().getName() + "】方法抛出的异常为:【" + e + "】");
}
public static void finallyLog(JoinPoint joinPoint) {
System.out.println("【后置通知】方法【" + joinPoint.getSignature().getName() + "】总要执行的记录....");
}
public Object aroundAdvice(ProceedingJoinPoint pjp) {
//获取当前执行方法的参数
Object[] args = pjp.getArgs();
//获取当前执行的方法
Signature signature = pjp.getSignature();
Object proceed = null;
try {
System.out.println("【环绕前置通知】+【" + signature.getName() + "】方法执行之前的通知,方法的参数为【" + Arrays.toString(args) + "】");
//相当于利用反射调用目标方法,相当于 invoke.method(obj,args)
proceed = pjp.proceed(args);
System.out.println("【环绕返回通知】+【" + signature.getName() + "】方法返回之后的通知,方法结果为【" + proceed + "】");
} catch (Exception e) {
System.out.println("【环绕异常通知】+【" + signature.getName() + "】方法执行异常的通知,异常原因为【" + e + "】");
throw new RuntimeException(e);
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
System.out.println("【环绕后置通知】+【" + signature.getName() + "】方法最终执行的通知");
}
return proceed;
}
}
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">
<!--注入目标对象-->
<bean id="caculate" class="org.lc.aop.CaculateImpl"></bean>
<!--注入切面类-->
<bean id="bLogUtils" class="org.lc.utils.BLogUtils"></bean>
<!--AOP配置-->
<aop:config>
<!--配置切点表达式,声明在切面外部,供整个AOP中的其他切面中使用,为全局切点表达式-->
<aop:pointcut id="mypoint2" expression="execution(* org.lc.aop.CaculateImpl.*(int,int))"/>
<!--指定切面类,引用外部注入的切面类-->
<aop:aspect ref="bLogUtils">
<!--配置切点表达式,相当于在bLogUtils切面类中声明的切点表达式,只能在本切面类使用-->
<aop:pointcut id="mypoint1" expression="execution(* org.lc.aop.CaculateImpl.*(int,int))"/>
<!--配置环绕通知,并引用切点表达式。
注意:若环绕通知配置在其他通知后,则环绕前置和普通前置顺序颠倒-->
<aop:around method="aroundAdvice" pointcut-ref="mypoint1"></aop:around>
<!--配置前置通知, 并自定义切点表达式-->
<aop:before method="startLog" pointcut="execution(* org.lc.aop.CaculateImpl.*(int,int))"></aop:before>
<!--配置返回通知,并引用切点表达式,并指定作为返回值的参数-->
<aop:after-returning method="returnLog" returning="object" pointcut-ref="mypoint1"></aop:after-returning>
<!--配置异常通知,并引用切点表达式,并指定作为异常对象的参数-->
<aop:after-throwing method="exceptionLog" pointcut-ref="mypoint1" throwing="e"></aop:after-throwing>
<!--配置后置通知,并指定切点表达式-->
<aop:after method="finallyLog" pointcut-ref="mypoint1"></aop:after>
</aop:aspect>
</aop:config>
</beans>
//classpath: 使用指定的xml文件
@ContextConfiguration(locations = "classpath:applicationContext.xml")
//使用junit4测试
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringTest {
@Autowired
private CaculateImpl caculate;
@Test
public void Test(){
caculate.add(1, 2);
}
}
【环绕前置通知】+【add】方法执行之前的通知,方法的参数为【[1, 2]】
【前置通知】执行:【add】方法之前的记录,方法的参数列表为【[1, 2]】
【环绕返回通知】+【add】方法返回之后的通知,方法结果为【3】
【环绕后置通知】+【add】方法最终执行的通知
【返回通知】执行:【add】方法之后的记录,方法的结果为【3】
【后置通知】方法【add】总要执行的记录....
13、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">
<!--注入目标对象-->
<bean id="caculate" class="org.lc.aop.CaculateImpl"></bean>
<!--注入切面类BLogUtils-->
<bean id="bLogUtils" class="org.lc.utils.BLogUtils"></bean>
<!--注入切面类LogUtils-->
<bean id="logUtils" class="org.lc.utils.LogUtils"></bean>
<!--AOP配置-->
<aop:config>
<!--配置切点表达式,声明在切面外部,供整个AOP中的其他切面中使用,为全局切点表达式-->
<aop:pointcut id="globalPoincut" expression="execution(* org.lc.aop.CaculateImpl.*(int,int))"/>
<!--指定切面类BLogUtils,引用外部注入的切面类bLogUtils-->
<!--使用order绝对执行的先后顺序,小的先执行-->
<aop:aspect ref="bLogUtils" order="2">
<!--配置切点表达式,相当于在bLogUtils切面类中声明的切点表达式,只能在本切面类使用-->
<aop:pointcut id="mypoint1" expression="execution(* org.lc.aop.CaculateImpl.*(int,int))"/>
<!--配置环绕通知,并引用切点表达式。
注意:若环绕通知配置在其他通知后,则环绕前置通知顺序会乱-->
<aop:around method="aroundAdvice" pointcut-ref="mypoint1"></aop:around>
<!--配置前置通知, 并自定义切点表达式-->
<aop:before method="startLog" pointcut="execution(* org.lc.aop.CaculateImpl.*(int,int))"></aop:before>
<!--配置返回通知,并引用切点表达式,并指定作为返回值的参数-->
<aop:after-returning method="returnLog" returning="object" pointcut-ref="mypoint1"></aop:after-returning>
<!--配置异常通知,并引用切点表达式,并指定作为异常对象的参数-->
<aop:after-throwing method="exceptionLog" pointcut-ref="mypoint1" throwing="e"></aop:after-throwing>
<!--配置后置通知,并指定切点表达式-->
<aop:after method="finallyLog" pointcut-ref="mypoint1"></aop:after>
</aop:aspect>
<!--指定切面类LogUtils,引用外部注入的切面类logUtils-->
<!--使用order绝对执行的先后顺序,小的先执行-->
<aop:aspect ref="logUtils" order="1">
<!--配置环绕通知,并引用切点表达式。
注意:若环绕通知配置在其他通知后,则环绕前置通知顺序会乱-->
<aop:around method="aroundAdvice" pointcut-ref="globalPoincut"></aop:around>
<!--配置前置通知, 并自定义切点表达式-->
<aop:before method="startLog" pointcut="execution(* org.lc.aop.CaculateImpl.*(int,int))"></aop:before>
<!--配置返回通知,并引用切点表达式,并指定作为返回值的参数-->
<aop:after-returning method="returnLog" returning="object" pointcut-ref="globalPoincut"></aop:after-returning>
<!--配置异常通知,并引用切点表达式,并指定作为异常对象的参数-->
<aop:after-throwing method="exceptionLog" pointcut-ref="mypoint1" throwing="e"></aop:after-throwing>
<!--配置后置通知,并指定切点表达式-->
<aop:after method="finallyLog" pointcut-ref="globalPoincut"></aop:after>
</aop:aspect>
</aop:config>
</beans>
三、事务
1、数据库事务及四大特性(ACID)
数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized
同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:
**原子性(Atomicity)😗*事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
**一致性 (Consistency)😗*事务执行后,数据库状态与其他业务规则保持一致。如转账业务,无论事务执行成功否,参与转账的两个账号余额之和应该是不变的。
**隔离性(Isolation)😗*并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
**持久性(Durability)😗*事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
2、MySQL中的事务
在默认情况下,MySQL**每执行一条SQL语句,都是一个单独的事务(自动提交)**。如果需要在一个事务中包含多条SQL语句,那么需要开启事务和结束事务。
开启事务:start transaction
业务SQL操作........
结束事务:commit或rollback
在执行SQL语句之前,先执行start transaction,这就开启了一个事务(事务的起点),然后可以去执行多条SQL语句,最后要结束事务,commit表示提交,即事务中的多条SQL语句所作出的影响会持久到数据库中,或者rollback,表示回滚到事务的起点,之前做的所有操作都被撤销了。
1)修改MySQL隔离级别(默认为可重复读)
SET [SESSION] | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
SESSION 当前会话; GLOBAL全局的;
READ UNCOMMITTED 读未提交;READ COMMITTED 读已提交;REPEATABLE READ 可重复读;SERIALIZABLE 串行化
例如:
SET SESSION TRANSACTION IOSLATION LEVEL READ UNCOMMITTED
设置当前会话事务的隔离级别为 读未提交
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | √ | √ | √ |
不可重复读(read-committed) | × | √ | √ |
可重复读(repeatable-read) | × | × | √ |
串行化(serializable) | × | × | × |
2)查询MySQL的隔离级别
SELECT @@global.tx_isolation ##查询全局隔离级别
SELECT @@session.tx_isolation ##查询当前会话隔离级别 (若我们用cmd进入mysql服务,则一个窗口代表一个会话)
SELECT @@tx_isolation ##同上
3)MySQL事务操作
开启事务:start transaction
提交事务:commit
回滚事务:rollback
2、JDBC中的事务处理
在JDBC中处理事务,都是通过Connection完成的。
同一事务中所有的操作,都在使用同一个Connection对象。
①JDBC中的事务
Connection的三个方法与事务有关:
- **setAutoCommit(boolean)😗*设置是否为自动提交事务,如果true(默认值为true)表示自动提交,也就是每条执行的SQL语句都是一个单独的事务,如果设置为false,那么相当于开启了事务了;con.setAutoCommit(false) 表示开启事务。
- commit():提交结束事务。
- rollback():回滚结束事务。
Connection con = 拿到连接,通过连接完成事务操作
try{
//开启事务 并关闭自动提交事务
con.setAutoCommit(false);
/**
*
*业务逻辑处理
*
**/
con.commit();//try的最后提交事务
} catch(Exception e) {
con.rollback();//回滚事务
}
3、配置声明式(注解)事务
声明式事务就好像我们使用AOP中的环绕通知一样来实现JDBC中的事务操作。
我们只需要在需要回滚的方法上加上一个类似于环绕通知的注解(@Transaction)来实现方法的回滚
我们的Spring已经为我们提供了一个切面类 PlatformTransactionManager 的抽象接口
我们只需要配置其实现该接口的不同数据库操作方法的事务管理器即可
- JPA 使用的:
JtaTransactionManager
事务管理器 - Hibernate使用的:
HibernateTransactionManager
事务管理器 - JDBCTemplate使用的 :
DataSourceTransactionManager
事务管理器
配置流程:
1)配置数据源
2)配置事务管理器(相当于切面类),并指定数据源
3)开启基于注解的事务控制,并指定事务管理器
表
CREATE TABLE `account` ( `username` varchar(50) NOT NULL, `balance` int(11) NOT NULL, PRIMARY KEY (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `book` ( `isbn` varchar(50) NOT NULL, `book_name` varchar(100) DEFAULT NULL, `price` int(11) DEFAULT NULL, PRIMARY KEY (`isbn`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `book_stock` ( `isbn` varchar(50) NOT NULL, `stock` int(11) DEFAULT NULL, PRIMARY KEY (`isbn`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
dao层
@Repository public class BookDao { @Autowired private JdbcTemplate jdbcTemplate; /** * 减去某个用户的余额 * @param username * @param money */ public void updataBalance(String username, int money) { String sql = "update account set balance=balance-? where username=?"; jdbcTemplate.update(sql, money, username); } /** * 按照ISBN号码获取图书的价格 * @param isbn * @return */ public int getPrice(String isbn) { String sql = "SELECT price from book where isbn=?"; return jdbcTemplate.queryForObject(sql,Integer.class, isbn); } /** * 按照ISBN号码减去库存 * 这里我们设置每次减1 * @param isbn */ public void updateStock(String isbn) { String sql = "update book_stock set stock=stock-1 where isbn=?"; jdbcTemplate.update(sql, isbn); } }
service层
在需要回滚的业务方法上加@Transactional注解
@Service public class BookService { @Autowired private BookDao bookDao; /** * 结账 * @param username * @param isbn */ //在需要回滚的业务方法上加@Transactional注解 @Transactional public void checkout(String username, String isbn) { //减库存 bookDao.updateStock(isbn); //查询图书价格 int price = bookDao.getPrice(isbn); //减余额 bookDao.updataBalance(username, price); } }
核心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"
xsi:schemaLocation="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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:jdbc.properties"/>
<context:component-scan base-package="org.lc"></context:component-scan>
<!--注入数据源-->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.dirverClass}"></property>
</bean>
<!--配置jdbctemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--事务控制-->
<!--配置事务管理器(相当于切面类)进行事务控制-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--我们控制事务管理器,就是要控制数据源,因为都是通过连接对象来进行事务的提交和回滚-->
<!--控制数据源-->
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--开启基于注解的事务控制,并指定事务管理器-->
<!--在方法上加@Transaction注解-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>
</beans>
测试:
@ContextConfiguration(locations = "classpath:tx.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class TranscationTest {
@Autowired
private BookService bookService;
@Test
public void Test(){
//在此方法操作中,只要有一步出错,则全部回滚
bookService.checkout("Tom","ISBN-001");
System.out.println("结账成功!");
}
}
4、@Transaction注解属性详解
@Transaction
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { @AliasFor("transactionManager") String value() default ""; @AliasFor("value") String transactionManager() default ""; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; //设置超时回滚 int timeout() default -1; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
0)transactionManager 配置多事务管理器
若配置多数据源则需要配置多事务管理器
在使用的使用指定@Transacation(transactionManager ="")
的事务管理器
1)timeout(单位秒)
事务超出指定执行时长后自动终止并回滚
设置的timeout为3秒,若执行业务操作的时间超过3秒则会业务回滚,并抛出以下异常
org.springframework.transaction.TransactionTimedOutException:
@Transactional(timeout = 3)
public void checkout(String username, String isbn) {
//减库存
bookDao.updateStock(isbn);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//查询图书价格
int price = bookDao.getPrice(isbn);
//减余额
bookDao.updataBalance(username, price);
}
2)readOnly
设置事务为只读事务,可以进行事务优化,加快查询速度。忽略并发安全和一些繁琐的操作
设置为只读事务的方法中只能有查询操作,不能有修改操作,否则会报错:
org.springframework.dao.TransientDataAccessResourceException: PreparedStatementCallback;
//默认为false
@Transactional(readOnly = true)
3)事务回滚异常分类
默认发生运行时异常都回滚,发生编译时异常不会回滚
运行时异常(非检查异常):可以不用处理,默认都回滚。
- 例如 空指针异常,数组索引越界异常等
编译时异常(检查异常):通过try-catch,或在方法上声明throws的异常默认不回滚。
- 例如 文件读写流等
注意:
所有被try-catch(包括运行时异常和非运行异常)的异常都不会回滚
运行时异常 只有通过throw或throws抛出异常才会回滚
非运行时异常默认不会回滚(因为运行之前就已经被try-catch了),通过throw和throws抛出异常也不会回滚。只有设置rollbackFor指定其非运行时异常,通过thows抛出的异常才能回滚。
4)指定不回滚异常
设置哪些异常不会回滚;可以设置让回滚的异常不回滚。
②noRollbackForClassName(不常用)
- 指定不会滚异常的全路径类名
@Transactional(noRollbackForClassName = {"java.lang.ArithmeticException"})
①noRollbackFor
- 指定不回滚异常的Class类
@Transactional(noRollbackFor = {ArithmeticException.class,NullPointerException.class})
设置 算数异常 和 空指针异常不会滚。(默认是运行时异常都要回滚)
@Transactional(noRollbackFor = {ArithmeticException.class,NullPointerException.class})
public void checkout(String username, String isbn) {
//减库存
bookDao.updateStock(isbn);
//查询图书价格
int price = bookDao.getPrice(isbn);
//减余额
bookDao.updataBalance(username, price);
//遇到此异常不会回滚 (默认回滚)
int i=1/0;
}
}
5)指定回滚异常
设置哪些异常会回滚;可以让不会滚的异常发生回滚
①rollbackForClassName(不常用)
- 指定会滚异常的全路径类名
@Transactional(rollbackForClassName = {"java.io.FileNotFoundException"})
②rollbackFor
- 指定回滚异常的Class类
@Transactional(rollbackFor = {FileNotFoundException.class})
设置 文件找不到异常 会回滚。(默认是编译时异常不回滚)
注意: 只有通过throws抛出异常才会回滚,如果try-catch则无效
@Transactional(rollbackFor = {FileNotFoundException.class})
public void checkout(String username, String isbn) throws FileNotFoundException {
//减库存
bookDao.updateStock(isbn);
//查询图书价格
int price = bookDao.getPrice(isbn);
//减余额
bookDao.updataBalance(username, price);
//找不到文件回滚。 (默认不回滚)
new FileInputStream("f://ddd");
}
}
6)事务的隔离级别
@Transactional(isolation = Isolation.REPEATABLE_READ )
默认隔离级别DEFAULT, 使用数据库设置的隔离级别 ( 默认 ) ,由 DBA 默认的设置来决定隔离级别 。
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
多个事务并发操作产生的问题,假设现在有两个事务T1和T2
①读未提交(READ UNCOMMITTED)--产生脏读和不可重复读和幻读
允许T1读取T2未提交后的修改数据,产生脏读。
②读已提交(READ COMMITTED)--避免脏读,产生不可重复读和幻读
事务T1不能够读取到T2未提交修改的数据。读已提交能够避免脏读。但是不能保证可重复读。
③可重复读(REPEATABLE-READ)--避免脏读和不可重复读,产生幻读
可重复读为MySQL默认的隔离级别,可重复读也叫快照读,即事物T1第一次读数据的时候,留下一个快照,T2事物对其数据进行任意操作(更新,删除),事物T1都是从快照中读取,数据保持一致,即便数据从数据库中删除,只要T1还未结束当前事务,读取到的数据依然和开始的不变。
1)什么是幻读?
InnoDB实现的可重复读(REPEATABLE-READ)通过mvcc(多版本并发控制 Multi-Version Concurrency Control)机制避免了这种幻读现象。
事务B执行插入操作后,在事务A中查询没有查到B添加的数据行,这就是可重复读。
**但是,**在事务A执行了对事务B中插入的数据进行update后,再查询时就查到了事务B中添加的数据,这就是幻读。
这种结果告诉我们其实在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决是不彻底的。
④串行化(SERIALIZABLE)--避免一切
确保事务T1可以多次从一个表中读取到相同的行,在T1事务执行期间,禁止其他事务对这个表添加,更新,删除操作,可以避免任何并发问题,但是其性能十分低下。
7)事务的传播行为
@Transactional(propagation = Propagation.REQUIRED)
默认的传播行为REQUIRED
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
事务传播行为类型 | 说明 |
---|---|
支持当前事务情况:↓ | |
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。Spring中的默认的传播行为,常用。 |
PROPAGATION_SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 |
PROPAGATION_MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) |
不支持当前事务:↓ | |
PROPAGATION_REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起。常用。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常。 |
其他情况:↓ | |
PROPAGATION_NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 |
REQUIRES_NEW和REQUIRED传播行为详解
DAO层
@Repository public class BookDao { @Autowired private JdbcTemplate jdbcTemplate; /** * 减去某个用户的余额 * @param username * @param money */ public void updataBalance(String username, int money) { String sql = "update account set balance=balance-? where username=?"; jdbcTemplate.update(sql, money, username); } /** * 按照ISBN号码获取图书的价格 * @param isbn * @return */ public int getPrice(String isbn) { String sql = "SELECT price from book where isbn=?"; return jdbcTemplate.queryForObject(sql,Integer.class, isbn); } /** * 按照ISBN号码减去库存 * 这里我们设置每次减1 * @param isbn */ public void updateStock(String isbn) { String sql = "update book_stock set stock=stock-1 where isbn=?"; jdbcTemplate.update(sql, isbn); } /** * 根据 isbn修改书的价格 * @param isbn * @param price */ public void updatePrice(String isbn, int price) { String sql="update book set price=? where isbn=?"; jdbcTemplate.update(sql, price, isbn); } }
service层 (内层事务)
@Service public class BookService { @Autowired private BookDao bookDao; /** * 结账 * @param username * @param isbn */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void checkout(String username, String isbn){ //减库存 bookDao.updateStock(isbn); //查询图书价格 int price = bookDao.getPrice(isbn); //减余额 bookDao.updataBalance(username, price); // int i=1/0; } @Transactional(propagation =Propagation.REQUIRED) public void updatePrice(String isbn, int price) { bookDao.updatePrice(isbn, price); int i=1/0; } }
service (外层事务)
@Service public class MultipleTransactionService { @Autowired private BookService bookService; //外层事务 @Transactional public void multipleTran() { //内层事务1 bookService.checkout("Tom", "ISBN-001"); //内层事务2 bookService.updatePrice("ISBN-003",500); // int i=1/0; } }
测试代码
@ContextConfiguration(locations = "classpath:tx.xml") @RunWith(SpringJUnit4ClassRunner.class) public class TranscationTest { @Autowired private MultipleTransactionService multipleTransactionService; @Test public void Test1(){ multipleTransactionService.multipleTran(); } }
外层事务默认为
REQUIRES
注意:下面的异常必须为抛出异常 ,try-catch异常不会回滚。
- 当内部事务1和内部事务2的传播行为是
REQUIRES_NEW
, 无论外层事务是否发生异常,内部事务都回新建一个自己的事务,不会回滚 - 当内部事务1和内部事务2的传播行为是
REQUIRES
, 内部事务会依赖外部事务,外层事务发生异常,内部事务全部回滚 - 当内部事务1为
REQUIRES
,事务2位REQUIRES_NEW
, 当内部事务1发生异常,则会向上抛出,若事务1在事务2前面执行,那么事务2并不会执行,事务1提前回滚。 - 当内部事务1为
REQUIRES_NEW
,事务2位REQUIRES
, 当内部事务2发生异常,则会向上抛出,事务1新建事务运行并不会回滚,事务2回滚。
- 当内部事务1和内部事务2的传播行为是
若内部事务依赖于外部事务,那么在内部事务设置的其他属性如:timeout并不会生效,需要在外部事务上设置。
不能在同一Service类中实现事务嵌套,否则无效,例如:
@Service class MultipleService{ @Autowired private BookDao bookDao; @Transaction void mA(){} @Transaction void mB(){} @Transaction void mC(){ //失效,相当于三个事务为同一个事务 mA(); mB(); } }
一个Service类只有一个代理对象。
5、基于xml的声明式事务配置
1)Spring提供事务管理器(事务切面),配置这个事务管理器
2)配置事务方法
3)告诉spring哪些是事务方法
Service层
@Service
public class BookService {
@Autowired
private BookDao bookDao;
/**
* 结账
* @param username
* @param isbn
*/
public void checkout(String username, String isbn){
//减库存
bookDao.updateStock(isbn);
//查询图书价格
int price = bookDao.getPrice(isbn);
//减余额
bookDao.updataBalance(username, price);
int i=1/0;
}
}
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/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:jdbc.properties"/>
<context:component-scan base-package="org.lc"></context:component-scan>
<!--注入数据源-->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.dirverClass}"></property>
</bean>
<!--配置jdbctemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--事务控制-->
<!--配置事务管理器(相当于切面类)进行事务控制-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--我们控制事务管理器,就是要控制数据源,因为都是通过连接对象来进行事务的提交和回滚-->
<!--控制数据源-->
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<aop:config>
<!--定义切点表达式 指定哪些方法需要增强-->
<!--指定ser开头的包及其下面的任意包和任意方法作为切点-->
<aop:pointcut id="txPoint" expression="execution(* org.lc.ser*.*.*(..))"/>
<!--事务增强
advice-ref:配置哪些方法需要进行事务的配置
pointcut-ref:指定哪些方法需要进行增强
-->
<aop:advisor advice-ref="myAdvice" pointcut-ref="txPoint"></aop:advisor>
</aop:config>
<!--配置事务增强
transaction-manager:指定事务管理器-->
<tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager">
<!--配置事务属性-->
<!--指定增强的方法的事务属性配置-->
<tx:attributes>
<!--为所有方法配置 事务-->
<tx:method name="*"/>
<!--为checkout 配送事务传播行为 REQUIRED,隔离级别REPEATABLE_READ,超时值 -1(永远不会超时)-->
<tx:method name="checkout" propagation="REQUIRED" timeout="-1" isolation="REPEATABLE_READ" />
<!--为所有get开头的方法 设为只读-->
<tx:method name="get*" read-only="true"/>
</tx:attributes>
</tx:advice>
</beans>
6、事务失效的几种情况
1)数据库不支持事务
以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。
从 MySQL 5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么搞都是白搭。
2)方法不是 public 的
@Transactional 只能用于 public 的方法上,否则事务不会生效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
3)在同一service中嵌套事务
@Service
class MultipleService{
@Autowired
private BookDao bookDao;
@Transaction
void mA(){}
@Transaction
void mB(){}
@Transaction
void mC(){
//失效,相当于三个事务为同一个事务,只相当于mC方法的事务生效,调用的mA,mB方法只是简单的java方法调用
mA();
mB();
}
}
发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,this是一个实际的对象,事务调用者为动态代理对象才生效
4)异常被try-catch
无论是运行时异常还是非运行时异常只要try-catch事务都不会回滚。只有指定非运行异常可回滚通过throw或throws抛出异常或运行时异常才能被回滚
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {
}
}
}
5)指定非运行时异常回滚
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {
throw new Exception("更新错误");
}
}
}
这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如: @Transactional(rollbackFor = Exception.class) 这个配置仅限于 Throwable 异常类及其子类。