@SpingFrameWork

@SpingFrameWork

一、技术体系结构

  1. 单一架构,一个大的项目,一个工程,导出war包在一个tomcat上运行(主要用spring、springMVC、Mybatis)
  2. 分布式架构,一个项目,但是有多个模块,但是每个模块都在一个tomcat上运行

这里需要与前后端分离、前后端混合做个辨析,架构是说软件系统的设计,后者是开发模式

  1. 框架 = jar包+配置文件,也就是说框架是可以通过配置文件个性化的

二、SpringFrameWork

  1. 其实是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管理好处

  1. 不用我们管理,方便管理和配置
  2. 降低了组件之间的耦合性,我们都把组件交给spring,组件之间的直接交流减少,比如:在A类中new了一个B对象
  3. 提高了代码的可重用性和可维护性,耦合性降低自然更好维护,一个组件可以到处用,不需要再反复的先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配置类方式

  1. XML配置方式:是Spring框架最早的配置方式之一,通过在XML文件中定义Bean及其依赖关系、Bean的作用域等信息,让Spring IoC容器来管理Bean之间的依赖关系。该方式从Spring框架的第一版开始提供支持。
  2. 注解方式:从Spring 2.5版本开始提供支持,可以通过在Bean类上使用注解来代替XML配置文件中的配置信息。通过在Bean类上加上相应的注解(如@Component, @Service, @Autowired等),将Bean注册到Spring IoC容器中,这样Spring IoC容器就可以管理这些Bean之间的依赖关系。
  3. Java配置类方式:从Spring 3.0版本开始提供支持,通过Java类来定义Bean、Bean之间的依赖关系和配置信息,从而代替XML配置文件的方式。Java配置类是一种使用Java编写配置信息的方式,通过@Configuration、@Bean等注解来实现Bean和依赖关系的配置。

为了迎合当下开发环境,我们将以配置类+注解方式为主进行讲解!

四、SpringIOC实践和应用

SpringIOC/DI实现步骤:

  1. 编写配置文件(三种方法)
  2. 读取配置文件,并选择合适的容器类进行实例化
  3. 通过容器对象,获取组件

基于XML的配置方式和组件管理

学习重点:如何通过定义XML配置文件,声明组件类信息,交给 Spring 的 IoC 容器进行组件管理!

  1. 准备项目

    1. 创建maven工程(spring-ioc-xml-01)

    2. 导入SpringIoC相关依赖

      pom.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文件中声明组件信息(事实上,就是我们告诉容器,怎么去创建组件对象)

**第一种:**组件类中是无参构造函数

package com.atguigu.ioc;


public class HappyComponent {

    //默认包含无参数构造函数

    public void doWork() {
        System.out.println("HappyComponent.doWork");
    }
}
<!-- 实验一 [重要]创建bean -->
<bean id="happyComponent" class="com.atguigu.ioc.HappyComponent"/>

**第二种:**组件中是静态工厂方法

public class ClientService {
  private static ClientService clientService = new ClientService();
  private ClientService() {}

    //静态工厂方法
  public static ClientService createInstance() {
    return clientService;
  }
}
<bean id="clientService"
  class="examples.ClientService"
  factory-method="createInstance"/>
  • class属性:指定工厂类的全限定符!
  • factory-method: 指定静态工厂方法,注意,该方法必须是static方法。

**第三种:**组件中是实例工厂方法

public class DefaultServiceLocator {

  private static ClientServiceImpl clientService = new ClientServiceImpl();

    //实例工厂方法
  public ClientServiceImpl createClientServiceInstance() {
    return clientService;
  }
}
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 的依赖注入。

基于构造函数的依赖注入

这里就不分几个参数的构造函数了,其实都是一个道理:

<!-- 下面都是多参数的例子,但是如果是单参数,其实就是只有一个<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方法

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...
}
<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"/>

以上示例中,要说明的是:

/*
1.property标签用于注入带有setter方法的组件
2.该标签下的name属性,就是setter方法中去掉set并且首字母小写的那段字符串,其实就是后面的“宾语”
3.还有ref属性和value属性是 二选一,这取决于参数的类型是“引用”还是“直接”,其中ref还是引用bean的id值,value随便赋值好了
*/

ioc容器的创建和使用

上面的实验只是讲解了 如何在XML格式的配置文件编写 IoC 和 DI 配置

下面我们要说的是 核心 ioc容器对象的声明,为什么说是核心?因为它是环境,没有它读取那些配置,上面都白做功夫了,下面将要介绍容器的实例化和使用

容器的实例化
/*
我们用下面的例子讲:有两种实例化方式
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对象的读取

/*
介绍方法:容器.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的作用域和周期方法设置

周期方法设置主要就是两个方法属性:

其中,下面方法的属性值其实就是方法名,随便起,对应上就行
<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对象

最后,怎么用?

// 实现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;
    }
}
<!-- FactoryBean机制 -->
<!-- 这个bean标签中class属性指定的是HappyFactoryBean,但是将来从这里获取的bean是HappyMachine对象 -->
<bean id="happyMachine7" class="com.atguigu.ioc.HappyFactoryBean">
    <!-- property标签仍然可以用来通过setXxx()方法给属性赋值 -->
    <property name="machineName" value="iceCreamMachine"/>
</bean>
@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方式进行组件管理!

  1. 准备东西:Controller层、Service层、Dao层;依赖JdbcTemplate、Druid;数据库和表信息
  2. Controller层需要提供完成需求的业务方法,就要调用Service层的方法:查询全部学生(学生表)信息
  3. Service层:向Controller层提供接口方法、完成方法编写,方法编写调用Dao层的方法
  4. Dao层:利用JdbcTemplate、Druid依赖,sql语句,完成对数据库的操作
  5. xml文件配置相关依赖、读取外部配置文件(主要是.properties文件)
  6. 把所有层的组件以及外部依赖进行ioc配置,并对三层组件进行DI(依赖注入)配置
  7. 最后在test中测试,获取ioc容器,获取controller组件,再调用组件提供的方法

基于注解的配置方式管理Bean

通过学习xml文件进行配置,管理bean,我们发现很难受:

  1. 组件需要被配置入ioc容器,而且组件还有属性,那么组件就需要为属性准备setter方法,用property标签在bean标签内再次用name属性,value/ref属性赋值
  2. 配置文件和java代码分离,编写时,需要根据java代码准备各种组件,并准备各种属性,特别麻烦
  3. xml配置文件的解析效率低

所以,我们学习注解方式,随时去对组件配置

怎么理解注解?

注解其实只是一个标志,起了一个名,交给一个特定的程序去识别,然后去做”交代的事“。所以,本质上,xml和注解都一样,为了告诉框架,你该怎么做。。。

怎么理解扫描?

Spring为了知道程序员们在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。

由于这只是对xml文件的代替,而且本质相同,所以我们大可以对比着学习

下面就直接展示注解,并做以下几方面的解释:

  1. 分别说明代替的部分
  2. 分别说明使用的地方
  3. 分别说明注解的参数

注解大全:

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代码)

怎么配置?

步骤:

  1. 准备一个java程序,作为配置ioc的程序,假设起名为JavaConfig

  2. 目的是写一个ioc配置类,注解:@Configuration,说明是一个配置类

  3. 根据配置类创建IOC容器对象,选择一个合适的实现类作为ioc容器对象并创建(new)

    类型名简介
    ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
    FileSystemXmlApplicationContext通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
    AnnotationConfigApplicationContext通过读取Java配置类创建 IOC 容器对象
    WebApplicationContext专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。
  4. 若需要外部配置文件,配合@PropertySource(value = “classpath:jdbc.properties”)注解和@Value注解读取配置文件并注入依赖给字段,@Value也可以作为参数直接传入

  5. 第三方依赖,直接通过方法返回对象,并记得为该方法使用 @Bean 标志是组件,可指定组件名字

  6. Bean之间的依赖注入:

方案一:直接在一个 @Bean 方法中直接调用其他 @Bean 方法来获取 Bean 实例

@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 实例+被注入部分作为参数

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 定义,如以下示例所示:

@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.classConfigB.class ,只需显式提供 ConfigB ,如以下示例所示:

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 面向切面编程

简单的说,当我们专注于业务代码的编写时,总有一些边界料来干扰我们,而且迫于某些要求,这些边角料非要不行,烦不烦?比如说:

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的依赖

<!-- 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>

下面就开始写了:

初步实现步骤:

  1. 直接src下创建一个类作为切面类(需要注解声明:@Aspect)(切面类就是声明冗杂任务的类,就是按照位置,某个位置需要完成一个工作,就创建一个方法,这一个个方法就构成了切面类)。

  2. 其中,方法也叫增强,切面类也叫“增强类”,切面 = 增强 + 切点。那么切点就用切入点表达式指定,并作为注解的参数

  3. 为这个类补全注解:

    @Component注解保证这个切面类能够放入IOC容器

    @EnableAspectJAutoProxy//开启AspectJ的注解支持||相当于在xml文件中加入aop:aspectj-autoproxy/

    @ComponentScan(“com.yym”)//扫描器,扫描增强类代码

实现细节:

需要获取方法的细节信息

主要就是获取方法的权限符、返回值、参数列表、异常对象

  1. JointPoint接口

    需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。

    • 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
    • 要点2:通过目标方法签名对象获取方法名
    • 要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组
// @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);
}
  1. 方法返回值

    在返回通知中,通过**@AfterReturning**注解的returning属性获取目标方法的返回值!

// @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);
}
  1. 异常对象捕捉

    在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象

// @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. 重用切点表达式优点
 // @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后置通知] 方法最终结束了");
}
  上面案例,是我们之前编写切点表达式的方式,发现, 所有增强方法的切点表达式相同!

  出现了冗余,如果需要切换也不方便统一维护!

  我们可以将切点提取,在增强上进行引用即可!
  1. 同一类内部引用

    提取

// 切入点表达式重用
@Pointcut("execution(public int com.atguigu.aop.api.Calculator.add(int,int)))")
public void declarPointCut() {}
  注意:提取切点注解使用@Pointcut(切点表达式) , 需要添加到一个无参数无返回值方法上即可!

  引用
@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
  1. 不同类中引用

    不同类在引用切点,只需要添加类的全限定符+方法名即可!

@Before(value = "com.atguigu.spring.aop.aspect.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {
  1. 切点统一管理

    建议:将切点表达式统一存储到一个类中进行集中管理和维护!

@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 结构,包括前面四种通知的所有功能。

// 使用@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;
}
切面优先级设置

介绍一个标签:

@Order(数字)
/*其中,数字越大,切面优先级越低
但是注意一个问题:这个切面其实是一个包裹,说白了,切面就是一层层的包裹着核心代码
所以,优先级越高,先执行,但是最后结束*/

六、声明式事务

在说声明式事务之前,我们先来认识一下什么叫做编程式事务

编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager)来实现编程式事务。

下面是一个编程事务的示例:

Connection conn = ...;
  
try {
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    // 核心操作
    // 业务代码
    // 提交事务
    conn.commit();
  
}catch(Exception e){
  
    // 回滚事务
    conn.rollBack();
  
}finally{
  
    // 释放数据库连接
    conn.close();
  
}

我们会发现,只要写一个事务,就是这套代码,复用性太差。细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。

因此,我们提出了一种方便程序员的方式,声明式事务。

在声明式事务中,和编程式事务最大的区别就是:

  1. 不用自己写代码管理事务
  2. 通过配置文件或者注解来控制事务

什么是声明式事务?

声明式事务是指使用注解或 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():回滚事务

基于注解的声明式事务

  1. 引入声明式事务注解需要的依赖

    spring-tx、spring-jdbc

  2. @EnableTransactionManagement在配置类上注解,开启事务支持

  3. 在配置类上装配事务管理实现对象(简单说就是增加一个组件用于返回声明式事务的框架对象),并放入ioc容器(@Bean)

  4. 在需要事务的方法上或类上(类中所有方法),使用声明事务注解@Transactional,可以加参数,设置事务为只读、超时时间、事务异常(指定异常类型回滚)、事务隔离级别、事务传播行为,具体知识点看下面:

    /**
     * 1.只读模式
     *  只读模式可以提高查询事务的效率,推荐 如果事务中只有查询语句,使用只读模式
     * 2.查询本来不需要设置事务,为什么要设置为只读模式的事务?
     * 解释:因为一般情况下,都是通过类去添加事务,因此会覆盖到类下的每一个方法开启事务,
     * 为了提高查询事务的效率,开启只读模式,提高效率
     * 3.方法上的事务注解覆盖类上的事务注解
     * 4.业务方法中,默认存在指定异常回滚(默认是RuntimeException);指定异常不回滚
     * 5.指定异常回滚和指定异常不回滚
     * 我们可以指定Exception异常使得一旦发生异常就回滚   rollbackFor = Exception.class
     *  noRollbackFor = 回滚范围内,控制某个异常不回滚
     * 6.设置事务隔离级别:Isolation = ...  推荐设置为 读已提交,效率最高
     * 7.事务之间存在调用情况,子事务是加入父事务?还是独立事务?
     * 这就由事务的传播行为决定,可以在子事务上设置 传播行为
     * 对应属性:propagation  (推荐使用默认值)
     * 常用的两个属性值:REQUIRED(默认值) :父方法有事务则加入,没事务就新建;REQUIRED_NEW:独立事务
     * 注意:同类下不产生任何事务传播行为(搞懂到底为什么?)
     */
    
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值