【框架】Spring
Spring
1 Spring简介
1.1 Spring概述
- 官网:http://spring.p2hp.com/
- Spring 是最受欢迎的企业级 Java 应用程序开发框架,数以百万的来自世界各地的开发人员使用。
- Spring 框架来创建性能好、易于测试、可重用的代码。
- Spring 框架是一个开源的 Java 平台,它最初是由 Rod Johnson 编写的,并且于 2003 年 6 月首次在 Apache 2.0 许可下发布。
- Spring 是轻量级的框架,其基础版本只有 2 MB 左右的大小。
- Spring 框架的核心特性是可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应用程序是需要扩展的。
- Spring 框架的目标是使 J2EE (JavaEE)开发变得更容易使用,通过启用基于 POJO编程模型来促进良好的编程实践。
1.2 Spring Framework
Spring 基础框架,可以视为 Spring 基础设施,基本上任何其他 Spring 项目都是以 Spring Framework为基础的。
1.2.1 Spring Framework特性
- 非侵入式:使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序时结构清晰、简洁优雅。
- 控制反转:IOC——Inversion of Control,翻转资源获取方向。把自己创建资源、向环境索取资源变成环境将资源准备好,我们享受资源注入。
- 面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码功能。
- 容器:
Spring IOC
是一个容器,因为它包含并且管理组件对象的生命周期。组件享受到了容器化的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率。 - 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭建超大型复杂应用系统。
- 声明式:很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现。
- 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基础上全部使用 Spring 来实现。
1.2.2 Spring Framework五大功能模块
功能模块功能介绍
功能模块 | 功能介绍 |
---|---|
Core Container | 核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。 |
AOP&Aspects | 面向切面编程 |
Testing | 提供了对 junit 或 TestNG 测试框架的整合。 |
Data Access/Integration | 提供了对数据访问/集成的功能。 |
Spring MVC | 提供了面向Web应用程序的集成功能。 |
2 IOC-IOC容器思想★★★
先简单说一下,IOC容器就是放置已经创建好的实例的一个地方。这样实例不需要程序员来手动创建,而是交给容器管理,更加高效。
下面这一部分更加详细阐述了,如果要深入了解IOC容器,需要阅读源码,这个以后安排上。这个笔记主要用于学习如何使用框架
IOC容器思想
IOC:Inversion of Control,翻译过来是反转控制。
①获取资源的传统方式
自己做饭:买菜、洗菜、择菜、改刀、炒菜,全过程参与,费时费力,必须清楚了解资源创建整个过程中的全部细节且熟练掌握。
在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。
②反转控制方式获取资源
点外卖:下单、等、吃,省时省力,不必关心资源创建过程的所有细节。
反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。
③DI
DI:Dependency Injection,翻译过来是依赖注入。
DI 是 IOC 的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入。相对于IOC而言,这种表述更直接。
所以结论是:IOC 就是一种反转控制的思想, 而 DI 是对 IOC 的一种具体实现。
IOC在Spring中的实现
Spring 的 IOC 容器就是 IOC 思想的一个落地的产品实现。IOC 容器中管理的组件也叫做 bean。在创建bean 之前,首先需要创建 IOC 容器。Spring 提供了 IOC 容器的两种实现方式:
①BeanFactory
这是 IOC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。
②ApplicationContext
BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用
ApplicationContext 而不是底层的 BeanFactory。
3 IOC-基于XML文件管理Bean
对于xml文件的方式也要学会掌握,因为对于工程里面的一些jar包,你是无法在上面给他加注释的,只能使用xml文件方式。
3.1 准备工作
maven依赖:
<dependencies>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
创建Spring的配置文件:
入门案例
实体类:
public class HelloWorld {
public void sayHello(){
System.out.println("Hello world!");
}
}
配置文件中:
<bean id="helloworld" class="com.atguigu.spring.pojo.HelloWorld"></bean>
测试:
@Test
public void test(){
//获取IOC容器
ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
//获取IOC容器中的bean
HelloWorld helloword = (HelloWorld) ioc.getBean("helloworld");
helloworld.sayHello();
}
个人理解,就是根据xml文件名获取ioc容器,在该ioc容器中(xml里),根据bean标签,获取id为helloworld所对应的全类名,映射到指定类,返回并转型为HelloWorld类,复制给HelloWorld helloword,之后调用方法。
3.2 获取bean
3.2.1 方式一:通过bean的id
配置文件中:
<bean id="studentOne" class="com.zylai.spring.pojo.Student"></bean>
<bean id="studentTwo" class="com.zylai.spring.pojo.Student"></bean>
测试:
@Test
public void testIOC(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
//获取bean
// 1.通过bean的id获取
Student studentOne = (Student) ioc.getBean("studentOne");
studentOne.X()//这里就可以调用类中方法了
}
3.2.2 方式二:通过类型(类的Class对象)(使用最多)
注意:要求ioc容器中有且仅有一个与之匹配的bean
若没有任何一个类型匹配的bean,抛出NoSuchBeanDefinitionException
若有多个类型匹配的bean,抛出NoUniqueBeanDefinitionException
@Test
public void testIOC(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
//获取bean
// 2.通过类的Class对象获取
Student studentOne = ioc.getBean(Student.class);
}
3.2.3 方式三:通过类型和id
@Test
public void testIOC(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
//3.通过类型和id获取
Student studentOne = ioc.getBean("studentOne",Student.class);
System.out.println(studentOne);
}
3.2.4 获取bean的方式总结
以后用的最多的就是第二种方式,一个类型的bean只需要配置一次就可以了,如果真的需要多个实例,那么配置时加上scope属性选择多例就可以了
注意:在IOC容器中通过工厂模式和反射技术创建对象,所以需要对象的无参构造器
(1)反射技术
SpringIOC创建对象时,需要利用到反射技术,用到两个主要的方法。
第一个:Class.forName();获取到创建对象的类。
第二个:类的newInstance();获取对象。
(2)工厂模式
SpringIOC创建对象是使用反射的技术,创建对象的过程是放在工厂里面的,此时我们不需要知道对象是如何创建的,只需要清楚我们需要什么对象就行,这样做有一个好处就是,降低了调用者和被调用者直接的耦合。
(3)为什么需要对象的无参构造器
通过 Class 类的 newInstance() 方法创建对象,该方法要求该 Class 对应类有无参构造方法。执行 newInstance()方法实际上就是使用对应类的无参构造方法来创建该类的实例,其代码的作用等价于Super sup = new Super()。
Class c = Class.forName("Super");
//通过Class类的newInstance()方法创建对象
Super sup = (Super)c.newInstance();
System.out.println(sup.supPublic());
如果 Super 类没有无参构造方法,运行程序时则会抛出一个 InstantiationException 实例化异常。
3.2.5 对于接口
xml文件中不能写接口,很简单,接口没有实例对象,也没有无参构造器
如果组件类实现了接口,根据接口类型可以获取 bean 吗?
可以,前提是bean唯一
如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗?
不行,因为bean不唯一
下面这个会得到接口的实现类对象
@Test
public void testIOC(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
//通过接口获取实例
Person person = ioc.getBean(Person.class);
System.out.println(person);
}
个人理解:其中Person是接口,IOC容器中,只有一个组件类实现了该接口,就可以通过接口类型获取bean
3.2.6 根据类型获取bean的实质
根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:
对象 instanceof 指定的类型
的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到。
即通过bean的类型、bean所继承的类的类型、bean所实现的接口的类型都可以获取bean
理解instanceof
instanceof 是 Java 的保留关键字,它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型
- 基本类型完全不能用于 instanceof 判断
- null 只能放在 instanceof 关键字的左边
- 引用类型数组和基本类型数组可以判断
//类java伪代码 实现原理
boolean rs;
if (obj == null) {
rs= false;
} else {
try {
T temp = (T) obj;
rs= true;
} catch (ClassCastException e) {
rs = false;
}
}
return rs;
总结:
- obj==null,返回 false
- (T) obj 不引发 ClassCastException,返回 true,否则返回 false
3.3 依赖注入
setter注入和构造器注入⭐
分别是调用类的set方法和有参构造
翻译:property→所有物
<!--主要属性和成员变量的区别-->
<!--setter注入-->
<bean id="studentTwo" class="com.zylai.spring.pojo.Student">
<!--
property:通过成员变量的set方法进行赋值
name:设置需要赋值的属性名(和set方法有关,set方法名,去掉set之后首字母大写)★与成员变量无关!!!
value:设置属性的值
-->
<property name="sid" value="1001"/>
<property name="sname" value="张三"/>
<property name="age" value="23"/>
<property name="gender" value="男"/>
</bean>
<!--构造器注入,有参构造 按顺序-->
<!--与构造器参数不明确就会报错-->
<bean id="studentThree" class="com.zylai.spring.pojo.Student">
<constructor-arg value="1002"/>
<constructor-arg value="李四"/>
<constructor-arg value="女"/>
<constructor-arg value="23" name="age"/>
<!--通过属性name 避免参数歧义-->
</bean>
3.3.1 特殊值处理
特殊字符和CDATA节的处理如下:
- null 赋值:
- xml实体 赋值:应该使用该特殊字符对应的实体 >
>
<<
- xml实体 赋值: CDATA节标签,]]> → ‘<王五>’,中括号中的值,原样解析。 CDATA节是xml中一个特殊的标签,不能写在属性中,只能通过一个标签写入。
<!--特殊值处理-->
<bean id="studentFour" class="com.zylai.spring.pojo.Student">
<property name="sid" value="1003"/>
<!--
对于特殊字符,应该使用该特殊字符对应的实体
< <
> >
另外也可以使用CDATA节其中的内容会原样解析,
CDATA节是xml中一个特殊的标签,不能写在属性中,只能通过一个标签写入
-->
<!--<property name="sname" value="<王五>"/>-->
<property name="sname">
<value><![CDATA[<王五>]]></value>
</property>
<property name="gender">
<!--使性别这个属性为null-->
<null/>
</property>
</bean>
3.3.2 类类型的属性赋值(Clazz类型)
三种方式:
ref:ref引用IOC容器中的某个bean的id
<bean id="studentFive" class="com.zylai.spring.pojo.Student">
<property name="sid" value="1006"/>
<property name="sname" value="张六"/>
<property name="age" value="23"/>
<property name="gender" value="男"/>
<!--ref引用IOC容器中的某个bean的id-->
<property name="clazz" ref="clazzOne"/>
</bean>
<bean id="clazzOne" class="com.atguigu.spring.pojo.Clazz">
<property name="cid" value="1111"></property>
<property name="cname" value="最好"></property>
</bean>
级联:级联的方式,要求clazz属性赋值或者实例化
。所以一般不用
<bean id="studentFive" class="com.zylai.spring.pojo.Student">
<property name="sid" value="1006"/>
<property name="sname" value="张六"/>
<property name="age" value="23"/>
<property name="gender" value="男"/>
<!--ref引用IOC容器中的某个bean的id-->
<property name="clazz" ref="clazzOne"/>
<!--级联的方式,要求保证提前给clazz属性赋值或者实例化。所以一般不用-->
<property name="clazz.cid" value="1122"/>
<property name="clazz.cname" value="哈哈班"/>
</bean>
内部bean:在属性中设置一个bean。
注意:不能通过ioc容器获取,相当于内部类,只能在当前student内使用
<bean id="studentFive" class="com.zylai.spring.pojo.Student">
<property name="sid" value="1006"/>
<property name="sname" value="张六"/>
<property name="age" value="23"/>
<property name="gender" value="男"/>
<!--内部bean,在属性中设置一个bean。
注意:不能通过ioc容器获取,相当于内部类,只能在当前student内使用-->
<property name="clazz">
<bean id="clazzInner" class="com.zylai.spring.pojo.Clazz">
<property name="cid" value="1122"/>
<property name="cname" value="王班"/>
</bean>
</property>
</bean>
3.3.3 数组
在属性中使用array
标签,标签中数组元素的值写在value
中
<!--数组-->
<bean id="studentSix" class="com.zylai.spring.pojo.Student">
<property name="sid" value="1006"/>
<property name="sname" value="张六"/>
<property name="age" value="23"/>
<property name="gender" value="男"/>
<property name="clazz">
<bean id="clazzInner" class="com.zylai.spring.pojo.Clazz">
<property name="cid" value="1122"/>
<property name="cname" value="王班"/>
</bean>
</property>
<!--数组属性-->
<property name="hobbies">
<array>
<!--数组元素类型是自变量类型用value,是类类型 用ref-->
<value>唱</value>
<value>跳</value>
<value>rap</value>
</array>
</property>
</bean>
3.3.4 list集合
方式一:在属性中使用list标签
<!--
list集合
1.在属性中使用list标签
2.再写一个集合类型的bean
-->
<bean id="clazzTwo" class="com.zylai.spring.pojo.Clazz">
<property name="cid" value="1111"/>
<property name="cname" value="王者班"/>
<property name="studentList">
<list>
<ref bean="studentOne"/>
<ref bean="studentTwo"/>
<ref bean="studentThree"/>
</list>
</property>
</bean>
方式二:配置一个集合类型的bean,需要使用util的约束
<!--
list集合
1.在属性中使用list标签
2.再写一个集合类型的bean
-->
<bean id="clazzTwo" class="com.zylai.spring.pojo.Clazz">
<property name="cid" value="1111"/>
<property name="cname" value="王者班"/>
<property name="students" ref="studentList"/>
</bean>
<!--配置一个集合类型的bean,需要使用util的约束-->
<util:list id="studentList">
<ref bean="studentFour"/>
<ref bean="studentFive"/>
<ref bean="studentSix"/>
</util:list>
3.3.5 map集合
方式和list差不多
方式一:设置map标签
<bean id="studentSix" class="com.zylai.spring.pojo.Student">
<property name="sid" value="1006"/>
<property name="sname" value="张六"/>
<property name="age" value="23"/>
<property name="gender" value="男"/>
<property name="clazz">
<bean id="clazzInner" class="com.zylai.spring.pojo.Clazz">
<property name="cid" value="1122"/>
<property name="cname" value="王班"/>
</bean>
</property>
<property name="hobbies">
<array>
<value>唱</value>
<value>跳</value>
<value>rap</value>
</array>
</property>
<!--map集合,和list差不多,就是需要设置键值对。第二种方式就是util了-->
<property name="teacherMap">
<map>
<entry key="语文老师" value-ref="teacherOne"/>
<entry key="数学老师" value-ref="teacherTwo"/>
</map>
</property>
</bean>
方式二:写一个集合类型的bean
<bean id="studentSix" class="com.zylai.spring.pojo.Student">
<!--中间省略···-->
<property name="teachers" ref="teacherMap"/>
</bean>
<util:map id="teacherMap">
<entry key="语文老师" value-ref="teacherOne"/>
<entry key="数学老师" value-ref="teacherTwo"/>
</util:map>
3.3.6 p命名空间
<!--p命名空间,需要在xml文件最开始设置引入-->
<!--p:都有两个属性 比如 sid和sid-ref 当属性为自变量时,用不带ref的,如果是类类型则需要使用带ref的-->
<bean id="studentSeven" class="com.zylai.spring.pojo.Student"
p:sid="1008" p:sname="小红" p:clazz-ref="clazzOne"
p:teacherMap-ref="teacherMap">
</bean>
3.4 管理数据源和外部文件
加入依赖
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
创建jdbc的配置文件
jdbc.propertiesjdbc.properties
PROPERTIES
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
jdbc.username=root
jdbc.password=527168zhao
initialSize=5
maxActive=9
配置DataSource
注意:需要通过context标签引入外部文件,注意context标签在xml文件头对应的链接
方法一(更好):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<!--引入jdbc.properties,之后可以通过${key}的方式访问value-->
<!--这里的context标签,注意上面引入的-->
<context:property-placeholder location="jdbc.properties"></context:property-placeholder>
<bean id="datasource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!--<property name="initialSize" value="${initialSize}"/>-->
<!--<property name="maxActive" value="${maxActive}"/>-->
</bean>
</beans>
方式二(直接输入配置信息):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="527168zhao"></property>
</bean>
</beans>
若导入依赖,但找不到com.alibaba.druid.pool.DruidDataSource类,File→setting→搜索maven→查看红色框地址(本地仓库是否有所需jar包,需要点亮Override)
3.5 bean的作用域
<!--
scope默认是单例,可以选择prototype多例
scope="singleton|prototype"
singleton:单例,表示获取该bean所对应的对象都是同一个
prototype:多例,表示获取该bean所对应的对象都不是同一个
-->
<bean id="student" class="com.zylai.spring.pojo.Student" scope="singleton">
<property name="sid" value="1001"/>
<property name="sname" value="张三"/>
</bean>
在测试中
@Test
public void testDataSource() throws SQLException {
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-datasource.xml");
Student student1 = ioc.getBean(Student.class);
Student student2 = ioc.getBean(Student.class);
System.out.println(student1 == student2);
//结果为true 所以是单例模式 使用比较多
//如果xml文件中设置 scope="prototype" 则是多例
}
3.6 bean的生命周期★
- 实例化,调用无参构造
- 实例注入,调用set方法(为属性赋值)
- 初始化之前的操作(由后置处理器负责)
- 初始化,需要通过bean的init-method属性指定初始化方法
- 初始化之后的操作(由后置处理器负责)
- IOC容器关闭时销毁
测试验证:
实例:
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
public User() {
System.out.println("生命周期:1、创建对象");
}
public User(Integer id, String username, String password, Integer age) {
this.id = id;
this.username = username;
this.password = password;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
System.out.println("生命周期:2、依赖注入");
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public void initMethod() {
System.out.println("生命周期:3、初始化");
}
public void destroyMethod() {
System.out.println("生命周期:4、销毁");
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
bean的后置处理器:
bean的后置处理器,会在生命周期的初始化前后添加额外的操作
,需要实现BeanPostProcessor接口,且配置到IOC容器
,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
需要在xml文件中声明
IDEA快捷键,重写方法,光标放置BeanPostProcessor处,Ctrl+O,选中重写方法,OK。
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
//此方法在bean的生命周期初始化之前执行
System.out.println("MyBeanPostProcessor-->后置处理postProcessBeforeInitialization");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//此方法在bean的生命周期初始化之后执行
System.out.println("MyBeanPostProcessor-->后置处理postProcessAfterInitialization");
return bean;
}
}
xml:
<!-- init-method初始化自动调用的方法 destroy-method销毁自动调用的方法-->
<bean class="atguigu.boot.User" init-method="initMethod" destroy-method="destroyMethod">
<property name="id" value="1"></property>
<property name="username" value="zhm"></property>
<property name="password" value="527168zhao"></property>
<property name="age" value="21"></property>
</bean>
<!--bean的后置处理器-->
<bean id="myBeanPostProcessor" class="com.atguigu.spring.process.MyBeanPostProcessor"></bean>
测试方法:
/**
* 1、实例化,调用无参构造
* 2、实例注入,调用set
* 3、后置处理的postProcessBeforeInitialization方法
* 4、初始化,需要通过bean的init-method属性指定初始化方法
* 使用bean
* 5、后置处理的postProcessAfterInitialization方法
* 6、IOC容器关闭时销毁,需要使用bean的destroy-method属性指定销毁方法
*
* bean的作用域是单例时,在创建IOC容器时,bean实例就会被创建(因为只有一个,可以被提前创建好)
* 作用域是多例时,只有在获取bean实例时才会被创建(因为需要创建多个,所以开始时,没必要创建实例)
* 如下代码,在调用ioc.getBean(User.class)时,即获取实例对象时才会执行。
* bean的后置处理器会在生命周期的初始化前后添加额外的操作,
* 需要实现BeanPostProcessor接口,
* 且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,
* 而是针对IOC容器中所有bean都会执行
*/
@Test
public void test(){
//ClassPathXmlApplicationContext扩展了刷新和销毁的子方法
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-lifecycle.xml");
User user = ioc.getBean(User.class);
/*
ApplicationContext中 无close函数 要用其子类 ConfigurableApplicationContext或ClassPathXmlApplicationContext(向下转型)★★★
*/
System.out.println("获取bean并使用:"+user);
ioc.close();
}
结果:
3.7 工厂bean
概念
FactoryBean是Spring提供的一种整合第三方框架的常用机制。
和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。
通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。
一句话:IOC容器会创建工厂bean
getObject
方法返回的实例类型,不会去创建工厂bean的实例。这样我们直接从ioc容器中获取工厂创建的实例对象
实现FactoryBean接口
接口中的三个方法:
- getObject():返回一个对象给IOC容器
- getObjectType():设置所提供对象的类型
- isSingleton():所提供的对象是否为单例
当把FactoryBean的实现类配置为bean时,会将当前类中的getObject方法返回的对象交给IOC容器管理
public class UserFactoryBean implements FactoryBean<User> {
@Override
public User getObject() throws Exception {
return new User();
}
@Override
public Class<?> getObjectType() {
return User.class;
}
}
public class FactoryTest {
@Test
public void test(){
// 在配置文件中只需要配置FactoryBean即可
// 当把FactoryBean的实现类配置为bean时,
// 真正交给IOC容器管理的对象,是FactoryBean工厂中getObject方法返回的对象
// 也就是说,省略了传统的工厂模式从工厂实例中获取产品的步骤,
// 而是直接把工厂的产品交给了ioc容器管理
// 另外,还可以设置是否为单例
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-factory.xml");
User user = ioc.getBean(User.class);//实际上是把UserFactoryBean中getObject()方法返回的对象交给IOC容器管理
//所以FactoryBean接口中,才有isSingleton()方法,设置是否单例,和bean标签中设置scope="singleton" 类似
System.out.println(user);
}
}
xml
<bean class="com.atguigu.spring.factory.UserFactoryBean"></bean>
3.8 基于XML的自动装配⭐
概念
根据指定的策略
,在IOC容器中匹配某个bean
,自动
为为bean中的类类型``属性
或者接口类型的属性
赋值
实现
可以通过bean标签的autowire属性设置自动装配的策略
自动装配的策略:
-
no,default:表示不装配,即bean中的属性不会自动匹配某个bean为某个属性赋值
-
byType
:根据赋值的属性的类型,在IOC容器中匹配某个bean为属性赋值异常情况:
- IOC中一个类型都匹配不上:属性就不会装配,使用默认值
- 有多个类型的bean,此时会抛出异常NoUniqueBeanDefinitionException
- 总结:当使用ByType实现自动装配时,IOC容器中有且仅有一个类型匹配的bean能够为属性赋值
-
byName
:将要赋值的属性的属性名作为bean的id在IOC容器中匹配某个bean,为属性赋值 -
总结:一般使用byType。特殊情况下:当类型匹配的bean有多个时,此时可以使用byName实现自动装配
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="com.zylai.spring.controller.UserController"
autowire="byType">
<!--<property name="userService" ref="userService"/>-->
</bean>
<bean id="userService"
class="com.zylai.spring.service.impl.UserServiceImpl"
autowire="byType">
<!--<property name="userDao" ref="userDao"/>-->
</bean>
<bean id="userDao" class="com.zylai.spring.dao.impl.UserDaoImpl"></bean>
</beans>
补充知识点:MVC模式 三层架构
1.MVC模式
MVC模式
MVC是Model-View-Controller(模型-视图-控制器)的简称,其主要作用是将视图展示和业务控制代码分离开来
Model(模型):
Model(模型):
指的就是数据或者数据的来源
他是数据模型,指的是获取到的数据,并且以及进行了封装。通常就是实体类对象。但是实体类对象的数据来源,是通过三层架构中的 业务层和持久层获取的(我们把他统称为业务模型)
View (视图):
指的就是可视化界面
他是用于展示数据的。是我们前面学过的jsp和html。既可以是jsp,也可以是html。在我们后面的学习中,他还可以是手机端展示
Controller(控制器):
控制器作用于模型和视图上,负责请求的调度,它使视图与模型分离开来
他是用来处理请求的,他把数据模型和视图进行分隔,从中调度。实现让逻辑控制,展示数据和封装数据模型互相独立。
他就是我们前面学过了Servlet,后面我们还会学Filter的,也可以作为控制器。但是目前就是Servlet。
2.三层架构
- 表示层(web层):与浏览器进行数据交互
- com.itheima.web
- 业务层(service层): 专门用于处理业务逻辑
- com.itheima.service
- 持久层(mapper层): 与数据库进行数据交换
- com.itheima.mapper
4 IOC-基于注解(Annotation)管理Bean
4.1 使用注解注册bean组件
@Component
:将类标识为普通组件@Controller
:将类标识为控制层组件@Service
:将类标识为业务层组件@Repository
:将类标识为持久层组件
这四个注解本质和功能上完全一样,后面三个相当于Component改了个名字,但是对于开发人员便于理解
注解实际上就是,将被注解的类作为组件进行管理(在IOC容器中,就有了加上注解的类所对应的Bean对象)
注意:
- 在service层和dao层,注解应该标识在接口的实现类上
- 加了注解的类在IOC容器中的默认id为类名的小驼峰
4.2 扫描组件
扫描包的时候,尽量精确一些,不然会降低效率
问题:
在SSM框架中,SpringMVC负责扫描控制层组件,Spring负责扫描其他组件
解决方法:
contxt:exclude-filter
排除扫描
,设置不扫描哪些
annotation
:根据注解类型
进行排除,expression中设置排除的注解的全类名
assignable
:根据类名
进行排除,expression中设置排除的类的全类名
<contxt:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<contxt:exclude-filter type="assignable" expression="com.zylai.spring.controller.UserController"/>
contxt:include-filter
包含扫描
,设置只扫描谁
注意
:需要在contxt:component-scan标签中设置属性use-default-filters="false"
- 为false时,设置的包下面所有的类都不需要扫描,此时可以使用包含扫描
- 为true时(默认的),设置的包下面所有的类都进行扫描,此时可以使用排除扫描
实例:
<!--开启组件扫描-->
<!-- <contxt:component-scan base-package="com.zylai.spring.controller,com.zylai.spring.service.impl,-->
<!--com.zylai.spring.dao.impl"/>-->
<!-- 这个包下面下的所有类都会扫描-->
<contxt:component-scan base-package="com.zylai.spring">
<!--<contxt:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>-->
<!--<contxt:exclude-filter type="assignable" expression="com.zylai.spring.controller.UserController"/>-->
<!--<contxt:include-filter type="assignable" expression="org.springframework.stereotype.Service"/>-->
</contxt:component-scan>
被注解的类对应Bean组件的id属性赋值
作用不大,因为一般获取Bean只需要通过类型,Bean类型
通过注解加扫描所配置的bean的id,默认值为类名的小驼峰,即类名的首字母为小写的结果,注意,接口应该是其实现类。可以通过标识组件注解的value属性设置bean的自定义的id
实例:
//默认型
@Controller
public void UserController(){
}
//则其Bean组件默认相当于 <bean id="userController" class="...UserController"></bean>
//自定义
@Controller("controller")
public void UserController(){
}
//则其Bean组件默认相当于 <bean id="controller" class="...UserController"></bean>
4.3 @Autowired自动装配
4.3.1 能够标识的位置
成员变量
上,此时不需要设置成员变量的set方法set方法
上- 为当前成员变量赋值的
有参构造器
上
4.3.2@Autowired原理
- @Autowired默认通过byType方式自动注入,在IOC容器中通过类型匹配某个bean为属性赋值
- 若有多个类型匹配的bean,此时会
自动转化
为byName的方式来实现自动装配的效果,即将要赋值的属性的属性名作为bean的id匹配某个bean为属性赋值 - 若byType和byName的方式都无法实现自动装配,即IOC容器中有多个类型匹配的bean,且这些bean的id和要复制的属性的属性名都不一致,此时抛异常。
- 此时可以在要赋值的属性上,添加一个注解
@Qualifier("value")
通过该注解的value属性值,指定某个bean的id,然后将这个bean为属性赋值
注意:若IOC容器中没有任何一个类型匹配bean,此时抛出异常:
NoSuchBeanDefinitionException
在@Autowired注解中有个
required属性
,默认值为true,要求必须完成自动装配可以将required设置为false,此时能装配则装配,无法装配则使用属性的默认值
4.4 例子
controller:
@Controller
public class UserController {
@Autowired//有属性required (required=ture)必须自动装配(默认) (required=false)不必须自动装配
private UserService userService;
public void saveUser(){
userService.saveUser();
}
}
service
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void saveUser() {
System.out.println("保存信息-->service");
userDao.saveUser();
}
}
dao
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("保存成功-->dao");
}
}
5 AOP-概念
5.1 代理模式
5.1.1 概念
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。
让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
5.1.2 静态代理
静态代理的思想就是代理对象和目标对象都实现同一个接口,然后在代理对象中调用目标对象的方法。
接口:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
目标对象:
public class CalculatorImpl implements Calculator{
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部,result:"+result);
return result;
}
}
代理对象:
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
System.out.println("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
}
可以看到,通过静态代理,达到了我们想要的目标。
缺点
:代理都写死了,不具备灵活性
。比如将来要给其他的类加上这个日志功能,那么还需要创建很多的代理类,产生大量重复的代码。
5.1.3 动态代理
比如说,将日志功能都集中到一个代理类中,将来有任何的日志需求,都通过这一个代理类实现。这里就用到了动态代理的技术。
使用JDK动态代理:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy(){
//使用JDK动态代理
//必须给出类加载器,根据类加载器来创建类(JVM知识)
ClassLoader classLoader = target.getClass().getClassLoader();
//ClassLoader loader:指定目标对象使用的类加载器
Class<?>[] interfaces = target.getClass().getInterfaces();
//Class<?>[] interfaces:获取目标对象实现的所有接口的class对象的数组
//InvocationHandler处理方法的执行
InvocationHandler h = new InvocationHandler() {
//InvocationHandler h :设置代理类中的抽象方法如何重写
//这里是设置代理类中的方法如何重写
//proxy:表示代理对象,method表示要执行的方法,args表示要执行的方法的参数列表
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object res = null;
try {
System.out.println("日志,方法:"+method.getName()+",参数:"+ Arrays.toString(args));
//调用目标对象的方法
res = method.invoke(target, args);
System.out.println("日志,方法:"+method.getName()+",结果:"+res);
return res;
} catch (Exception e) {
e.printStackTrace();
System.out.println("日志,方法:"+method.getName()+",异常:"+e);
} finally {
System.out.println("日志,方法:"+method.getName()+",方法执行完毕");
}
return res;
}
};
return Proxy.newProxyInstance(classLoader,interfaces,h);
}
}
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator)proxyFactory.getProxy();
proxy.add(1,2);
动态代理有两种
-
jdk动态代理
:要求必须有接口
,最终生成的代理类和目标类实现相同的接口在com.sun.proxy包下,$proxy2
-
cglib动态代理
:最终生成的代理类会继承目标类,并且和目标类在相同包下
5.2 AOP概念及相关术语
AOP
AOP(Aspect Oriented Programming)面向切面编程
,是一种设计思想,是面向对象编程的一种补充和完善。它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术
简单来说:就是把非核心业务抽取出来,给切面类管理。再把抽取出来的放到相应的位置。
横切关注点
从目标对象中抽取出来的非核心业务,比如之前代理模式中的日志功能,针对于计算器功能来说,日志就是非核心业务。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
通知
非核心的业务再目标对象中叫做横切关注点
,将横切关注点
抽取出来封装到切面类
中,他就是这个类中的一个方法叫做通知。
每一个横切关注点要做的事情都封装成一个方法,这样的方法就叫做通知方法。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
切面
封装横切关注点的类,通知的方法都写在切面类中
目标
被代理的目标对象,比如计算器的实现类
代理
代理对象
连接点
一个纯逻辑的概念:抽取横切关注点的位置,比如方法执行之前,方法捕获异常的时候等等。
连接点的作用:我们不但要抽取出来,还要套回去。
切入点
定位连接点的方式。
总结:
抽和套,抽取出来横切关注点,封装到切面中,就是一个通知。然后通过切入点找到连接点,把通知套到连接点的位置。
6 AOP-基于注解的AOP
6.1 技术说明
- 动态代理(InvocationHandler):JDK原生的实现方式,需要
被代理的目标类必须实现接口
。因为这个技术要求代理对象和目标对象实现同样的接口
(兄弟两个拜把子模式)。 - cglib:通过继承
被代理的目标类
(认干爹模式)实现代理,所以不需要目标类实现接口
。 - AspectJ:
本质上是静态代理
,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态
的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
6.2 配置
maven依赖
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
配置文件:
- 切面类和目标类都需要交给IOC容器管理
- 切面类必须通过@Aspect注解标识为一个切面
- 切面类必须通过@Aspect注解标识为一个切面
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns: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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--
AOP的注意事项:
切面类和目标类都需要交给IOC容器管理
切面类必须通过@Aspect注解标识为一个切面
在spring的配置文件中设置<aop:aspectj-autoproxy/>标签,开启基于注解的AOP功能
-->
<context:component-scan base-package="com.zylai.spring.aop.annotation"/>
<!--开启基于注解的AOP功能-->
<aop:aspectj-autoproxy/>
</beans>
6.3 切面类(重点)
-
在切面中,需要通过指定的注解将方法标识为通知方法
-
@Before:
前置通知
,在目标方法执行之前
执行 -
@After:
后置通知
,在目标对象方法的finally子句
中执行 -
@AfterReturning:
返回通知
,在目标对象方法返回值之后
执行 在返回通知中,若获取目标对象方法的返回值,只需要通过@AfterReturning注解的returning属性
就可以将
通知方法
的某个参数
指定为接收对象方法
的返回值的参数
@AfterReturning(value = "pointCut()",returning = "re") public void beforeAdviceMethod(JoinPoint joinPoint,Object re){ //获取连接点所对应方法的签名信息(方法的声明信息) Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,方法:"+signature.getName()+",返回值:"+ re ); //参数Object re与returning属性(名字相同),re就相当于方法的返回值 }
-
@AfterThrowing:
异常通知
,在目标对象方法的catch子句
中执行 在返回通知中,若获取目标对象方法的返回值,只需要通过@AfterThrowing注解的throwing属性
就可以将
通知方法
的某个参数
指定为接收对象方法
的出现异常的参数
@AfterThrowing(value = "pointCut()",throwing = "ex") public void beforeAdviceMethod(JoinPoint joinPoint,Throwable ex){ //获取连接点所对应方法的签名信息(方法的声明信息) Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,方法:"+signature.getName()+",异常:"+ ex ); //参数Throwable ex和throwing 属性(名字相同),ex就相当于获取异常 }
-
@Around("pointCut()") //环绕通知的方法返回值一定要和目标方法的返回值一致
之前几种的综合,
总结中有案例
顺序:
Spring5.3.x之前
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
Spring5.3.x之后
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
-
-
切入点表达式:设置在表示通知的注解的value属性中
- "execution(public int com.zylai.spring.aop.annotation.CalculatorImpl.add(int,int))"
第二种写法 "execution(*com.zylai.spring.aop.annotation.*.*(..))" 第一个*表示任意的访问修饰符和返回值类型 第二个*表示包下所有的类 第三个*表示类中任意的方法 ..表示任意的参数列表
@Component @Aspect public class LoggerAspect{ //@Before 前置通知:在目标对象方法执行之前 执行 @Before("execution(public int com.zylai.spring.aop.annotation.CalculatorImpl.add(int,int))") public void beforeAdviceMethod(){ System.out.println("LoggerAspect,前置通知"); } }
-
重用连接点表达式
声明一个公共的切入点表达式 @Pointcut("execution(* com.zylai.spring.aop.annotation.CalculatorImpl.*(..))") public void pointCut(){} 使用方式: @Before("pointCut()") @After("pointCut()")
-
重用连接点表达式
在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点所对应的方法信息 //获取连接点对应方法的签名信息 Signature signature = joinPoint.getSignature(); //获取连接点所对应的参数 Object[] args = joinPoint.getArgs();
@Component @Aspect public class LoggerAspect{ //@Before 前置通知:在目标对象方法执行之前 执行 @Before("execution(*com.zylai.spring.aop.annotation.*.*(..))") public void beforeAdviceMethod(JoinPoint joinPoint){ //获取连接点所对应方法的签名信息(方法的声明信息) Signature signature = joinPoint.getSignature(); //获取连接点所对应方法的参数 Object[] args = joinPoint.getArgs(); System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+Arrays.toString(args)); } }
-
切面的优先级
可以通过@Order注解的value属性设置优先级,
默认值为Integer.MAX
value值越小优先级越高比如:
@Order(1) public void A(){ } @Order(3) public void B(){ } @Order(2) public void C(){ } //通知顺序:先 A 再 C 再 B
总的:
* 1. 在切面中,需要通过指定的注解将方法标识为通知方法
*
* 2. 切入点表达式:设置在表示通知的注解的value属性中
* "execution(public int com.zylai.spring.aop.annotation.CalculatorImpl.add(int,int))"
* "execution(* com.zylai.spring.aop.annotation.*.*(..))"
* 第一个*表示任意的访问修饰符和返回值类型
* 第二个*表示包下所有的类
* 第三个*表示类中任意的方法
* ..表示任意的参数列表
*
* 3.重用连接点表达式
* //声明一个公共的切入点表达式
* @Pointcut("execution(* com.zylai.spring.aop.annotation.CalculatorImpl.*(..))")
* public void pointCut(){}
* 使用方式: @After("pointCut()")
*
*
* 4. 获取连接点信息
* 在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点所对应的方法信息
* //获取连接点对应方法的签名信息
* Signature signature = joinPoint.getSignature();
* //获取连接点所对应的参数
* Object[] args = joinPoint.getArgs();
*
* 5.切面的优先级
* 可以通过@Order注解的value属性设置优先级,默认值为Integer.MAX
* value值越小优先级越高
@Component
@Aspect//将当前组件表示为切面
public class LoggerAspect {
@Pointcut("execution(* com.zylai.spring.aop.annotation.CalculatorImpl.*(..))")
public void pointCut(){}
// @Before("execution(public int com.zylai.spring.aop.annotation.CalculatorImpl.add(int,int))")
//表示这个类下所有的方法,用*表示所有,参数列表用..表示所有的参数列表
// @Before("execution(* com.zylai.spring.aop.annotation.CalculatorImpl.*(..))")
@Before("pointCut()")
public void beforeAdviceMethod(JoinPoint joinPoint){
//获取连接点对应方法的签名信息(签名信息就是方法的声明信息)
Signature signature = joinPoint.getSignature();
//获取连接点所对应的参数
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect,前置通知,方法:"+signature.getName()+",参数:"+ Arrays.toString(args));
}
@After("pointCut()")
public void afterAdviceMethod(JoinPoint joinPoint){
//获取连接点对应方法的签名信息(签名信息就是方法的声明信息)
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,后置通知,方法:"+signature.getName()+",执行完毕");
}
//在返回通知中若要获取目标对象方法的返回值,只需要通过注解的returning属性值
//就可以将通知方法的某个参数指定为接收目标对象方法的返回值
@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result){
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,返回通知,方法:"+signature.getName()+",结果:"+result);
}
//在返回通知中若要获取目标对象方法的异常,只需要通过注解的throwing属性值
//就可以将通知方法的某个参数指定为接收目标对象方法出现的异常
@AfterThrowing(value = "pointCut()",throwing = "ex")
public void afterThrowingAdviceMethod(JoinPoint joinPoint,Throwable ex){
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,异常通知,方法:"+signature.getName()
+"异常:"+ex);
}
//要同时使用四种通知,即可以直接单独使用环绕通知(四种通知的综合,类似动态代理)
@Around("pointCut()")
//环绕通知的方法返回值一定要和目标方法的返回值一致
public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint){
Object result = null;
//joinPoint.proceed()代表了目标方法的执行
//joinPoint.proceed()异常建议使用try-catch
try {
System.out.println("环绕通知-->前置通知");
//表示目标对象方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知-->返回通知");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->异常通知");
}finally {
System.out.println("环绕通知-->后置通知");
}
return result;
}
}
7 声明式事务
在Spring中,提供了封装了jdbc的JdbcTemplate,在之后的事务模块中,我们使用JdbcTemplate执行SQL
7.1 JdbcTemplate
7.1.1 配置
maven
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个
jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.1</version>
</dependency>
配置文件:
<!--引入jdbc.properties,之后可以通过${key}的方式访问value-->
<!--这里的context标签,注意上面引入的-->
<!--在web项目中,有两种路径,一个是类路径,一个是web资源路径-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!--<property name="initialSize" value="${initialSize}"/>-->
<!--<property name="maxActive" value="${maxActive}"/>-->
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
7.1.2 使用(在Spring中进行测试)🎾
//指定当前测试类在spring的测试环境中执行,此时就可以通过注入的方法直接获取IOC容器的bean
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring测试环境的配置文件
@ContextConfiguration("classpath:spring-jdbc.xml")
public class JdbcTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testInsert(){
String sql = "insert into t_user values(null,?,?,?,?,?)";
int update = jdbcTemplate.update(sql, "root", "123", 23, "女", "123@163.com");
System.out.println(update);
}
@Test
public void testGetUserById(){
String sql = "select * from t_user where id = ?";
User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1);
System.out.println(user);
}
@Test
public void testGetAllUser(){
String sql = "select * from t_user";
List<User> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
System.out.println(list);
}
@Test
public void testGetCount(){
String sql = "select count(*) from t_user";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println(count);
}
}
7.2 声明式事务的概念
7.2.1 编程式事务
7.2.2 声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来的好处:
- 好处1:提高开发效率
- 好处2:消除了冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
编程式:自己写代码实现功能
声明式:通过配置让框架实现功能
7.3 基于注解的声明式事务
场景模拟
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
此时执行sql语句会抛出SQLException
加入事务
- 配置事务管理器,并且加上数据源属性
- 开启事务的注解驱动
<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 https://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.zhm.spring">
</context:component-scan>
<!-- 导入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 装配数据源 -->
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!--1.配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!--
2.开启事务的注解驱动
将使用@Transactional注解所标识的 方法 或 类中所有的方法 使用事务进行管理
@Transactional注解在哪(方法或类),连接点就在哪里
transaction-manager属性设置事务管理器的id
若事务管理器的id默认为transactionManager,则改属性可以不写
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
在需要进行事务操作的类或方法上加上注解即可
@Service
public class BookServiceImpl implements BookService {
@Autowired//自动装配
private BookDao bookDao;
@Override
@Transactional
public void buyBook(Integer userId, Integer bookId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId,price);
}
}
结果:如果更新图书的库存顺利执行,而更新用户余额执行失败,那么将会回滚事务,图书库存将会恢复。
7.4 事务的属性
1、只读
-
介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化
-
使用方式
@Transactional(readOnly = true) public void buyBook(Integer userId, Integer bookId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId,price); }
-
注意:
对增删改操作设置只读会抛出下面异常
:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
2、超时
-
介绍:
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。概括来说就是一句话:超时回滚,释放资源。
-
使用方式:
@Transactional(timeout = 3)//3S中 如果事务没有执行完 当前事务强制回滚 抛出异常 public void buyBook(Integer bookId, Integer userId) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); }
-
观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out:
deadline was Fri Jun 04 16:25:39 CST 2022
3、回滚策略
声明式事务默认对于运行时异常都进行回滚
,一般使用的是在此基础上加上不因为哪个异常而回滚
可以通过@Transactional中相关属性设置回滚策略
rollbackFor属性:需要设置一个Class类型的对象
rollbackForClassName属性:需要设置一个字符串类型的全类名
noRollbackFor属性:需要设置一个Class类型的对象
rollbackFor属性:需要设置一个字符串类型的全类名
@Transactional(noRollbackFor = ArithmeticException.class)//可以多个 { A.class,B.class,C.class}
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
System.out.println(1/0);
}
4、隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
- 读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。 - 读已提交:READ COMMITTED
Oracle
要求Transaction01只能读取Transaction02已提交的修改。 - 可重复读:REPEATABLE READ
mysql默认隔离级别
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。 - 串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
使用:
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
5、事务的传播
简单来说:
结账,买两本书。
如果以结账时间为事务,第二本买失败,第一本一会回滚。
如果用买书本身的操作,就是能买几本就是几本。
详细介绍:
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
@Transactional(propagation = Propagation.REQUIRED) //默认 使用调用者事务 一本书买不了,都买不了
@Transactional(propagation = Propagation.REQUIRES_NEW) //开启一个新的事务,即使用自己的事务 一本书买不了,就这本书不能买
7.5基于XML的声明式事务
<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 https://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.zhm.spring">
</context:component-scan>
<!-- 导入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 装配数据源 -->
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!--1.配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!--
2.配置事务通知
-->
<tx:advice id="tx" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="buyBook" /><!--连接点的buyBook方法 其中属性除name,还有 事务的只读、传播行为、回滚策略、隔离级别、超时时间-->
<tx:method name="*" /><!--连接点的所有方法-->
</tx:attributes>
</tx:advice>
<!--将当前的事务管理器,通过切入点表达式,定位到连接点-->
<aop:config>
<aop:advisor advice-ref="tx" pointcut="execution(*com.zhm.spring.service.imp1.*.*(..))">
</aop:config>
</beans>
注意缺少jar包,记得导入 spring-aspects依赖