1 简介
Spring 是一款目前主流的 Java EE 轻量级开源框架,是 Java 世界最为成功的框架之一。Spring 由“Spring 之父”Rod Johnson(罗宾·约翰逊) 提出并创立,其目的是用于简化 Java 企业级应用的开发难度和开发周期。
1.1 spring体系结构
spring 是一个分层的框架,每个模块既可以单独存在也可以联合工作,体系结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B8BDaXSL-1689594303823)(images/19e40f66fe9a4be28b1f3a4050c47efb.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gr0ACqdj-1689594303824)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑
- Spring Core:Core模块是Spring的核心类库,Spring的所有功能都依赖于该类库,Core主要实现IOC功能,Spring的所有功能都是借助IOC实现的。
- Spring AOP:AOP模块是Spring的AOP库,为Spring容器管理的对象提供了对面向切面编程的支持。
- Spring DAO:Spring 提供对JDBC的支持,对JDBC进行封装,允许JDBC使用Spring资源,同时还基于AOP模块提供了事务管理。
- Spring ORM:Spring 的ORM模块提供对常用的ORM框架的管理和辅助支持,Spring支持常用的Hibernate,ibtas,jdo等框架的支持,Spring本身并不对ORM进行实现,仅对常见的ORM框架进行封装,并对其进行管理。
- Spring Context:Context模块提供框架式的Bean访问方式,其他程序可以通过Context访问Spring的Bean资源。增加了对国际化、事件传播,以及验证等的支持,此外还提供了许多企业服务及对模版框架集成的支持。
- Spring Web:WEB模块是建立于Context模块之上,提供了一个适合于Web应用的上下文。另外,也提供了Spring和其它Web框架(如Struts1、Struts2、JSF)的集成。
- Spring Web MVC:WEB MVC模块为Spring提供了一套轻量级的MVC实现,是一个全功能的 Web应用程序,容纳了大量视图技术,如 JSP、Velocity、POI等。
1.2 控制反转与依赖注入
控制反转
传统创建对象,以及决定对象什么时候死亡是由程序员决定的,也就是说对象的控制权在我们程序员手中,而如今我们要把控制权交出去,交给ioc容器,别不舍得,大胆放心交出去,会很大程度进行解耦。
依赖注入
当我们声明一个类时,一个类可能依赖于其他类或者简单类型数据,比如Student类要有String的name属性,Integer的age属性以及Address类的地址属性,那么我们就可以说Student依赖于String,Integer和Address。而我们都是通过构造器或者set方法来对这些属性进行初始化的,而依赖注入就是我们不再执行这些set操作了,而是由spring来根据依赖关系,将对应类型的数据注入到当前对象中,这就是所谓的依赖注入。
IOC工作原理
ioc底层通过工厂模式+反射+解析xml来将代码的耦合度降到最低。
1. 我们会在配置文件中配置各种bean,对各个对象的依赖关系进行配置;
2. ioc就可以当作一个工厂,生产的就是一个一个的java bean; ···
3. 容器启动的时候,会加载各种bean的信息,以及得到他们之间的依赖关系;
4. 然后通过反射机制来实例对象,通过他们的依赖关系进行依赖注入。
1.3 入门搭建spring环境
创建一个java项目,然后引入spring-context依赖,因为它依赖与core,aop,beans和expression,通过依赖传递我们只需要引入此一个依赖即可引入其他四个依赖。
照常创建一个类,然后在配置文件中通过bean标签进行配置,最后在测试类中通过ApplicationContext读取配置文件,并调用getBean方法来获取对应的实例,以此实现我们的控制反转,有一些注意事项在代码中已经写的很清楚了。
实例化bean的三种方式:
- 通过id获取bean
优点:精确,可以直接找到对应的bean
缺点:需要自己进行类型强转- 通过class获取bean
优点:直接获得对应类型的实例对象,无需强转
缺点:ioc容器中若有两个同类的bean时会报错,有局限- 通过id和class获取bean
优点:精确,而且也不需要强转类型
确定:参数变成两个
bean对象默认是单例模式进行创建,单例模式的bean对象是在创建ioc容器时进行创建的,而工厂模式的对象是在调用对应的getBean时才创建的。
- 引入依赖
<!--
springcontext包
基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包
spring-core: spring核心包
spring-beans: spring-beans
spring-expression: spring表达式包
spring-aop: spring面向切面编程包
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.28</version>
</dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0uTmjXtO-1689594303825)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
- 创建java bean
public class Address {
String province;
String city;
String area;
String street;
Integer num;
public Address(){
System.out.println("Address的无参构造被调用了");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NEBtFIJi-1689594303825)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
- 创建applicationContext配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!-- Spring配置文件xml,采用schema约束规范(一个xml文件可以采用多个文件的规范) -->
<!--
xmlns:扩展当前xml的规范。
xsi:schemaLocation:规范文件,成对出现。
-->
<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标签:用于管理的bean对象
id属性:ioc容器中bean对象的唯一标识
class属性:bean对象的类型
scope属性:bean对象的模式
singleton单例模 默认情况下是单例模式,单例模式下会在创建ioc容器时创建所有的单例对象
prototype工厂模式 工厂模式下使用到该bean对象时才会去调用该类的构造函数
-->
<bean id="address" class="com.wjx.bean.Address" scope="singleton"></bean>
</beans>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PTmq7uxo-1689594303825)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
- 创建测试类进行测试
public class IOCTest {
@Test
public void testGetBean(){
// 初始化容器的时候会将单例的bean全部进行初始化,并不是调用时候才进行创建
ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");
// 注意使用ioc容器获取bean对象时,要提供对应的无参构造,因为它的原理时通过反射调用其构造函数来创建的实例对象
Address address = (Address)iocContainer.getBean("address");
System.out.println(address);
// ioc容器实例化对象默认的单例模式的
Address address1 = (Address) iocContainer.getBean("address");
System.out.println(address == address1);
}
@Test
public void testGetBeanMethod(){
ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");
/*
* 1. 通过id获取bean
* 优点:精确,可以直接找到对应的bean
* 缺点:需要自己进行类型强转
* 2. 通过class获取bean
* 优点:直接获得对应类型的实例对象,无需强转
* 缺点:ioc容器中若有两个同类的bean时会报错,有局限
* 3. 通过id和class获取bean
* 优点:精确,而且也不需要强转类型
* 确定:参数变成两个
* */
//1. getBean(String name)
Object o1 = iocContainer.getBean("address");
System.out.println(o1 instanceof Address);
//2. getBean(Class c)
Address o2 = iocContainer.getBean(Address.class);
System.out.println(o2 instanceof Address);
//3. getBean(String name, Clacc c)
Address o3 = iocContainer.getBean("address", Address.class);
System.out.println(o3 instanceof Address);
System.out.println(o1 == o2);
System.out.println(o2 == o3);
}
}
2 bean配置
2.1 bean配置方式
bean基于xml的配置我们之前已经简单介绍并使用过了,而注解的方式可配置类的方式我们会在后续章节介绍。
- 基于xml配置bean
- 基于java注解配置bean
- 基于java config配置bean
2.2 获取bean方式
-
通过构造方法实例化bean
实际上默认情况下spring底层就是通过反射的机制,调用bean的无参构造来创建bean对象的。
-
通过工厂实例化bean
-
通过FactoryBean实例化bean
后两种获取bean的方式,会在后续章节进行介绍。
2.3 SpringIOC容器
Spring为我们提供了两种ioc容器,一种是BeanFactory,另一种是ApplicationContext。
BeanFactory是spring底层所使用,一般不供开发者使用,但是也能用!而我们一般都是用ApplicationContext,而这两个都是接口,不能直接new,需要使用他们的实现类来进行获取容器,ApplicationContext的实现类主要有以下三个:
- ClassPathXmlApplicationContext:通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象。
- FileSystemXmlApplicationContext:通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象。
- WebApplicationContext:专门为WEB应用而准备的,它允许从相对于WEB根目录的路径中完成初始化工作。
从容器中获取bean的方式在第一种也已经介绍过了,调用容器的getBean方法,通过class获取bean,通过id获取bean,或者通过id和class获取bean。
2.4 依赖注入
依赖注入其实就是属性注入,而什么是属性呢?
首先需要声明,成员变量未必是属性,setXXX方法中的XXX才是属性。
2.4.1 基于属性注入
基于属性注入,也就是基于setter方法注入,如果没有提供setter方法,则会报错。
通过setter设置属性
property标签:
name:属性名
value:属性值
<bean id="person" class="com.wjx.bean.Person">
<property name="name" value="张三"></property>
<property name="age" value="18"></property>
<property name="money">
<value>10000</value>
</property>
</bean>
2.4.2 基于构造器注入
同样,基于构造器注入要提供对应的构造函数,否则会报错。
通过构造器注入
constructor-arg标签:
name:参数名
value:参数值
<!-- 构造器注入(推荐) -->
<bean id="person2" class="com.wjx.bean.Person">
<constructor-arg name="name" value="lisi"></constructor-arg>
<constructor-arg name="age">
<value>20</value>
</constructor-arg>
<constructor-arg name="money" value="20000"></constructor-arg>
</bean>
<!--
通过构造器注入(不推荐,了解即可)
如果只使用value属性,则按照构造器中参数列表顺序进行匹配
index属性:对应构造器参数顺序的下标
type属性:对应构造函数中参数的类型
-->
<bean id="car1" class="com.wjx.bean.Car">
<constructor-arg value="奔驰"/>
<constructor-arg value="越野"/>
<constructor-arg value="280000"/>
<constructor-arg value="4"/>
</bean>
<bean id="car2" class="com.wjx.bean.Car">
<constructor-arg value="宝马" index="0"/>
<constructor-arg value="轿车" index="1"/>
<constructor-arg value="380000" index="3"/>
<constructor-arg value="6" index="2"/>
</bean>
<!--切记使用基本类型时候,type对应的也要写成基本类型,它可不是mybatis中的别名-->
<bean id="car3" class="com.wjx.bean.Car">
<constructor-arg value="特斯拉" type="java.lang.String"/>
<constructor-arg value="新能源" type="java.lang.String"/>
<constructor-arg value="1880000" type="double"/>
<constructor-arg value="4" type="int"/>
</bean>
注意:
可以在property标签、constructor-arg标签中使用value标签,意义同value属性一样;
但是注意value只能赋字面值,比如基本类型,包装类,String,date……
2.5 注入各种类型属性值
2.5.1 字面值
我们在2.4章节中初步使用依赖注入的就是字面值,之前也提到过字面值中包含date,但是当我们注入date时一定要写成”2001/12/1“的格式,否则解析会出错。
<bean id="person" class="com.wjx.bean.Person">
<property name="name" value="张三"></property>
<property name="age" value="18"></property>
<property name="money">
<value>10000</value>
</property>
<!-- 属性时date类型时,value要写成xxxx/xx/xx的格式,否则会报错 -->
<property name="bithday" value="2001/12/1"></property>
</bean>
2.5.2 引用其他bean
只能引用ioc容器中已经创建的bean,通过ref标签或者ref属性来引用对应的id值进行注入;也可以通过内部bean的方式进行注入。
<!--
引用其他bean:
1. 使用内部bean
2. 使用ref属性或者ref标签(引用ioc容器中的bean)
-->
<bean id="order1" class="com.wjx.bean.Order">
<property name="orderNum" value="101"></property>
<property name="price" value="999"></property>
<property name="customer">
<!-- 内部bean不需要写id,因为他不会被调用 -->
<bean class="com.wjx.bean.Customer">
<property name="username" value="张三"/>
<property name="password" value="123456"/>
</bean>
</property>
</bean>
<bean id="customer" class="com.wjx.bean.Customer">
<property name="username" value="zhangsan"></property>
<property name="password" value="111111"></property>
</bean>
<bean id="order2" class="com.wjx.bean.Order">
<property name="orderNum" value="102"></property>
<property name="price" value="6999"></property>
<!--<property name="customer" ref="customer"></property>-->
<!--也可以使用ref标签-->
<property name="customer">
<!--使用ref标签时候通过bean属性来引用容器中的其他bean对象-->
<ref bean="customer"></ref>
</property>
</bean>
2.5.3 集合属性
顾名思义,当我们的属性是一个集合或者数组时候,该如何进行注入呢:
-
使用集合相关的标签,在这些标签内部引用相应的bean对象;
list:集合
array:数组
set:无重复集合
map:键值对集合
entry标签:键值对
key,value:字面值键值对
key-ref,value-ref:引用bean类型键值对
props:配置文件(键值对)
prop标签:key属性为键,标签中的内容为值 -
内部bean的方式,不太推荐也可以用,只用一次还好,要重复用的话太冗余;
-
ref标签来引用bean对象。
<!--属性集合-->
<bean id="course1" class="com.wjx.bean.Course">
<property name="id" value="1"/>
<property name="cname" value="计算机组装"/>
</bean>
<bean id="course2" class="com.wjx.bean.Course">
<property name="id" value="2"/>
<property name="cname" value="c语言"/>
</bean>
<bean id="course3" class="com.wjx.bean.Course">
<property name="id" value="3"/>
<property name="cname" value="mysql数据库"/>
</bean>
<!--
list:集合
array:数组
set:无重复集合
map:键值对集合
entry标签:键值对
key,value:字面值键值对
key-ref,value-ref:引用bean类型键值对
props:配置文件(键值对)
prop标签:key属性为键,标签中的内容为值
-->
<bean id="student" class="com.wjx.bean.Student">
<property name="id" value="001"></property>
<property name="name" value="小小怪"></property>
<property name="courseList">
<list>
<!--也可以使用内部bean的方式-->
<ref bean="course1"/>
<ref bean="course2"/>
<ref bean="course3"/>
</list>
</property>
<property name="ids">
<array>
<value>1</value>
<value>2</value>
<value>3</value>
</array>
</property>
<property name="stringSet">
<set>
<value>hello</value>
<value>world</value>
</set>
</property>
<property name="courseMap">
<map>
<entry key="1" value-ref="course1"></entry>
<entry key="2" value-ref="course2"></entry>
</map>
</property>
<property name="props">
<props>
<prop key="good">好好</prop>
<prop key="study">学习</prop>
</props>
</property>
</bean>
<!--
单例集合:
util:list
util:map
util:set
……
-->
<util:list id="myList">
<ref bean="course1"/>
<ref bean="course2"/>
<ref bean="course3"/>
<bean class="com.wjx.bean.Course">
<property name="id" value="4"/>
<property name="cname" value="java"/>
</bean>
</util:list>
<bean id="student2" class="com.wjx.bean.Student">
<property name="courseList" ref="myList"/>
</bean>
注意:可以通过声明util命名空间,然后通过util:xxx来定义单例集合,引用时只需要ref=”单例集合id“即可。
单例集合: util:list util:map util:set
2.5.4 短命名空间注入
通过p命名空间和c命名空间来进行依赖注入,这是对property标签和constructor-arg标签的简化,可以优化我们的代码,更加简洁。
<!--
短命名空间
p:属性名="属性值" 代替 property标签
c:参数名="参数值" 代替 constructor-arg标签
p:属性名-ref
c:属性名-ref
-->
<bean id="dept" class="com.wjx.bean.Dept" p:deptno="10" p:dname="开发" p:loc="河南郑州"/>
<bean id="dept2" class="com.wjx.bean.Dept" c:deptno="20" c:dname="测试" c:loc="北京"/>
<bean id="emp1" class="com.wjx.bean.Emp" p:empno="101" p:ename="小虎" p:dept-ref="dept"/>
<bean id="emp2" class="com.wjx.bean.Emp" c:empno="102" c:ename="小龙" c:dept-ref="dept2"/>
2.5.5 autowire自动装配
自动装配,就是我们不再手动注入相关依赖,由spring自动给我们注入。
通过名字注入:查看属性名或者参数名,然后找对应的id,进行注入;
通过类型注入:查看属性名或参数名的类型,然后找该类型的bean对象,前提是该类型的bean应唯一,否则会报错,然后进行注入;
还可以设置自动注入为default,没啥卵用,就是在beans标签加一个default-autowire属性,根据该属性的值来进行自动装配。
<!--
autowire属性: 自动装配
byName:从ioc容器中找与属性名相同id的bean进行装配
byType:从ioc容器中找与属性类型相同的bean进行装配 (有缺陷)
no:默认值,不使用自动装配
default:跟随beans标签的default-autowire属性进行自动装配 (没太大用处)
-->
<bean id="emp3" class="com.wjx.bean.Emp" autowire="byName"/>
<!-- <bean id="emp4" class="com.wjx.bean.Emp" autowire="byType"/>-->
<bean id="emp5" class="com.wjx.bean.Emp" autowire="default"/>
2.6 引用外部资源文件
- 编写配置文件
- 加入相关声明,可利用idea自动纠错功能一键加入
- 引入外部属性文件
- 通过表达式来对bean对象进行依赖注入
<!--
引用外部属性文件
-->
<!-- 读取外部的资源文件-->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${url}"/>
<property name="driverClassName" value=" ${driverClassName}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>
2.7 bean作用域
在springIOC容器中,通过设置scope属性,来设定bean对象的作用域,常用的就单例和多例两种,其他都不太常用,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dEZDTXBG-1689594303826)(images/image-20230712215329195.png)]
2.8 bean对象生命周期
在Bean生命周期的特定点执行定制的任务,大致可以分为以下阶段:
- bean 对象创建(调用无参构造器)
- 给 bean 对象设置属性(调用属性对应的 setter 方法)
- bean 对象初始化之前操作(由 bean 的后置处理器负责)
- bean 对象初始化(需在配置 bean 时指定初始化方法)
- bean 对象初始化之后操作(由 bean 的后置处理器负责)
- bean 对象就绪可以使用
- bean 对象销毁(需在配置 bean 时指定销毁方法)
- IOC 容器关闭
Spring 根据 Bean 的作用域来选择 Bean 的管理方式:
- 对于 singleton 作用域的 Bean 来说,Spring IoC 容器能够精确地控制 Bean 何时被创建、何时初始化完成以及何时被销毁。
- 对于 prototype 作用域的 Bean 来说,Spring IoC 容器只负责创建,然后就将 Bean 的实例交给客户端代码管理,Spring IoC 容器将不再跟踪其生命周期。
2.8.1 生命周期流程
Bean 生命周期的整个执行过程描述如下:
- Spring 启动,查找并加载需要被 Spring 管理的 Bean,对 Bean 进行实例化。
- 对 Bean 进行属性注入。
- 如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值。
- 如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。
- 如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。
- 如果 Bean 实现了 BeanPostProcessor 接口,则 Spring 调用该接口的预初始化方法postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的。
- 如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。
- 如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。
- 如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了。
- 如果在 中指定了该 Bean 的作用域为 singleton,则将该 Bean 放入 Spring IoC 的缓存池中,触发 Spring 对该 Bean 的生命周期管理;如果在 中指定了该 Bean 的作用域为prototype,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该Bean。
- 如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法销毁 Bean;如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean进行销毁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yr0IhuOb-1689594303827)(images/image-20230712215635428.png)]
bean对象的生命周期(简洁版,*表示我们主要学习的生命周期)
- 创建
- 设置属性
- 初始化之前 *
- 初始化 *
- 初始化之后 *
- 就绪可以使用
- 销毁 *
- ioc容器关闭
2.8.2 自定义bean的生命周期回调
bean生命周期回调的方法有两种:初始化回调和销毁回调。
初始化回调可以使用实现接口来进行回调,也可以在xml文件中指定自定义的回调方法,接口的回调方法要比xml中指定的回调方法先执行。
我们可以在 Spring Bean 的 Java 类中,通过实现 InitializingBean 和 DisposableBean 接口,指定Bean 的生命周期回调方法。但不推荐使用,因为这样会造成代码耦合度过高。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lIxB3BAi-1689594303827)(images/image-20230712220537935.png)]
下面是通过xml指定自定义的生命周期初始化和销毁回调:
<!-- 测试bean的生命周期 -->
<!--
init-method属性:指定自定义的生命周期初始化回调
在接口的初始化回调之后执行
destroy-method:指定自定义的生命周期销毁回调
在接口的销毁回调之后执行
-->
<bean id="dog" class="com.wjx.bean.Dog" p:name="小狗儿" p:owner="boss" p:age="2" init-method="init" destroy-method="destro"/>
下面是Dog类,包含了通过接口实现的生命周期初始化和销毁回调,以及自定义回调的方法:
public class Dog implements InitializingBean, DisposableBean {
private String name;
private String owner;
private int age;
public Dog() {
System.out.println("Dog对象被创建...");
}
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("调用setName方法....");
this.name = name;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
System.out.println("调用setOwner方法....");
this.owner = owner;
}
public int getAge() {
return age;
}
public void setAge(int age) {
System.out.println("调用setAge方法....");
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", owner='" + owner + '\'' +
", age=" + age +
'}';
}
@Override
public void destroy() throws Exception {
System.out.println("dog bean 接口销毁回调");
}
public void destro(){
System.out.println("dog bean 自定义销毁回调");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("dog bean 接口初始化回调");
}
public void init(){
System.out.println("dog bean 自定义初始化回调");
}
}
bean的后置处理器
通过创建自定义类实现 BeanPostProcessor接口,并在ioc容器中加入该类的实例对象,来实现bean对象生命周期中初始化前和初始化后的回调,该回调对所有bean对象都生效,也就是说每创建一个bean对象,就会在初始化前后调用一次该类的方法。
public class MyBeanProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println(beanName + " bean初始化之前¥¥¥¥¥¥¥¥ " + bean);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println(beanName + " bean初始化之后¥¥¥¥¥¥¥¥ " + bean);
return bean;
}
}
<!--
后置处理器要放在ioc容器中才能生效,且对所有的bena对象都生效
-->
<bean id="myBeanProcessor" class="com.wjx.process.MyBeanProcessor"></bean>
2.9 bean之间的关系
2.9.1 继承关系
bean对象的继承,并不是类的继承,而是继承某些bean的特性,通过parent属性来设置该bean要继承的bean对象。父对象如果加上abstract=“true”,则该bean不能创建,只能被继承。
<!--
bean对象的继承
注意:不是class的继承
abstract属性:抽象bean,即只能被继承,不能被实例
parent属性:继承其他bean对象
继承之后可以通过依赖注入覆盖父亲的特性
-->
<bean id="animal" class="com.wjx.bean.Animal" p:name="动物" p:age="2" abstract="true"/>
<bean id="dog2" class="com.wjx.bean.Dog" parent="animal" p:owner="金闪闪"/>
2.9.2 依赖关系
bean对象的依赖关系就是在进行依赖注入时,要依赖另一个bean对象,通过depends-o来绑定依赖关系,绑定依赖关系之后,当获取该bean时会先创建其依赖的bean再创建获取该bean,但作用不大。因为即使不加该属性,就算先初始化了该bean,在注入依赖的时候也会先初始化依赖的bean对象,然后再进行注入。
<!--
bean对象的依赖
depends-on属性:设置依赖的bean
注意:
如果依赖的bean写在当前bean之后,那么默认情况下是先创建user,再创建address
当使用depends-on之后,就会先创建address再创建user,意义不大,因为有没有此属性都不影响程序运行。
-->
<bean id="user" class="com.wjx.bean.User" p:address-ref="add" depends-on="add"/>
<bean id="add" class="com.wjx.bean.Address"/>
2.10 工厂实例化bean
2.10.1 静态工厂
我们之前实例化bean都是通过调用有参构造和无参构造来进行的,其实也可以通过工厂来进行实例化,可以是静态工厂,也可以是实例工厂。
静态工厂需要提供一个工厂类,和一个静态的创建实例的方法。
public class CatStaticFactory {
public static Cat getInstance(String name, Integer age) {
return new Cat(name, age);
}
}
写好静态工厂后,配置bean对象,class使用静态工厂的全路径,然后设置factory-method属性,指定实例化对象的工厂方法。若有参数,可以通过constructor-arg进行注入。
<!--
通过静态工厂实例化bean
1. 编写静态工厂,提供一个静态方法
2. 配置bean对象,class为对应的静态工厂类全路径,并设置factory-method属性,为工厂中获得实例的方法
3. 在未指定scope时,默认是单例的
注意:
若factory-method指定的方法中有参数,可以通过constructor-arg标签注入参数值。
-->
<bean id="cat" class="com.wjx.factory.CatStaticFactory" factory-method="getInstance">
<constructor-arg name="name" value="小猫咪"/>
<constructor-arg name="age" value="2"/>
</bean>
2.10.2 实例工厂
通过实例工厂来实例化bean与静态工厂类似,这里工厂中提供一个获得实例对象的成员方法。
public class CatInstanceFactory {
public Cat getInstance() {
return new Cat(name, age);
}
private String name;
private Integer age;
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
}
因为是成员方法,故我们只有获得工厂的实例才能获得我们想要的对象,因此需要配置工厂bean,然后再配置我们需要的bean对象,而我们不能写class,因为写了class那还要工厂干嘛,直接通过factory-bean属性指定实例工厂bean对象,然后通过factory-method属性指定获得实例的方法即可。
<!--
通过实例工厂实例化bean
1. 编写实例工厂,并提供一个获得实例的方法
2. 配置工厂bean
3. 配置需要工厂产生的bean,不用指定class,只需设置factory-bean属性为对应工厂的bean id,配置factory-method属性为获取实例的方法
注意:
可以在配置工参bean时进行依赖注入,然后生产实例时通过注入的属性对实例进行初始化。
-->
<bean id="catFactory" class="com.wjx.factory.CatInstanceFactory">
<property name="name" value="大猫咪"/>
<property name="age" value="5"/>
</bean>
<bean id="cat2" factory-bean="catFactory" factory-method="getInstance"/>
2.11 FactoryBean实例化
-
Spring中有两种类型的bean,一种是普通bean,另一种是工厂bean,即FactoryBean;
-
FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的 bean 不同,配置一个
-
FactoryBean 类型的 bean,在获取 bean 的时候得到的并不是 class 属性中配置的这个类的对象,
-
而是 getObject() 方法的返回值。通过这种机制,Spring 可以帮我们把复杂组件创建的详细过程和
-
繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
-
整合mybatis时,spring就是通过FactoryBean来为我们创建SqlSessionFactory对象的。
-
-
通过FactoryBean实例bean
- 创建对应的XXXFactoryBean并实现FactoryBean接口,需要设定泛型
- Object()和getObjectType()方法
- 配置bean对象,class为对应的XXXFactoryBean的全路径
- 注入,可以在XXXFactoryBean中声明私有属性,然后通过property标签进行注入
注意:
当我们获取bean时,spring会找到对应的FactoryBean,然后发现他继承了FactoryBean接口,
因此spring会自动的调用其getObjec()方法,来创建我们想要得到的bean对象。
/**
* FactoryBean
* 整合mybatis时,spring就是通过FactoryBean来为我们创建SqlSessionFactory对象的
*/
public class CatFactoryBean implements FactoryBean<Cat> {
private String name;
private Integer age;
/**
* 获得实例
* @return
* @throws Exception
*/
@Override
public Cat getObject() throws Exception {
return new Cat(name, age);
}
/**
* 获得实例类型
* @return
*/
@Override
public Class<?> getObjectType() {
return Cat.class;
}
/**
* 设置实例对象是否为单例模式 默认为true
* @return
*/
@Override
public boolean isSingleton() {
return FactoryBean.super.isSingleton();
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
}
<!--
通过FactoryBean实例bean
1. 创建对应的XXXFactoryBean并实现FactoryBean接口,需要设定泛型
2. 重写getObject()和getObjectType()方法
3. 配置bean对象,class为对应的XXXFactoryBean的全路径
4. 若需要依赖注入,可以在XXXFactoryBean中声明私有属性,然后通过property标签进行注入
注意:
当我们获取bean时,spring会找到对应的FactoryBean,然后发现他继承了FactoryBean接口,
因此spring会自动的调用其getObjec()方法,来创建我们想要得到的bean对象。
-->
<bean id="cat3" class="com.wjx.factory.CatFactoryBean">
<property name="name" value="小黄"/>
<property name="age" value="6"/>
</bean>
2.12 bean标签常用属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZeS7NhUP-1689594303827)(images/image-20230713211603175.png)]
2.13 lombok
通过注解+反射机制,为我们生成java bean中的getXxx,setXxxx以及构造函数,toString等一些列方法。
-
引入依赖
<!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> <scope>provided</scope> </dependency>
-
在类中加上相关注解
@Getter/@Setter:自动生成getter/setter;
@ToString:自动重写 toString() 方法,会印出所有变量;
@EqualsAndHashCode:自动生成 equals(Object other) 和 hashcode() 方法,包括所有非静态变量和非 transient 的变量;
@NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor:这三个很像,都是在自动生成该类的构造器,差别只在生成的构造器的参数不一样而已;
@NoArgsConstructor : 生成一个没有参数的构造器
@AllArgsConstructor : 生成一个包含所有参数的构造器
@RequiredArgsConstructor : 生成一个包含 “特定参数” 的构造器,特定参数指的是那些有加上 final 修饰词的变量们;补充一下,如果所有的变量都是正常的,都没有用 final 修饰的话,那就会生成一个没有参数的构造器
@Data:整合包,只要加了 @Data 这个注解,等于同时加了以下注解 @Getter/@Setter、
@ToString、@EqualsAndHashCode、@RequiredArgsConstructor;
3 AOP 面向切面编程
3.1 代理模式
为什么会有代理模式呢?当我们想要对我们编写的代码进行增强的时候,比如我们需要在业务代码那块添加日志的功能,那么就要再原有的代码基础上进行代码添加,比如执行了什么什么操作,操作成功或失败等等,也有可能是更为复杂的代码。
这样违反了面向对象的单一职责原则,我们的业务逻辑代码不再是只纯粹的完成业务逻辑的功能,参杂了很多的非业务逻辑,解决起来也很简单,比如把这些很多具有共性的代码提取出来封装成一个类,但是这样虽然使代码看起来不臃肿,带来的却是高耦合,有违反了软件工厂中程序设计原则。
在这样的前提下,我们引入了代理的概念,通过声明一个代理来包装我们的目标对象,通过调用代理的方法,然后由代理做一些业务代码额外的功能,也就是进行增强。也就是说我们调用者不再直接与目标对象取得联系,而是通过代理对象与目标对象取得联系,再由代理对象去调用目标对象的相关方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6M6BCfir-1689594303828)(images/image-20230713213443713.png)]
3.1.1 静态代理
静态代理就是我们自己声明的代理,通过实现目标对象所实现的接口,并包装目标对象,来对代码进行增强。
如下,我们的UserServiceImpl类实现了UserService接口,因此UserServiceStaticProxy静态代理类也实现此接口,并包含一个该接口的实现对象,届时我们可以通过初始化静态对象时,传入一个UserServiceImpl对象,然后调用静态对象的相关方法即可。这样我们的业务代码就不违反单一职责原则,而且也可以增加一些额外的代码。
public class UserServiceStaticProxy implements UserService {
private UserService userService;
public UserServiceStaticProxy(UserService userService) {
this.userService = userService;
}
@Override
public void insertUser() {
System.out.println("日志记录:insert前置增强");
userService.insertUser();
System.out.println("日志记录:insert后置增强");
}
@Override
public void updateUser() {
System.out.println("日志记录:update前置增强");
userService.updateUser();
System.out.println("日志记录:update后置增强");
}
@Override
public void deleteUser(int id) {
System.out.println("日志记录:delete前置增强");
userService.deleteUser(id);
System.out.println("日志记录:delete后置增强");
}
@Override
public List<String> selectUser() {
System.out.println("日志记录:select前置增强");
List<String> list = null;
try {
list = userService.selectUser();
System.out.println("日志记录:select返回增强");
} catch (Exception e) {
System.out.println("日志记录:select异常增强");
} finally {
System.out.println("日志记录:select后置增强");
}
return list;
}
}
3.1.2 动态代理
静态代理对象的弊端也很明显,就是我们业务一旦复杂庞大起来,需要声明很多的静态代理类,这样无疑是一件很痛苦的事,但是我们的静态代理类中方法体很多都是模板代码,因此我们引入了动态代理,意思就是这个代理类不由我们自己创建了,通过其他程序帮我们创建。在spring中使用的动态代理就是jdk动态代理和cglib代理。
-
jdk动态代理
jdk动态代理要求业务类必须事接口+实现类的形式,Proxy.newProxyInstance()方法中我们也可以看出,我们需要提供业务量的接口以及实现类
/** * JDK动态代理 * 必须实现InvocationHandler接口 * * 要求:业务类类必须是接口+实现类的形式 */ public class LoggerProxy implements InvocationHandler { /** * 代理类要完成的方法就写在invoke中 * @param proxy 代理对象 一般用不到,但可以用 * @param method 要执行的业务方法 * @param args 方法中的参数数组 * @return 目标方法的返回值 * */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 执行目标方法之前 System.out.println(targetClass.getName() + "的" + "日志记录:方法名:"+method.getName()+" 前置增强"); Object invoke = null; try { invoke = method.invoke(targetClass.newInstance(), args); System.out.println("日志记录:方法名:"+method.getName()+" 返回增强"); } catch (Exception e) { // 发生异常之后 System.out.println("日志记录:方法名:"+method.getName()+" 异常增强"); } finally { // 执行目标方法之后 System.out.println("日志记录:方法名:"+method.getName()+" 后置增强"); } return invoke; } /** * 目标类类型 */ private Class targetClass; /** * 获取代理类实例对象 * @param targetClass 目标类类型 * @return */ public Object getProxy(Class targetClass){ this.targetClass = targetClass; // 提现到了接口+实现类的形式 return Proxy.newProxyInstance(this.getClass().getClassLoader(), targetClass.getInterfaces(), this); } }
-
cglib动态代理
public class CGlibProxy implements MethodInterceptor { /** * * @param o 代理对象可不用 * @param method 目标对象要执行的方法 * @param objects 目标方法的参数 * @return 目标方法的返回结果 * */ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { // 执行目标方法之前 System.out.println(targetClass.getName() + "的" + "日志记录:方法名:"+method.getName()+" 前置增强"); Object invoke = null; try { invoke = method.invoke(targetClass.newInstance(), objects); System.out.println("日志记录:方法名:"+method.getName()+" 返回增强"); } catch (Exception e) { // 发生异常之后 System.out.println("日志记录:方法名:"+method.getName()+" 异常增强"); } finally { // 执行目标方法之后 System.out.println("日志记录:方法名:"+method.getName()+" 后置增强"); } return invoke; } /** * 目标类类型 */ private Class targetClass; /** * 获取代理类实例对象 */ public Object getProxy(Class targetClass){ //为目标对象target赋值 this.targetClass = targetClass; Enhancer enhancer = new Enhancer(); //设置父类,因为Cglib是针对指定的类生成一个子类,所以需要指定父类 enhancer.setSuperclass(targetClass); //设置回调 enhancer.setCallback(this); //创建并返回代理对象 Object result = enhancer.create(); return result; } }
测试:
/** * JDK动态代理 */ @Test public void testJDKDynamicProxy(){ // 获取代理对象 (此代理类代理对象由jdk给我们生成,不用自己重复手写了) UserService userService = (UserService) new LoggerProxy().getProxy(UserServiceImpl.class); userService.selectUser(); userService.deleteUser(2); } /** * CGlib动态代理 */ @Test public void testCGlibProxy(){ // 获取代理对象 UserService userService = (UserService) new CGlibProxy().getProxy(UserServiceImpl.class); userService.selectUser(); userService.deleteUser(2); }
3.2 AOP概述
AOP 的全称是“Aspect Oriented Programming”,译为“面向切面编程”,和 OOP(面向对象编程)类似,它也是一种编程思想。与 OOP 中纵向的父子继承关系不同,AOP 是通过横向的抽取机制实现的。它将应用中的一些非业务的通用功能抽取出来单独维护,并通过声明的方式(例如配置文件、注解等)定义这些功能要以何种方式作用在那个应用中,而不是在业务模块的代码中直接调用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HlsrRM8L-1689594303828)(images/image-20230713213947360.png)]
AOP常用术语
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ke5lDASj-1689594303829)(images/image-20230713214033267.png)]
- 连接点:在我们的代理模式中,每一个可以增强的业务类中的方法都可以成为连接点
- 切入点:我们对业务类中的代码进行增强,那么此连接点就被成为切入点
- 通知:前置增强,后置增强,返回后增强,异常后增强和环绕增强都成为通知
- 目标:目标对象
- 代理:代理对象
- 织入:生成代理对象的过程
- 切面:所有的通知和切入点共同组成了切面,只是一个概念
通知类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56VskHaW-1689594303829)(images/image-20230713214447742.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iuBIB4Ij-1689594303829)(images/image-20230713214512184.png)]
3.3 AOP开发步骤
3.3.1 引入依赖
当我们使用xml配置的方式配置AOP的话需要引入springAspects包,使用注解的话只引入AOP包即可,spring-context已经依赖了AOP包。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.28</version>
</dependency>
<!-- springAspects包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.28</version>
</dependency>
3.3.2 编写切面类
public class LogAspect {
public void beforeMethod(JoinPoint joinPoint) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
Class declaringType = signature.getDeclaringType();
int modifiers = signature.getModifiers();
System.out.println("name :" + name);
System.out.println("declaringType :" + declaringType);
System.out.println("modifiers :" + modifiers);
// 获得切入点参数列表
Object[] args = joinPoint.getArgs();
System.out.println("args : " + Arrays.toString(args));
System.out.println("AOP日志记录:" + name + "前置通知……");
}
public void afterMethod(JoinPoint joinPoint) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
System.out.println("AOP日志记录:" + name + "后置通知……");
}
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
System.out.println("AOP日志记录:" + name + "返回后通知……" + result);
}
public void afterThrowingMethod(JoinPoint joinPoint, Exception e) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
System.out.println("AOP日志记录:" + name + "异常后通知……" + e.getMessage());
}
/**
* 环绕通知
*/
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
System.out.println("AOP日志记录:" + name + "前置通知……");
Object proceed = null;
try {
proceed = joinPoint.proceed();
System.out.println("AOP日志记录:" + name + "返回后通知……");
} catch (Throwable e) {
System.out.println("AOP日志记录:" + name + "异常后通知……");
}
System.out.println("AOP日志记录:" + name + "后置通知……");
return proceed;
}
}
3.3.3 配置AOP
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c"
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/util https://www.springframework.org/schema/util/spring-util.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:
1. 编写切面类
2. 配置切面类bean和目标对象bean
3. 配置各种通知
-->
<!-- 切面类 -->
<bean id="logAsp" class="com.wjx.aspect.LogAspect"/>
<bean id="otherAsp" class="com.wjx.aspect.OtherAspect"/>
<!-- 目标对象 -->
<bean id="userService" class="com.wjx.service.impl.UserServiceImpl"/>
<bean id="deptService" class="com.wjx.service.DeptService"/>
<!-- 配置AOP -->
<!--
aop:config标签:配置AOP
aop:aspect标签:配置切面 ref属性:引用切面bean对象
aop:before标签:前置通知
aop:after标签:后置通知
aop:after-returning标签:返回后通知
returning属性:指定返回结果的参数
aop:after-throwing标签:异常后通知
throwing属性:指定抛出异常的参数
*** method属性:指定的增强通知方法 pointcut属性:指定切入点 pointcut-ref属性:引用切入点
*** execution() 切入点表达式:方法访问修饰符 返回值类型 方法所在的包名.类名.方法名(参数类型,...)
aop:pointcut标签:配置切入点 id属性:唯一标识 expression属性:切点表达式
此标签若定义在aspect外部,config内部是对所有切面共享的,若定义在aspect内部只由本切面才可以引用
-->
<aop:config>
<aop:pointcut id="exp1" expression="execution(* com.wjx.service.impl.*Impl.select*(..))"/>
<aop:aspect ref="logAsp" order="2">
<aop:before method="beforeMethod" pointcut="execution(public void com.wjx.service.impl.UserServiceImpl.*(..))"/>
<aop:after method="afterMethod" pointcut="execution(public void com.wjx.service.impl.UserServiceImpl.*(..))"/>
<aop:after-returning method="afterReturningMethod"
pointcut="execution(* com.wjx.service.impl.UserServiceImpl.*(..))" returning="result"/>
<aop:after-throwing method="afterThrowingMethod"
pointcut="execution(* com.wjx.service.impl.UserServiceImpl.*(..))" throwing="e"/>
<aop:around method="aroundMethod" pointcut-ref="exp1"/>
<!-- 引用切入点 -->
<aop:before method="beforeMethod" pointcut-ref="exp1"/>
<aop:after method="afterMethod" pointcut-ref="exp1"/>
</aop:aspect>
<!--
规则:
优先级高的在外面
优先级低的在里面
优先级一样看配置顺序,先配置的在外面,后配置的在里面
order属性值越小优先就越高
-->
<aop:aspect ref="otherAsp" order="1">
<aop:before method="beforeM" pointcut-ref="exp1"/>
<aop:after method="afterM" pointcut-ref="exp1"/>
<aop:before method="beforeM" pointcut="execution(* com.wjx.service.DeptService.*(..))"/>
</aop:aspect>
</aop:config>
</beans>
3.3.4 测试
/**
* 测试AOP配置
*/
@Test
public void testAOP(){
ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService)iocContainer.getBean("userService", UserService.class);
userService.insertUser();
userService.selectUser();
}
3.3.5 切面优先级
- 优先级高的在外面
- 优先级低的在里面
- 优先级一样看配置顺序,先配置的在外面,后配置的在里面
- order属性值越小优先就越高
3.3.6 没有接口情况
- 当目标类有接口时,使用的是jdk动态代理
- 当目标类没有接口时,使用的时cglib动态代理(可以通过debug测试得知)
4 注解开发
4.1 注解与xml方式对比
注解优点
- 简化了配置
- 使用比较直观,提高开发效率
- 类型安全容易检测出问题
注解缺点
- 修改维护比xml麻烦
- 对项目不了解,会对项目开发和维护造成一定影响
xml配置优点
- 降低类与类之间的耦合,修改方便,容易扩展。
- 容易和其他系统进行数据交互。
- 对象之间的关系一目了然。
xml配置缺点
- 配置冗长,需要额外维护,影响开发效率。
- 类型不安全,校验不出来,出错不好排查。
4.2 入门使用
4.2.1 开启注解
spring开发,默认是不使用注解配置的,如果我们要使用注解进行开发,要先开启组件扫描。
context:component-scan 元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类,如果类上使用了 @Component 注解,就将该类装配到容器中。一般base-package都是配置为com.公司名.项目名即可。
<!--
开启注解:组件扫描
-->
<!-- <context:component-scan base-package="com.wjx.entity"/>
<context:component-scan base-package="com.wjx.dao"/>
<context:component-scan base-package="com.wjx.service"/>
<context:component-scan base-package="com.wjx.controller"/>-->
<!-- 此配置包含了以上三个配置 -->
<context:component-scan base-package="com.wjx"/>
4.2.2 配置bean
配置bean,通过@Component来配置bean对象,代替了我们的bena标签,在xml中配置bean,需要指定id和class。因为我们是直接在类上写注解,因此class可以省略,而id通过注解中的value属性来指定,默认为空,若使用默认值,获取bean的时候通过类名首字母小写来获取,若设置了注解的value属性,则可通过指定的id进行获取bean对象。
另外还提供了@Repository,@Service,@Controller注解,他们都与@Component功能一样,相当于起的别名,是为了我们bena对象的语义更加清晰而设置的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7oYrAj0-1689594303830)(images/image-20230714211741048.png)]
@Repository("userDao")
public class UserDaoImpl implements UserDao {
@Override
public void addUser() {
System.out.println("userDao中的addUser方法执行了……");
}
}
//………………其余层的bean对象类似,用对应的注解或@Component即可
4.2.3 依赖注入
注入字面值
注入字面值贼简单,在对应的属性上使用@Value(“字面值”)注解即可,如果是date类型的属性,仍需使用”xxx/xx/xx“的日期格式。
@Data
@NoArgsConstructor
@AllArgsConstructor
@Component
public class Dog {
@Value("旺财")
private String name;
@Value("12")
private Integer age;
@Value("2022/04/06")
private Date birthday;
}
注入其他bean
注入其他bean,首先要保证该bean确实存在,然后通过@Autowired注解进行自动装配。(也可以用@Resouce,与之功能类似,只不过流程不一样)
@Autowired自动装配规则:
-
注解中有一个required属性,默认值为true,代表此属性必须要注入值,否则就会抛出异常,为false时代表不是必须要注入的。
-
当使用此注解进行自动装配时,会先检查是否有此类型的bean对象,若有一个则自动注入
-
若有两个以上该类型的bean对象,会以该属性的名称进行匹配,若匹配成功则注入,匹配失败则抛异常
-
若有@Qualifier注解,则使用@Qualifier注解中value的值进行匹配,若匹配到则注入,匹配不到则抛异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rSdIL7Yu-1689594303830)(images/image-20230714212900601.png)]
@Controller
public class UserController {
@Autowired(required = false)
@Qualifier(value = "userService1")
private UserService userService;
public void addUser(){
System.out.println("userController中的addUser()方法执行了……");
userService.insertUser();
}
}
4.2.4 java Config配置bean
我们之前提到过,配置bean有三种方式,现在xml配置和注解配置bean的方式我们都知晓了,其实还可以通过配置类的方式来配置bean。
-
首先我们要有一个配置类,通过@Configuration来声明一个配置类
-
在类中编写获得bean的方法,并加上@Bean标签,那么默认情况下方法名就是bean id,返回值类型就是bean对象类型
-
可以通过指定@Bean注解的value属性,来指定bean对象的名字
-
在方法中我们可以给bean对象注入依赖
-
若要注入引用依赖,可以设置bean标签的autowire属性
/** * @Configuration 该注解标记该类为Java配置类 */ @Configuration @ComponentScan("com.wjx") @EnableAspectJAutoProxy public class MyConfig { /** * autowire属性设置 该bean对象的属性注入方式 * @return */ @Bean(autowire = Autowire.BY_TYPE) public Emp getEmp(){ Emp emp = new Emp(); emp.setEname("zhangsan"); emp.setEmpno(101); return new Emp(); } @Bean("dept") public Dept getDept(){ Dept dept = new Dept(); dept.setDeptno(10); dept.setDname("开发"); return dept; } }
4.3 AOP注解开发
4.3.1 开启AOP注解
在xml配置文件中做一下配置,开启AOP配置,后续可以在配置类中加入注解来开启AOP配置。
<!--
开启AOP注解
-->
<aop:aspectj-autoproxy/>
4.3.2 配置bean对象
这个和使用xml配置AOP步骤中是一样的,只不过换成了注解的方式,用到哪些bean对象,就在其类上配置为spring bean对象即可,注意切面类也要配置成bean对象,给spring容器管理。
4.3.3 配置切面类
在切面类上使用@Aspect注解,配置切面类,可以通过@Order注解配置切面的优先级,数字越小优先级越高,优先级越高在越外层。
4.3.4 配置通知
在切面类的方法上,通过@Before,@Afte,@AfterReturning,@AfterThrowing,@Around配置通知的类型,并设置其value属性为切点表达式,来描述作用与那些包中那些类中的那些方法上。
@Aspect:配置切面类
@Order:配置切面优先级
@Before:配置前置通知
value属性:切点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
@After:配置后置通知
@AfterReturning:配置返回通知
@AfterThrowing:配置异常通知
@Around:配置环绕通知
在异常通知中获取目标方法抛出的异常分两步:
第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称;
第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们;
@Pointcut:配置切点表达式
在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。易于维护,一处修改,处处生效。
同一个类内部引用:value=“公共切点表达式方法名()”;
在不同类中引用:value=“包名.类名.公共切点表达式方法名()”;
推荐:集中管理,定义一个组件类作为存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于统一管理;
注:该切面类必须添加@Component注解;
以下是一个切面类配置的例子:
/*
* 若要配置切面类,首先它必须是一个spring bean对象
* @Aspect 标记该类为切面类
* @Order 设置切面优先级 数字越小级别越高
* */
@Component
@Aspect
@Order(2)
public class LogAspect {
/*
* 在切面类中使用@Pointcut注解定义的切点只能在本切面中使用
* 注意:它其实也是公共的切点,在其它切面类使用的时候只需要写上全路径就可以了
* 但是更建议将公共的切点写到一个专门声明切点的类中,便于管理和维护
* */
@Pointcut("execution(* com.wjx.service.UserService.selectUser(..))")
public void exp1(){};
@Pointcut("execution(* com.wjx.service.UserService.insertUser(..))")
public void exp2(){}
@Before("exp1()")
public void beforeMethod(JoinPoint joinPoint) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
Class declaringType = signature.getDeclaringType();
int modifiers = signature.getModifiers();
System.out.println("name :" + name);
System.out.println("declaringType :" + declaringType);
System.out.println("modifiers :" + modifiers);
// 获得切入点参数列表
Object[] args = joinPoint.getArgs();
System.out.println("args : " + Arrays.toString(args));
System.out.println("AOP日志记录:" + name + "前置通知……");
}
@After(value = "exp1()")
public void afterMethod(JoinPoint joinPoint) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
System.out.println("AOP日志记录:" + name + "后置通知……");
}
@AfterReturning( value = "exp2()", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
System.out.println("AOP日志记录:" + name + "返回后通知……" + result);
}
@AfterThrowing(value = "com.wjx.aspect.Execution.exp3()", throwing = "e")
public void afterThrowingMethod(JoinPoint joinPoint, Exception e) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
System.out.println("AOP日志记录:" + name + "异常后通知……" + e.getMessage());
}
/**
* 环绕通知
*/
@Around(value = "com.wjx.aspect.Execution.exp3()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
// 获得切入点方法签名
Signature signature = joinPoint.getSignature();
// 通过方法前面获取各种信息
String name = signature.getName();
System.out.println("AOP日志记录:" + name + "前置通知……");
Object proceed = null;
try {
proceed = joinPoint.proceed();
System.out.println("AOP日志记录:" + name + "返回后通知……");
} catch (Throwable e) {
System.out.println("AOP日志记录:" + name + "异常后通知……");
}
System.out.println("AOP日志记录:" + name + "后置通知……");
return proceed;
}
}
当需要使用其他类中的切点时,也就是公共切点,需要带上全路径:
/**
* 另一个切面类
*/
@Component
@Aspect
@Order(1)
public class OtherAspect {
@Before("com.wjx.aspect.Execution.exp3()")
public void beforeM() {
System.out.println("OtherAspect的beforeM方法.....");
}
@After("com.wjx.aspect.Execution.exp3()")
public void afterM(){
System.out.println("OtherAspect的afterM方法.....");
}
}
4.4 完全注解开发
我们已经可以通过注解来配置bean,注入依赖,配置AOP。但是呢我们的xml配置中还有两个配置,一个是开启组件扫描,一个是开启AOP配置。这两个配置也可通过注解的方式进行配置,组件扫描使用@ComponentScan(“com.wjx”)注解,可以指定一个value属性或者basepackage属性,意义一样都是扫描的包名;开启aop使用@EnableAspectJAutoProxy,此时我们配置的切面类和通知都可以对切点的代码进行增强。
/**
* @Configuration 该注解标记该类为Java配置类
*
* @ComponentScan("com.wjx") 使用注解的方式开启组件扫描
*
* @EnableAspectJAutoProxy 使用注解的方式开启AOP配置
*/
@Configuration
@ComponentScan("com.wjx")
@EnableAspectJAutoProxy
public class MyConfig {
/**
* autowire属性设置 该bean对象的属性注入方式
* @return
*/
@Bean(autowire = Autowire.BY_TYPE)
public Emp getEmp(){
Emp emp = new Emp();
emp.setEname("zhangsan");
emp.setEmpno(101);
return new Emp();
}
@Bean("dept")
public Dept getDept(){
Dept dept = new Dept();
dept.setDeptno(10);
dept.setDname("开发");
return dept;
}
}
/**
* 测试使用配置类代替xml配置文件
* 通过配置类获取ioc容器
*/
@Test
public void testJavaConfigReplaceXml(){
ApplicationContext iocConByJavaConfig = new AnnotationConfigApplicationContext(MyConfig.class);
UserService userService1 = iocConByJavaConfig.getBean("userService1", UserService.class);
userService1.selectById();
}
5 事务管理
5.1 事务简介
事务就是一系列的动作,当做一个独立的工作单元,这些动作要么全部成功,要么全部失败。事务具有 4 个特性:原子性、一致性、隔离性和持久性,简称为 ACID 特性。
A:原子性,一个事务是不可分割的单位,要么全部成功,要么全部失败
C:一致性,包装数据库从一个一致性状态变到另一个一致性状态,建立在原子性的基础之上
I:隔离性,事务之间相互隔离,一个事务的执行不能被其他事务所干扰
D:持久性,事务一旦提交,对数据的操作是永久性的
5.2 spring事务管理
Spring既支持编程式事务管理,也支持声明式的事务管理。
- 编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在编程式管理事务时,必须在每个事务操作中包含额外的事务管理代码。
- 声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。Spring通过AOP框架支持声明式事务管理。
编程式事务细粒度更高,我们可以清晰的控制事务的边界,但是与代码耦合度太高。
声明式事务,简便,只需要一些简单的声明,无需更改代码即可实现,而且耦合度低,易于维护。
Spring声明事务的两种方式:
- 基于xml配置声明事务
- 基于注解声明事务(仍需xml配置开启相关注解)
Spring事务管理器
Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。Spring的核心事务管理器是PlatformTransactionManager接口。它为事务管理封装了一组独立于技术的方法,无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OfkGdAPn-1689594303830)(images/image-20230717192220776.png)]
Spring为不同的持久化框架,开发了各种事务管理器,比如jdbc和Mybatis使用的就是org.springframework.jdbc.datasource.DataSourceTransactionManager。
5.3 开发步骤
5.3.1 配置数据源
配置数据源都是之前提到过的东西,不再过多赘述。
<!-- 读取数据库配置文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 数据库连接池bean -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.mysql.driver}"/>
<property name="url" value="${jdbc.mysql.url}"/>
<property name="username" value="${jdbc.mysql.username}"/>
<property name="password" value="${jdbc.mysql.password}"/>
</bean>
<!-- 配置jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
5.3.2 配置事务管理器
我们需要使用jdbc的事务管理器,因此要将它放入ioc容器中进行管理。
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据源属性 -->
<property name="dataSource" ref="dataSource"/>
</bean>
5.3.3 配置事务通知
配置事务通知,就是设置事务的各种属性,以及作用在那些方法当中。
对于 tx:advice 来说,事务属性是被定义在 tx:attributes 中的,该元素可以包含一个或多个tx:method 元素。tx:method包含的属性如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LEYj2VKT-1689594303831)(images/image-20230717193046967.png)]
<!--
配置事务通知
transaction-manager:指定事务管理器,默认值为transactionManager
<tx:attributes>:定义事务属性
<tx:method>
name属性:方法名 可以使用通配符
各种属性:propagation,isolation,read-only,timeout,rollback-for,no-rollback-for
-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--
propagation属性:事务传播行为
REQUIRED:新的事务加入不,并入当前事务,不再新创建事务(默认值)
REQUIRES_NEW:新的事务加入,则新创建一个事务
SUPPORTS:如果当前有事务就并入该事务,否则就不用在事务中运行
isolation属性:事务隔离级别
DEFAULT:使用底层数据库默认的事务隔离级别
READ_UNCOMMITTED:允许事务读取其他事务未提交但已修改的数据,会出现脏读,不可重复读和幻读
READ_COMMITTED:只允许事务读取其他事务提交过的数据,避免了脏读,会出现不可重复度,幻读
rollback-for属性:遇到时必须进行回滚
no-rollback-for属性:遇到时不用进行回滚
read-only属性:设置事务为只读属性
timeout属性:设置超时时间,单位秒,超过时间未执行完事务就回滚
注意:默认情况下,当发生运行时异常时才会回滚,发生编译时异常不会进行回滚,可以通过 rollback-for 和 no-rollback-for来指定什么异常回滚或者不回滚。
-->
<tx:method name="*" propagation="REQUIRES_NEW" isolation="READ_COMMITTED" timeout="3"
rollback-for="java.lang.ClassNotFoundException"
no-rollback-for="java.lang.NullPointerException"/>
<tx:method name="find*" read-only="true"/>
</tx:attributes>
</tx:advice>
5.3.4 配置事务切入点
配置事务切入点,这里切入点的概念和aop中一样,而spring进行事务管理其底层原理就是用到了aop。
<!-- 配置事务切入点 -->
<aop:config>
<!--事务切入点-->
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.wjx.service.impl.*Impl.*(..))"/>
</aop:config>
接下来就大功告成了,可以尽情测试,如果事务未执行完发生异常则会进行回滚操作,而且代码上是一点更改都没有,非常强大。
5.4 事务属性
5.4.1 propagation属性
事务传播行为(propagation behavior)指的是,当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QeseTti5-1689594303831)(images/image-20230717193358215.png)]
5.4.2 isolation属性
事务隔离级别,一个应用程序或者多个应用程序在一个共享数据集上进行并发操作就会出现以下问题:
- 脏读:对于两个事务TX1和TX2,TX1读取了已经被 TX2更新但还没有被提交的数据。之后,如果TX2回滚,TX1读取的内容就是临时且无效的。
- 不可重复读:对于两个事务TX1和TX2,TX1读取了一个字段,然后TX2更新了该字段。之后,TX1再次读取同一个字段,值就不同了。
- 幻读:对于两个事务TX1和TX2,TX1从一个表中读取了读取几行数据,然后TX2在该表中插入了一些新的记录。之后,如果TX1再次读取同一个表,就会多出几行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9Uw1D4k-1689594303831)(images/image-20230717193549842.png)]
5.4.3 rollback-for属性
默认情况下,当发生运行时异常时才会回滚,发生编译时异常不会进行回滚,可以通过 rollback-for 指定什么异常回滚。
5.4.4 no-rollback-for属性
默认情况下,当发生运行时异常时才会回滚,发生编译时异常不会进行回滚,可以通过 no-rollback-for 指定什么异常不回滚。
注意:
- 范围不同
- 不管哪个范围设置更大,都是大范围内排除小范围
- 范围一致
- 回滚和不会滚的异常设置了相同范围,出于安全考虑就回滚
5.4.5 read-only属性
针对一些查询方法可以设置只读属性未true,但是如果是增删改操作设置只读,则会报错。
5.4.6 timeout属性
设置事务执行的超时时间,一旦超时,则会进行回滚。时间单位是秒,如果设置为-1,则永不超时。
5.5 注解事务开发
-
配置事务管理器并开启事务注解
<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 注入数据源属性 --> <property name="dataSource" ref="dataSource"/> </bean> <!-- 2.开启事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/>
-
使用@Transactiona注解配置事务
注解中的参数(属性),与xml配置中的属性一致,值得一提的是可以将该注解定义在类中,提取一些公用的属性,再在各个方法中声明独属于他们自己的属性。
package com.wjx.service.impl; import com.wjx.dao.AccountDao; import com.wjx.entity.Account; import com.wjx.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.Map; /* * @Transactional注解:事务注解 * 可以写在类上,也可以写在方法上,写在类上对类中所有的方法都生效,然后可以再根据需求配置方法上的一些属性。参数对应注解配置事务的属性一直! * * */ @Service("accountService") @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED, timeout = 3) public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Override @Transactional(rollbackFor = {ClassNotFoundException.class}, noRollbackFor = {NullPointerException.class}) public void transferMoney(int fromId, int toId, double money) throws Exception { Account fromAccount = accountDao.findAccountById(fromId); fromAccount.setBalance(fromAccount.getBalance() - money); accountDao.updateBalance(fromAccount); //模拟程序执行过程中出现问题,异常出现导致程序终止(方法结束) // System.out.println(10 / 0); Class.forName("xx"); // String s = null; // s.length(); Thread.sleep(3500); Account toAccount = accountDao.findAccountById(toId); toAccount.setBalance(toAccount.getBalance() + money); accountDao.updateBalance(toAccount); System.out.println("一次转账完成..."); } @Autowired private AccountService accountService; @Override public void transferToMany(int fromId, Map<Integer, Double> map) throws Exception { for (Integer toId : map.keySet()) { accountService.transferMoney(fromId, toId, map.get(toId)); // 第一次转账之后出现异常 int i = 10/0; } } }
ctory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
/*
- @Transactional注解:事务注解
- 可以写在类上,也可以写在方法上,写在类上对类中所有的方法都生效,然后可以再根据需求配置方法上的一些属性。参数对应注解配置事务的属性一直!
- */
@Service(“accountService”)
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED, timeout = 3)
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
@Transactional(rollbackFor = {ClassNotFoundException.class}, noRollbackFor = {NullPointerException.class})
public void transferMoney(int fromId, int toId, double money) throws Exception {
Account fromAccount = accountDao.findAccountById(fromId);
fromAccount.setBalance(fromAccount.getBalance() - money);
accountDao.updateBalance(fromAccount);
//模拟程序执行过程中出现问题,异常出现导致程序终止(方法结束)
// System.out.println(10 / 0);
Class.forName(“xx”);
// String s = null;
// s.length();
Thread.sleep(3500);
Account toAccount = accountDao.findAccountById(toId);
toAccount.setBalance(toAccount.getBalance() + money);
accountDao.updateBalance(toAccount);
System.out.println(“一次转账完成…”);
}
@Autowired
private AccountService accountService;
@Override
public void transferToMany(int fromId, Map<Integer, Double> map) throws Exception {
for (Integer toId : map.keySet()) {
accountService.transferMoney(fromId, toId, map.get(toId));
// 第一次转账之后出现异常
int i = 10/0;
}
}
}