spring
1,Spring简介
1.1Spring概述
Spring是最受欢迎的企业级Java应用程序开发架构,数以百万的来自世界各地开发人员使用spring框架来创建性能好、易于测试、可重用的代码。
Spring框架是一个开源的Java平台,它最初是由Rod Johnson编写的,并且于2003年6月首次在Apache2.0许可下发布
Spring是轻量级的框架,其基础版本只有2MB左右大小
Spring框架的核心特性是可以用于开发任何Java应用程序,但是在JavaEE平台上构建web应用程序是需要拓展的。Spring框架的目标是使J2EE开发变得更容易使用,通过启用基于POJO编程模型来促进良好的编程实践。
1.2Spring家族
1.3Spring Framework
Spring基础框架,可以视为Spring基础设备,基本上任何其他Spring项目都是以Spring Framework为基础的。
1.3.1Spring Framework特性
- 非侵入式:使用Spring Framework开发应用程序时,Spring对应用程序本身的结构影响非常小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会破坏原有结构,反而能将组件结构进一步简化。这就使得基于Spring Framework开发应用程序时结构清晰、简洁优雅。
- 控制反转 :IOC--Inversion of Control,翻转资源获取方向。把自己创建资源、向环境索取资源变成环境将资源准备好,我们享受资源注入。
- 面向切面编程:AOP--Aspect Oriented Programming,在不修改源代码的基础上增强代码能力。
- 容器:Spring IOC是一个容器,因为包含并且管理组件对象的生命周期。组件享受到了容器化的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率
- 组件化:Spring实现了使用简单的组件配置组合成一个复杂的应用。在Spring中可以使用XML和Java注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不(xu)的搭建超大复杂应用系统
- 声明式:很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现
- 一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且Spring旗下的项目已经覆盖了广泛领域,很多方面的功能需求都可以在Spring Framework的基础上全部使用Spring来实现
1.3.2Spring Framework五大功能模块
2,IOC
2.1IOC容器
2.1.1 IOC思想
IOC:Inversion of Control 翻译过来是反转控制
- -获取资源的传统方式
在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。
- -反转控制方式获取资源
反转控制的思想完全颠覆了应用程序组件获取资源的传统方式,反转了资源的获取方向--改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供资源的方式即可,极大降低了学习成本,提高了开发效率。这种行为也称为查找的被动形式
- -DI
DI是IOC的另一种表达方式:即组件以一些预先定义好的方式(例如:setter方法)接受来自于容器的资源注入。相当于IOC而言,这种表述更直接
结论:IOC就是一种反转控制的思想,而DI是对IOC的一种具体实现
2.1.2 IOC容器再Spring中的实现
Spring的IOC容器就是IOC思想的一个落地的产品实现。IOC容器中管理的组件也叫bean。在创建bean之前,首先需要创建IOC容器。Spring提供了IOC容器的两种实现方式:
- BeanFactory
这是IOC容器的基本实现,是Spring内部使用的接口。面向Spring本身,不提供给开发人员使用。
- ApplicationContext
BeanFactory的子接口,提供了更多高级特性。面向Spring的使用者,几乎所有场合都使用ApplicationContext而不是底层的BeanFactory。
- ApplicationContext的主要实现类
| 功能模块 | 功能介绍 |
|---|---|
| Core Container | 核心容器,在Spring环境下使用任何功能都必须基于IOC容器 |
| AOP & Aspects | 面向切面编程 |
| Testing | 提供了对junit或TestNG测试框架的整合 |
| Data Access/Integration | 提供了对数据访问/集成的功能 |
| Springmvc | 提供了面向web应用程序的集成功能 |
| 类型名 | 简介 |
|---|---|
| ClassPathXmlApplicationContext | 通过读取类路径下的xml格式的配置文件创建IOC容器对象 |
| FileSystemXmlApplicationContext | 通过文件系统路径读取XML格式的配置文件创建IOC容器对象 |
| ConfigurableApplicationContext | ApplicationContext的子接口,包含一些拓展refresh()和close(),让ApplicationContext具有启动、关闭和刷新上下文的能力 |
| WebApplicationContext | 专门为Web应用准备,基于web环境创建IOC容器对象,并将对象引入存入ServletContext域中 |
ctrl+h查看当前类或接口的一个继承实现关系
2.2基于xml管理bean
2.2.1实验一:入门案例
- 创建Maven Module
- 引入依赖
<dependencies>
<!--基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包-->
<dependency>
<groupId>org.sunyur.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>
先创建一个类 定义一个sayHello方法
再创建一个spring配置文件 选中resources - new - XML Configuration File - Spring Config->applicationContext.xml
<!--
bean:配置一个bean对象,将对象交给IOC容器管理
属性:
id:bean的唯一标识,不能重复
class:设置bean对象所对应的类型
-->
<bean id="helloworld" class="全类名路径"></bean>
对象交给IOC容器管理,验证下先获取IOC,再获取IOC中对象
测试
//获取IOC容器
ApplicationContext ioc = new ClassPathXmlApplicationContext("配置文件路径")
//resources与Java都会被加载到同一路径下,就是类路径下
//获取IOC容器中bean对象 根据bean的id获取bean对象
HelloWorld helloworld=(HelloWorld)ioc.getBean("bean的唯一标识id")//因为我们是通过id获取的,所以并不清楚他是什么类型,可以通过强转改变
helloworld.sayHello()
大部分通过反射创建实例化对象都是使用无参构造方法
删除无参构造运行会报错得到NoSuchMethodException
2.2.2获取bean的三种方式
-
根据bean的id获取 getBean(String name) 返回object
-
根据bean的类型获取 getBean(Class requiredType) 直接传入bean的类型 返回类型 较多使用 一个类型的bean只需要配置一次即可
注意:根据类型获取bean时,要求IOC容器中有且只有一个类型匹配的bean
-
根据bean的类型和id获取getBean(String name,Class requiredType)
第二种情况中applicationContext.xml内有两个或多个bean id的bean,但是类型相同
当使用 getBean(Class requiredType)调用的时候就会出现NoUniqueBeanDefinitionException
第二种情况中applicationContext.xml内一个bean都没有
当使用 getBean(Class requiredType)调用的时候就会出现NoSuchBeanDefinitionException
在applicationContext.xml中bean的参数scope设置单例或者多例
- 拓展
如果组件类实现了接口,根据接口类型可以获取bean吗?
可以,前提是bean是唯一
如果一个接口有多个实现类,这些实现类都配置了bean,根据接口类型可以获取bean吗?
不行,因为bean不唯一
- 结论
根据类型来获取bean时,在满足唯一性的前提下,其实只是看【对象instanceof指定的类型】的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到
即通过bean的类型,bean所继承的类的类型、bean所实现的接口都可以获取bean
2.2.3实验三:依赖注入之setter注入
- 创建学生类Student
public class Student{
private Integer id;
private String name;
private Integer age;
private String sex;
//加上 get/set toString
}
可以看出Student这个类依赖于属性 id name age sex 那么就可以在IOC容器中为它所依赖的属性进行赋值 为当前属性进行赋值就称为依赖注入
注入有set注入与构造器注入
- 设置bean时为属性赋值
<bean id="唯一标识" class="类的全类名路径">
<!--
property:通过成员变量的set方法进行赋值 看到property就是set注入形式
name:设置需要赋值的属性名(和set方法有关)
value:设置为属性所赋的值
-->
<property name="属性名" value="给予的值"></property> //鼠标悬停可以发现name的位置出现了set方法。。。
</bean>
2.2.4实验四:依赖注入之构造器注入
- 在Student类中添加有参构造 也得保证有无参构造方法
<bean id="唯一标识id" class="类的全类名路径">
<!--如果我们实体类中只有一个有参构造的话,可以直接按照参数的顺序去赋值就行了 constructor-->
<constructor-arg value=""></constructor-arg><!--如果这里只填写一个constructor-arg的话,就会去找只要一个参数的构造方法-->
</bean>
<!--如果实体类中有两个构造方法(仅最后一个参数不同),而applicationContext.xml文件中bean中constructor-arg都对应,执行测试会发现获取的是第二个构造器的内容赋值 如果想让获取到另一个构造方法可以使用constructor-arg中name指定-->
2.2.5 实验五:特殊值处理
- 字面量赋值
什么是字面量?
int a = 10;
声明一个变量a,初始化为10,此时a就不代表字母a了,而是作为一个变量的名字。当我们引用a的时候,我们实际上拿到的值是10
而如果a是带引号的:‘a’,那么它现在不是一个变量,它就是代表a这个字母本身,这就是字面量。所以字面量没有引申含义,就是我们看到的这个数据本身
<!--使用value属性给bean的属性赋值时,spring会把value属性的值看作字面量-->
<property name="name" value="张三">
- null值
<property name="name">
<null /> <!--这才是为name属性设置null值-->
</property>
<!--注意-->
<property name="name" value="null"></property> 为name所赋的值是字符串null
<:< >:>大小符号
- xml实体
<!--小号在sml文档中用来定义标签的开始,不能随便使用-->
<!--解决方案一:使用xml实体来代替-->
<property name="expression" value="a < b"/>
- CDATA节
<!--CDATA节其中的内容会原样解析<![CDATA[...内容值]]>-->
<property name="expression">
<!--解决方案二:使用CDATA节-->
<!--CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据-->
<!--xml解释器看到CDATA节就知道这里是纯文本,就不会当作xml标签或属性来解析-->
<!--所以CDATA节中写什么符号都很随意-->
<value>![CDATA[a<b]]</value>
</property>
2.2.6实验六:为类类型属性赋值
- 创建一个班级类Clazz
private Integer cid;
private String cname;
设置有参/无参构造方法 get/set方法 toString方法 对一对应对象 对多对应集合
表示学生所对应的班级,在学生类中加入班级类型的属性 Clazz clazz(不能使用value,value是给字面量初始赋值的)
给类类型赋值的话有三种方式
- 引入外部的bean
<bean id="唯一标识符id class="类的全类名路径">
<property name="属性名" value="字面量赋值"></property>//property要与构造方法匹配 进行set赋值
<property name="clazz" ref="引用clazz的bean id"></property>//ref引用IOC容器中的某个bean的id
</bean>
<bean id="唯一标识id" class="类的全类路径">//Clazz
<property name="" value=""></property>//为clazz中属性赋值set设置值
</bean>
- 级联的方式
<bean id="唯一标识符id class="类的全类名路径">
<property name="属性名" value="字面量赋值"></property>//property要与构造方法匹配 进行set赋值
<property name="clazz" ref="引用clazz的bean id"></property>//ref引用IOC容器中的某个bean的id
<!--级联方式,要保证提前为clazz属性赋值或者实例化-->
<property name="clazz.属性名" value="xxx"></property>//要对应类中的属性数量 这里级联相当于修改了下面赋值的值
</bean>
<bean id="唯一标识id" class="类的全类路径">//Clazz
<property name="" value=""></property>//为clazz中属性赋值set设置值
</bean>
- 内部bean
<bean id="唯一标识符id class="类的全类名路径">
<property name="属性名" value="字面量赋值"></property>//property要与构造方法匹配 进行set赋值
<property name="clazz">
<bean id="唯一标识id" class="类的全类名路径">
<property name="直接填写clazz的属性名不用带clazz" value></property>
</bean>
</property>
</bean>
测试尝试从内部bean能不能通过ioc容器获取 显然不行报 NoSuchBeanDefinitionException
No bean named 'clazz' available 没有一个叫clazz的bean可用
内部bean,只能在当前bean的内部使用,不能直接通过IOC容器获取
2.2.7实验七:为数组类型的属性赋值
在Student类中加入String[] hobby,在生成get/set 构造 toString方法
<property name="唯一标识">
<array>
<!--<value></value>如果数组中存储的类型是自变量类型那就使用value
<ref></ref>如果数组中存储的类型是类类型那就使用ref ref表示引用,引用我们ioc容器中某些bean,然后把它放入数组中
-->
<value>赋值</value>
</array>
</property>
2.2.8实验八:为list集合类型的属性赋值
在班级Clazz类中添加 List student 多对一对象 一对多集合
<bean id="唯一标识id" class="类的全类路径">//clazz类
<property name="" value=""></property>//为clazz类中属性赋值
<property name="students">
<list>
<ref bean="引用ioc容器中其他bean"></ref>
</list>
</property>
</bean>
- 配置一个集合类型的bean,需要使用util的约束
<util:list id="唯一标识"><ref bean="ioc中其他bean"></ref></util:list> //引入util list标签 直接用就可以在applicationContext.xml中使用
2.2.9实验九:为map集合类型的属性赋值
新建一个Teacher类 get/set toString 有参/无参构造
在学生类Student类中添加Map<Sring,Teacher> teacherMap; 添加get/set 重写toString
- 第一种方式
//先找到一个学生bean
<property name="属性名">
<map>
<entry key="10086" value-ref="唯一标识1(其他bean id)"></entry>
<entry key="10010" value-ref="唯一标识2(其他bean id)"></entry> key,value要根据map定义的类型决定
</map>
</property>
<bean id="唯一标识1" class="类的全类名路径">
<property name="属性名" value="赋值"></property> //根据类的属性数量 也就是构造方法中参数数量
</bean>
<bean id="唯一标识2" class="类的全类名路径">
<property name="属性名" value="赋值"></property> //根据类的属性数量 也就是构造方法中参数数量
</bean>
- 第二种方式
<util:map id="唯一标识符">
<entry key="" value-ref="(其他bean id)"></entry>
</util:map>
//然后找到具体的bean
<property name="属性名" ref="唯一标识符"></property>
2.2.10 实验十 依赖注入之P命名空间
<bean id="唯一标识" class="类的全类名路径" p:sid="1005" p:sname="小明" p:teacherMap-ref="map的唯一标识">//引入p
</bean>
测试可得没有赋值的属性可以为null
2.2.11 实验十一:引入外部属性文件
- 加入依赖
<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>
- 创建外部属性文件 spring-datasource.xml
<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/数据库名称?serverTImezone=UTC"></property>
<property name="username" value="xxx"></property>
<property name="password" value="xxx"></property>
</bean>
测试连接成功
再新建一个new Resource Bundle jdbc
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/数据库名称?serverTImezone=UTC
jdbc.username=xxx
jdbc.password=xxx
要将当前的properties文件引入到spring bean配置文件中
两种方式 其中一种被淘汰了
<!--引入jdbc.properties-->
<bean class="PropertyPlaceholderConfigurer"></bean> 这个bean是由ioc容器自己获取来实现相对应的功能
spring5中已经淘汰了
另一种更加简便的约束
<context:property-placeholder location="jdbc.properties"></context:property-placeholder>
之后就可以通过${key}的方式访问value
<bean id="datasource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
测试可以连接成功
2.2.12 实验十二:bean的作用域
- 在Spring中可以通过配置bean标签的scope属性来指定bean的作用范围,各取值含义参加下表
| 取值 | 含义 |
|---|---|
| singleton(默认) | 在IOC容器中,这个bean的对象始终为单实例 |
| prototype | 这个bean在IOC容器中有多个实例 |
如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用):
| 取值 | 含义 |
|---|---|
| request | 在一个请求范围内有效 |
| session | 在一个会话范围内有效 |
新建一个配置文件spring-scope.xml
<bean id="唯一标识符" class="类的全类名路径">
<property name="属性名" value="赋值"></property>
</bean>
进行测试
@Test
public void testScope(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-scope.xml");
Student s1 = ioc.getBean(Student.class);
Student s2 = ioc.getBean(Student.class);
//判断是否同一个对象 可以采用== equals 因为==和equals默认功能都是一样的 比较内存地址 一些特殊的类重写了equals的话 那就可能比较的不是内存地址 例如String类型 它比较的不是地址,比较的是内容 比较内存地址没有意义因为字符串的本质就是一个常量,常量存储在常量池中,每一次给一个字符串变量赋值 先在常量值中找一下 有的话就直接用 没有的话就开辟空间 来创建我们当前这个常量
假设s1=hello,s2=hello 有没有必要去比较s1和s2的内存地址 没有必要 因为所对应的hello一定是对应常量池里的同一个常量,所以string类的equals方法被进行了重写,比较的是内容
}
}
通过==进行比较 看内存地址是否一致 如果一致的话说明通过IOC容器所获取的当前bean所对应对象默认就是单例
测试发现true 默认情况就是单例
<bean id="唯一标识符" class="类的全类名路径" scope="singleton">
<property name="属性名" value="赋值"></property>
</bean>
scope是singleton测试结果为true 表示获取该bean所对应的对象都是同一个
然后测试scope是prototype 表示获取该bean所对应的对象都不是同一个
<bean id="唯一标识符" class="类的全类名路径" scope="prototype">
<property name="属性名" value="赋值"></property>
</bean>
2.2.13实验十三:bean的生命周期
-
具体的生命周期过程
- bean对象创建(调用无参构造器)
- 给bean对象设置属性
- bean对象初始化之前操作(由bean的后置处理器负责)
- bean对象初始化(需在配置bean时指定初始化方法)
- bean对象初始化之后操作(由bean的后置处理器负责)
- bean对象就绪可以使用
- bean对象销毁(需在配置bean时指定销毁方法)
- IOC容器关闭
-
创建类User
public class User{
private Integer id;
private String username;
private String password;
private Integer age;
//添加有参/无参构造方法 get/set toString方法
public User(){
System.out.println("生命周期1:实例化")
}
public void setId(Integer id){
System.out.println("生命周期2:依赖注入(为属性赋值)");
this.id=id;
}
//添加两个方法init destroy
public void initMethod(){
System.out.println("生命周期3:初始化")
}
public void destroyMethod(){
System.out.println("生命周期4:销毁")
}
}
- 创建spring-lifecycle.xml
//将user交给IOC容器管理
<bean id="user" class="xx.xx.xx.User" init-method="initMethod" destroy-method="destroyMethod">
<property name="id" value="10086"></property>
<property name="username" value="giao"></property>
<property name="password" value="jinitaimei"></property>
<property name="age" value="18"></property>
</bean>
ioc管理对象的时候通过工厂模式加上反射模式来创建对象,通过反射来创建对象时是通过无参构造
bean的初始化方法需要通过配置文件设置的init-method,destroy-method,它不像servlet中的init,destroy
@Test
public void test(){
ApplicationContext ioc = new ClassPathApplicationContext("Spring-lifecycle.xml");
User user = ioc.getBean(User.class);
System.out.println(user);
}
执行效果如下
生命周期1:实例化
生命周期2:依赖注入
生命周期3:初始化
User{id=10086,username='giao',password='jinitaimei',age=18}
//为什么没有出现销毁呢 销毁方法什么时候执行呢 当服务器关闭时才会执行销毁方法servlet 当ioc容器关闭时才会执行销毁方法 ApplicationContext中没有close关闭方法 ApplicationContext的子接口ConfigurableApplicationContext提供了刷新和关闭容器的方法 还有ClassPathXmlApplicationContext也可以 修改测试类
@Test
public void test(){
ConfigurableApplicationContext ioc = new ClassPathApplicationContext("Spring-lifecycle.xml");
User user = ioc.getBean(User.class); System.out.println(user);
ioc.close();}
执行效果如下
生命周期1:实例化
生命周期2:依赖注入
生命周期3:初始化
User{id=10086,username='giao',password='jinitaimei',age=18}
生命周期4:销毁
- 不同bean作用域对生命周期的影响和后置处理器
修改测试类
@Test
public void test(){
ConfigurableApplicationContext ioc = new ClassPathApplicationContext("Spring-lifecycle.xml");}
执行可以发现运行效果为
生命周期1:实例化
生命周期2:依赖注入
生命周期3:初始化
前三个方法执行了,换句话来说就是获取ioc容器时就执行的,并不是获取bean的时候执行的
ioc容器中配置的bean默认是单例 可以思考有没有必要在获取的时候创建 没有必要 在获取这个ioc容器时,直接把这个对象创建好,那么以后使用都是这同一个
如果将scope设置为prototype时测试结果为空 因为设置为多例 每一次通过bean获取的对象都是新的对象,那么就没有必要在ioc容器获取的时候创建对象,每一次获取都是一个新的对象
再次修改测试类
@Test
public void test(){
ConfigurableApplicationContext ioc = new ClassPathApplicationContext("Spring-lifecycle.xml");
User user = ioc.getBean(User.class); System.out.println(user);
ioc.close();}
测试结果如下
生命周期1:实例化
生命周期2:依赖注入
生命周期3:初始化
User{id=10086,username='giao',password='jinitaimei',age=18}
所以当把bean设置为多例时,它的销毁方法就不是ioc管理了
注意:
若bean的作用域为单例时,生命周期的前三个步骤会在获取IOC容器时执行
若bean的作用域为多例时,生命周期的前三个步骤在获取bean时执行
一整套步骤下来可以发现缺少bean的后置处理器
- bean的后置处理器
bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
创建bean的后置处理器
public class MyBeanProcessor implements BeanPostProcessor{
@Override
public Object postProcessBeforeInitalization(Object bean,String beanName)throws BeansException{ //方法返回值是object,它可以对我们当前创建bean做额外操作 Object bean 就是当前ioc容器所管理的bean 可以直接把它返回
//此方法在bean的生命周期初始化之前执行
System.out.println("MyBeanPostProcessor-->后置处理器postProcessBeforeInitialization");
return bean;//返回的是我们额外操作后的一个bean对象
}
@Override
public Object postProcessAfterInitialization(Object bean,String beanName)throws BeansException{
//此方法在bean的生命周期初始化之后执行
System.out.println("MyBeanPostProcessor-->后置处理器postProcessAfterInitialization");
return bean;
}
}
实现BeanPostProcessor(有两个抽象方法)没有报错 这说明它的两个抽象方法有默认方法体或者是被static修饰的 查看内部代码发现方法加上了default
要想重写也是可以的使用ctrl+o 选中需要重写的两个方法体
还必须配置在ioc容器中
<bean id="唯一标识" class="类的全类名路径">
使用单例测试
测试结果如下
生命周期1:实例化
生命周期2:依赖注入
MyBeanPostProcessor-->后置处理器postProcessBeforeInitialization
生命周期3:初始化
MyBeanPostProcessor-->后置处理器postProcessAfterInitialization
User{id=10086,username='giao',password='jinitaimei',age=18}
生命周期4:销毁
2.2.14实验十三:FactoryBean
- FactoryBean是spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,spring可以帮助我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们
将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的
普通的工厂如何管理 使用了ioc就要将工厂交给ioc管理,要想使用工厂功能,先通过获取ioc容器,再获取ioc容器中这个工厂,工厂bean所对应的对象 我们获得工厂后 通过工厂获取所创建的xxx对象
使用FactoryBean后可以省略上面一个过程 我们获取工厂这一过程 也就是说我们将FactoryBean配置到ioc容器中,虽然我们配置bean的类型是FactoryBean,但是当我们获取的时候,我们可以直接获取当前这个工厂所提供的对象
FactoryBean是一个接口,使用时要创建实现类来实现这个接口 泛型工厂所提供的对象
方法 getObject getObjectType isSingleton
那么把FactoryBean配置到ioc容器中,获取的是FactoryBean所提供的对象 ???啥意思呢
创建一个包 factory 再创建一个类UserFactoryBean
public class UserFactoryBean implements FactoryBean<User>{
//重写方法 三个方法只需要重写两个getObject getObjectType 那么isSingleton不是抽象方法,它一定有默认方法体 默认是单例
@Override
public User getObject() throws Exception(){
return new User();
}
@Override
public Class<?> getObjectType(){
return User.class;
}
}
再创建一个spring-factory.xml
<bean class="xx.UserFactoryBean">
</bean>
//当我们去加载配置文件去获取ioc容器时 其实是将UserFactoryBean里的getObject方法所返回的对象交给了IOC容器
测试
@Test
public void testFactoryBean(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-factory.xml");
User user = ioc.getBean(User.class);
System.out.println(user);
}
结果如下
生命周期1:实例化
User{id=null,username='null',password='null',age=null}
2.2.15实验十五:基于xml的自动装配
自动装配:根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口属性赋值
创建三层架构 mvc
public class UserController{
private UserService userService;
//生成get、set方法
public void saveUser(){
userService.saveUser();
}
}
public class UserServiceImpl implements UserService{
private UserDao userDao;
//设置get/set方法
//ctrl+o方法重写
@Override
public void saveUser(){
userDao.saveUser();
}
}
public interface UserService{
void saveUser();
}
public interface UserDao{
void saveUser();
}
public class UserDaoImpl implement UserDao{
@Override
public void saveUser(){
System.out.println("保存成功");
}
}
创建配置文件spring-autowire-xml.xml
<bean id="userController" class="xx.UserController">
<property name="userService" ref="userService"></property>
</bean>
<bean id="userService" class="xx.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="xx.UserDaoImpl"></bean>
编写测试类
@Test
public void testAutowire(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-autowire-xml.xml");
UserController userController = ioc.getBean(UserController.class);
userController.saveUser();
}
执行效果
保存成功
自动装配:根据指定的策略,在IOC容器中匹配某个bean,自动为bean中的类类型的属性或接口类型的属性赋值
如果将配置文件中property都注释掉 那么就会出现NullPointerException
在bean标签中有一个标签叫autowire-default -no -byType -byName -constructor
1,default与no效果一致 表示不装配 即bean中属性不会自动匹配某个bean为属性赋值,此时属性值采用默认值
2,byType:根据要赋值的属性的类型,在ioc容器中匹配某个bean 为属性赋值
注意
a>若通过类型没有找到任何一个类型匹配的bean,此时不装配,属性使用默认值
b>若通过类型找到了多个类型匹配的bean,此时抛出异常:NoUniqueBeanDefinitionException
总结:当使用byType实现自动装配时,IOC容器中有且只有一个类型匹配的bean能够为属性赋值
3,byName:将要赋值的属性的属性名作为bean的id在IOC容器中匹配某个bean,为属性赋值
当配置文件中含有两个bean的唯一标识相同时出现 报错already userd in this element
一般使用byType只有byType不行的情况才用byName byType不行根据类型匹配了多个类型的bean
以后我们在配置文件中同一个类型的bean也不会有多个
总结:当类型匹配的bean有多个时,此时可以使用byName实现自动装配
2.3基于注解管理bean
2.3.1实验一:标记与扫描
- 注解:和xml配置文件一样,注解本身不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作
本质上:所有一切操作都是Java代码来完成的,xml和注解只是告诉框架中的Java代码如何执行
- 扫描:spring为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。
重新创建一个工程引入依赖
<dependencies>
<!--基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包-->
<dependency>
<groupId>org.sunyur.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配置文件
- 标识组件的常用注解
@Component:将类标识为普通组件
@Controller:将类标识为控制层组件
@Service:将类标识为业务层组件
@Repository:将类标识为持久层组件
以上四个注解有什么关系和区别?
@Controller @Service @Repository 都是由@Component拓展
对于spring使用IOC容器管理这些组件来说没有区别。所以@Controller @Service @Repository 这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用
注意:虽然它们本质上一样,但是为了代码的可读性,为了程序构造严谨我们肯定不能随便胡乱标记
在xxController类加上@Controller 在@xxServiceImpl实现类加上@Service 在xxDaoImpl实现类加上@Repository
开启扫描在配置文件spring-ioc-annotation.xml
<context:component-scan base-package="xx.Controller,xx.Service.Impl,xx.Dao.Impl"></context:component-scan>
<context:component-scan base-package="直接扫描最大的包"></context:component-scan>
测试
@Test
public void test(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc-annotation.xml");
UserController uc = ioc.getBean(UserController.class);
System.out.println(uc);
UserService us = ioc.getBean(UserService.class);
System.out.println(us);
UserDao ud = ioc.getBean(UserDao.class);
System.out.println(ud);
}
测试完成发现可以获取到
整合ssm,springmvc需要扫描controller控制层,spring则只需要扫描除controller控制层以外的其他层
在spring-ioc-annotation.xml配置文件中
<context:component-scan base-package="直接扫描最大的包"> 默认情况下把所有包都扫描
<context:exclude-filter type="annotation" expression="需要排除的类的全类名路径"/>
<context:exclude-filter type="assignable" expression="也是全类类名路径,类型(xxx.某一个类的名称)"/>
<context:include-filter type="annotation" expression="需要排除的类的全类名路径"/>//只扫描到我们的控制层 要想实现这个效果,必须前提所有包都不扫描 也就是在context:component-scan标签中开启 use-default-filters="false"(也就是所有包都不扫描)
</context:component-scan>
context:exclude-filter 中type属性 -annotation(通过注解) -assignable(通过类)
- @Autowired:实现自动装配功能的注解
- 标识在成员变量上,此时不需要设置成员变量的set方法
- 标识在set方法上
- 标识在为当前成员变量赋值的有参构造上
- @Autowired注解的原理
- 默认通过byType的方式,在IOC容器中通过类型匹配某个bean为属性赋值
- 若有多个类型匹配的bean,此时会自动转换为byName的方式实现自动装配的效果 即将要赋值的属性的属性名作为bean的id匹配某个bean为属性赋值
- 若byType和byName的方式都无法实现自动装配,即IOC容器中有多个类型匹配的bean且这些bean的id和要赋值的属性的属性名都不一致,此时抛异常:NoUniqueBeanDefinitionException
- 此时可以在要赋值的属性上,添加一个注解@Qualifier,通过该注解的value属性值,指定某个bean的id,将这个bean为属性赋值
注意:若IOC容器中没有任何一个类型匹配的bean,此时抛出异常:NoSunchBeanDefinitionException ,在@Autowired注解中有个属性required,默认是true,要求必须完成自动装配 可以将required设置为false,此时能自动装配则装配,无法装配则使用属性的默认值
3,Aop
3.1场景模拟
3.1.1声明接口
声明计算器接口Calculator,包含加减乘除的抽象方法
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){
System.out.println("日志,方法:add,参数:"+i+","+j);
int result = i+j;
System.out.println("方法内部,result:"+result);
System.out.println("日志,方法:add,结果:"+result);
return result;
}
//这样的实现方式会发现核心代码与其他的代码(非核心代码)都展示出来了,没有特别好的方法进行优化,将代码分割出来
}
3.1.2创建实现类
public class CalculatorPureImpl implements Calculator{
@Override
public int add(int i,int j){
int result = i+j;
System.out.println("方法内部result="+result);
return result;
}
//剩下三个一样形式
}
3.1.3创建带日志功能的实现类
public class CalculatorLogImpl implements Calculator{
@Override
public int add(int i,int j){
System.out.println("[日志]add方法开始了,参数是:"+i+","+j);
int result = i+j;
System.out.println("方法内部result="+result);
System.out.println("[日志]add方法结束了,结果是:"+result);
return result;
}
//其余三个如上格式
}
3.1.4提出问题
-
现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
- 对核心业务功能有干扰,导致程序员在开发核心业务时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
-
解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来
-
困难
解决问题的困难,要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新技术
3.2代理模式
3.2.1概念
- 介绍
二十三种设计模式中的一种,属于结构性模型。它的作用就是通过提供了一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来--解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法
- 目标:将代理”套用“了非核心逻辑代码的类、对象、方法
3.2.2静态代理
创建静态代理类:
public class CalculatorStaticProxy implements Calculator{
//将被代理的目标对象声明成成员变量
private CalculatorImpl target;
public CalculatorStaticProxy(CalculatorImpl 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;
}
//其他三个方法如同以上形式
}
测试
public class ProxyTest{
@Test
public void testProxy(){
CalculatorStaticProxy proxy = new CalculatorStaticProxy(new CalculatorImpl());
proxy.add(1,2);
}
}
执行效果
日志,方法:add,参数:1,2
方法内部,result:3
日志,方法:add,结果:3
出现异常的情况,要加入什么操作 或者在finally中要加入什么操作 都可以通过我们这个代理模式来实现
例如添加try-catch-finally 差不多对应着通知:前置通知后置通知返回通知异常通知
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性,就拿日志来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
3.2.3动态代理
上层方法调用目标方法
生产代理对象的工厂类:
public class ProxyFactory{
private Object target;
public Object(Object target){
this.target = target;
}
public Object getProxy(){
//通过反射来实现动态代理 就是通过这个方法创建动态代理类动态代理实例
//ClassLoader loader:指定加载动态生成的代理类的类加载器
//Class<?>[] interfaces:获取目标对象实现的所有接口的class对象的数组
//InvocationHandler h:设置代理类中的抽象方法如何重写
ClassLoader classLoader = this.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler h = new InvocationHandler(){
@Overrride
public Object invoke(Object proxy,Method method,Object[] args)throws Throwable{
System.out.println("日志,方法:"+method.getName()+",参数:"+Arrays.toString(args));
//proxy表示代理对象 method表示要执行的方法 args表示要执行的方法到的参数列表
Object result = method.invoke(target,args);
System.out.println("日志,方法:"+method.getName()+",结果:"+result);
return result;
}
};
return Proxy.newProxyInstance(classLoader,interfaces,h);
}
}
我们可以通过动态代理工厂类创建我们的动态代理类
@Test
public void testProxy(){
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator)proxyFactory.getProxy();//不知道它什么类型 知道它实现的接口 通过向上转型获取所对应的接口类型对象
proxy.add(1,2)
}
测试
日志,方法:add,参数:[1,2]
方法内部,result:3
日志,方法:add,结果:3
动态代理为啥叫动态:动态指的是他能帮助我们,任意一个目标类动态生成它所对应的代理类
可以在ProxyFactory中可以对方法执行前后加通知,还有异常通知,最终通知等
动态代理有两种:
1,jdk动态代理,要求必须有接口,最终生成的代理类和目标类实现相同的接口,在com.sun.proxy包下,类名为$proxy-x -x代表数字
2,cglib动态代理,最终生成的代理类会继承目标类,并且和目标类在相同的包下
3.3AOP
3.3.1概述
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期间
动态代理实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。
3.3.2相关术语
- 横切关注点
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需求:有十个附加功能,就有十个横切关注点
- 通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:在被代理目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
- 切面
封装通知方法的类
- 目标
被代理的目标对象
- 代理
向目标对象应用通知之后创建的代理对象
- 连接点
这也是一个纯逻辑概念,不是语法定义的
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点
- 切入点
定位连接点的方式
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事务(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询纪录的SQL语句。
spring的AOP技术可以通过切入点定位到特定的连接点
切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件
目标对象与代理对象不需要我们自己创建,从我们目标对象中把非核心业务代码(横切关注点)抽取出来,不仅要将代码抽取出来还要套在当前目标对象上,所以说从哪里抽取出来就要把它套到那,如果是目标执行之前抽取出来的,我们就要把他套到目标执行之前,如果你是目标执行之后就要把它套到你抽取的横切关注点的位置,那么这个位置就叫连接点,连接点就是一个位置一个概念,现在从代码层面应该如何定位这个连接点呢,要使用到切入点。
所以说AOP主要干什么抽和套,抽横切关注点封装到切面中它就是一个通知,再通过切入点定位到连接点,就可以做到不改变目标对象代码同时,然后把我们切面中这些通知,通过切入点表达式套到我们的连接点上,来实现功能的增强
3.3.3作用
- 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性
- 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法被切面给增强了。
3.4基于注解的AOP
3.4.1技术说明
- 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
- cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口
- AspctJ:本质上是静态代理,将代理逻辑”织入“被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。spring只是借用了AspecJ中的注解。
测试前置通知案例
创建aop-annotation.xml
<context:component-scan base-packge=xxx.xxx.xxx(最大的包)></context:component-scan>
切面
在切面中,需要通过指定的注解将方法标识为通知方法
@Before:前置通知,在目标对象方法执行之前执行
切入点表达式:设置在表示通知的注解的value属性中
@Before("execution(public int xxx.xxx.xxx.方法名(参数))")
@Before("execution(* xxx.xxx.xxx.代理对象.*(..))") 第一个*标识任意的访问修饰符和返回值类型 第二个*表示类中任意的方法 ,,表示任意的参数列表
@Before("execution(* xxx.xxx.xxx.*.*(..))")
类的地方也可以使用*,表示包下所有的类
@Component
@Aspect
public class LoggerAspect(){
@Before("execution(public int xxx.xxx.xxx.方法名(参数))")
public void beforeAdviceMethod(){
System.out.println("LoggerAspect,前置通知");
}
}
@Test
public void testAOPByAnnotation(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
CalculatorImpl calculator = ioc.getBean(CalculatorImpl.class);
calculator.add(1,1);
}
报错:NoSuchBeanDefinitionException:No qualifying bean of type xxx.xxx.CalculatorImpl available
我们通过ioc获取某个bean的时候,可以使用它所继承的父类,或者所实现的接口都能获取到,接着再来测试
@Test
public void testAOPByAnnotation(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.add(1,2);
}
LoggerAspect,前置通知
方法内部,result:3
以上前置通知被输出了,那么就说明当前切面有作用了,切面中的通知确实是通过切入点表达式作用到连接点
以及无法通过ioc获取目标对象,只能从ioc中获取它的代理对象
如果要将前置通知加在CalculatorImpl每一个方法上,该如何去做?
总不能重新创建四个前置通知把方法名都改了 这样来写还不如使用动态代理简单
可以从切入点表达式入手
@Before("execution(* xxx.xxx.xxx.代理对象.*(..))")
@Before("execution(* xxx.xxx.xxx.*.*(..))")
修改切面类LoggerAspect
public void beforeAdviceMethod(JoinPoint joinPoint){ joinPoint帮助获取连接点信息的 或者是切入点表达式定位的是哪个方法 那么我们joinPoint表示的就是那个方法
//获取连接点所对应方法的签名信息 签名信息也就是方法的声明信息
Signaturn signaturn = joinPoint.getSignature();
//获取连接点所对应方法的参数
Object[] args = joinPoint.getArgs();
signature.getName()获取连接点所对应方法的方法名
切入点表达式的重用 声明一个公共的切入点表达式
@Pointcut("execution(* xxx.xxx.代理对象).*(..)")
public void pointCut(){}
引用公共切入点表达式
@Before("pointCut()")
}
@After:后置通知,在目标对象方法得finally字句中执行
@After("pointCut()") //其中参数returning:接受目标对象方法返回的一个参数的参数名的 result
public void afterAdviceMethod(JoinPoint joinPoint){//与这里面传递 Object result
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignaturn();
System.out.println("LoggerAspect,方法:"+signture.getName()+",执行完毕")
}
测试哪里调用div除 且参数为 1/0
测试结果
LoggerAspect,方法:div,参数:[1,0]
LoggerAspect,方法:div,执行完毕
Java.lang.ArithmeticException:/ by zero
@AfterReturning:返回通知,在目标对象方法返回值之后执行,有异常怎么可能会有返回值呢 返回通知是在返回通知之后执行的
将测试方法中参数改为 div(1,1)
测试结果如下
LoggerAspect,方法:div,参数:[1,1]
方法内部,result:1
LoggerAspect,返回通知
LoggerAspect,方法:div,执行完毕
在返回通知中若要获取目标对象方法的返回值,只需要通过@AfterReturning注解的returning属性就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数
@AfterThrowing:异常通知,在目标对象方法的catch字句中执行
@AfterThrowing("pointCut()") 有参数throwing
public void afterThrowingAdviceMethod(JoinPoint joinPoint){ Throwable
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",异常通知");
}
这个异常通知只有发生异常的时候才能看到
假设10/0测试结果如下
LoggerAspect,方法:div,参数:[10,0]
LoggerAspect,方法:div,异常通知
LoggerAspect,方法:div,执行完毕
给@AfterThrowing("pointCut()",throwing="ex")
public void afterThrowAdviceMethod(JoinPoint joinPoint,Throwable ex){
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",异常"+ex);
}
LoggerAspect,方法:div,参数:[10,0]
LoggerAspect,方法:div,异常:java.lang.ArithmeticException:/by zero
LoggerAspect,方法:div,执行完毕
在返回通知中若要获取目标对象方法的返回值,只需要通过@AfterThrowing注解的throwing属性就可以将通知方法的某个参数指定为接收目标对象方法的异常的参数
执行顺序:前置通知 目标对象方法的执行有异常就会异常通知 没有异常发生的是返回通知 最后是后置通知 (跟版本关系有关)
@Around:环绕通知
@Around("pointCut")
public void aroundAdviceMethod(ProceedingJoinPoint joinPoint){
joinPoint.proceed();//表示目标对象方法的执行 有异常 建议使用try-catch 根据这个环绕通知执行目标对象方法,就可以在它执行过程中加一些操作
//相当于编写动态代理中result=method.invoke(target,args);
}
改造代码如下
@Around("pointCut()")
//环绕通知的方法一定要和目标对象方法的返回值一致
public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint){
Object result = null;
try{
System.out.println("环绕通知-->前置通知")
//表示目标对象方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知-->返回通知")
}catch(Throwable throwable){
throwable.printStackTrace();
System.out.println("环绕通知-->异常通知")
}finally{
System.out.println("环绕通知-->后置通知")
}
return result;
}
进行测试
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.div(10,1);
测试结果如下:
环绕通知-->前置通知
LoggerAspect,方法:div,参数:[10,1]
方法内部,result:10
LoggerAspect,方法:div,结果:10
LoggerAspect,方法:div,执行完毕
环绕通知-->返回通知
环绕通知-->后置通知
重新创建一个ValidateAspect类验证功能类
@Component
@Aspect
public class ValidateAspect{
//@Before("exection(* xxx.xxx.CalculatorImpl.*(..))")//切入点表达式 切入点表达式可以引用别的类定义好的
@Before("xxx.xxx.xxx.LoggerAspect.pointCut()")
public void beforeMethod(){
System.out.println("ValidateAspect-->前置通知");
}
}
执行效果如下
环绕通知-->前置通知
LoggerAspect,方法:div,参数:[10,1]
ValidateAspect-->前置通知 会发现执行顺序先传入参数再进行校验,搞反了
方法内部,result:10
LoggerAspect,方法:div,结果:10
LoggerAspect,方法:div,执行完毕
环绕通知-->返回通知
环绕通知-->后置通知
解决方法+一个注解@Order 切面的优先级:可以通过@Order注解的value注解设置优先级,默认值Integer的最大值 @Order注解的value属性值越小,优先级越高。
Order的value属性给定数值越小优先级就越高
在验证类上添加注解@Order注解给定一个值1 @Order(1)
运行结果
ValidateAspect-->前置通知
环绕通知-->前置通知
LoggerAspect,方法:div,参数:[10,1]
方法内部,result:10
LoggerAspect,方法:div,结果:10
LoggerAspect,方法:div,执行完毕
环绕通知-->返回通知
环绕通知-->后置通知
将所有类中关于AOP注解相关的都删除,再新建一个aop-xml.xml
<!--扫描组件-->
<context:component-scan base-package="导包,导入最大范围的包"></context:component-scan>
<!--<aop:aspectj-autoproxy></aop:aspectj-autoproxy>-->
<aop:config><!--参数aspect:把组件设置为切面 advisor:设置当前通知 pointcut:设置切入点表达式-->
<!--设置一个公共的切入点表达式-->
<aop:pointcut id="pointCut" expression="execution(* xxx.xxx.CalculatorImpl.*(..))"/>
<!--将IOC容器中某个bean设置为切面-->
<aop:aspect ref="loggerAspect">
<aop:before method="beforeAdviceMethod" pointcut-ref="pointCut"></aop:before>
<aop:after method="afterAdviceMethod" pointcut-ref="pointCut"></aop:after>
<aop:after-returning method="afterReturningAdviceMethod" returning="result" pointcut-ref="pointCut"></aop:after-returning>
<aop:after-throwing method="afterThrowAdviceMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing>
<aop:around method="aroundAdviceMethod" pointcut-ref="pointCut"></aop:around>
</aop:aspect>
</aop:config>
创建测试方法
@Test
public void testAOPByXml(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-xml.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.add(1,1);
}
测试结果
LoggerAspect,方法:add,参数:[1,1]
环绕通知-->前置通知
方法内部,result:2
环绕通知-->返回通知
环绕通知-->后置通知
LoggerAspect,方法:add,结果:2
LoggerAspect,方法:add,执行完毕
在aop-xml.xml中添加校验切面
<aop:aspect ref="validateAspect">
<aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
</aop:aspect>
LoggerAspect,方法:add,参数:[1,1]
环绕通知-->前置通知
ValidateAspect-->前置通知
方法内部,result:2
环绕通知-->返回通知
环绕通知-->后置通知
LoggerAspect,方法:add,结果:2
LoggerAspect,方法:add,执行完毕
设置优先级 在环绕通知中添加order,值越小优先级越大 给它设置order=1
ValidateAspect-->前置通知
LoggerAspect,方法:add,参数:[1,1]
环绕通知-->前置通知
方法内部,result:2
环绕通知-->返回通知
环绕通知-->后置通知
LoggerAspect,方法:add,结果:2
LoggerAspect,方法:add,执行完毕
学习了aop后就是要完成声明式事务
4声明式事务
4.1JdbcTem
4.1.1简介
spring框架对JDBC进行封装,使用jdbcTemplate方便实现对数据库操作
4.1.2准备工作
- 加入依赖
spring-context 实现ioc
spring-orm 对象关系映射 持久化
spring-test spring测试
junit
mysql-connector-java
druid
创建jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/数据库名称?serverTImezone=UTC
jdbc.username=xxx
jdbc.password=xxx
创建文件spring-jdbc.xml
<!--引入jdbc.properties-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<bean class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<property name="dataSource"></property>
</bean>
测试功能
//指定当前测试类在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,?,?,?,?,?)";
jdbcTemplate.update(sql,"root","123",23,"女","123@qq.com");
}
@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> user = jdbcTemplate.query(sql,new BeanPropertyRowMapper<>(User.class));
list.forEach(System.out::println);
}
@Test
public void testGetCount(){
String sql = "select count(*) from t_user";
Integer count = jdbcTemplate.queryForObject(sql,Integer.class);
System.out.println(count);
}
}
创建实体类用来接收查询数据
private Integer id;
private String username;
private String password;
private Integer age;
private String gender;
private String email;
//创建有参构造 无参构造 设置toString方法 还有set/get方法
4.2.1编程式事务
事务功能的相关操作全部通过自己编写代码来实现
Connection conn = ...
try{
//开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
//核心操作
//提交事务
conn.commit();
}catch(Exception e){
//回滚事务
conn.rollBack();
}finally{
//释放数据库连接
conn.close();
}
编程式的实现方式存在缺陷:
- 细节没有被屏蔽,具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
4.2.2声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作
- 好处1:提高开发效率
- 好处2:消除冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
所以我们可以总结下面两个概念:
- 编程式:自己写代码实现功能
- 通过配置让框架实现功能
4.3基于注解的声明式事务
4.3.1准备工作
对于一些部分事务的回滚之类的解决方法可以从数据库与Java入手
例如:用户下单,库存数量不能为负数(可以在数据库层面加上unsigned,设置无符号,int取值可以有负数,添加这个关键字就能避免符号了)
先配置事务管理器
在开启事务的注解驱动(@Transactional注解所标识的方法或类中所有方法使用事务进行管理)
transaction-manager属性设置事务管理器的id
若事务管理器的bean的id默认为transactionManager,则该属性可不写
声明式事务的配置步骤:
1在spring的配置文件中配置事务管理器
2开启事务的注解驱动
在需要被事务管理的方法上,添加@transactional注解,该方法就会被事务管理
@transactional注解标识的位置:
1可以标识在方法上
2标识在类上,则类中所有的方法都会被事务管理
4.3.2事务属性:只读
- 对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化
- 使用方式
@Transactional(readOnly=true)
public void buyBook(Integer bookId,Integer userId){
//方法
}
- 注意
对增删改查操作设置只读会抛出下面异常:
Caused by:java.sql.SQLException:Connection is read-only.Queries leading to data modification are not allowed
4.3.3 事务属性:超时
- 介绍
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或者MySQL程序或网络连接等)
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行(超时回滚,释放资源)
TimeUnit 可以使用这个延迟
4.3.4事务属性:回滚策略
介绍:
声明式事务默认只针对运行时异常回滚,编译时异常不回滚
可以通过@Transational中相关属性设置回滚策略
- rollbackFor属性:需要设置一个class类型的对象
- rollbackForClassName属性:需要设置一个字符串类型的全类名
- noRollbackFor属性:需要设置一个Class类型的对象
- rollbackFor属性:需要设置一个字符串类型的全类名
使用方式
System.out.println(1/0);
观察结果
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行
4.3.5事务属性:事务隔离级别
介绍:
数据库系统必须具有隔离并发运行各个事务的能力,使他们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱
隔离级别一共有四种:
- 读未提交:READ UNCOMMITTED
允许transactional01读取transactional02未提交得修改
- 读已提交:READ COMMITTED
要求:transactional01只能读取transactional02已提交得修改
- 可重复读:REPEATABLE READ
确保transactional01可以多次从一个字段中读取到相同的值,即transactional01执行期间禁止其他事务对这个字段进行更新
- 可串行化:SERIALIZABLE
确保transactional01可以多次从一个表中读取到相同的行,在transactional执行期间,禁止其他事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但是效率十分低下。
各个隔离级别解决并发问题的能力见下表:
各种数据库产品对事务隔离级别的支持程度:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 有 | 有 | 有 |
| READ COMMITTED | 无 | 有 | 有 |
| REPEATABLE READ | 无 | 无 | 有 |
| SERIALIZABLE | 无 | 无 | 无 |
4.3.6事务属性:事务传播行为
- 介绍
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可以开启一个新事务,并在自己的事务中运行
propagation = Propagation.REQUIRED 默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。
propagation = Propagation.REQUIRES_NEW,表示不管当前线程上是否已经开启的事务,都要开启新事务
基于xml实现的声明式事务,必须引入aspectJ的依赖
spring-aspects
我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行
4.3.5事务属性:事务隔离级别
介绍:
数据库系统必须具有隔离并发运行各个事务的能力,使他们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱
隔离级别一共有四种:
- 读未提交:READ UNCOMMITTED
允许transactional01读取transactional02未提交得修改
- 读已提交:READ COMMITTED
要求:transactional01只能读取transactional02已提交得修改
- 可重复读:REPEATABLE READ
确保transactional01可以多次从一个字段中读取到相同的值,即transactional01执行期间禁止其他事务对这个字段进行更新
- 可串行化:SERIALIZABLE
确保transactional01可以多次从一个表中读取到相同的行,在transactional执行期间,禁止其他事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但是效率十分低下。
各个隔离级别解决并发问题的能力见下表:
| 隔离级别 | Oracle | MySQL |
|---|---|---|
| READ UNCOMMITTED | 错 | 对 |
| READ COMMITTED | 对(默认) | 对 |
| REPEATABLE READ | 错 | 对(默认) |
| SERIALIZABLE | 对 | 对 |
各种数据库产品对事务隔离级别的支持程度:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 有 | 有 | 有 |
| READ COMMITTED | 无 | 有 | 有 |
| REPEATABLE READ | 无 | 无 | 有 |
| SERIALIZABLE | 无 | 无 | 无 |
4.3.6事务属性:事务传播行为
- 介绍
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可以开启一个新事务,并在自己的事务中运行
propagation = Propagation.REQUIRED 默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。
propagation = Propagation.REQUIRES_NEW,表示不管当前线程上是否已经开启的事务,都要开启新事务
基于xml实现的声明式事务,必须引入aspectJ的依赖
spring-aspects
| 隔离级别 | Oracle | MySQL |
|---|---|---|
| READ UNCOMMITTED | 错 | 对 |
| READ COMMITTED | 对(默认) | 对 |
| REPEATABLE READ | 错 | 对(默认) |
| SERIALIZABLE | 对 | 对 |