@SpingFrameWork
一、技术体系结构
- 单一架构,一个大的项目,一个工程,导出war包在一个tomcat上运行(主要用spring、springMVC、Mybatis)
- 分布式架构,一个项目,但是有多个模块,但是每个模块都在一个tomcat上运行
这里需要与前后端分离、前后端混合做个辨析,架构是说软件系统的设计,后者是开发模式
- 框架 = jar包+配置文件,也就是说框架是可以通过配置文件个性化的
二、SpringFrameWork
- 其实是spring全家桶的最基础的框架
功能模块 | 功能介绍 |
---|---|
Core Container | 核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。 |
AOP&Aspects | 面向切面编程 |
TX | 声明式事务管理。 |
Spring MVC | 提供了面向Web应用程序的集成功能。 |
三、Spring IOC容器和核心概念
组件和组件概念
什么是组件
简单的说就是各种java程序,控制层程序:Servlet、业务层程序:Service、持久化层程序:Dao
整个项目就是由各种组件搭建而成的
我们的期待
我们希望更加注重于对业务逻辑的思考和编写,把一些对对象的创建、销毁、属性的赋值等都交给别人去做
这样的角色其实就是spring:
组件可以完全交给Spring 框架进行管理,Spring框架替代了程序员原有的new对象和对象属性赋值动作等!
Spring具体的组件管理动作包含:
- 组件对象实例化
- 组件属性属性赋值
- 组件对象之间引用
- 组件对象存活周期管理
- ...
我们只需要编写元数据(配置文件)告知Spring 管理哪些类组件和他们的关系即可!
注意:组件是映射到应用程序中所有可重用组件的Java对象,应该是可复用的功能对象!
- 组件一定是对象
- 对象不一定是组件
综上所述,Spring 充当一个组件容器,创建、管理、存储组件,减少了我们的编码压力,让我们更加专注进行业务编写!
Spring管理好处
- 不用我们管理,方便管理和配置
- 降低了组件之间的耦合性,我们都把组件交给spring,组件之间的直接交流减少,比如:在A类中new了一个B对象
- 提高了代码的可重用性和可维护性,耦合性降低自然更好维护,一个组件可以到处用,不需要再反复的先new再用
SpringIOC容器介绍
SpringIOC容器其实就是Spring容器的核心之一,负责存储、实例化、配置、组装组件(IOC:中文是控制反转的意思)
实例化这些(类)组件,本来是我们程序员的事情,new对象、赋值等等;但是现在权力交给了IOC容器;
怎么实例化:容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。
配置组件时又通过另一个核心之一:DI (依赖注入),去配置组件之间的依赖关系;
SpringIOC容器的具体接口和实现类
最高的接口:BeanFactory,但是它只是提供了基本的依赖注入功能
子接口:ApplicationContext:它提供了更为具体且高级的功能,如事件发布、国际化支持、资源访问等
ApplicationContext容器实现类:(以下这些容器都是更为具体的容器,分别指定了特定的方式创建容器对象)
类型名 | 简介 |
---|---|
ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 |
FileSystemXmlApplicationContext | 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象 |
AnnotationConfigApplicationContext | 通过读取Java配置类创建 IOC 容器对象 |
WebApplicationContext | 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。 |
由上面的表我们知道,Spring框架提供了多种配置方式:XML配置方式、注解方式和Java配置类方式
- XML配置方式:是Spring框架最早的配置方式之一,通过在XML文件中定义Bean及其依赖关系、Bean的作用域等信息,让Spring IoC容器来管理Bean之间的依赖关系。该方式从Spring框架的第一版开始提供支持。
- 注解方式:从Spring 2.5版本开始提供支持,可以通过在Bean类上使用注解来代替XML配置文件中的配置信息。通过在Bean类上加上相应的注解(如@Component, @Service, @Autowired等),将Bean注册到Spring IoC容器中,这样Spring IoC容器就可以管理这些Bean之间的依赖关系。
- Java配置类方式:从Spring 3.0版本开始提供支持,通过Java类来定义Bean、Bean之间的依赖关系和配置信息,从而代替XML配置文件的方式。Java配置类是一种使用Java编写配置信息的方式,通过@Configuration、@Bean等注解来实现Bean和依赖关系的配置。
为了迎合当下开发环境,我们将以配置类+注解方式为主进行讲解!
四、SpringIOC实践和应用
SpringIOC/DI实现步骤:
- 编写配置文件(三种方法)
- 读取配置文件,并选择合适的容器类进行实例化
- 通过容器对象,获取组件
基于XML的配置方式和组件管理
学习重点:如何通过定义XML配置文件,声明组件类信息,交给 Spring 的 IoC 容器进行组件管理!
-
准备项目
-
创建maven工程(spring-ioc-xml-01)
-
导入SpringIoC相关依赖
pom.xml
-
XML
<dependencies>
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.6</version>
</dependency>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
</dependencies>
对于在xml文件中声明组件信息(事实上,就是我们告诉容器,怎么去创建组件对象)
**第一种:**组件类中是无参构造函数
java
package com.atguigu.ioc;
public class HappyComponent {
//默认包含无参数构造函数
public void doWork() {
System.out.println("HappyComponent.doWork");
}
}
xml
<!-- 实验一 [重要]创建bean -->
<bean id="happyComponent" class="com.atguigu.ioc.HappyComponent"/>
**第二种:**组件中是静态工厂方法
java
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
//静态工厂方法
public static ClientService createInstance() {
return clientService;
}
}
xml
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
- class属性:指定工厂类的全限定符!
- factory-method: 指定静态工厂方法,注意,该方法必须是static方法。
**第三种:**组件中是实例工厂方法
java
public class DefaultServiceLocator {
private static ClientServiceImpl clientService = new ClientServiceImpl();
//实例工厂方法
public ClientServiceImpl createClientServiceInstance() {
return clientService;
}
}
xml
1. 创建ClientServiceImpl类:
package com.example;
public class ClientServiceImpl {
// 可以添加一些服务方法
public void doSomething() {
System.out.println("Doing something...");
}
}
2.配置 DefaultServiceLocator类
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.springframework.org/schema/xsi"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置 ClientServiceImpl -->
<bean id="clientService" class="com.example.ClientServiceImpl" />
<!-- 配置 DefaultServiceLocator -->
<bean id="serviceLocator" class="com.example.DefaultServiceLocator">
<!-- 指定使用工厂方法,想要使用这个方法实例化一个ClientService对象-->
<property name="clientService"
factory-method="createClientServiceInstance" />
</bean>
</beans>
<!--其中,这两个类的配置顺序并没有严格的规定,但是最好按照依赖顺序配置;这里是ClientServiceImpl注入了DefaultServiceLocator
DefaultServiceLocator依赖ClientServiceImpl,那么就先配置clientService类,
-->
组件依赖注入配置(DI)
(其实,上面的两个类就已经涉及到了依赖配置了)
通过配置文件, 实现IoC容器中Bean之间的引用(依赖注入DI配置)。
主要涉及注入场景:基于构造函数的依赖注入和基于 Setter 的依赖注入。
基于构造函数的依赖注入
这里就不分几个参数的构造函数了,其实都是一个道理:
xml
<!-- 下面都是多参数的例子,但是如果是单参数,其实就是只有一个<constructor-arg>标签,其中要说的是:标签中的属性
1.value:可以直接赋值的类型,我们把它先叫做"基本类型注入",包括String、int等类型
2.ref:引用其他组件的类型,我们先把他叫做"引用类型",用于指向引用的组件的名字(id属性),那么被引用的类型也需要在ioc中声明bean
-->
<!-- 场景1: 多参数,可以按照相应构造函数的顺序注入数据 -->
<beans>
<bean id="userService" class="x.y.UserService">
<!-- value直接注入基本类型值 -->
<constructor-arg value="18"/>
<constructor-arg value="赵伟风"/>
<constructor-arg ref="userDao"/>
</bean>
<!-- 被引用类bean声明 -->
<bean id="userDao" class="x.y.UserDao"/>
</beans>
<!-- 场景2: 多参数,可以按照相应构造函数的名称注入数据 -->
<beans>
<bean id="userService" class="x.y.UserService">
<!-- value直接注入基本类型值 -->
<constructor-arg name="name" value="赵伟风"/>
<constructor-arg name="userDao" ref="userDao"/>
<constructor-arg name="age" value="18"/>
</bean>
<!-- 被引用类bean声明 -->
<bean id="userDao" class="x.y.UserDao"/>
</beans>
<!-- 场景2: 多参数,可以按照相应构造函数的角标注入数据
index从0开始 构造函数(0,1,2....)
-->
<beans>
<bean id="userService" class="x.y.UserService">
<!-- value直接注入基本类型值 -->
<constructor-arg index="1" value="赵伟风"/>
<constructor-arg index="2" ref="userDao"/>
<constructor-arg index="0" value="18"/>
</bean>
<!-- 被引用类bean声明 -->
<bean id="userDao" class="x.y.UserDao"/>
</beans>
基于 Setter 的依赖注入
使用条件:
组件类中有setter方法
java
public Class MovieFinder{
}
public class SimpleMovieLister {
private MovieFinder movieFinder;
private String movieName;
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void setMovieName(String movieName){
this.movieName = movieName;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
xml
<bean id="simpleMovieLister" class="examples.SimpleMovieLister">
<!-- setter方法,注入movieFinder对象的标识id
name = 属性名 ref = 引用bean的id值
-->
<property name="movieFinder" ref="movieFinder" />
<!-- setter方法,注入基本数据类型movieName
name = 属性名 value= 基本类型值
-->
<property name="movieName" value="消失的她"/>
</bean>
<bean id="movieFinder" class="examples.MovieFinder"/>
以上示例中,要说明的是:
java
/*
1.property标签用于注入带有setter方法的组件
2.该标签下的name属性,就是setter方法中去掉set并且首字母小写的那段字符串,其实就是后面的"宾语"
3.还有ref属性和value属性是 二选一,这取决于参数的类型是"引用"还是"直接",其中ref还是引用bean的id值,value随便赋值好了
*/
ioc容器的创建和使用
上面的实验只是讲解了 如何在XML格式的配置文件编写 IoC 和 DI 配置
下面我们要说的是 核心 ioc容器对象的声明,为什么说是核心?因为它是环境,没有它读取那些配置,上面都白做功夫了,下面将要介绍容器的实例化和使用
容器的实例化
java
/*
我们用下面的例子讲:有两种实例化方式
1.(常用)直接选择一种ioc容器实现类,new出来就好了,参数为指定的配置文件
2.(源码常用)先new,但不指定参数,用setConfigLocations方法中指定配置文件,最后调用刷新方法refresh方法
3.配置文件的指定都是可以多个的
*/
//方式1:实例化并且指定配置文件
//参数:String...locations 传入一个或者多个配置文件
ApplicationContext context =
new ClassPathXmlApplicationContext("services.xml", "daos.xml");
//方式2:先实例化,再指定配置文件,最后刷新容器触发Bean实例化动作 [springmvc源码和contextLoadListener源码方式]
ApplicationContext context =
new ClassPathXmlApplicationContext();
//设置配置配置文件,方法参数为可变参数,可以设置一个或者多个配置
iocContainer1.setConfigLocations("services.xml", "daos.xml");
//后配置的文件,需要调用refresh方法,触发刷新配置
iocContainer1.refresh();
ioc容器对象的使用
ioc怎么用?用它干嘛?
我们说了,ioc容器是用来存储组件并对他们进行管理的,有了容器,想要使用它,不就是打开它,拿出里面的东西嘛!!!所以呢,ioc容器对象的使用其实就是获得组件并且使用。组件是什么?是一个一个的类,类里面有属性,有方法,所以拿到组件其实就是为了得到里面的方法和属性
bean对象的读取
java
/*
介绍方法:容器.getBean
通过上面的方法就可以得到bean对象,但是要注意参数:
1.可以用id(bean的名字)【但是获得的是Object类型,需要转换类型】
2.可以用类型获取,但是要求,同类型(当前类,或者之类,或者接口的实现类)只能有一个对象交给IoC容器管理。也就是说,组件是单例的,只能有一个对象
3.根据id和类型获取
根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类型』的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到。
其实,这句话的意思就是:用接口的类型也可以获取bean组件,但是,放入ioc容器的都是实现类
*/
//方式1: 根据id获取
//没有指定类型,返回为Object,需要类型转化!
HappyComponent happyComponent =
(HappyComponent) iocContainer.getBean("bean的id标识");
//使用组件对象
happyComponent.doWork();
//方式2: 根据类型获取
//根据类型获取,但是要求,同类型(当前类,或者之类,或者接口的实现类)只能有一个对象交给IoC容器管理
//配置两个或者以上出现: org.springframework.beans.factory.NoUniqueBeanDefinitionException 问题
HappyComponent happyComponent = iocContainer.getBean(HappyComponent.class);
happyComponent.doWork();
//方式3: 根据id和类型获取
HappyComponent happyComponent = iocContainer.getBean("bean的id标识", HappyComponent.class);
happyComponent.doWork();
/*根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类型』的返回结果,
只要返回的是true就可以认定为和类型匹配,能够获取到。*/
bean的作用域和周期方法设置
周期方法设置主要就是两个方法属性:
xml
其中,下面方法的属性值其实就是方法名,随便起,对应上就行
<beans>
<bean id="beanOne" class="examples.BeanOne" init-method="init" />
<bean id="beanTwo" class="examples.BeanTwo" destroy-method="cleanup" />
</beans>
作用域就一个属性:
它是bean的属性,所以直接就是scope="???"
其中参数,单例:singleton ;多例:prototype
一般其实都是单例,默认也是
高级特性:FactoryBean的特性和使用
简介:FactoryBean接口是Spring IoC容器实例化逻辑的可插拔性点。
用于配置复杂的Bean对象,可以将创建过程存储在FactoryBean 的getObject方法!
首先,它是什么?
简单的说:它是一个接口
其次,他有什么用?
用来配置复杂的Bean对象
最后,怎么用?
java
// 实现FactoryBean接口时需要指定泛型
// 泛型类型就是当前工厂要生产的对象的类型
public class HappyFactoryBean implements FactoryBean<HappyMachine> {
private String machineName;
public String getMachineName() {
return machineName;
}
public void setMachineName(String machineName) {
this.machineName = machineName;
}
@Override
public HappyMachine getObject() throws Exception {
// 方法内部模拟创建、设置一个对象的复杂过程
HappyMachine happyMachine = new HappyMachine();
happyMachine.setMachineName(this.machineName);
return happyMachine;
}
@Override
public Class<?> getObjectType() {
// 返回要生产的对象的类型
return HappyMachine.class;
}
}
xml
<!-- FactoryBean机制 -->
<!-- 这个bean标签中class属性指定的是HappyFactoryBean,但是将来从这里获取的bean是HappyMachine对象 -->
<bean id="happyMachine7" class="com.atguigu.ioc.HappyFactoryBean">
<!-- property标签仍然可以用来通过setXxx()方法给属性赋值 -->
<property name="machineName" value="iceCreamMachine"/>
</bean>
java
@Test
public void testExperiment07() {
ApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-bean-07.xml");
//注意: 直接根据声明FactoryBean的id,获取的是getObject方法返回的对象
HappyMachine happyMachine = iocContainer.getBean("happyMachine7",HappyMachine.class);
System.out.println("happyMachine = " + happyMachine);
//如果想要获取FactoryBean对象, 直接在id前添加&符号即可! &happyMachine7 这是一种固定的约束
Object bean = iocContainer.getBean("&happyMachine7");
System.out.println("bean = " + bean);
}
以上三个代码块,分别展示了:factoryBean的实现类、Bean的配置、读取Bean对象和factoryBean对象。
我们可以这样理解:我们的组件是Bean,我们的助手是factoryBean,它帮助我们生产Bean,所以,我们在配置文件中只声明配置这里干活的"助手"------factoryBean,但是在test中,我们通过factoryBean的方法直接获得的是Bean对象。
我们再来辨析一个与上面无关的话题:
FactoryBean和BeanFactory区别
**FactoryBean **是 Spring 中一种特殊的 bean,可以在 getObject() 工厂方法自定义的逻辑创建Bean!是一种能够生产其他 Bean 的 Bean。FactoryBean 在容器启动时被创建,而在实际使用时则是通过调用 getObject() 方法来得到其所生产的 Bean。因此,FactoryBean 可以自定义任何所需的初始化逻辑,生产出一些定制化的 bean。
一般情况下,整合第三方框架,都是通过定义FactoryBean实现!!!
BeanFactory 是 Spring 框架的基础,其作为一个顶级接口定义了容器的基本行为,例如管理 bean 的生命周期、配置文件的加载和解析、bean 的装配和依赖注入等。BeanFactory 接口提供了访问 bean 的方式,例如 getBean() 方法获取指定的 bean 实例。它可以从不同的来源(例如 Mysql 数据库、XML 文件、Java 配置类等)获取 bean 定义,并将其转换为 bean 实例。同时,BeanFactory 还包含很多子类(例如,ApplicationContext 接口)提供了额外的强大功能。
总的来说,FactoryBean 和 BeanFactory 的区别主要在于前者是用于创建 bean 的接口,它提供了更加灵活的初始化定制功能,而后者是用于管理 bean 的框架基础接口,提供了基本的容器功能和 bean 生命周期管理。
基于xml方式整合三层架构组件
搭建一个三层架构案例,模拟查询全部学生(学生表)信息,持久层使用JdbcTemplate和Druid技术,使用XML方式进行组件管理!
- 准备东西:Controller层、Service层、Dao层;依赖JdbcTemplate、Druid;数据库和表信息
- Controller层需要提供完成需求的业务方法,就要调用Service层的方法:查询全部学生(学生表)信息
- Service层:向Controller层提供接口方法、完成方法编写,方法编写调用Dao层的方法
- Dao层:利用JdbcTemplate、Druid依赖,sql语句,完成对数据库的操作
- xml文件配置相关依赖、读取外部配置文件(主要是.properties文件)
- 把所有层的组件以及外部依赖进行ioc配置,并对三层组件进行DI(依赖注入)配置
- 最后在test中测试,获取ioc容器,获取controller组件,再调用组件提供的方法
基于注解的配置方式管理Bean
通过学习xml文件进行配置,管理bean,我们发现很难受:
- 组件需要被配置入ioc容器,而且组件还有属性,那么组件就需要为属性准备setter方法,用property标签在bean标签内再次用name属性,value/ref属性赋值
- 配置文件和java代码分离,编写时,需要根据java代码准备各种组件,并准备各种属性,特别麻烦
- xml配置文件的解析效率低
所以,我们学习注解方式,随时去对组件配置
怎么理解注解?
注解其实只是一个标志,起了一个名,交给一个特定的程序去识别,然后去做"交代的事"。所以,本质上,xml和注解都一样,为了告诉框架,你该怎么做。。。
怎么理解扫描?
Spring为了知道程序员们在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。
由于这只是对xml文件的代替,而且本质相同,所以我们大可以对比着学习
下面就直接展示注解,并做以下几方面的解释:
- 分别说明代替的部分
- 分别说明使用的地方
- 分别说明注解的参数
注解大全:
Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成
下面是 Spring IOC 容器配置中常用的注解,以及它们的基本说明,以表格形式展示:
注解 | 说明 |
---|---|
@Component |
标识一个类为 SpringIoc容器管理的组件。(IOC)参数:beanID |
@Service |
特定于服务层的 @Component 注解。(IOC)参数:beanID |
@Repository |
特定于数据访问层的 @Component 注解。(IOC)参数:beanID |
@Controller |
特定于表现层的 @Component 注解。(IOC)参数:beanID |
@RestController |
用于创建 RESTful Web 服务的控制器,组合了 @Controller 和 @ResponseBody 。(IOC)参数:beanID |
@Autowired |
用于自动注入依赖,可以作用于字段、构造函数、设置方法。(DI)放在被注入的字段、构造函数、 setter 方法 |
@Qualifier | 和所需类型匹配的 bean 不止一个时,可以有参数id |
@Value |
读取配置文件(.properties)的属性值,注入外部化属性.用法:"${key}", 其实就是让被注解部分与配置文件内的内容相对应 |
@Qualifier |
当有多个同类型的 bean 时,用于指定 @Autowired 要注入的 bean 名称。参数就是那个需要注入的组件名 |
@Scope |
用于指定 bean 的作用域。属性:singleton/prototype/request/session(后面的两个在WebApplicationContext环境下,但不常用) |
以上就是用纯注解配置ioc容器和Di的注解
基于配置类方式管理Bean
Spring 完全注解配置(Fully Annotation-based Configuration)是指通过 Java配置类 代码来配置 Spring 应用程序,使用注解来替代原本在 XML 配置文件中的配置。相对于 XML 配置,完全注解配置具有更强的类型安全性和更好的可读性。(注解 + java代码)
怎么配置?
步骤:
-
准备一个java程序,作为配置ioc的程序,假设起名为JavaConfig
-
目的是写一个ioc配置类,注解:@Configuration,说明是一个配置类
-
根据配置类创建IOC容器对象,选择一个合适的实现类作为ioc容器对象并创建(new)
类型名 简介 ClassPathXmlApplicationContext 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 FileSystemXmlApplicationContext 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象 AnnotationConfigApplicationContext 通过读取Java配置类创建 IOC 容器对象 WebApplicationContext 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。 -
若需要外部配置文件,配合@PropertySource(value = "classpath:jdbc.properties")注解和@Value注解读取配置文件并注入依赖给字段,@Value也可以作为参数直接传入
-
第三方依赖,直接通过方法返回对象,并记得为该方法使用 @Bean 标志是组件,可指定组件名字
-
Bean之间的依赖注入:
方案一:直接在一个 @Bean 方法中直接调用其他 @Bean 方法来获取 Bean 实例
java
@Configuration
public class JavaConfig {
@Bean
public HappyMachine happyMachine(){
return new HappyMachine();
}
@Bean
public HappyComponent happyComponent(){
HappyComponent happyComponent = new HappyComponent();
//直接调用方法即可!
happyComponent.setHappyMachine(happyMachine());
return happyComponent;
}
}
方案二:一个 @Bean 方法中直接调用其他 @Bean 方法来获取 Bean 实例+被注入部分作为参数
java
package com.atguigu.config;
import com.atguigu.ioc.HappyComponent;
import com.atguigu.ioc.HappyMachine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* projectName: com.atguigu.config
* description: 配置HappyComponent和HappyMachine关系
*/
@Configuration
public class JavaConfig {
@Bean
public HappyMachine happyMachine(){
return new HappyMachine();
}
/**
* 可以直接在形参列表接收IoC容器中的Bean!
* 情况1: 直接指定类型即可
* 情况2: 如果有多个bean,(HappyMachine 名称 ) 形参名称等于要指定的bean名称!
* 例如:
* @Bean
* public Foo foo1(){
* return new Foo();
* }
* @Bean
* public Foo foo2(){
* return new Foo()
* }
* @Bean
* public Component component(Foo foo1 / foo2 通过此处指定引入的bean)
*/
@Bean
public HappyComponent happyComponent(HappyMachine happyMachine){
HappyComponent happyComponent = new HappyComponent();
//赋值
happyComponent.setHappyMachine(happyMachine);
return happyComponent;
}
}
扩展:@import标签
@Import
注释允许从另一个配置类加载 @Bean
定义,如以下示例所示:
Java
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
现在,在实例化上下文时不需要同时指定 ConfigA.class
和 ConfigB.class
,只需显式提供 ConfigB
,如以下示例所示:
Java
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
此方法简化了容器实例化,因为只需要处理一个类,而不是要求您在构造期间记住可能大量的 @Configuration
类。
三种方式总结:
4.5.1 XML方式配置总结
1. 所有内容写到xml格式配置文件中
2. 声明bean通过<bean标签
3. <bean标签包含基本信息(id,class)和属性信息 <property name value / ref
4. 引入外部的properties文件可以通过<context:property-placeholder
5. IoC具体容器实现选择ClassPathXmlApplicationContext对象
4.5.2 XML+注解方式配置总结
1. 注解负责标记IoC的类和进行属性装配
2. xml文件依然需要,需要通过<context:component-scan标签指定注解范围
3. 标记IoC注解:@Component,@Service,@Controller,@Repository
4. 标记DI注解:@Autowired @Qualifier @Resource @Value
5. IoC具体容器实现选择ClassPathXmlApplicationContext对象
4.5.3 完全注解方式配置总结
1. 完全注解方式指的是去掉xml文件,使用配置类 + 注解实现
2. xml文件替换成使用@Configuration注解标记的类
3. 标记IoC注解:@Component,@Service,@Controller,@Repository
4. 标记DI注解:@Autowired @Qualifier @Resource @Value
5. <context:component-scan标签指定注解范围使用@ComponentScan(basePackages = {"com.atguigu.components"})替代
6. <context:property-placeholder引入外部配置文件使用@PropertySource({"classpath:application.properties","classpath:jdbc.properties"})替代
7. <bean 标签使用@Bean注解和方法实现
8. IoC具体容器实现选择AnnotationConfigApplicationContext对象
五、Spring AOP 面向切面编程
简单的说,当我们专注于业务代码的编写时,总有一些边界料来干扰我们,而且迫于某些要求,这些边角料非要不行,烦不烦?比如说:
java
package com.atguigu.proxy;
/**
* 在每个方法中,输出传入的参数和计算后的返回结果!
*/
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
这些代码相似嘛?都带了个日志的作用,代码都一摸一样,那我们为什么还要写这么多次呢?能不能写一次,然后到处用呢?在 javaSE 部分我们接触过一个设计模式--代理模式,虽然和这里不太一样,但其实用到的思维是相同的----AOP编程思维
问题来了,代理怎么还有几种呢?怎么代理?怎么写?
首先,代理分为:静态代理和动态代理。动态代理中又根据底层用到的技术不同分为:jdk的代理、cglib的代理
在 Spring 中, SpringAOP框架封装了动态代理技术,简化了使用动态代理的步骤。我们只需要写配置文件,告诉框架,指定生效范围就行了。
所以怎么写呢?
不急,既然我们要用框架,得先了解了解框架。AspectJ是早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解。因此得先导入框架和AspectJ的依赖
xml
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.6</version>
</dependency>
下面就开始写了:
初步实现步骤:
-
直接src下创建一个类作为切面类(需要注解声明:@Aspect)(切面类就是声明冗杂任务的类,就是按照位置,某个位置需要完成一个工作,就创建一个方法,这一个个方法就构成了切面类)。
-
其中,方法也叫增强,切面类也叫"增强类",切面 = 增强 + 切点。那么切点就用切入点表达式指定,并作为注解的参数
-
为这个类补全注解:
@Component注解保证这个切面类能够放入IOC容器
@EnableAspectJAutoProxy//开启AspectJ的注解支持||相当于在xml文件中加入aop:aspectj-autoproxy/
@ComponentScan("com.yym")//扫描器,扫描增强类代码
实现细节:
需要获取方法的细节信息
主要就是获取方法的权限符、返回值、参数列表、异常对象
-
JointPoint接口
需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。
- 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
- 要点2:通过目标方法签名对象获取方法名
- 要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组
Java
// @Before注解标记前置通知方法
// value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
// 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
// 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
@Before(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
public void printLogBeforeCore(JoinPoint joinPoint) {
// 1.通过JoinPoint对象获取目标方法签名对象
// 方法的签名:一个方法的全部声明信息
Signature signature = joinPoint.getSignature();
// 2.通过方法的签名对象获取目标方法的详细信息
String methodName = signature.getName();
System.out.println("methodName = " + methodName);
int modifiers = signature.getModifiers();
System.out.println("modifiers = " + modifiers);
String declaringTypeName = signature.getDeclaringTypeName();
System.out.println("declaringTypeName = " + declaringTypeName);
// 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
Object[] args = joinPoint.getArgs();
// 4.由于数组直接打印看不到具体数据,所以转换为List集合
List<Object> argList = Arrays.asList(args);
System.out.println("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
}
-
方法返回值
在返回通知中,通过**@AfterReturning**注解的returning属性获取目标方法的返回值!
Java
// @AfterReturning注解标记返回通知方法
// 在返回通知中获取目标方法返回值分两步:
// 第一步:在@AfterReturning注解中通过returning属性设置一个名称
// 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
@AfterReturning(
value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
returning = "targetMethodReturnValue"
)
public void printLogAfterCoreSuccess(JoinPoint joinPoint, Object targetMethodReturnValue) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP返回通知] "+methodName+"方法成功结束了,返回值是:" + targetMethodReturnValue);
}
-
异常对象捕捉
在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象
Java
// @AfterThrowing注解标记异常通知方法
// 在异常通知中获取目标方法抛出的异常分两步:
// 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
// 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(
value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
throwing = "targetMethodException"
)
public void printLogAfterCoreException(JoinPoint joinPoint, Throwable targetMethodException) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP异常通知] "+methodName+"方法抛异常了,异常类型是:" + targetMethodException.getClass().getName());
}
*5.5.5 重用(提取)切点表达式
1. 重用切点表达式优点
Java
// @Before注解:声明当前方法是前置通知方法
// value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
@Before(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
public void printLogBeforeCore() {
System.out.println("[AOP前置通知] 方法开始了");
}
@AfterReturning(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
public void printLogAfterSuccess() {
System.out.println("[AOP返回通知] 方法成功返回了");
}
@AfterThrowing(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
public void printLogAfterException() {
System.out.println("[AOP异常通知] 方法抛异常了");
}
@After(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
public void printLogFinallyEnd() {
System.out.println("[AOP后置通知] 方法最终结束了");
}
上面案例,是我们之前编写切点表达式的方式,发现, 所有增强方法的切点表达式相同!
出现了冗余,如果需要切换也不方便统一维护!
我们可以将切点提取,在增强上进行引用即可!
-
同一类内部引用
提取
Java
// 切入点表达式重用
@Pointcut("execution(public int com.atguigu.aop.api.Calculator.add(int,int)))")
public void declarPointCut() {}
注意:提取切点注解使用@Pointcut(切点表达式) , 需要添加到一个无参数无返回值方法上即可!
引用
Java
@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
-
不同类中引用
不同类在引用切点,只需要添加类的全限定符+方法名即可!
Java
@Before(value = "com.atguigu.spring.aop.aspect.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {
-
切点统一管理
建议:将切点表达式统一存储到一个类中进行集中管理和维护!
Java
@Component
public class AtguiguPointCut {
@Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
public void atguiguGlobalPointCut(){}
@Pointcut(value = "execution(public int *..Calculator.add(int,int))")
public void atguiguSecondPointCut(){}
@Pointcut(value = "execution(* *..*Service.*(..))")
public void transactionPointCut(){}
}
*环绕通知
这个其实,就是用一个注解,用try-catch-finally代替以上三种注解。
一个例子去对应前面的三种注解:
环绕通知对应整个 try...catch...finally 结构,包括前面四种通知的所有功能。
Java
// 使用@Around注解标明环绕通知方法
@Around(value = "com.atguigu.aop.aspect.AtguiguPointCut.transactionPointCut()")
public Object manageTransaction(
// 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,
// Spring会将这个类型的对象传给我们
ProceedingJoinPoint joinPoint) {
// 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
Object[] args = joinPoint.getArgs();
// 通过ProceedingJoinPoint对象获取目标方法的签名对象
Signature signature = joinPoint.getSignature();
// 通过签名对象获取目标方法的方法名
String methodName = signature.getName();
// 声明变量用来存储目标方法的返回值
Object targetMethodReturnValue = null;
try {
// 在目标方法执行前:开启事务(模拟)
log.debug("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));
// 过ProceedingJoinPoint对象调用目标方法
// 目标方法的返回值一定要返回给外界调用者
targetMethodReturnValue = joinPoint.proceed(args);
// 在目标方法成功返回后:提交事务(模拟)
log.debug("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);
}catch (Throwable e){
// 在目标方法抛异常后:回滚事务(模拟)
log.debug("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
}finally {
// 在目标方法最终结束后:释放数据库连接
log.debug("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
}
return targetMethodReturnValue;
}
切面优先级设置
介绍一个标签:
java
@Order(数字)
/*其中,数字越大,切面优先级越低
但是注意一个问题:这个切面其实是一个包裹,说白了,切面就是一层层的包裹着核心代码
所以,优先级越高,先执行,但是最后结束*/
六、声明式事务
在说声明式事务之前,我们先来认识一下什么叫做编程式事务
编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager
)来实现编程式事务。
下面是一个编程事务的示例:
java
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 业务代码
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
我们会发现,只要写一个事务,就是这套代码,复用性太差。细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
因此,我们提出了一种方便程序员的方式,声明式事务。
在声明式事务中,和编程式事务最大的区别就是:
- 不用自己写代码管理事务
- 通过配置文件或者注解来控制事务
什么是声明式事务?
声明式事务是指使用注解或 XML 配置的方式来控制事务的提交和回滚。
开发者只需要添加配置即可, 具体事务的实现由第三方框架实现,避免我们直接进行事务操作!
使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。
区别:
- 编程式事务需要手动编写代码来管理事务
- 而声明式事务可以通过配置文件或注解来控制事务。
Spring事务管理器
没错,有了声明式事务这个方法后,我们就可以通过配置文件来使用第三方框架进行事务操作,但是,有一个问题是:你用哪一个框架?每一个框架的代码编写不都是一样的,所以Spring需要整合他们,都能用我这个框架,怎么做?那就是提出事务管理器接口
所以事实上,事务管理器就是实现了事务管理器接口的实现类,目前spring为第三方框架准备了三个实现类
还有一个是:JSP TM 专门为jsp框架实现的事务管理器
我们要学的Mybatis框架和JDBC还有JDBCTemplate框架都是DataSourceTransactionManager类事务管理器。要使用需要导入相应的依赖:
- spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)
- spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManager
要使用其他的事务管理器我们只需要导入:
spring-orm的依赖
DataSourceTransactionManager类中的主要方法:
- doBegin():开启事务
- doSuspend():挂起事务
- doResume():恢复挂起的事务
- doCommit():提交事务
- doRollback():回滚事务
基于注解的声明式事务
-
引入声明式事务注解需要的依赖
spring-tx、spring-jdbc
-
@EnableTransactionManagement在配置类上注解,开启事务支持
-
在配置类上装配事务管理实现对象(简单说就是增加一个组件用于返回声明式事务的框架对象),并放入ioc容器(@Bean)
-
在需要事务的方法上或类上(类中所有方法),使用声明事务注解@Transactional,可以加参数,设置事务为只读、超时时间、事务异常(指定异常类型回滚)、事务隔离级别、事务传播行为,具体知识点看下面:
java/** * 1.只读模式 * 只读模式可以提高查询事务的效率,推荐 如果事务中只有查询语句,使用只读模式 * 2.查询本来不需要设置事务,为什么要设置为只读模式的事务? * 解释:因为一般情况下,都是通过类去添加事务,因此会覆盖到类下的每一个方法开启事务, * 为了提高查询事务的效率,开启只读模式,提高效率 * 3.方法上的事务注解覆盖类上的事务注解 * 4.业务方法中,默认存在指定异常回滚(默认是RuntimeException);指定异常不回滚 * 5.指定异常回滚和指定异常不回滚 * 我们可以指定Exception异常使得一旦发生异常就回滚 rollbackFor = Exception.class * noRollbackFor = 回滚范围内,控制某个异常不回滚 * 6.设置事务隔离级别:Isolation = ... 推荐设置为 读已提交,效率最高 * 7.事务之间存在调用情况,子事务是加入父事务?还是独立事务? * 这就由事务的传播行为决定,可以在子事务上设置 传播行为 * 对应属性:propagation (推荐使用默认值) * 常用的两个属性值:REQUIRED(默认值) :父方法有事务则加入,没事务就新建;REQUIRED_NEW:独立事务 * 注意:同类下不产生任何事务传播行为(搞懂到底为什么?) */