Spring基础

启发

1 软件开发原则之OCP开闭原则

OCP时软件七大开发原则当中最基本的一个原则:开闭原则

  • 对扩展开放
  • 对修改关闭

OCP原则是最核心最基本的,其他的六个原则都是为这个原则服务的

OCP原则即开放封闭原则(Open-Closed Principle),是面向对象设计中的一个核心原则,旨在确保软件实体(如类、模块、函数等)能够适应未来的变化,同时保持系统的稳定性和可维护性。

OCP原则的核心思想是软件实体应该是可扩展的,而对修改应该是封闭的。这意味着,当面对新的需求或变化时,应该通过扩展现有代码来适应这些新情况,而不是直接修改已有的代码。这种原则的实现依赖于抽象编程,而不是具体编程,因为抽象相对稳定。通过面向对象的继承和多态机制,可以实现抽象体的继承,通过覆写其方法来改变固有行为,从而实现新的扩展方法。这种机制建立在Liskov替换原则合成/聚合复用原则的基础上。

OCP原则的重要性在于它能够帮助软件设计者在不修改原有系统的情况下,实现灵活的系统扩展。这有助于降低软件各部分之间的耦合性,提高系统的适应性、灵活性和稳定性。遵循OCP原则的设计模式主要有[Template Method模式](https://m.baidu.com/s?word=Template Method模式&sa=re_dqa_zy)和Strategy模式

然而,OCP原则的应用也面临一些挑战和局限性,如过度抽象可能导致接口过于复杂、性能开销增加以及学习成本提高等。因此,在遵循OCP原则时,需要权衡好抽象程度和易用性,谨慎使用抽象类和接口,特别是在性能敏感的场景中。此外,单一职责原则、里氏替换原则和依赖倒置原则等其他设计原则与OCP原则相互补充,共同构成了面向对象设计的核心思想。

2 依赖倒置原则(DIP原则)

2.1 概念

依赖倒置原则(Dependence Inversion Principle, DIP), 其含义:

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象 -->避免底层修改导致顶层‘也要进行修改
  2. 抽象不应该依赖细节, 细节应该依赖于抽象
  3. 面向接口编程,不要面向实现编程 -->面向抽象编程

2.2 什么是依赖呢?

这里的依赖关系我们理解为UML关系中的依赖。简单的说就是A use B,那么A对B产生了依赖。具体请看下面的例子。

从上图中我们可以发现, 类A中的方法a()里面用到了类B, 其实这就是依赖关系, A依赖了B. 需要注意的是: 并不是说A中声明了B就叫依赖, 如果引用了但是没有真实调用方法, 那么叫做零耦合关系. 如下图:

2.3 依赖的关系种类

  1. 零耦合关系:如果两个类之间没有耦合关系,称之为零耦合

  2. 直接耦合关系: 具体耦合发生在两个具体类(可实例化的)之间,经由一个类对另一个类的直接引用造成。

  3. 抽象耦合关系: 抽象耦合关系发生在一个具体类和一个抽象类(或者java接口)之间,使两个必须发生关系的类之间存在最大的灵活性。

依赖倒转原则就是要针对接口编程,不要针对实现编程。这就是说,应当使用接口或者抽象类进行变量的类型声明参数的类型声明方法的返回类型说明以及数据类型的转换等。

2.4 DIP的目的

降低程序的耦合度,提高扩展力

3 控制反转(IoC)

3.1 什么是控制反转(IoC)

控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。

  • 反转的两件事:

    1. 不在程序中采用硬编码的方式来new对象了(将new对象的权力交出去)

    2. 不在程序中采用硬编码的方式来维护对象的关系了(将对象的维护权交出去)

  • 控制反转是一种编程思想。或者叫一种新型的设计模式。

3.2 作用

管理对象的创建和依赖关系的维护。对象的创建并不是一件简单的事,在对象关系比较复杂时,如果依赖关系需要程序猿来维护的话,那是相当头疼的

解耦,由容器去维护具体的对象

托管了类的产生过程,比如我们需要在类的产生过程中做一些处理,最直接的例子就是代理,如果有容器程序可以把这部分处理交给容器,应用程序则无需去关心类是如何完成代理的

4 Spring框架

  • Spring框架实现了控制反转IoC的这种思想

    • Spring框架可以帮你new对象

    • Spring框架可以帮你维护对象和对象之间的关系

  • Spring是一个实现了IoC思想的容器

  • 控制反转实现的方式有多种,其中比较重要的叫做:依赖注入Dependency Injection, 简称DI),依赖查找

  • 控制反转是思想。依赖注入是这种思想的具体实现。

  • 依赖注入包括常见的两种方式:

    • setter方法注入
    • 构造方法注入
    • 接口注入(Spring4 弃用)
  • 依赖注入:

    • 依赖:A对象和B对象的关系,对象之间的关系
    • 注入:是一种手段,通过这种手段,可以让A对象和B对象产生关系
    • 依赖注入:对象A和对象B之间的关系,靠依靠注入的手段来维护

Spring概述

1 简介

1.1 什么是 Spring 框架

Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转(IoC)面向切面编程(AOP)。Spring 是可以在 Java SE/EE 中使用的轻量级开源框架。

官网: Spring | Home

Spring 的主要作用就是为代码 “解耦”,降低代码间的耦合度。它使得对象和对象(模块和模块)之间的关系不是使用代码来关联,而是通过配置来说明。即在 Spring 中说明对象(模块)的关系。 这减轻了对项目模块之间以及类和类之间的管理难度, 帮助开发人员创建对象,管理对象之间的关系。

Spring 是一个框架,是一个半成品的软件。由 20 个模块组成。它是一个用来管理对象的容器,容器是拿来装东西的,而 Spring 容器不装文本、数字,装的是对象。所以 Spring 是存储对象的容器。 
我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。

Spring全家桶:Spring , SpringMVC ,Spring Boot , Spring Cloud

1.2 Spring 的优点

1)轻量
Spring 框架使用的 jar 文件都比较小,一般在 1M 以下。Spring 核心功能的所需的 jar 文件总共在 3M 左右。Spring 框架运行占用的资源少,运行效率高,而且不依赖其他 jar 文件。

依赖:classA 类中含有 classB 的实例,在 classA 中调用 classB 的方法完成功能,即 classA 对 classB 有依赖。

2)面向接口编程,解耦合
Spring 提供了 IoC 控制反转,由容器管理对象以及对象之间的依赖关系。原来在程序代码中的对象创建方式,现在由容器完成。使得对象之间的依赖解耦合。

3)支持 AOP 编程
通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 在 Spring 中轻松应付,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。

4)方便集成各种优秀框架
Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring 提供了对各种优秀框架(如 Struts,Hibernate、MyBatis等)的直接支持,简化框架的使用。Spring 像排插一样,其他框架是插头,可以容易地组合到一起。需要使用哪个框架,就把这个插头插入排插,不需要的话可以轻易地移除。

2 Spring6的8大模块

在这里插入图片描述

Spring Core

Spring Core是Spring框架的核心模块,提供了IoC(Inversion of Control)容器的实现和支持。它负责创建、配置和管理应用程序中的对象,并通过依赖注入的方式解耦组件之间的依赖关系。

Spring AOP

Spring AOP模块实现了面向切面编程(AOP)的支持。它允许开发者通过定义切点和切面,将横切关注点(如日志记录、性能监控等)与业务逻辑分离,从而提高代码的模块化和可维护性。

Spring Web MVC

Spring Web MVC是Spring框架的Web应用程序开发模块。它提供了一种基于MVC(Model-View-Controller)的架构,用于构建灵活、可扩展的Web应用程序。开发者可以使用注解或配置文件定义控制器、视图和模型,并实现Web请求的处理和响应。

Spring WebFlux

Spring WebFlux是Spring框架的响应式Web开发模块。它基于反应式编程模型,提供了一种异步、非阻塞的方式处理Web请求。开发者可以使用注解或函数式编程风格定义处理器函数,并利用响应式流处理请求和响应。

Spring Web

Spring Web模块是Spring框架的Web应用程序支持模块,提供了与Servlet API和其他Web相关技术的集成。它包括与Web安全、文件上传、WebSockets等相关的功能和工具,帮助开发者构建全功能的Web应用程序。

Spring DAO

Spring DAO模块提供了对数据访问对象(DAO)的支持。它简化了与数据库的交互,提供了一组抽象和实现,用于执行CRUD操作、批处理、存储过程调用等。开发者可以集成各种数据访问技术(如JDBC、Hibernate、JPA等)来实现灵活和可扩展的数据访问层。

Spring ORM

Spring ORM模块用于集成和支持各种对象关系映射(ORM)框架,如Hibernate、JPA等。它提供了事务管理、异常转换和对象关系映射等功能,简化了与关系型数据库的交互。

Spring Context

Spring Context是Spring框架的核心模块之一,实现了IoC容器的功能。它负责管理和组织应用程序中的各个组件,包括Bean管理、依赖注入、生命周期管理、事件机制等。Spring Context提供了一个上下文环境,使得开发者能够更方便地构建和管理应用程序。

3 Spring的特点

  1. 依赖注入(Dependency Injection, DI)

    • 特点:通过控制反转(Inversion of Control, IoC)容器来管理对象的依赖关系,使代码更加松耦合、易于测试和维护。

    • 好处:减少对象之间的耦合性,提升代码的可测试性和可维护性。

  2. 面向切面编程(Aspect-Oriented Programming, AOP)

    • 特点:支持在不修改代码的情况下添加横切关注点(如事务管理、日志记录、权限控制等)。

    • 好处:实现关注点分离,提高代码的模块化和重用性。

  3. 丰富的模块支持

    • 特点:Spring 提供了多个模块,如 Spring Core、Spring AOP、Spring ORM、Spring Data、Spring Security、Spring MVC、Spring WebFlux 等。

    • 好处:可以根据需求选择和组合使用不同的模块,提供全面的功能支持,简化开发过程。

  4. 轻量级

    • 特点:Spring 框架本身比较轻量,核心容器的基本版本大约只有几兆字节。Spring是非侵入式的,Spring应用中的对象不依赖于Spring的特定类

    • 好处:启动速度快,资源占用少,适合构建高性能应用。

  5. 声明式编程

    • 特点:支持通过注解和 XML 配置来声明式地管理事务、缓存、安全性等功能。

    • 好处:简化配置,减少样板代码,提高开发效率。

  6. 集成能力强

    • 特点:Spring 与各种主流技术和框架无缝集成,如 Hibernate、JPA、MyBatis、JMS、RabbitMQ、Kafka 等。

    • 好处:灵活性高,能够与现有系统和技术栈无缝结合。

  7. 灵活的配置

    • 特点:支持多种配置方式,包括 XML 配置、注解配置和 Java 配置类。

    • 好处:根据开发人员的偏好和项目需求,选择最合适的配置方式,提高开发效率。

  8. Spring Boot 的支持

    • 特点:Spring Boot 是基于 Spring 框架的一个子项目,提供了开箱即用的默认配置,简化了 Spring 应用的启动和部署过程。

    • 好处:大大简化了 Spring 应用的开发和部署,特别适合快速开发和微服务架构。

  9. 强大的测试支持

    • 特点:提供了对 JUnit 和 TestNG 的全面支持,能够方便地进行单元测试、集成测试和端到端测试。

    • 好处:提高代码质量,确保应用的可靠性和稳定性。

每一个被Spring管理的对象叫做Bean

4 Spring入门程序

依赖

<!--Spring context依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.1.4</version>
</dependency>

目录结构

src_
   |-main
   |   |-java
   |   |   |-com.example.spring6
   |   |   |   |-bean
   |   |   |   |-dao
   |   |   |   |-...
   |   |-resource
   |   |   |-spring.xml
   |   |   |-...
   |-test
   |   |-...

spring6.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--spring配置文件,文件名可改-->
    <!--放在resource根目录下,相当于放在类的根目录下-->

    <!--配置bean,这样spring才能帮助我们管理这个对象-->

    <!--
        bean的两个重要属性
            id:bean的唯一标识,不能重复
            class:全限定类名
    -->
    <bean id="userBean" class="com.klein.spring6.bean.User"/>

    <bean id="userDaoBean" class="com.klein.spring6.dao.impl.UserDaoImplForMySql"/>
</beans>

Test

@Test
public void testFirstSpringCode(){
    // 1.获取spring对象
    /*
      ApplicationContext:应用上下文。spring容器,是一个接口
      ClassPathXmlApplicationContext:专门从类路径中加载spring配置文件的一个spring的上下文对象
      这段代码执行后,就会解析spring.xml文件,并且实例化所有的bean对象,放到spring容器当中
     */
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml", "xml/beans.xml");

    // 2.根据bean的id获取容器中的对象
    Object userBean = applicationContext.getBean("userBean");
    System.out.println("userBean = " + userBean);

    //Object userDaoBean = applicationContext.getBean("userDaoBean");
    UserDao userDaoBean = applicationContext.getBean("userDaoBean", UserDao.class);
    userDaoBean.insert();
    System.out.println("userDaoBean = " + userDaoBean);

    //Date nowTime = (Date)applicationContext.getBean("nowTime");
    Date nowTime = applicationContext.getBean("nowTime", Date.class);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS");
    String time = sdf.format(nowTime);
    System.out.println("time = " + time);
}

4.1 spring程序剖析

  • 每一个bean的id都要唯一

  • 通过反射机制来创建实例对象

    Class clazz = Class.forName("类的全限定名");
    Object obj = clazz.newInstance();
    
  • 创建好的对象以Map<Sting, Object>形式存储,key是Bean的id, value是Bean对象

  • 配置文件中的类可以使用JDK中的类(含有无参构造方法的)

  • ApplicationContext接口的顶层父接口是:BeanFactory(Bean工厂,产生Bean对象的工厂对象)

    • BeanFactory是IoC容器的顶级接口
    • Spring底层IoC实现;XML解析 + 工厂模式 + 反射机制

4.2 Spring6 启用Log4j2日志框架

依赖

<!--log4j2框架-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.19.0</version>
</dependency>

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j2-impl</artifactId>
    <version>2.19.0</version>
</dependency>

类的根目录下提供log4j2.xml配置文件

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
    
    <loggers>
        <!--
            level指定日志级别,从低到高的优先级
                ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
        -->
        <root level="DEBUG">
            <appender-ref ref="spring6log"/>
        </root>
    </loggers>
    
    <appenders>
        <!--输出日志信息到控制台-->
        <console name="spring6log" target="SYSTEM_OUT">
            <!--控制日志输出格式-->
            <PatternLayout pattern="%d{yyyy-MM-dd hh:mm:ss SSS} [%t] %-3level %logger{1024} -%msg%n"/>
        </console>
    </appenders>
</configuration>

Spring对IoC的实现

1 IoC控制反转

  • 控制反转是一种思想
  • 控制反转是为了降低程序的耦合度,提高程序的扩展力,达到OCP原则,达到DIP原则
  • 控制反转:
    • 将对象的创建权力交出去,交给第三放容器负责
    • 将对象和对象之间的关系的维护权交出去,交给第三方容器负责
  • 实现
    • DI(Dependency Injection):依赖注入

2 依赖注入

  • 依赖注入实现了控制反转的思想

Spring通过依赖注入的方式来完成Bean管理的

Bean管理说的是:Bean对象的创建,以及Bean属性的赋值(或者叫Bean对象之间关系的维护)

  • 依赖注入:
    • 依赖指的是对象和对象之间的关联关系
    • 注入指的是一种数据传递行为,通过注入行为来让对象和对象产生关系
  • 依赖注入常见实现方式
    • set注入
    • 构造注入

2.1 set注入

set注入,基于set方法实现,底层会通过反射机制调用属性对应的set方法然后给属性赋值。这种方式要求必须对外提供set方法

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userDaoBean" class="com.klein.dao.impl.UserDaoImpl"/>

    <bean id="userServiceBean" class="com.klein.service.impl.UserServiceImpl">
        <!--想让Spring调用对应的set方法,需要配置property标签-->
        <!--name属性:set的方法名,去掉set后小驼峰-->
        <!--ref属性:引用,reference。ref后面指定要注入的Bean-->
        <property name="userDao" ref="userDaoBean"/>
    </bean>
    
</beans>

2.2 构造注入

通过构造方法给属性赋值

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userDaoBean" class="com.klein.dao.impl.UserDaoImpl"/>
    <bean id="csBean" class="com.klein.service.impl.CustomerServiceImpl">
        <!--构造注入-->
        <!--
            index:指定构造方法的参数下标,起始下标是0
            ref:对象引用,指定注入的bean的id
        -->
        <constructor-arg index="0" ref="userDaoBean"/>
        <!--也可以使用name属性值来代替index,name属性值为方法参数名-->
        <!--
        <constructor-arg name="userDao" ref="userDaoBean"/>
        -->
        <!--根据类型注入:不指定index、name,让spring自己做类型匹配-->
    </bean>

</beans>

3 set注入专题

3.1 注入外部bean

外部bean的特点:bean定义到外面,在property标签中使用ref属性进行注入。通常这种方式是常用

<bean id="orderServiceBean" class="com.klein.service.impl.OrderServiceImpl">
    <!--注入外部bean-->
    <property name="orderDao" ref="orderBean"/>
</bean>

3.2 注入内部bean

<bean id="orderServiceBean2" class="com.klein.service.impl.OrderServiceImpl">
    <property name="orderDao">
        <!--在property标签中使用嵌套的bean标签,这就是内部bean-->
        <bean class="com.klein.dao.impl.OrderDaoImpl"/>
    </property>
</bean>

3.3 注入简单类型

<!--注入简单类型-->
<bean id="userBean" class="com.klein.bean.User">
    <!--给简单类型赋值,使用value, String类型属于简单类型-->
    <property name="username" value="张三"/>
    <property name="password" value="123"/>
    <property name="age" value="18"/>
</bean>

简单数据类型

// package org.springframework.util;
// ClassUtil

public static boolean isSimpleValueType(Class<?> type) {
    return (!isVoidType(type) &&
            (isPrimitiveOrWrapper(type) || // 基本数据类型及其包装类
            Enum.class.isAssignableFrom(type) ||
            CharSequence.class.isAssignableFrom(type) ||
            Number.class.isAssignableFrom(type) ||
            Date.class.isAssignableFrom(type) ||  // java.util.Date
            Temporal.class.isAssignableFrom(type) || // Java8提供的时间和时区类型
            ZoneId.class.isAssignableFrom(type) ||
            TimeZone.class.isAssignableFrom(type) ||
            File.class.isAssignableFrom(type) ||
            Path.class.isAssignableFrom(type) ||
            Charset.class.isAssignableFrom(type) ||
            Currency.class.isAssignableFrom(type) ||
            InetAddress.class.isAssignableFrom(type) ||
            URI.class == type ||
            URL.class == type ||
            UUID.class == type ||
            Locale.class == type ||  // 语言类
            Pattern.class == type ||
            Class.class == type));
}
  • Date类型一般不当作简单类型,一般会采用ref给Date属性赋值。当作简单类型赋值有格式要求

    <property name="birth" value="Wed Jun 12 19:25:52 CST 2024"/>
    

    简单应用:数据源配置管理

    <!--让spring管理数据源-->
    <bean id="myDataSource" class="com.klein.jdbc.MyDataSource">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>
    

3.4 级联属性赋值

Clazz.java

public class Clazz{
    private String name;
    
    public void setName(String name){
        this.name = name;
    }
    
    public String getName(){
        return this.name;
    }
}

Student.java

public class Student{
    private String name;
    private Clazz clazz;
    
    public void setName(String name){
        this.name = name;
    }
    
    public void setClazz(Clazz clazz){
        this.clazz = clazz;
    }
    
    // 使用级联属性赋值,必须要有get方法
    public Clazz getClazz(){
        return this.clazz;
    }
}

set-di.xml

<bean id="studentBean" class="com.example.spring6.bean.Student">
	<property name="name" value="张三"/>
    <property name="clazz" value="clazzBean"/>
   <!--级联属性赋值-->
    <property name="clazz.name" value="高三二班"/>
</bean>

<bean id="clazzBean" class="com.example.spring6.bean.Clazz"/>

3.5 注入数组

<bean id="f1" class="com.klein.bean.Friend">
    <property name="name" value="小王"/>
</bean>
<bean id="f2" class="com.klein.bean.Friend">
    <property name="name" value="小刘"/>
</bean>
<bean id="f3" class="com.klein.bean.Friend">
    <property name="name" value="小李"/>
</bean>

<bean id="boyBean" class="com.klein.bean.Boy">
    <!--数组为基本数据类型 String[]-->
    <property name="hobbies">
        <array>
            <value></value>
            <value></value>
            <value>rap</value>
        </array>
    </property>

    <!--数组元素不是简单类型-->
    <property name="friends">
        <array>
            <ref bean="f1"/>
            <ref bean="f2"/>
            <ref bean="f3"/>
        </array>
    </property>
</bean>

3.6 List和Set集合注入

<bean id="personBean" class="com.klein.bean.Person">
    <property name="names">
        <!--list有序可重复-->
        <list>
            <value>Tom</value>
            <value>King</value>
            <value>Lucy</value>
            <value>Jack</value>
        </list>
    </property>
    <property name="addrs">
        <!--set无序不可重复-->
        <set>
            <value>NewYork</value>
            <value>SiChuan</value>
            <value>BeiPing</value>
            <value>ShangHai</value>
        </set>
    </property>
</bean>

3.7 Map注入

<property name="phones">
    <map>
        <entry key="1" value="110"/>
        <entry key="2" value="119"/>
        <entry key="3" value="120"/>
        <entry key="4" value="112"/>
        <!--对于非简单类型,使用-->
        <!--<entry key-ref="" value-ref=""/>-->
    </map>
</property>
  • Properties本质上也是一个Map集合

    • Properties的父类HashTable,HashTable实现了Map接口

    • 虽然Properties也是一个Map集合,和Map的注入方式有点像但不一样

    • Properties的注入数据类型只能是String

      <property name="properties">
          <!--注入Properties属性类对象-->
          <props>
              <prop key="Driver">com.mysql.cj.jdbc.Driver</prop>
              <prop key="url">jdbc:mysql://localhost:3306/spring6</prop>
              <prop key="username">root</prop>
              <prop key="password">123456</prop>
          </props>
      </property>
      

3.8 注入空字符串和null

  • 不给属性注入,属性就是默认值,例如:String默认值是nul,也可以手动注入null

    <peoperty name="name">
    	<null/>
    </peoperty>
    
  • 注入空字符串

    <peoperty name="name" value=""/>
    

    或者

    <peoperty name="name">
        <value/>
    </peoperty>
    

3.9 注入特殊符号

XML中有5个特殊字符:**<、>、'、*、 & **

以上5个特殊字符会被当作XML语法的一部分进行解析,如果这些特殊符号直接出现在注入的字符串中,会报错

第一种方案:使用实体符号代替特殊符号

符号实体符号
< \color{black}{<} < & l t ; \color{black}{\&lt;} &lt;
> \color{black}{>} > & g t ; \color{black}{\&gt;} &gt;
′ \color{black}{'} & a p o s ; \color{black}{\&apos;} &apos;
∗ \color{black}{*} & q u o t ; \color{black}{\&quot;} &quot;
& \color{black}{\&} & & a m p ; \color{black}{\&amp;} &amp;

第二种方案:使用**<![CDATA[]]>**当中,因为放在CDATA中的数据不会被XML文件解析,并且只能用标签注入

<property name="result">
	<value><![CDATA[2 < 3]]></value>
</property>

4 p命名空间注入

目的

简化set注入的配置

使用条件

  • 在XML头部信息中添加p命名空间配置信息:http://wwww.springframework.org/schema/p
  • p命名空间注入是基于setter方法的,所以需要对应的属性提供setter方法,底层还是set注入

使用

  • p:属性名

  • 如果是引用类型:p:属性名-ref

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--
        1.在XML头部信息中添加p命名空间配置信息:http://wwww.springframework.org/schema/p
        2.使用
    -->
    <bean id="dogBean" class="com.klein.bean.Dog" p:name="dog" p:age="12" p:birth-ref="birthBean"/>

    <!--获取当前日期-->
    <bean id="birthBean" class="java.util.Date"/>
</beans>

5 c命名空间注入

目的

简化构造方法注入配置

使用条件

  • 需要在xml配置文件头部添加信息:xmlns:c=http://www.springframework.org/schema/c
  • 需要提供构造方法

使用

  • c:_下标
  • c:属性名
  • 如果是引用类型:c:属性名-ref 或者 c:_下标-ref
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--
        1. 需要在xml配置文件头部添加信息:xmlns:c=http://www.springframework.org/schema/c
        2.使用
    -->
    <bean id="peopleBean" class="com.klein.bean.People" c:_0="Tom" c:age="18" c:_2="true"/>
</beans>

6 util命名空间

目的

配置复用

使用条件

在spring配置文件添加头部配置信息:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:uitl="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans        
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util
                           http://www.springframework.org/schema/util/spring-util.xsd">

未使用util命名空间

<!--数据源1-->
<bean id="ds1" class="com.klein.jdbc.MyDataSource1">
    <property name="properties">
        <props>
            <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
            <prop key="url">jdbc:mysql://localhost:3306/spring6</prop>
            <prop key="username">rot</prop>
            <prop key="password">123</prop>
        </props>
    </property>
</bean>

<!--数据源2-->
<bean id="ds2" class="com.klein.jdbc.MyDataSource2">
    <property name="properties">
        <props>
            <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
            <prop key="url">jdbc:mysql://localhost:3306/spring6</prop>
            <prop key="username">rot</prop>
            <prop key="password">123</prop>
        </props>
    </property>
</bean>

使用了util命名空间

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:uitl="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util
                           http://www.springframework.org/schema/util/spring-util.xsd">

    <!--引入util命名空间
         xmlns:uitl="http://www.springframework.org/schema/util"
         http://www.springframework.org/schema/util 
		http://www.springframework.org/schema/util/spring-util.xsd
    -->
    <uitl:properties id="prop">
        <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
        <prop key="url">jdbc:mysql://localhost:3306/spring6</prop>
        <prop key="username">root</prop>
    </uitl:properties>

    <!--数据源1-->
    <bean id="ds1" class="com.klein.jdbc.MyDataSource1">
        <property name="properties" ref="prop"/>
    </bean>

    <!--数据源2-->
    <bean id="ds2" class="com.klein.jdbc.MyDataSource2">
        <property name="properties" ref="prop"/>
    </bean>

</beans>

7 基于XML的自动装配

Spring可以完成自动化的注入,自动化注入又被称为自动装配。它可以根据名字进行自动装配,也可以根据类型进行自动装配

7.1 基于名称自动装配

也是基于setter方法实现的:autowire="byName"

缺陷:在有效的配置文件中,某一种类型实例只能有一个

<!--根据名字进行自动装配-->
<!--id一般也叫bean的名称, 命名符合规范:setter方法去掉set后首字母小写-->
<bean id="orderDao" class="com.klein.dao.impl.OrderDaoImpl"/>
<bean id="orderService" class="com.klein.service.impl.OrderServiceImpl" autowire="byName" />

<!--非自动装配-->
<!--
<bean id="orderDao" class="com.klein.dao.impl.OrderDaoImpl"/>
<bean id="orderService" class="com.klein.service.impl.OrderServiceImpl">
    <property name="orderDao" ref="orderDao"/>
</bean>
-->

7.2 基于类型自动装配

也是基于setter方法实现的:autowire="byType"

<!--根据类型进行自动装配-->
<!---->
<bean class="com.klein.dao.impl.OrderDaoImpl"/>
<bean class="com.klein.dao.impl.UserDaoImpl"/>
<bean id="orderService" class="com.klein.service.impl.OrderServiceImpl" autowire="byType" />

8 spring引入外部属性配置文件

将某些配置信息单独写到一个配置文件当中,方便用户修改

  • **注意:使用${key}**加载默认首先加载系统的环境变量,然后才是配置文件,所以最好给配置文件的key加一个前缀,防止和系统变量冲突

jdbc.properties

jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring6
jdbc.username=root
jdbc.password=123

spring-properties.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!--
        引入外部的properties文件
            1.引入context命名空间
            2.引入配置文件,使用标签context:property-placeholder的location属性来加载指定路径的配置文件
                location默认从类的根路径下开始加载资源
            3.使用${key},引入值
    -->
    <context:property-placeholder location="jdbc.properties"/>

    <!--配置数据源-->
    <bean id="ds" class="com.klein.jdbc.MyDataSource">
        <property name="driver" value="${jdbc.driverClass}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

</beans>

Bean的作用域

1 单例与多例

默认情况下Bean是**单例(singleton)**的。在Spring上下文初始化的时候实例化,每次调用getBean方法,都是返回那个单例对象,也可以配置

<bean id="sb" class="com.klein.spring6.bean.SpringBean" scope="singleton"/>

Test

@Test
public void testBeanScope(){
    ClassPathXmlApplicationContext applicationContext = 
        new ClassPathXmlApplicationContext("spring-scope.xml");
    SpringBean sb = applicationContext.getBean("sb", SpringBean.class);
    System.out.println(sb);

    SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
    System.out.println(sb2);

    SpringBean sb3 = applicationContext.getBean("sb", SpringBean.class);
    System.out.println(sb3);
}
SpringBean的无参构造方法执行了
com.klein.spring6.bean.SpringBean@409bf450
com.klein.spring6.bean.SpringBean@409bf450
com.klein.spring6.bean.SpringBean@409bf450

更改默认配置

<bean id="sb" class="com.klein.spring6.bean.SpringBean" scope="prototype"/>

此时,在spring初始化上下文的时候并不会实例化bean,只有在调用getBean方法的时候,才会调用构造方法实例化对象,每调用一次,实例化一次该Bean对象

prototype(原型/多例)

SpringBean的无参构造方法执行了
com.klein.spring6.bean.SpringBean@42a48628
SpringBean的无参构造方法执行了
com.klein.spring6.bean.SpringBean@293a5bf6
SpringBean的无参构造方法执行了
com.klein.spring6.bean.SpringBean@6aeb35e6

2 scope的其它选项

目前来说:scope属性有两个值

  • singleton 单例
  • prototype 原型/多例

其实scope属性可以有多个值

例如,想要加入web项目的scope需要引入web框架,例如spring mvc

<!--引入spring mvc-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>6.1.4</version>
</dependency>
  • request 一次请求当中一个bean
  • session 一次会话当中一个bean

8种scope

  • singleton:默认的,单例
  • prototype:原型,每调用一次getBean则获取一个新的Bean对象,或每次注入的时候都是新对象
  • request:一个请求对应一个Bean,仅限于WEB应用中使用
  • session:一个会话对应一个Bean,仅限于WEB应用中使用
  • global session:portlet应用中专用。如果Servlet的WEB应用使用global session的话,和session一个效果。(portlet和servelet都是规范。servlet运行在servlet容器,例如Tomcat。portlet运行在portlet容器中)
  • application:一个应用对应一个Bean,仅限于WEB应用中使用
  • websocket:一个websocket生命周期对应一个Bean,仅限于WEB应用中使用
  • 自定义scope:很少使用

*3 自定义scope

例:自定义一个线程级别的scope,相同线程同一个Bean,不同线程则是不同的对象

  1. 自定义scope,实现Scope接口

    • spring内置了线程范围的类:org.springframework.context.support.SimplethreadScope,可以直接使用
  2. 将自定义的Scope注册到容器中

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="myThread">
                    <!--Scope接口实现类-->
                    <bean class="org.springframework.context.support.SimpleThreadScope"/>
                </entry>
            </map>
        </property>
    </bean>
    
  3. 使用Scope

    <bean id="sb" class="com.klein.spring6.bean.SpringBean" scope="myThread"/>
    
  4. 测试程序

    @Test
    public void testThreadScope(){
        ClassPathXmlApplicationContext applicationContext = 
            new ClassPathXmlApplicationContext("spring-scope.xml");
        SpringBean sb = applicationContext.getBean("sb", SpringBean.class);
        System.out.println(sb);
        SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
        System.out.println(sb1);
    
        // 启动新的线程
        new Thread(()->{
            SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
            System.out.println(sb2);
    
            SpringBean sb3 = applicationContext.getBean("sb", SpringBean.class);
            System.out.println(sb3);
        }).start();
    }
    
    -- 使用singleton时
    SpringBean的无参构造方法执行了
    com.klein.spring6.bean.SpringBean@409bf450
    com.klein.spring6.bean.SpringBean@409bf450
    com.klein.spring6.bean.SpringBean@409bf450
    com.klein.spring6.bean.SpringBean@409bf450
    
    
    -- 使用自定义scope myScope时
    SpringBean的无参构造方法执行了
    com.klein.spring6.bean.SpringBean@3bd82cf5
    com.klein.spring6.bean.SpringBean@3bd82cf5
    SpringBean的无参构造方法执行了
    com.klein.spring6.bean.SpringBean@6a73b0d7
    com.klein.spring6.bean.SpringBean@6a73b0d7
    

GoF之工厂模式

设计模式 | 菜鸟教程 (runoob.com)

1 GoF概述

1.1 设计模式

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

1.2 GoF

在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 的书,该书首次提到了软件开发中设计模式的概念。

四位作者合称 GOF(四人帮,全拼 Gang of Four)。他们所提出的设计模式主要是基于以下的面向对象设计原则。

1.3 GoF的23中设计模式

设计模式的分类

总体来说设计模式分为三大类:

创建型模式(5种):解决对象创建问题

结构型模式(7种):一些类或对象组合在一起的经典结构

  • 适配器模式
  • 装饰器模式
  • 代理模式
  • 外观模式
  • 桥接模式
  • 组合模式
  • 享元模式

行为型模式(11种):解决类和对象之间的交互问题

  • 策略模式
  • 模板方法模式
  • 观察者模式
  • 迭代子模式
  • 责任链模式
  • 命令模式
  • 备忘录模式
  • 状态模式
  • 访问者模式
  • 中介者模式
  • 解释器模式
其他设计模式

JavaEE设计模式(DAO模式、MVC模式)

1.4 设计模式的六大原则

1、开闭原则(Open Close Principle,OCP)

开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

2、里氏代换原则(Liskov Substitution Principle)

里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

3、依赖倒转原则(Dependence Inversion Principle,DIP)

这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

4、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

5、迪米特法则,又称最少知道原则(Demeter Principle)

最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

6、合成复用原则(Composite Reuse Principle)

合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。

2 工厂模式概述

Spring框架大量使用了工厂模式

2.1工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一,它提供了一种创建对象的方式,使得创建对象的过程与使用对象的过程分离。属于创建型设计模式

工厂模式提供了一种创建对象的方式,而无需指定要创建的具体类。

通过使用工厂模式,可以将对象的创建逻辑封装在一个工厂类中,而不是在客户端代码中直接实例化对象,这样可以提高代码的可维护性和可扩展性。

2.2 工厂模式的类型

  1. 简单工厂模式(Simple Factory Pattern)
    • 简单工厂模式不是一个正式的设计模式,但它是工厂模式的基础。它使用一个单独的工厂类来创建不同的对象,根据传入的参数决定创建哪种类型的对象。不属于23种设计模式,它是工厂方法模式的一种特殊实现方式,又叫做静态工厂方法模式
  2. 工厂方法模式(Factory Method Pattern)
    • 工厂方法模式定义了一个创建对象的接口,但由子类决定实例化哪个类。工厂方法将对象的创建延迟到子类。23种设计模式之一。相比于简单工厂模式,每一个类使用单独的工厂
  3. 抽象工厂模式(Abstract Factory Pattern)
    • 抽象工厂模式提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。23种设计模式之一

2.3 结构

工厂模式包含以下几个主要角色:

  • 抽象产品(Abstract Product):定义了产品的共同接口或抽象类。它可以是具体产品类的父类或接口,规定了产品对象的共同方法。
  • 具体产品(Concrete Product):实现了抽象产品接口,定义了具体产品的特定行为和属性。
  • 抽象工厂(Abstract Factory):声明了创建产品的抽象方法,可以是接口或抽象类。它可以有多个方法用于创建不同类型的产品。
  • 具体工厂(Concrete Factory):实现了抽象工厂接口,负责实际创建具体产品的对象。

对于简单工厂模式,包含3个角色

抽象产品、具体产品、工厂类

2.4 概要

意图

定义一个创建对象的接口,让其子类决定实例化哪一个具体的类。工厂模式使对象的创建过程延迟到子类。

主要解决

接口选择的问题。

何时使用

当我们需要在不同条件下创建不同实例时。

如何解决

通过让子类实现工厂接口,返回一个抽象的产品。

关键代码

对象的创建过程在子类中实现。

应用实例
  1. 汽车制造:你需要一辆汽车,只需从工厂提货,而不需要关心汽车的制造过程及其内部实现。
  2. Hibernate:更换数据库时,只需更改方言(Dialect)和数据库驱动(Driver),即可实现对不同数据库的切换。
优点
  • 工厂方法模式

    1. 调用者只需要知道对象的名称即可创建对象。

    2. 扩展性高,如果需要增加新产品,只需扩展一个工厂类即可。

    3. 屏蔽了产品的具体实现,调用者只关心产品的接口。

缺点
  • 简单工厂模式

    1. 每次增加一个产品时,都需要增加一个具体类,使系统中类的数量成倍增加,增加了系统的复杂度和具体类的依赖。违背了OCP原则

    2. 工厂类责任重大,不能出现任何问题,因为这个工厂类负责所有产品的生产,成为全能类。工厂类一旦出现问题,整个系统就会瘫痪

  • 工厂方法模式

    • 每次增加一个产品时,都需要增加一个具体类和对应的工厂,使系统中类的数量成倍增加,增加了系统的复杂度具体类的依赖
使用场景
  1. 日志记录:日志可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志的位置。
  2. 数据库访问:当用户不知道最终系统使用哪种数据库,或者数据库可能变化时。
  3. 连接服务器的框架设计:需要支持 “POP3”、“IMAP”、“HTTP” 三种协议,可以将这三种协议作为产品类,共同实现一个接口。
注意事项

工厂模式适用于生成复杂对象的场景。如果对象较为简单,通过 new 即可完成创建,则不必使用工厂模式。使用工厂模式会引入一个工厂类,增加系统复杂度。

实现

简单工厂模式

我们将创建一个 Shape 接口和实现 Shape 接口的实体类。下一步是定义工厂类 ShapeFactory

FactoryPatternDemo 类使用 ShapeFactory 来获取 Shape 对象。它将向 ShapeFactory 传递信息(CIRCLE / RECTANGLE / SQUARE),以便获取它所需对象的类型。

工厂模式的 UML 图

Bean的实例化

Spring为Bean的实例化提供了多种实例化方式,通常包括4种

  • 通过构造方法实例化
  • 通过简单工厂模式实例化
  • 通过factory-bean实例化
  • 通过FactoryBean接口实例化

1 通过构造方法实例化

<!--
	Spring提供的实例化方式,在Spring配置文件中直接配置全路径,
	Spring会自动调用该类的无参构造方法,实例化该Bean
-->
<bean id="sb" class="com.klein.test.bean.SpringBean"/>

2 通过简单工厂模式实例化

<!--Spring提供的实例化方式2,通过简单工厂,需要在Spring配置文件告诉Spring框架,哪个类的哪个方法获取Bean-->
<!--factory-method指定的是工厂类的静态方法-->
<bean id="star" class="com.klein.test.bean.StarFactory" factory-method="get"/>

3 通过factory-bean实例化

通过工厂方法模式实例化Bean对象

<!--Spring提供的实例化方式3,通过工厂方法模式。通过factory-bean属性+factory-method属性来共同完成-->
<bean id="gunFactory" class="com.klein.test.bean.GunFactory"/>
<!--告诉Spring框架调用哪个对象的哪个方法来获取Bean-->
<bean id="gun" factory-bean="gunFactory" factory-method="get"/>

4 通过FactoryBean接口实例化

以上的第三种方式中,factory-bean是我们自定义的,factory-method也是我们自定义的。

在Spring中,当你编写类直接实现FactoryBean接口后,factory-bean就不需要指定了,factory-method也不需要指定了。

factory-bean会自动指向实现FactoryBean接口的类,factory-method会自动指向getObject()方法

实现FactoryBean接口

public class PersonFactoryBean implements FactoryBean<Person> {

    @Override
    public Person getObject() throws Exception {
        return new Person();
    }

    @Override
    public Class<?> getObjectType() {
        return Person.class;
    }

    /**
     * 默认返回true,表示是单例的
     * 如果想多例,返回false
     * @return bool
     */
    @Override
    public boolean isSingleton() {
        return true;
    }
}

spring.xml

<!--Spring提供的实例化方式4, 通过FactoryBean接口来实现-->
<!--这种方式实际上是第三种方式的简化-->
<!--通过一个特殊的Bean:工厂Bean。来返回一个普通的Bean:Person对象-->
<!--通过FactoryBean这个工厂Bean主要想对Bean进行加工-->
<bean id="person" class="com.klein.test.bean.PersonFactoryBean"/>

5 BeanFactory和FactoryBean的区别

5.1 BeanFactory

Spring IoC容器的顶级对象,BeanFactory被翻译为“Bean工厂”,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象

BeanFactory是工厂

5.2 FactoryBean

FactoryBean:它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean

在Spring中Bean分为两类:

​ 第一类,普通Bean

​ 第二类:工厂Bean(工厂Bean也是一种Bean,只不过这种Bean比较特殊,它可以辅助Spring实例化其它Bean对象)

6 注入自定义Date

背景

当把Java.util.Date当作简单类型进行注入时,直接使用value属性赋值对格式要求严格:Mon Oct 10 14:25:56 CST 2023。其他格式不会被识别

Student.java

public class Student {

    private Date birth;

    @Override
    public String toString() {
        return "Student{" +
                "birth=" + birth +
                '}';
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }
}

DateFactoryBean.java

public class DateFactoryBean implements FactoryBean<Date> {

    // DateFactoryBean这个工厂类Bean协助我们Spring创建这个普通的Bean:Date

    private String strDate;

    public DateFactoryBean(String strDate) {
        this.strDate = strDate;
    }

    @Override
    public Date getObject() throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.parse(strDate);
    }

    @Override
    public Class<?> getObjectType() {
        return Date.class;
    }
}

spring.xml

<bean id="date" class="com.klein.test.bean.DateFactoryBean">
    <constructor-arg index="0" value="2002-01-23"/>
</bean>

<bean id="student" class="com.klein.test.bean.Student">
    <property name="birth" ref="date"/>
</bean>

Bean的生命周期

1 概述

Bean的生命周期

Spring其实就是一个管理Bean对象的工厂,负责对象的创建和销毁,Bean的生命周期就是**对象从创建开始到最终销毁的的整个过程****

2 Bean生命周期之5步

Bean生命周期的管理,可以参考Spring源码:AbstractAutowireCapableFactory类的doCreateBean()方法

Bean的生命周期可以粗略的划分为五大步:

  1. 实例化Bean(无参构造方法)
  2. Bean属性赋值(setter方法)
  3. 初始化Bean(调用Bean的init方法,这个init需要自己实现,并且自己配置init-method
  4. 使用Bean
  5. 销毁Bean(会调用Bean的destroy方法,这个destroy需要自己实现,并且自己配置destroy-method

在这里插入图片描述

User.java

public class User {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("2.给对象属性赋值");
        this.name = name;
    }

    public User() {
        System.out.println("1.无参构造方法执行");
    }

    public void initBean(){
        System.out.println("3.初始化Bean");
    }

    public void destroyBean(){
        System.out.println("5.销毁Bean");
    }

}

spring.xml

<!--手动指定初始化方法和销毁方法-->
<bean id="user" class="com.klein.spring6.bean.User"
      init-method="initBean" destroy-method="destroyBean">
    <property name="name" value="张三"/>
</bean>

Test.java

@Test
public void testBeanLifecycleFive(){
    ClassPathXmlApplicationContext applicationContext = 
        new ClassPathXmlApplicationContext("spring.xml");
    User user = applicationContext.getBean("user", User.class);
    System.out.println("4.使用Bean:"+user);

    // 注意:必须手动关闭Spring容器,Spring容器才会销毁Bean
    applicationContext.close();

}

结果

1.无参构造方法执行
2.给对象属性赋值
3.初始化Bean
4.使用Bean:com.klein.spring6.bean.User@3eb25e1a
5.销毁Bean

注意:

  • 只有正常关闭的Spring容器,bean的销毁方法才会调用
  • 配置文件中init-method指定初始化方法,desroy-method指定销毁方法

3 Bean生命周期之7步

在以上的5步中,第3步是初始化Bean,如果还想在初始化前和初始化后添加代码,可以加入Bean后处理器

编写一个类实现BeanPostProcessor类,并且重写before和after方法

  1. 实例化Bean(无参构造方法)
  2. Bean属性赋值(setter方法)
  3. 执行Bean后处理器before方法
  4. 初始化Bean(调用Bean的init方法,这个init需要自己实现,并且自己配置init-method
  5. 执行Bean后处理器after方法
  6. 使用Bean
  7. 销毁Bean(会调用Bean的destroy方法,这个destroy需要自己实现,并且自己配置destroy-method

在这里插入图片描述

LogBeanPostProcessor.java

public class LogBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("执行Bean后处理器的before方法");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    /**
     * @param bean the new bean instance
     * @param beanName the name of the bean
     * @return Object
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("执行Bean后处理器的after方法");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}

spring.xml

<!--配置Bean后处理器-->
<!--这个Bean后处理器将作用于配置文件中所有的Bean-->
<bean class="com.klein.spring6.bean.LogBeanPostProcessor"/>

<!--手动指定初始化方法和销毁方法-->
<bean id="user" class="com.klein.spring6.bean.User"
      init-method="initBean" destroy-method="destroyBean">
    <property name="name" value="张三"/>
</bean>

结果

1.无参构造方法执行
2.给对象属性赋值
执行Bean后处理器的before方法
3.初始化Bean
执行Bean后处理器的after方法
4.使用Bean:com.klein.spring6.bean.User@4116aac9
5.销毁Bean

注意:

  • 配置Bean后处理器后,Bean后处理器将作用于该配置文件中所有的Bean,不会作用于其它的配置文件

4 Bean生命周期之10步

如果根据源码追踪,可以划分为更细粒度的步骤

  1. 实例化Bean(无参构造方法)
  2. Bean属性赋值(setter方法)
  3. 检查Bean是否实现了**Aware的相关接口,并设置相关依赖**
  4. 执行Bean后处理器before方法
  5. 检查Bean是否实现了**InitializingBean接口,并调用接口方法**
  6. 初始化Bean(调用Bean的init方法,这个init需要自己实现,并且自己配置init-method
  7. 执行Bean后处理器after方法
  8. 使用Bean
  9. 检查Bean是否实现了**DisposableBean接口,并调用接口方法**
  10. 销毁Bean(会调用Bean的destroy方法,这个destroy需要自己实现,并且自己配置destroy-method

在这里插入图片描述

4.1 Aware相关接口

aware : [adj]意识到的;明白的;知道的

Aware接口的目的是传递一些想要的数据,方便使用

BeanNameAware

  • 当Bean实现了BeanNameAware接口,Spring会将Bean的名字传递给Bean

BeanClassLoaderAware

  • 当Bean实现了BeanClassLoaderAware接口,Spring会将加载该Bean的类加载器传递给Bean

BeanFactoryAware

  • 当Bean实现了BeanFactoryAware接口,Spring会将Bean工厂对象传递给Bean

4.2 测试

测试以上10步可以让User类实现5个接口

  • BeanNameAware
  • BeanClassLoaderAware
  • BeanFactoryAware
  • InitializingBean
  • DisposableBean

User.java

public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("2.给对象属性赋值");
        this.name = name;
    }

    public User() {
        System.out.println("1.无参构造方法执行");
    }

    public void initBean(){
        System.out.println("4.初始化Bean");
    }

    public void destroyBean(){
        System.out.println("7.销毁Bean");
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println("Bean的类加载器:" + classLoader);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("生产这个Bean的工厂对象是:" + beanFactory);
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("bean的名字是:" + name);
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean's destroy执行");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean's afterPropertiesSet执行");
    }
}

结果

1.无参构造方法执行
2.给对象属性赋值
bean的名字是:user
Bean的类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c
生产这个Bean的工厂对象是:org.springframework.beans.factory.support.DefaultListableBeanFactory@78aab498: 	defining beans [com.klein.spring6.bean.LogBeanPostProcessor#0,user]; root of factory hierarchy
3.执行Bean后处理器的before方法
InitializingBean's afterPropertiesSet执行
4.初始化Bean
5.执行Bean后处理器的after方法
6.使用Bean:com.klein.spring6.bean.User@15de0b3c
DisposableBean's destroy执行
7.销毁Bean

5 Bean的作用域与生命周期

Spring根据Bean的作用域来选择管理方式

  • 对于singleton作用域的Bean,Spring能够精确地知道该Bean何时被创建、何时初始化完成、以及何时被销毁
  • 而对于prototype作用域地Bean,Spring只负责创建,当容器创建了Bean的实例后,Bean的实例就交给客户端代码管理,Spring容器将不再跟中其生命周期

将scope改为prototype

<!--配置Bean后处理器-->
<!--这个Bean后处理器将作用于配置文件中所有的Bean-->
<bean class="com.klein.spring6.bean.LogBeanPostProcessor"/>

<!--手动指定初始化方法和销毁方法-->
<bean id="user" class="com.klein.spring6.bean.User"
      init-method="initBean" destroy-method="destroyBean"
      scope="prototype"
>
    <property name="name" value="张三"/>
</bean>

执行结果

1.无参构造方法执行
2.给对象属性赋值
bean的名字是:user
Bean的类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c
生产这个Bean的工厂对象是:org.springframework.beans.factory.support.DefaultListableBeanFactory@78aab498: defining beans [com.klein.spring6.bean.LogBeanPostProcessor#0,user]; root of factory hierarchy
3.执行Bean后处理器的before方法
InitializingBean's afterPropertiesSet执行
4.初始化Bean
5.执行Bean后处理器的after方法
6.使用Bean:com.klein.spring6.bean.User@6ed3ccb2

6 自己new的对象让Spring管理

DefaultListableBeanFactory

@Test
public void testRegisterBean(){
    // 自己new的对象
    Student student = new Student();
    System.out.println(student);

    // 将对象交给Spring对象管理
    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    factory.registerSingleton("studentBean", student);

    // 从Spring容器中获取
    Object studentBean = factory.getBean("studentBean");
    System.out.println(studentBean);
}
com.klein.spring6.bean.Student@5594a1b5
com.klein.spring6.bean.Student@5594a1b5

Bean的循环依赖问题

1 概述

循环依赖

  • A对象有B属性。B对象有A属性。这就是循环依赖
  • 例如丈夫类Husband,妻子类Wife。Husband中有Wife的引用,Wife中有Husband的引用

2 singleton+setter模式

spring.xml

<!--singleton + setter模式下的循环依赖-->
<bean id="husbandBean" class="com.klein.spring6.bean.Husband">
    <property name="name" value="张三"/>
    <property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.klein.spring6.bean.Wife">
    <property name="name" value="小花"/>
    <property name="husband" ref="husbandBean"/>
</bean>

结果

Husband{name='张三', wife=小花}
Wife{name='小花', husband=张三}

结论

  • singleton + setter模式下注入循环依赖是没有问题的
  • singleton表示在Spring容器中是单例的,独一无二的对象

解释

singleton + setter模式下,循环依赖不会出现问题的主要原因是:

  • 在这种模式下Spring对Bean的管理主要分为清晰的两个阶段:
    1. 第一阶段:在Spring容器加载的时候,实例化Bean,只要其中任意一个Bean实例化之后,马上进行“曝光”【不等属性赋值就曝光,暴露内存地址】
    2. 第二阶段:“曝光”之后再进行属性的赋值
  • 核心解决方案:实例化对象和对象的属性赋值分为两个阶段来完成【只有在singleton+setter模式下才Bean会采取提前曝光】

3 prototype+setter模式

prototype + setter模式下的循环依赖

<!--prototype + setter模式下的循环依赖-->
<bean id="husbandBean" class="com.klein.spring6.bean.Husband" scope="prototype">
    <property name="name" value="张三"/>
    <property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.klein.spring6.bean.Wife" scope="prototype">
    <property name="name" value="小花"/>
    <property name="husband" ref="husbandBean"/>
</bean>

结果

  • 存在问题,会出现异常
Error creating bean with name 'husbandBean' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'wifeBean' while setting bean property 'wife'

单个prototype + setter模式下的循环依赖

<!--单个prototype + setter模式下的循环依赖-->
<bean id="husbandBean" class="com.klein.spring6.bean.Husband" scope="singleton">
    <property name="name" value="张三"/>
    <property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.klein.spring6.bean.Wife" scope="prototype">
    <property name="name" value="小花"/>
    <property name="husband" ref="husbandBean"/>
</bean>

结果

  • 不会出现问题
Husband{name='张三', wife=小花}
Wife{name='小花', husband=张三}

4 构造模式注入

<!--构造注入模式下的循环依赖-->
<bean id="husbandBean" class="com.klein.spring6.bean.Husband">
    <constructor-arg name="name" value="张三"/>
    <constructor-arg name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.klein.spring6.bean.Wife">
    <constructor-arg name="name" value="小花"/>
    <constructor-arg name="husband" ref="husbandBean"/>
</bean>

结果

Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'husbandBean' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'wifeBean' while setting constructor argument

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'husbandBean' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'wifeBean' while setting constructor argument

结论

  • 基于构造注入的方式下产生的循环依赖也是无法解决的,所以编写代码时一定要注意

5 Spring解决循环依赖的原理

Spring为什么可以解决singleton+setter模式下的循环依赖

  • 根本原因在于:实例化对象和对象的属性赋值分为两个阶段来完成
    • 实例化对象时:调用无参构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界
    • 给Bean属性赋值的时候:调用setter方法来完成
  • 以上两个步骤完全可以分离去完成,并且两个步骤不要求同一时间点完成
  • 也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合中(我们可以称之为缓存),所有的单例Bean全部实例化完成后,以后我们在慢慢调用setter方法给属性赋值。这样就解决了循环依赖问题

源码:AbstractAutowireCapableBeanFactory.javadoCreateBean()

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
        throws BeanCreationException {

    // Instantiate the bean.
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
        instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    Object bean = instanceWrapper.getWrappedInstance();
    Class<?> beanType = instanceWrapper.getWrappedClass();
    if (beanType != NullBean.class) {
        mbd.resolvedTargetType = beanType;
    }

    // Allow post-processors to modify the merged bean definition.
    synchronized (mbd.postProcessingLock) {
        if (!mbd.postProcessed) {
            try {
                applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
            }
            catch (Throwable ex) {
                throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                        "Post-processing of merged bean definition failed", ex);
            }
            mbd.markAsPostProcessed();
        }
    }

    // Eagerly cache singletons to be able to resolve circular references
    // even when triggered by lifecycle interfaces like BeanFactoryAware.
    // 主动缓存单例,以便能够解析循环引用,即使是由BeanFactoryAware等生命周期接口触发时也是如此
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        if (logger.isTraceEnabled()) {
            logger.trace("Eagerly caching bean '" + beanName +
                    "' to allow for resolving potential circular references");
        }
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // Initialize the bean instance.
    // 初始化bean实例
    Object exposedObject = bean;
    try {
        // 填充bean,给bean属性赋值
        populateBean(beanName, mbd, instanceWrapper);
        exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    catch (Throwable ex) {
        if (ex instanceof BeanCreationException bce && beanName.equals(bce.getBeanName())) {
            throw bce;
        }
        else {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex);
        }
    }

    if (earlySingletonExposure) {
        Object earlySingletonReference = getSingleton(beanName, false);
        if (earlySingletonReference != null) {
            if (exposedObject == bean) {
                exposedObject = earlySingletonReference;
            }
            else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
                String[] dependentBeans = getDependentBeans(beanName);
                Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
                for (String dependentBean : dependentBeans) {
                    if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                        actualDependentBeans.add(dependentBean);
                    }
                }
                if (!actualDependentBeans.isEmpty()) {
                    throw new BeanCurrentlyInCreationException(beanName,
                            "Bean with name '" + beanName + "' has been injected into other beans [" +
                            StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                            "] in its raw version as part of a circular reference, but has eventually been " +
                            "wrapped. This means that said other beans do not use the final version of the " +
                            "bean. This is often the result of over-eager type matching - consider using " +
                            "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
                }
            }
        }
    }

    // Register bean as disposable.
    try {
        registerDisposableBeanIfNecessary(beanName, bean, mbd);
    }
    catch (BeanDefinitionValidationException ex) {
        throw new BeanCreationException(
                mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
    }

    return exposedObject;
}

DefaultSingletonBeanRegistry.javaaddSingletonFactorygetSingleton

DefaultSingletonBeanRegistry类中比较重要的三个缓存

/**
* Cache of singleton objects: bean name to bean instance. 
* 一级缓存
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/**
* Cache of early singleton objects: bean name to bean instance. 
* 二级缓存
*/
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

/**
* Cache of singleton factories: bean name to ObjectFactory. 
* 三级缓存
*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);


/**
 * Add the given singleton factory for building the specified singleton
 * if necessary.
 * <p>To be called for eager registration of singletons, e.g. to be able to
 * resolve circular references.
 * @param beanName the name of the bean
 * @param singletonFactory the factory for the singleton object
 */
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

/**
 * Return the (raw) singleton object registered under the given name.
 * <p>Checks already instantiated singletons and also allows for an early
 * reference to a currently created singleton (resolving a circular reference).
 * @param beanName the name of the bean to look for
 * @param allowEarlyReference whether early references should be created or not
 * @return the registered singleton object, or {@code null} if none found
 */
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // Consistent creation of early reference within full singleton lock
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}
  • 三个缓存都是Map集合
    • Map集合的key存储的都是bean的name(bean id)
    • 一级缓存:存储的是完整的Bean对象。也就是说这个缓存中Bean对象的属性都已经赋值了。是一个完整的对象
    • 二级缓存:存储的是早期的单例Bean对象。这个缓存中的单例Bean对象的属性没有赋值。只是一个早期的实例对象
    • 三级缓存:存储的是单例工厂对象。里面的存储了大量的工厂对象,每一个单例Bean对象都会对应一个单例工厂对象。这个集合中存储的是创建该单例对象时对应的那个单例工厂对象

回顾反射机制

1 分析方法四要素

调用一个的方法的要素:

  • 调用哪一个对象
  • 调用哪个方法
  • 调用方法传入的参数
  • 方法执行结束后的结果

SomeService.java

public class SomeService {

    public void doSome(){
        System.out.println("public void doSome()执行");
    }

    public String doSome(String s){
        System.out.println("public void doSome(String s)执行");
        return s;
    }

    public String doSome(String s, int i){
        System.out.println("public String doSome(String s, int i)执行");
        return s + i;
    }
}

Test.java

public static void main(String[] args) {
    // 不使用反射机制调用对象
    SomeService someService = new SomeService();
    someService.doSome();

    String s1 = someService.doSome("张三");
    System.out.println(s1);

    String s2 = someService.doSome("李四", 20);
    System.out.println(s2);
}

2 反射机制调用方法

Test.java

public static void main(String[] args) throws Exception {
    // 获取类
    Class<?> clazz = Class.forName("com.klein.reflect.SomeService");

    // 获取方法
    Method doSomeMethod = clazz.getDeclaredMethod("doSome", String.class, int.class);

    // 调用方法:invoke
    // 四要素:对象、方法、参数、结果
    // 实例化对象
    Constructor<?> constructor = clazz.getDeclaredConstructor();
    Object obj = constructor.newInstance();
    Object retVal = doSomeMethod.invoke(obj, "张三", 250);
    System.out.println(retVal);

}
public static void main(String[] args) throws Exception {
        /*
 需求:
    已知:
        1.已知类com.klein.reflect.User
        2.这个类符合java bean规范,对外提供公开的getter、setter方法
        3.已知一个类的属性:age
    使用反射机制调用相关方法给age赋值
 */
    String className = "com.klein.reflect.User";
    String property = "age";
    // 获取类
    Class<?> clazz = Class.forName(className);

    // 获取方法名
    String methodName = "set" + Character.toUpperCase(property.charAt(0)) + property.substring(1);

    // 根据属性名获取属性类型
    Field field = clazz.getDeclaredField(property);

    // 获取setter方法
    Method method = clazz.getDeclaredMethod(methodName, field.getType());

    // 调用方法
    Object obj = clazz.getDeclaredConstructor().newInstance();
    method.invoke(obj, 18);
    System.out.println(obj);

}

Spring IoC注解式开发

1 回顾注解

注解的存在是为了简化XML的配置

注解就是Java代码里的特殊标记,比如:@Override、@Test等,作用是让其他程序根据注解信息来决定怎么执行该程序。

public @interface 注解名称{
    public 属性类型 属性名() default 默认值;
}

1.1 元注解

  • 元注解:修饰注解的注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface Test{
        
    }
    
    @Target
    	作用:声明被修饰的注解只能在哪些位置使用
        @Target(ElementType.TYPE)
            1.TYPE,类,接口
            2.FIELD,成员变量
            3.METHOD,成员方法
            4.PARAMETER,方法参数
            5.CONSTRUCTOR,构造器
            6.LOCAL_VARIABLE,局部变量
    
    @Retention
    	作用:声明注解的保留周期
        @Retention(RetentionPolicy.RUNTIME)
            1.SOURCE:只作用在源码阶段,字节码文件中不存在
            2.CLASS(默认值):保留到字节码文件阶段,运行阶段不存在
            3.RUNTIME:一直保留到运行阶段
    

1.2 解析注解

AnnotatedElement接口提供的解析注解的方法说明
public Annotation[] getDeclaredAnnotations()获取当前对象上面的注解
public T getDeclaredAnnotation(Class annotationClass)获取指定的注解对象
public boolean isAnnotationPresent(Class annotationClass)判断当前对象上是否存在某个注解

2 声明Bean的注解

负责声明Bean的注解,常见的包括4个:

  • @Component (组件)
  • @Controller (控制器)–表示层
  • @Service (业务)–业务层
  • @Repository (仓库)–持久层

源码如下

  • @Component

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Indexed
    public @interface Component {
    
    	/**
    	 * The value may indicate a suggestion for a logical component name,
    	 * to be turned into a Spring bean name in case of an autodetected component.
    	 * @return the suggested component name, if any (or empty String otherwise)
    	 */
    	String value() default "";
    
    }
    
  • @Controller

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Controller {
    
    	/**
    	 * Alias for {@link Component#value}.
    	 * 别名
    	 */
    	@AliasFor(annotation = Component.class)
    	String value() default "";
    
    }
    
  • @Service

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Service {
    
    	/**
    	 * Alias for {@link Component#value}.
    	 */
    	@AliasFor(annotation = Component.class)
    	String value() default "";
    
    }
    
  • @Repository

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Repository {
    
    	/**
    	 * Alias for {@link Component#value}.
    	 */
    	@AliasFor(annotation = Component.class)
    	String value() default "";
    
    }
    

通过源码,可以知道@Controller、@Service、@Repository都是@Component的别名,增强程序的可读性

以上注解都有默认值,默认值为类名的首字母变小写

3 Spring的使用

使用步骤:

  1. 加入aop的依赖 --加入spring-context依赖后,会关联并加入aop依赖

  2. 在配置文件中添加context命名空间

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation=
           "http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans.xsd                   
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    </beans>
    
  3. 在配置文件中指定要扫描的包

    <!--开启包的扫描-->
    <!--给Spring框架指定要扫描哪些包-->
    <context:component-scan base-package="com.klein.spring.bean"/>
    
    
    <!--对于向指定扫描多个包-->
    <!--
    	1.多个包之间使用逗号隔开,或者
    	2.指定共同父包,会牺牲一部分效率
    -->
    <context:component-scan base-package="com.klein.spring.bean, com.klein.spring.dao"/>
    
  4. 在Bean类上使用注解

    @Component("userBean")
    public class User {
    
    }
    

4 选择性实例化Bean

假设有很多Bean,我们只选择实例化@Controller,其他注解都不实例化

<!--
    第一种解决方案:
        use-default-filter="false"
            如果这个属性是false,表示com.klein.spring.bean2包下的所有的带有声明Bean的注解全部失效。
            @Component、@Service、@Controller、@Repository全部失效
        <context:include-filter type="" expression=""/>
            包含进来的生效 
-->
<context:component-scan base-package="com.klein.spring.bean2" use-default-filters="false">
    <context:include-filter 
                            type="annotation" 
                            expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
    <!--
    第二种解决方案:
        use-default-filter="true"
            如果这个属性是true,表示com.klein.spring.bean2包下的所有的带有声明Bean的注解全部生效。                     @Component、@Service、@Controller、@Repository全部生效
            use-default-filter默认为true,可以不用写
        <context:include-filter type="" expression=""/>
            包含进来的生效
    -->
    <context:component-scan base-package="com.klein.spring.bean2">
        <!--排除掉-->
        <context:exclude-filter 
                                type="annotation" 
                                expression="org.springframework.stereotype.Service"/>
        <context:exclude-filter 
                                type="annotation" 
                                expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>

5 负责注入的注解

@Component、@Controller、@Service、@Repository这四个注解是用来声明Bean的,声明后这些Bean将被实例化。想要给Bean属性赋值,可以使用:

  • @Value
  • @Autowired
  • @Qualifier
  • @Resource

5.1 @Value

当属性为简单类型时,可以使用@Value注解进行注入

@Component
public class MyDataSource implements DataSource {

    @Value("com.mysql.cj.jdbc.Driver")
    private String driver;

    @Value("jdbc:mysql://localhost:3306/spring6")
    private String url;

    @Value("root")
    private String username;

    @Value("123456")
    private String password;

    @Override
    public String toString() {
        return "MyDataSource{" +
                "driver='" + driver + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

	...
}

使用@Value注解注入的话,可以用在属性上,并且可以不提供setter方法。

5.2 @Autowired与@Qualifier

@Autowired单独使用

@Autowired注解可以用来注入非简单类型。被翻译为:自动连线的,或者叫自动装配

单独使用@Autowired注解,默认根据类型装配。【默认是byType】

一起使用@Autowired和@Qualifier则会根据属性名进行装配。【byName】

@Autowired源码

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

	/**
	 * Declares whether the annotated dependency is required.
	 * <p>Defaults to {@code true}.
	 */
	boolean required() default true;
}
  • 该注解可以标注在
    • 构造方法上
    • 方法上
    • 形参上
    • 属性上
    • 注解上
  • required属性:
    • 默认值为true,表示在注入的时候要求注入的Bean必须存在,如果不存在则报错。
    • false:表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话也不报错

OrderDao.java

public interface OrderDao {
    void insert();
}

OrderDaoImplForMySQL.java

@Repository
public class OrderDaoImplForMySQL implements OrderDao {
    @Override
    public void insert() {
        System.out.println("mysql is inserting....");
    }
}

OrderService.java

@Service
public class OrderService {

    @Autowired
    private OrderDao orderDao;

    public void generate(){
        orderDao.insert();
    }

}

spring-autowired.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.klein.dao, org.klein.service"/>

</beans>
@Autowired和@Qualifier联合使用

一起使用@Autowired和@Qualifier则会根据属性名进行装配。【byName】

可以用来解决注入同一个接口,但是该接口有多个实现类的情况,而导致根据类型注入失败的情况

比如OrderDao新增了实现类OrderDaoForOracle

OrderDaoForOracle.java

@Repository
public class OrderDaoImplForOracle implements OrderDao {
    @Override
    public void insert() {
        System.out.println("Oracle is inserting ....");
    }
}

这时候直接使用@Autowired会报错。这时候可以@Autowired和@Qualifier联合使用,根据属性名进行自动装配

@Service
public class OrderService {

    @Autowired
    @Qualifier("orderDaoImplForOracle")
    private OrderDao orderDao;

    public void generate(){
        orderDao.insert();
    }
}
@Autowired位置

该注解可以标注在

  • 构造方法上

    @Autowired
    public OrderService(OrderDao orderDao) {
        this.orderDao = orderDao;
    }
    
  • 方法上

    @Autowired
    public void setOrderDao(OrderDao orderDao) {
        this.orderDao = orderDao;
    }
    
  • 形参上

    public OrderService(@Autowired OrderDao orderDao) {
        this.orderDao = orderDao;
    }
    
  • 属性上

    @Autowired
    private OrderDao orderDao;
    
  • 注解上

    @Autowired
    @Qualifier("orderDaoImplForOracle")
    private OrderDao orderDao;
    

当一个类只有一个构造方法,且参数名符合规范(构造方法上的参数和属性对应),@Autowired可以省略,不建议省略

5.3 @Resource

@Resource和@Autowired

@Resource注解也可以完成非简单类型注入。@Resource和@Autowired的区别:

  • @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250)
  • @Autowired注解是Spring框架自己的
  • @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启用通过类型byType装配
  • @Autowired注解默认根据类型装配byType,如果想要根据名称装配,需要配合@Qualifier注解一起使用
  • @Resource注解用在属性、setter方法上
  • @Autowired注解用在属性、setter方法、构造方法、构造方法参数上

@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【如果是JDK8的话不需要额外引入依赖。高于JDK8或低于JDK8都需要引入依赖】

<!--Spring6+版本使用-->
<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>2.1.1</version>
</dependency>

注意:Spring6不在支持JavaEE,它支持的是JakartaEE9。(Oracle把JavaEE贡献给了Apache了,Apache把JavaEE的名字改为了JakartaEE,原先的包名**javax.*统一修改为jakarta.***了)

<!--Spring5-版本使用-->
<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>
@Resource注解源码
@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
@Repeatable(Resources.class)
public @interface Resource {
    /**
    * 资源的JNDI名称。
    * 对于字段注释,默认值是字段名。
    * 对于方法注释,默认值是与方法对应的JavaBeans属性名。
    * 对于类注释,没有默认值,必须指定。
    */
    String name() default "";

    /**
     * The name of the resource that the reference points to. It can
     * link to any compatible resource using the global JNDI names.
     *
     * @since 1.7, Common Annotations 1.1
     */

    String lookup() default "";

    /**
     * 资源的Java类型。
     * 对于字段注释,默认值是字段的类型。
     * 对于方法注释,默认是JavaBeans属性的类型。
     * 对于类注释,没有默认值,必须指定
     */
    Class<?> type() default java.lang.Object.class;

    /**
     * The two possible authentication types for a resource.
     */
    enum AuthenticationType {
	    CONTAINER,
	    APPLICATION
    }

    /**
     * The authentication type to use for this resource.
     * This may be specified for resources representing a
     * connection factory of any supported type, and must
     * not be specified for resources of other types.
     */
    AuthenticationType authenticationType() default AuthenticationType.CONTAINER;

    /**
     * Indicates whether this resource can be shared between
     * this component and other components.
     * This may be specified for resources representing a
     * connection factory of any supported type, and must
     * not be specified for resources of other types.
     */
    boolean shareable() default true;

    /**
     * A product-specific name that this resource should be mapped to.
     * The <code>mappedName</code> element provides for mapping the
     * resource reference to the name of a resource known to the
     * applicaiton server.  The mapped name could be of any form.
     * <p>Application servers are not required to support any particular
     * form or type of mapped name, nor the ability to use mapped names.
     * The mapped name is product-dependent and often installation-dependent.
     * No use of a mapped name is portable.</p>
     */
    String mappedName() default "";

    /**
     * Description of this resource.  The description is expected
     * to be in the default language of the system on which the
     * application is deployed.  The description can be presented
     * to the Deployer to help in choosing the correct resource.
     */
    String description() default "";
}
@Resource使用
@Service
public class StudentService {

    @Resource(name = "studentDaoImplForOracle")
    private StudentDao studentDao;

    /*
    @Resource(name = "studentDaoImplForOracle")
    public void setStudentDao(StudentDao studentDao) {
        this.studentDao = studentDao;
    }
    */
    
    
    
    public void deleteStudent(){
        studentDao.deleteById();
    }
}

6 全注解式开发

所谓的全注解式开发就是不再使用spring配置文件了。写一个配置类来代替配置文件

@Configuration
@ComponentScan({"cn.klein.dao", "cn.klein.service"})
public class Spring6Config {

}

Test

@Test
public void testNoXML(){ 
    AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(Spring6Config.class);
    StudentService studentService = context.getBean("studentService", StudentService.class);
    studentService.deleteStudent();
}

GoF之代理模式

1 理解

1.1 概述

代理模式(Proxy Pattern)是一种结构性模式。代理模式为一个对象提供了一个替身,以控制对这个对象的访问。即通过代理对象访问目标目标对象,可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

被代理的对象可以是远程对象、创建开销大得对象或需要安全控制得对象。代理模式主要有三种形式,分别是静态代理动态代理(也称JDK代理、接口代理)和cglib代理(在内存动态创建对象而不需要实现接口,也可属于动态代理得范畴)

在这里插入图片描述

使用场景

  1. 当一个对象需要受到保护
  2. 需要给某个对象的功能进行增强的时候
  3. A对象无法和B对象直接交互时

1.2 代理模式的结构

代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,下面来分析其基本结构。

代理模式的主要角色如下。

  1. 抽象主题(Subject)类(业务接口类):通过接口或抽象类声明真实主题和代理对象实现的业务方法,服务端需要实现该方法。
  2. 真实主题(Real Subject)类(业务实现类,目标类):实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  3. 代理(Proxy)类(代理主题):提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

其结构图如图 1 所示。

代理模式的结构图
图1 代理模式的结构图

在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知

1.3 模式实现

根据代理的创建时期,代理模式分为静态代理和动态代理。

  • 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
  • 动态:在程序运行时,运用反射机制动态创建而成。

2 静态代理

静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码),非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

静态代理实现步骤:

  1. 定义一个接口及其实现类;
  2. 创建一个代理类同样实现这个接口
  3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

下面通过代码展示!

1. 定义发送短信的接口

public interface SmsService {
    String send(String message);
}

2. 实现发送短信的接口

public class SmsServiceImpl implements SmsService {
    @Override
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

3. 创建代理类并同样实现发送短信的接口

public class SmsProxy implements SmsService {

    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String send(String message) {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method send()");
        smsService.send(message);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method send()");
        return null;
    }
}

4. 实际使用

public class Main {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsProxy smsProxy = new SmsProxy(smsService);
        smsProxy.send("java");
    }
}

运行上述代码之后,控制台打印出:

before method send()
send message:java
after method send()

可以输出结果看出,我们已经增强了 SmsServiceImplsend()方法。

静态代理的缺点

  • 类爆炸:当系统中接口十分庞大时,每一个接口都有一个代理,那么会导致类数量急剧膨胀,维护性差 --使用**动态代理(字节码生成技术)**来解决这个问题。动态代理也是代理模式,可以来内存中动态生成字节码。

3 动态代理

在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量。解决代码复用问题

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

说到动态代理,Spring AOP、RPC 框架应该是两个不得不提的,它们的实现都依赖了动态代理。

动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。

就 Java 来说,动态代理的实现方式有很多种,比如

  • **JDK 动态代理:**只能代理接口
  • **CGLIB 动态代理:**CGLIB(Code Generation Library)是一个开源项目。是一个强大的、高性能、高质量的Code生成类库,它可以在运行期扩展Java类与实现接口。它既可以代理接口,又可以代理类,底层是通过继承方式实现的。性能也比JDK动态代理好。(底层有一个小而快的字节码处理框架ASM)
  • **Javassist动态代理:**Javassist是一个开源的分析、编辑和创建java字节码的类库。有东京工业大学的数学和计算机科学系的Shigeru Chiba(千叶 滋)所创建的。它已加入了开放源代码JBoss应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态“AOP”框架

3.1 JDK动态代理

3.1.1 Proxy.newProxyInstance()
public static Object newProxyInstance(
									ClassLoader loader, // 类加载器
									Class<?>[] interfaces, // 代理类需要实现的接口
									InvocationHandler h // 调用的处理器
) {
    ...
}

执行:Proxy.newProxyInstance()的执行,做了两件事

  1. 在内存中动态的生成了一个代理类的字节码class
  2. 通过内存中生成的代理类的字节码,实例化了代理对象

参数

  1. ClassLoader loader

    • 类加载器:在内存当中生成的字节码也是class文件,要执行也得先加载到内存当中。加载类就需要类加载器。所以这里需要指定类加载器
    • 并且JDK要求,目标类的类加载器和代理类的类加载器必须使用同一个
  2. Class<?>[] interfaces

    • 代理类和目标类要实现同一个或同一些接口
    • 在内存中生成代理类的时候,这个代理类需要你告诉它实现哪些接口
  3. InvocationHandler h

    • 调用处理器,是一个接口 --> 需要实现类
    • 在调用处理器接口中编写的就是增强代码

注意

  • 代理对象和目标对象实现的接口一样所以可以向下转型
3.1.2 InvocationHandler
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
    ...
}

实现

必须要实现InvocationHandler接口中的**invoke()**方法,因为JDK在底层调用invoke()方法的程序已经提前写好了。

注意:invoke()方法不是由我们程序员负责调用,是JDK负责调用的。

invoke方法调用时机

当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器接口中的invoke()方法会被调用

invoke方法的三个参数

invoke方法是JDK负责调用的,所以JDK调用方法的时候会自动给我们传过来这三个参数

我们可以在invoke方法的大括号中直接使用

  • Proxy proxy 代理对象的引用,这个参数使用较少
  • Method method 目标对象上的目标方法(要执行的目标方法method.invoke(Object obj, Object[] args)
    • method.invoke(Object obj, Object[] args)方法四要素
      • 哪个对象(obj,目标对象),哪个方法(method)、参数(args)、返回值(return)
  • Object[] args 目标方法上的参数
3.1.3 实例

1.定义发送短信的接口

public interface SmsService {
    String send(String message);
}

2.实现发送短信的接口

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

3.定义一个 JDK 动态代理类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 11:23:00
 */
public class DebugInvocationHandler implements InvocationHandler {
    /**
     * 代理类中的真实对象
     */
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
        throws InvocationTargetException, IllegalAccessException {
        
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return result;
    }
}

4.获取代理对象的工厂类

public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 目标类的类加载器
                target.getClass().getInterfaces(),  // 代理需要实现的接口,可指定多个
                new DebugInvocationHandler(target)   // 代理对象对应的自定义 InvocationHandler
        );
    }
}

5.测试

SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("java");

输出

before method send
send message:java
after method send

3.2 CGLIB动态代理

3.2.1 介绍

JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。

为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。

CGLIBopen in new window(Code Generation Library)是一个基于ASMopen in new window的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIBopen in new window, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。

CGLIB既可以代理接口又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰

使用CGLIB需要引入依赖

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.0.3</version>
</dependency>

你需要自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法。

public interface MethodInterceptor
extends Callback{
    // 拦截被代理类中的方法
    public Object intercept(
        Object obj, 
        java.lang.reflect.Method method, 
        Object[] args,
        MethodProxy proxy
    ) throws Throwable;
}
  1. obj : 被代理的对象(需要增强的对象)
  2. method : 被拦截的方法(需要增强的方法)
  3. args : 方法入参
  4. proxy : 用于调用原始方法

你可以通过 Enhancer类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor 中的 intercept 方法。

3.2.2 CGLIB 动态代理类使用步骤
  1. 定义一个类;
  2. 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
  3. 通过 Enhancer 类的 create()创建代理类;
3.2.3 实例

1.实现一个使用阿里云发送短信的类

public class AliSmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

2.自定义 MethodInterceptor(方法拦截器)

public class DebugMethodInterceptor implements MethodInterceptor {

    /**
     * @param target           被代理的对象(需要增强的对象)
     * @param method      被拦截的方法(需要增强的方法)
     * @param args        方法入参
     * @param methodProxy 用于调用原始方法
     */
    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 方法调用之前增强
        System.out.println("before method: " + method.getName());
        // 调用方法
        Object result = methodProxy.invokeSuper(target, args);
        // 方法调用之后增强
        System.out.println("after method: " + method.getName());
        return result;
    }
}

3.获取代理类

public class CglibProxyFactory {
    public static Object getProxy(Class<?> clazz){
        // 创建动态代理增强类  -- enhancer:增强器
        Enhancer enhancer = new Enhancer();
        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());
        // 设置被代理类
        enhancer.setSuperclass(clazz);
        // 设置方法拦截器
        enhancer.setCallback(new DebugMethodInterceptor());
        // 创建代理类
        return enhancer.create();
    }
}

4.实际使用

public static void main(String[] args) {
    AliSmsService proxy = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
    proxy.send("java");
}

输出

before method: send
send message:java
after method: send

注意

  • 在Java 9及更高版本中,Java类加载器的defineClass方法被标记为opens,这意味着它只能被Java标准库中的某些类访问。这会导致一些第三方库(如cglib)无法访问该方法,从而引发InaccessibleObjectException异常。 要解决这个问题,你可以添加虚拟机参数和程序实参:

    VM Optinons: --add-opens java.base/java.lang=ALL-UNNAMED
    Program arguments: --add-opens java.base/sun.net.util=ALL-UNNAMED
    

3.3 JDK动态代理和CGLIB动态代理对比

JDK动态代理:
优点:

  • 基于接口,更加面向对象,符合Java设计原则。
  • 在JDK 6之后经过优化,对于少量方法调用,性能可能优于CGLIB。
  • 不需要引入额外的库,因为Java自身就支持。

缺点:

  • 只能代理实现了接口的类,限制了其应用范围。
  • 相比于CGLIB在某些场景下的性能,可能略低。

CGLIB代理:
优点:

  • 不受接口限制,可以代理任何类,包括没有实现接口的类。
  • 通过字节码技术生成代理类,对于大量方法调用的场景,性能通常优于JDK动态代理。
  • 提供更多的灵活性,如可代理final类中的非final方法(但不能代理final类和final方法)。

缺点:

  • 需要引入第三方库。
  • 由于使用字节码技术,生成的类较难阅读和调试。
  • 与JDK动态代理相比,可能会有更高的内存占用,尤其是在频繁创建代理对象的场景下。

面向切面编程AOP

IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使用的功能,把它转化为组件

**AOP(Aspect Oriented Progarming):**面向切面编程,面向方面编程。(AOP是一种编程技术)

AOP是对面向对象编程(OOP)的一种补充延伸

AOP底层使用就是动态代理来实现的

Spring的AOP使用的动态代理是:JDK动态代理+CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果需要代理某个类,这个类没有实现就扣,就会切换CGLIB。当然也可以通过配置让Spring只使用CGLIB

1 AOP介绍

一般一个系统当中都会有一些系统服务,例如日志、事务管理、安全等。这些系统服务被称之为:交叉业务

这些交叉业务基本是通用的,不管你是做银行账户转账,还是删除用户数据。日志,事务管理、安全等,这些都是需要做的。

如果在每一个业务处理过程当中,都参杂这些交叉业务代码进去的话,存在两个问题:

  • 交叉业务代码在多个业务流程中反复出现,显然这个交叉业务的代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
  • 程序员无法专注核心代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务

使用AOP可以很轻松解决以上问题。

在这里插入图片描述

一句话总结AOP:将与核心业务无关的代码(交叉业务)独立抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被成为AOP

AOP的优点:

  • 代码复用性增强
  • 代码易维护
  • 使开发者更关注业务逻辑

2 AOP的七大术语

  1. 通知(Advice):
  • 定义:切面也需要完成工作。在 AOP 术语中,切面的工作被称为通知。
  • 工作内容:通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决何时执行这个工作。
  • Spring 切面可应用的 5 种通知类型:
  1. Before——在方法调用之前调用通知
  2. After——在方法完成之后调用通知,无论方法执行成功与否
  3. After-returning——在方法执行成功之后调用通知
  4. After-throwing——在方法抛出异常后进行通知
  5. Around——通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
  1. 连接点(Join point):
  • 定义:连接点是一个应用执行过程中能够插入一个切面的点。
  • 连接点可以是调用方法时、抛出异常时、甚至修改字段时
  • 切面代码可以利用这些点插入到应用的正规流程中
  • 程序执行过程中能够应用通知的所有点。
  1. 切点(Point cut):
  • 定义:如果通知定义了“什么”和“何时”。那么切点就定义了“何处”。切点会匹配通知所要织入的一个或者多个连接点。
  • 通常使用明确的类或者方法来指定这些切点。
  • 作用:定义通知被应用的位置(在哪些连接点)
  1. 切面(Aspect):
  • 定义:切面是通知和切点的集合,通知和切点共同定义了切面的全部功能——它是什么,在何时何处完成其功能。
  1. 引入:
  • 引入允许我们向现有的类中添加方法或属性
  1. 织入(Weaving):
  • 织入是将切面应用到目标对象来创建的代理对象过程。
  • 切面在指定的连接点被织入到目标对象中,在目标对象的生命周期中有多个点可以织入
  1. 编译期——切面在目标类编译时期被织入,这种方式需要特殊编译器。AspectJ的织入编译器就是以这种方式织入切面。
  2. 类加载期——切面在类加载到
  3. JVM ,这种方式需要特殊的类加载器,他可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5 的 LTW 就支持这种织入方式
  4. 运行期——切面在应用运行期间的某个时刻被织入。一般情况下,在织入切面时候,AOP 容器会为目标对象动态的创建代理对象。Spring AOP 就是以这种方式织入切面。
  1. 目标对象(Target):目标对象是含有连接点的对象
  2. 代理对象(Proxy):代理对象是 Spring AOP 创建的一个包含切面代码的对象。

3 切点表达式

切点表达式用来定义通知(Advice)往哪些方法上切入。

匹配方法切入点。根据表达式描述匹配方法,是最通用的表达式类型,可以匹配方法、类、包。

execution([访问权限控制符] 返回值类型 [全限定类名]方法名(形式参数列表)[异常])

execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)
  • modifier:匹配修饰符,public, private 等,省略时匹配任意修饰符
  • ret-type:匹配返回类型,使用 * 匹配任意类型
  • declaring-type:匹配目标类,省略时匹配任意类型
    • .. 匹配包及其子包的所有类
  • name-pattern:匹配方法名称,使用 * 表示通配符
    • * 匹配任意方法
    • set* 匹配名称以 set 开头的方法
  • param-pattern:匹配参数类型和数量
    • () 匹配没有参数的方法
    • (..) 匹配有任意数量参数的方法
    • (*) 匹配有一个任意类型参数的方法
    • (*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
  • throws-pattern:匹配抛出异常类型,省略时匹配任意类型
// 匹配service包下所有类中以delete开始的所有public方法
excution(public * com.klein.mall.service.*.delete*(..))
    
// 匹配mall包及其子包下所有类的任意方法
execution(* com.klein.mall..*())
    
// 匹配任意类的任意方法
execution(* *(..))

4 Spring对AOP的实现

Spring对AOP的实现包括以下3种方式:

  • Spring框架结合AspectJ框架实现的AOP,基于注解的方式
  • Spring框架集合AspectJ框架实现的AOP,基于XML的方式
  • Spring自己实现的AOP,基于XML配置方式

实际开发中都是Spring+AspectJ来实现AOP。

**AepectJ:**Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ

4.1 准备工作

Spring+AspectJ的依赖引入

<!--spring context依赖,会自动关联spring aop依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.1.4</version>
</dependency>

<!--AspectJ依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.1.4</version>
</dependency>

Spring配置文件种添加context和aop命名空间

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

</beans>

4.2 基于AspectJ的基于注解的方式

4.2.1 快速开始

1. 目标类

@Service("userService")
public class UserService { // 目标类

    public void login(){ // 目标方法
        System.out.println("系统正在进行身份认证...");
    }
}

2. 切面类

@Component("logAspect")
@Aspect // 切面类需要使用@Aspect注解进行标记
public class LogAspect { // 切面

    // 切面=通知+切点
    // 通知:增强代码,通知以方法的形式出现
    // @Before注解标注的方法就是一个前置通知
    // @Before(切点表达式)
    // 切点表达式:execution([访问权限控制符] 返回值类型 [全限定类名]方法名(形式参数列表)[异常])

    @Before("execution(public * com.klein.spring.service.*.*(..))")
    public void  loginAdvice(){
        System.out.println("我是一个通知Advice,我是一段增强代码...");
    }

}

3. 开启组件扫描和自动代理

<!--组件扫描-->
<context:component-scan base-package="com.klein.spring.service"/>

<!--开启自动代理-->
<!--spring容器在扫描类的时候,查看该类上是否有@Aspect注解,如果有,则给这个类生成代理对象-->
<!--
    proxy-target-class="true"   表示强制使用CGLIB动态代理
    proxy-target-class="false"  默认值,表示接口使用JDK动态代理,类使用CGLIB动态代理
-->
<aop:aspectj-autoproxy/>

4. 测试程序

@Test
public void testBefore(){
    ClassPathXmlApplicationContext applicationContext = 
        new ClassPathXmlApplicationContext("spring.xml");
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.login();
}

输出

我是一个通知Advice,我是一段增强代码...
系统正在进行身份认证...
4.2.2 通知类型

通知类型包括:

  • 前置通知:@Before目标方法执行之前的通知
  • 后置通知:@AfterReturning目标方法成功执行之后的通知
  • 环绕通知:@Around目标方法之前添加通知,同时目标方法执行之后添加通知
  • 异常通知:@AfterThrowing发生异常之后执行通知
  • 最终通知:@After放在finally语句块中的通知,即无论方法执行成功与否

代码示例

@Component
@Aspect
public class MyAspect {

    /**
     * 前置通知
     */
    @Before("execution(public * com.klein.spring.service.*.*(..))")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }

    /**
     * 后置通知
     */
    @AfterReturning("execution(public * com.klein.spring.service.*.*(..))")
    public void afterReturningAdvice(){
        System.out.println("后置通知");
    }

    /**
     * 环绕通知(环绕是最大的通知,在前置通知之前,后置通知之后)
     * @param joinPoint 连接点
     */
    @Around("execution(public * com.klein.spring.service.*.*(..))")
    public void roundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 前面的代码
        System.out.println("前环绕");
        // 执行目标
        joinPoint.proceed();
        // 后面的代码
        System.out.println("后环绕");

    }

    /**
     * 异常通知(finally语句块中的通知)
     */
    @AfterThrowing("execution(public * com.klein.spring.service.*.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }

    /**
     * 最终通知
     */
    @After("execution(public * com.klein.spring.service.*.*(..))")
    public void afterAdvice(){
        System.out.println("最终通知");
    }

}

测试程序

@Test
public void testMyAspect(){
    ClassPathXmlApplicationContext applicationContext = 
        new ClassPathXmlApplicationContext("spring.xml");

    OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
    orderService.generate();
}

无异常时执行结果

前环绕
前置通知
生成订单...
后置通知
最终通知
后环绕

有异常时执行结果

前环绕
前置通知
生成订单...
异常通知
最终通知
4.2.3 切面顺序

切面的执行顺序可以使用**@Order注解**来进行排序,@Order的值越小,优先级越高

@Component
@Aspect
@Order(1)
public class SecurityAspect {

    @Before("execution(* com.klein.spring.service.*.*(..))")
    public void beforeAdvice(){
        System.out.println("security aspect: 前置通知");
    }
}
4.2.4 通用注解

使用**@Pointcut注解**可以定义通用的切点表达式,定义通知时可以直接使用,例如@Before(“yourPointcutName”),通用的切点表达式也可以跨类使用,需要使用完整的类名

@Component
@Aspect
@Order(2)
public class MyAspect {

    /**
     * 定义通用的切点表达式
     */
    @Pointcut("execution(* com.klein.spring.service.*.*(..))")
    public void universalPointcut(){

    }


    /**
     * 前置通知
     */
    @Before("universalPointcut()")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }

    /**
     * 后置通知
     */
    @AfterReturning("universalPointcut()")
    public void afterReturningAdvice(){
        System.out.println("后置通知");
    }

    /**
     * 环绕通知(环绕是最大的通知,在前置通知之前,后置通知之后)
     * @param joinPoint 连接点
     */
    @Around("universalPointcut()")
    public void roundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 前面的代码
        System.out.println("前环绕");
        // 执行目标
        joinPoint.proceed();
        // 后面的代码
        System.out.println("后环绕");

    }

    /**
     * 异常通知(finally语句块中的通知)
     */
    @AfterThrowing("universalPointcut()")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }

    /**
     * 最终通知
     */
    @After("universalPointcut()")
    public void afterAdvice(){
        System.out.println("最终通知");
    }
}
4.2.5 连接点

JoinPoint:在spring容器调用切面时会将JoinPoint自动传给切面方法,通过JoinPoint我们可以获取到目标方法的方法签名

@Before("execution(* *(..))")
public void beforeAdvice(JoinPoint joinPoint){
    ...
    Signature signature = joinPoint.getSingnature();
    ...
}

通过方法签名,我们可以获取到方法的具体信息:方法名(getName())、声明类型(getDeclaringType)、修饰符(getModifiers())、声明类型名(getDeclaringTypeName)

4.3.5 全注解开发

全注解开发,就是完全脱离配置文件,使用配置类来代替配置文件

@Configuration
@ComponentScan({"com.klein.spring.service"}) // 组件扫描
@EnableAspectJAutoProxy() //启用aspect自动代理
public class Spring6Config {

}

测试程序

@Test
public void testNoXml(){
    AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(Spring6Config.class);
    OrderService orderService = context.getBean("orderService", OrderService.class);
    orderService.generate();
}

4.3 基于AspectJ的基于XML的方式(了解)

不使用注解,使用XML配置文件的方式

<!--纳入spring ioc容器进行管理-->
<bean id="userService" class="com.klein.spring.service.UserService"/>
<bean id="timerAspect" class="com.klein.spring.service.TimerAspect"/>

<!--aop配置-->
<aop:config>
	<!--切点表达式-->
    <aop:pointcut id="mypointcut" expression="execution(* com.klein.spring.service..*(..))"/>
    <!--切面-->
    <aop:aspect ref="timerAspect">
    	<aop:around method="aroundAdvice" pointcut-ref="mypointcut"/>
    </aop:aspect>
</aop:config>

5 AOP的实际案例:事务处理

5.1 场景描述

项目中的事务控制是在所难免的。在一个业务流程中,可能需要多条DML(数据操作语言)语句共同完成,为了保证数据安全,这多条DML语句要么同时成功,要么同时失败。这就需要添加事务控制的代码。例如:

伪代码:

class ServiceClass{
    
    public void serviceMethod1(){
        try{
            // 开启事务
            startTranscation();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ...
            
            //提交事务
            commitTranscation();    
        }catch(Exception e){
            // 回滚事务
            rollbackTranscation();
        }
    }
    
        public void serviceMethod2(){
        try{
            // 开启事务
            startTranscation();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ...
            
            //提交事务
            commitTranscation();    
        }catch(Exception e){
            // 回滚事务
            rollbackTranscation();
        }
    }
    ...
}
class ServiceClass2{
        public void serviceMethod1(){
        try{
            // 开启事务
            startTranscation();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ...
            
            //提交事务
            commitTranscation();    
        }catch(Exception e){
            // 回滚事务
            rollbackTranscation();
        }
    }
    
        public void serviceMethod2(){
        try{
            // 开启事务
            startTranscation();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ...
            
            //提交事务
            commitTranscation();    
        }catch(Exception e){
            // 回滚事务
            rollbackTranscation();
        }
    }
    ...
}

提取一下交叉业务

try{
    // 开启事务
    startTranscation();

    // 执行核心业务逻辑
    ...

    //提交事务
    commitTranscation();    
}catch(Exception e){
    // 回滚事务
    rollbackTranscation();
}

这个控制事务的代码就是和业务逻辑没有关系的交叉业务。以上伪代码中可以看到这些交叉业务的代码没有得到复用,并且如果交叉代码需要修改的话,那必然需要修改很多处,难以维护。因此可以采用AOP的思想来解决。可以把控制事务的代码作为环绕通知,切入到目标类的方法当中。

5.2 实例

1. 订单业务

@Service
public class OrderService {

    /**
     * 生成订单业务
     */
    public void generate(){
        System.out.println("正在生成订单...");
    }

    /**
     * 订单取消业务
     */
    public void cancel(){
        System.out.println("订单已取消...");
    }

}

2. 账户业务

@Service
public class AccountService {

    /**
     * 转账业务
     */
    public void transfer(){
        System.out.println("银行账户正在进行转账操作...");
    }

    /**
     * 取款业务
     */
    public void withdraw(){
        System.out.println("银行账户正在进行取款操作...");
    }

}

3. 切面类

@Component
@Aspect
public class TransactionAspect {

    @Around("execution(* com.klein.spring6.service..*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint){
        try {
            // 前环绕
            System.out.println("开启事务");
            //执行目标方法
            joinPoint.proceed();
            // 后环绕
            System.out.println("提交事务");
        } catch (Throwable e) {
            System.out.println("回滚事务");
        }

    }

}

4. 切面配置类

@Configuration
@ComponentScan({"com.klein.spring6.service","com.klein.spring6.aspect"})
@EnableAspectJAutoProxy
public class AspectConfig {
}

6. 测试程序

@Test
public void testTransactionNonXml(){
    AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(AspectConfig.class);

    AccountService accountService = context.getBean("accountService", AccountService.class);
    accountService.transfer();
    accountService.withdraw();

    OrderService orderService = context.getBean("orderService", OrderService.class);
    orderService.generate();
    orderService.cancel();
}

输出

开启事务
银行账户正在进行转账操作...
提交事务
开启事务
银行账户正在进行取款操作...
提交事务
开启事务
正在生成订单...
提交事务
开启事务
订单已取消...
提交事务

6 AOP的实际案例:安全日志

6.1 需求

项目开发结束了,已经上线了。运行正常。客户提出了新的需求:凡是在系统中进行修改操作、删除操作、新增操作的,都要把操作记录下来。因为这几个操作时危险行为。

1. 用户类

@Service
public class UserService {

    public void saveUser(){
        System.out.println("新增用户信息");
    }

    public void deleteUser(){
        System.out.println("删除用户信息");
    }

    public void modifyUser(){
        System.out.println("修改用户信息");
    }

    public void getUser(){
        System.out.println("获取用户信息");
    }

}

2. VIP类

@Service
public class VipService {

    public void saveVip(){
        System.out.println("新增会员信息");
    }

    public void deleteVip(){
        System.out.println("删除会员信息");
    }

    public void modifyVip(){
        System.out.println("修改会员信息");
    }

    public void getVip(){
        System.out.println("获取会员信息");
    }

}

3. 切面类

@Component
@Aspect
public class SecurityLogAspect {
    
    @Pointcut("execution(* com.klein.spring6.biz..save*(..))")
    private void savePointcut(){}
    
    @Pointcut("execution(* com.klein.spring6.biz..delete*(..))")
    private void deletePointcut(){}
    
    @Pointcut("execution(* com.klein.spring6.biz..get*(..))")
    private void getPointcut(){}
    
    @Pointcut("execution(* com.klein.spring6.biz..modify*(..))")
    private void modifyPointcut(){}
    

    @Before("savePointcut() || deletePointcut() || getPointcut() || modifyPointcut()")
    public void beforeAdvice(JoinPoint joinPoint){

        // 系统时间
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS");
        String now = sdf.format(new Date());

        System.out.println(now + 
                           " zhangsan : " + joinPoint.getSignature().getDeclaringTypeName()+"."+
                            joinPoint.getSignature().getName());
    }

}

4. 配置类

@Configuration
@ComponentScan({"com.klein.spring6.aspect", "com.klein.spring6.biz"})
@EnableAspectJAutoProxy
public class AspectConfig {
}

5. 测试程序

@Test
public void testSecurityLog(){
    AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(AspectConfig.class);

    UserService userService = context.getBean("userService", UserService.class);
    userService.saveUser();
    userService.getUser();
    userService.deleteUser();
    userService.modifyUser();

    VipService vipService = context.getBean("vipService", VipService.class);
    vipService.saveVip();
    vipService.getVip();
    vipService.deleteVip();
    vipService.modifyVip();
}

输出

2024-06-18 10:30:36 543 zhangsan : com.klein.spring6.biz.UserService.saveUser
新增用户信息
2024-06-18 10:30:36 545 zhangsan : com.klein.spring6.biz.UserService.getUser
获取用户信息
2024-06-18 10:30:36 545 zhangsan : com.klein.spring6.biz.UserService.deleteUser
删除用户信息
2024-06-18 10:30:36 546 zhangsan : com.klein.spring6.biz.UserService.modifyUser
修改用户信息
2024-06-18 10:30:36 546 zhangsan : com.klein.spring6.biz.VipService.saveVip
新增会员信息
2024-06-18 10:30:36 547 zhangsan : com.klein.spring6.biz.VipService.getVip
获取会员信息
2024-06-18 10:30:36 547 zhangsan : com.klein.spring6.biz.VipService.deleteVip
删除会员信息
2024-06-18 10:30:36 548 zhangsan : com.klein.spring6.biz.VipService.modifyVip
修改会员信息

Spring对事务的支持

1 事务概述

  • 什么是事务

    • 在一个业务流程中,通常需要多条DML(insert、delete、update)语句共同完成,这多条DML语句你必须同时成功,或者同时失败,这样才能保证数据的安全。
    • 多条DML要么同时成功,要么同时失败,这叫做事务
    • 事务:Transaction(tx)
  • 事务的四个处理过程

    1. 开启事务(start transaction)
    2. 执行核心业务代码
    3. 提交事务(核心业务处理过程中没有出现异常)(commit transaction)
    4. 回滚事务(核心业务处理过程中出现了异常)(rollback transaction)
  • 事务的四个特性(ACID)

    • A原子性:事务的最小工作单元,不可再分
    • C一致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变
    • I隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰
    • D持久性:持久性是事务结束的标志

2 事务场景

银行转账事务:账户act-001向act–002转账10000,有两个事务,act-001减10000,act-002加10000。这两个事务必须同时成功,或同时失败

连接数据库技术采用Spring框架的JdbcTemplate

采用三层架构搭建

在这里插入图片描述

模块名spring6-012-tx-bank

依赖pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.klein</groupId>
    <artifactId>spring6-012-tx-bank</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>20</maven.compiler.source>
        <maven.compiler.target>20</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <!--spring-context-->
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.1.4</version>
        </dependency>

        <!--spring JdbcTemplate-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.1.4</version>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.32</version>
        </dependency>

        <!--德鲁伊连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.3</version>
        </dependency>

        <!--@Resource注解-->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

2.1 准备数据库表

创建表

CREATE TABLE `t_act`  (
  `actno` varchar(255) NOT NULL COMMENT '账号',
  `balance` double NULL COMMENT '余额',
  PRIMARY KEY (`actno`)
);

填充数据

INSERT INTO `t_act` (`actno`, `balance`) VALUES ('act-001', 50000);
INSERT INTO `t_act` (`actno`, `balance`) VALUES ('act-002', 0);

2.2 创建包结构

-com.klein.bank
	|--pojo
	|--service
	|	|--impl
	|--dao
	|	|--impl
	|...

1. 账户类

public class Account {

    private String actno;

    private Double balance;

    @Override
    public String toString() {
        return "Account{" +
                "actno='" + actno + '\'' +
                ", balance=" + balance +
                '}';
    }

    public Account() {
    }

    public Account(String actno, Double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }
}

2. 业务接口

public interface AccountService {

    /**
     * 转账业务
     * @param fromActno 转出账户
     * @param toActno 转入账户
     * @param money 金额
     */
    void transfer(String fromActno, String toActno, double money);

}

3. 业务实现

@Service("accountService")
public class AccountServiceImpl implements AccountService {


    @Resource(name="accountDao")
    private AccountDao accountDao;

    @Override
    public void transfer(String fromActno, String toActno, double money) {

        // 开启事务

        // 1.查询转出账户余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);

        if (fromActno.equals(toActno)){
            throw new RuntimeException("不能向自己转账");
        }

        if (fromAct.getBalance() < money) {
            throw new RuntimeException("账户余额不足");
        }

        // 2.余额充足
        Account toAct = accountDao.selectByActno(toActno);
        if (toAct == null) {
            throw new RuntimeException("账户不存在 : " + toActno);
        }

        // 修改余额
        fromAct.setBalance(fromAct.getBalance()-money);
        toAct.setBalance(toAct.getBalance()+money);

        // 数据库更新
        int count = accountDao.update(fromAct);

        // 模拟异常
        String s = null;
        s.toString();

        count += accountDao.update(toAct);

        if (count != 2){
            throw new RuntimeException("转账失败....");
        }

        // 如果没有出现异常,提交事务

        // 如果出现异常,回滚事务
    }
}

4. 持久层接口

public interface AccountDao {

    /**
     * 根据账号查询账户信息
     * @param actno actno
     * @return Account
     */
    Account selectByActno(String actno);

    /**
     * 更新账户信息
     * @param act Account
     * @return int
     */
    int update(Account act);

}

5.持久层实现

@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Resource(name="jdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    @Override
    public Account selectByActno(String actno) {
        String sql = "select actno, balance from t_act where actno = ?";
        return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
    }

    @Override
    public int update(Account act) {
        String sql = "update t_act set balance = ? where actno=?";
        return jdbcTemplate.update(sql, act.getBalance(), act.getActno());
    }
}

6. 配置类

@Configuration
@PropertySource("classpath:datasource.properties")
@ComponentScan({"com.klein.bank.dao", "com.klein.bank.service"})
public class Spring6Config {

    @Value("${spring6.datasource.url}")
    private String dbUrl;

    @Value("${spring6.datasource.username}")
    private String username;

    @Value("${spring6.datasource.password}")
    private String password;

    @Value("${spring6.datasource.driver-class-name}")
    private String driverClassName;

    @Bean
    public DataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(dbUrl);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setDriverClassName(driverClassName);

        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }
}

也可以使用配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    <!--组件扫描-->
    <context:component-scan base-package="com.klein.bank.service, com.klein.bank.dao"/>
    
    <!--引入配置文件-->
    <context:property-placeholder location="datasource.properties"/>
    
    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${spring6.datasource.driver-class-name}"/>
        <property name="url" value="${spring6.datasource.url}"/>
        <property name="username" value="${spring6.datasource.username}"/>
        <property name="password" value="${spring6.datasource.password}"/>
    </bean>
    
    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

7.属性配置文件

spring6.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring6.datasource.url=jdbc:mysql://localhost:3306/spring6
spring6.datasource.username=yuor-name
spring6.datasource.password=password

8.测试程序

@Test
public void testSpringTx(){
    AnnotationConfigApplicationContext context = 
        new AnnotationConfigApplicationContext(Spring6Config.class);
    AccountService accountService = context.getBean("accountService", AccountService.class);
    try{
        accountService.transfer("act-001", "act-002", 10000);
        System.out.println("转账成功");
    }catch (Exception e){
        e.printStackTrace();
    }
}

3 Spring对事务的支持

3.1 Spring实现事务的两种方式

  • 编程式事务
    • 通过编写代码的方式来实现事务的管理
  • 声明式事务(建议)
    • 基于注解方式 @Transactional
    • 基于XML配置方式

3.2 Spring事务管理API

Spring对事务管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装,所以Spring专门针对事务开发了一套API,API核心接口为:PlatformTransactionManager接口 --方便接入其他ORM框架

PlatformTransactionManager接口:spring事务管理器的核心接口。在spring6中有两个实现:

  • DataSourceTransactionManager:支持JdbcTemplate、Mybatis、Hibernate等事务管理
  • JtaTransactionManager:支持分布式事务管理

3.3 声明式事务之注解实现方式

  • 第一步:在Spring配置事务管理器

    • 配置类的方式

      @Configuration
      @PropertySource("classpath:datasource.properties")
      @ComponentScan({"com.klein.bank.dao", "com.klein.bank.service"})
      @EnableTransactionManagement
      public class Spring6Config {
      	
          //其他配置
          ...
          
          /**
           * 配置事务管理器
           * @param dataSource 数据源
           * @return 事务管理器
           */
          @Bean
          public DataSourceTransactionManager txManager(DataSource dataSource){
              DataSourceTransactionManager txManager = new DataSourceTransactionManager();
              txManager.setDataSource(dataSource);
              return txManager;
          }
      }
      
    • Spring配置文件方式

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:context="http://www.springframework.org/schema/context"
             xmlns:tx="http://www.springframework.org/schema/tx"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
                                 http://www.springframework.org/schema/beans/spring-beans.xsd
                                 http://www.springframework.org/schema/context
                                 http://www.springframework.org/schema/context/spring-context.xsd
                                 http://www.springframework.org/schema/tx
                                 http://www.springframework.org/schema/tx/spring-tx.xsd">
          
          <!--组件扫描-->
          <context:component-scan base-package="com.klein.bank.service, com.klein.bank.dao"/>
          
          <!--引入配置文件-->
          <context:property-placeholder location="datasource.properties"/>
          
          <!--配置数据源-->
          <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
              <property name="driverClassName" value="${spring6.datasource.driver-class-name}"/>
              <property name="url" value="${spring6.datasource.url}"/>
              <property name="username" value="${spring6.datasource.username}"/>
              <property name="password" value="${spring6.datasource.password}"/>
          </bean>
          
          <!--配置JdbcTemplate-->
          <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
              <property name="dataSource" ref="dataSource"/>
          </bean>
          
          <!--配置事务管理器-->
          <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
              <property name="dataSource" ref="dataSource"/>
          </bean>
          
          <!--启用事务注解驱动器-->
          <tx:annotation-driven transaction-manager="txManager"/>
      
      </beans>
      
  • 第二步:使用@Transactional注解

    @Override
    @Transactional
    public void transfer(String fromActno, String toActno, double money) {
    
        // 1.查询转出账户余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);
    
        if (fromActno.equals(toActno)){
            throw new RuntimeException("不能向自己转账");
        }
    
        if (fromAct.getBalance() < money) {
            throw new RuntimeException("账户余额不足");
        }
    
        // 2.余额充足
        Account toAct = accountDao.selectByActno(toActno);
        if (toAct == null) {
            throw new RuntimeException("账户不存在 : " + toActno);
        }
    
        // 修改余额
        fromAct.setBalance(fromAct.getBalance()-money);
        toAct.setBalance(toAct.getBalance()+money);
    
        // 数据库更新
        int count = accountDao.update(fromAct);
    
    /*        // 模拟异常
        String s = null;
        s.toString();*/
    
        count += accountDao.update(toAct);
    
        if (count != 2){
            throw new RuntimeException("转账失败....");
        }
    }
    
    • @Transactional注解也可以在类上使用,表示该类的所有方法都开启事务管理

3.4 事务属性

3.4.1 事务中的重点属性
  • 事务传播行为Propagation propagation() default Propagation.REQUIRED;
  • 事务隔离行为Isolation isolation() default Isolation.DEFAULT;
  • 事务超时int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
  • 只读事务boolean readOnly() default false;
  • 设置出现哪些异常回滚事务Class<? extends Throwable>[] rollbackFor() default {};
  • 设置出现那些异常不回滚事务Class<? extends Throwable>[] noRollbackFor() default {};
3.4.2 事务传播行为

什么是事务的传播行为

在Service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()放啊执行过程中调用了b()方法,事务是如何传递的?还是开启一个新的事务?这就是事务的传播行为。

事务传播行为在Spring框架中被定义为枚举类型:

public enum Propagation {
	REQUIRED(0),
	SUPPORTS(1),
	MANDATORY(2),
	REQUIRES_NEW(3),
	NOT_SUPPORTED(4),
	NEVER(5),
	NESTED(6);
}

一共有七种传播行为

  • REQUIRED(required):支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
  • SUPPORTS(supports):支持当前事务,如果当前没有事务,就以非事务方法执行【有就加入,没有就不管】
  • MANDATORY(mandatory,强制性的):必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】
  • REQUIRES_NEW(requires new):开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前的事务被挂起】
  • NOT_SUPPORTED(not supported):以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
  • NEVER(never):以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛出异常】
  • NESTED(nested,嵌套的):如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套事务当中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样【有事务的话,就在这个事务里面嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样】

设置传播行为

@Transactional(propagation = Propagation.REQUIRED)
3.4.3 事务隔离级别

事务隔离级别就像一道墙,隔离级别越高,墙体越厚

数据库中读取数据存在三大问题:

  • 脏读:某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的
  • 不可重复读:在同一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读:在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就有几列数据是未查询出来的,如果此时插入和另外一个事务插入的数据,就会报错。【只要多个事务并发执行,就一定存在幻读问题】

事务隔离级别包括四个级别:

  • 读未提交:READ_UNCOMMITTED
    • 这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据
  • 读提交:READ_COMMITTED
    • 解决了脏读问题,其他事务提交之后才能读到,但存在不可重复读问题。
  • 可重复读:REPEATABLE_READ
    • 解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取的数据一直都是一样的。确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。但存在幻读问题。
  • 序列化:SERIALIZABLE
    • 解决了幻读问题,事务排队执行,不支持并发
隔离级别脏读不可重复读幻读
读未提交
读提交
可重复读
序列化

事务隔离级别在Spring框架中定义为枚举类型

public enum Isolation {
	DEFAULT(-1),
	READ_UNCOMMITTED(1),
	READ_COMMITTED(2),
	REPEATABLE_READ(4),
	SERIALIZABLE(8);
}
@Transactional(isolation=Isolation.DEFAULT)
3.4.4 事务超时
@Transactional(timeout=10)

以上代表设置事务的超时时间为10s,表示超过10s如果该事务中所有的DML语句还没有执行完毕的话,最终结果会选择回滚。

默认值-1,表示没有时间限制。

**注意:**事务的超时时间指的是最后一条DML语句执行之前的时间。如果最后一条DML语句后面有很多业务逻辑代码(不含DML语句),这些业务代码执行时间不被计入超时时间。

@Transactional(timeout = 10)
public void save(User user){
    sleep(20s);		// 伪代码,表示程序睡眠20s
    userDao.insert(user);
} ===> 超时
    
@Transactional(timeout = 10)
public void save(User user){
    userDao.insert(user);
    sleep(20s);		// 伪代码,表示程序睡眠20s
} ===> 不超时
3.4.5 只读事务
@Transactional(readOnly = true)

将该事务设置为只读事务,在该事务执行过程中只允许select语句执行,delete、insert、update均不执行。

该特性的作用是:启动spring的优化策略。提高select语句执行效率

如果该事务中确实没有增删改操作,建议设置为只读事务

3.4.6 异常回滚事务
@Transactional(rollbackFor = NumberFormatException.class)

表示只有发生NumberFormatException或该异常的子类才回滚

3.4.7 异常不回滚事务
@Trasactional(noRollbackFor = NullPointerException.class)

表示只要发生NullPointerException或该异常的子类就不会回滚,其他异常则回滚

3.5 事务的全注解式开发

即使用配置类来代替配置文件

@Configuration
@PropertySource("classpath:datasource.properties")
@ComponentScan({"com.klein.bank.dao", "com.klein.bank.service"})
@EnableTransactionManagement
public class Spring6Config {

    @Value("${spring6.datasource.url}")
    private String dbUrl;

    @Value("${spring6.datasource.username}")
    private String username;

    @Value("${spring6.datasource.password}")
    private String password;

    @Value("${spring6.datasource.driver-class-name}")
    private String driverClassName;

    
    // Spring框架在看到@Bean这个注解后,会调用这个被标注的方法,这个方法的返回值是一个java对象
    // 这个java对象会自动纳入IoC容器管理
    // 返回的对象就是Spring容器当中的一个Bean
    // 这个Bean的名字默认为方法名,可以自定义,比如@Bean(name="dataSource")
    @Bean
    public DataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(dbUrl);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setDriverClassName(driverClassName);

        return dataSource;
    }

    @Bean
    // Spring在调用这个方法的时候会自动传过来一个dataSource对象(前提是存在这么一个对象)
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }
    
    @Bean
    public DataSourceTransactionManager txManager(DataSource dataSource){
        DataSourceTransactionManager txManager = new DataSourceTransactionManager();
        txManager.setDataSource(dataSource);
        return txManager;
    }
}

3.6 声明式事务之XML实现方式

配置步骤:

  • 第一步:配置事务管理器
  • 第二步:配置通知
  • 第三步:配置切面

记得添加aspectj的依赖

<!--aspectj-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.1.4</version>
</dependency>

Spring配置文件

记得添加aop命名空间

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--组件扫描-->
    <context:component-scan base-package="com.klein.bank.service, com.klein.bank.dao"/>

    <!--引入配置文件-->
    <context:property-placeholder location="datasource.properties"/>

    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${spring6.datasource.driver-class-name}"/>
        <property name="url" value="${spring6.datasource.url}"/>
        <property name="username" value="${spring6.datasource.username}"/>
        <property name="password" value="${spring6.datasource.password}"/>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置通知, 具体的增强代码-->
    <!--需要关联事务管理器-->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!--配置通知的相关属性-->
        <tx:attributes>
            <!--之前的所讲的所有事务属性都可以在以下标签中配置-->
            <tx:method name="transfer" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <!--模糊匹配,匹配以save开头的所有方法-->
            <tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="select*" readOnly=true/>
        </tx:attributes>
    </tx:advice>

    <!--配置切面-->
    <aop:config>
        <!--切点-->
        <aop:pointcut id="txPointcut" expression="execution(* com.klein.bank.service..*(..))"/>
        <!--切面 = 通知 + 切点-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>

</beans>

Spring6整合JUnit5

1 Spring对JUnit4的支持

引入junit和spring对junit支持的相关依赖依赖

<!--spring对junit支持的依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <!--spring6即支持junit4又支持junit5-->
    <version>6.1.4</version>
</dependency>

<!--junit4依赖-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

使用测试程序

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class) // 配置类
// @ContextConfiguration("classpath:spring.xml")  --对于配置文件这样引入,classpath:表示类路径
public class SpringJUnit4Test {

    @Autowired
    private User user;

    @Test
    public void testUser(){
		// ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
		// User user = context.getBean("user", User.class);
		System.out.println(user);
    }
}

2 Spring对JUnit5的支持

引入junit和spring对junit支持的相关依赖依赖

<!--spring对junit支持的依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <!--spring6即支持junit4又支持junit5-->
    <version>6.1.4</version>
</dependency>

<!--junit5的依赖-->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
</dependency>

使用测试程序

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SpringConfig.class)
public class SpringJUnit5Test {
    
    @Autowired
    private User user;

    @Test
    public void testUser(){
        System.out.println(user);
    }
}

Spring6集成MyBatis3.5

1 实现步骤

  • 第一步:准备数据库

    • 使用t_act表(账户表)
  • 第二步:IDEA中创建模块,并引入依赖

    • spring-context

    • spring-jdbc

    • mysql驱动

    • mybatis

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.15</version>
      </dependency>
      
    • mybatis-spring:mybatis提供的与spring框架集成的依赖

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>3.0.3</version>
      </dependency>
      
    • 德鲁伊连接池(druid)

    • junit

  • 第三步:基于三层架构实现

    • com.klein.bank.mapper
    • com.klein.bank.service
    • com.klein.bank.service.impl
    • com.klein.bank.pojo
  • 第四步:编写pojo

    • Account,属性私有化,提供公开的setter、getter、toString方法
  • 第五步:编写mapper接口

    • AccountMapper接口,定义方法
  • 第六步:编写mapper配置文件

    • 在配置文件中配置命名空间,以及每一个方法对应的sql
  • 第七步:编写service接口

    • AccountService
    • AccountServiceImpl
  • 第八步:编写jdbc.properties配置文件

    • 数据库连接池相关信息
  • 第九步:编写mybatis-config.xml配置文件

    • 该文件可以没有,大部分配置可以转移到spring配置文件中

    • 如果遇到mybatis相关的系统及配置,还是需要这个文件

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration
              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>
      
          <!--帮助我们打印mybatis的日志信息-->
          <settings>
              <setting name="logImpl" value="STDOUT_LOGGING"/>
          </settings>
      
      </configuration>
      
  • 第十步:编写spring.xml配置文件(也可以使用配置类)

    • 组件扫描

    • 引入外部属性文件

    • 数据源

    • SqlSessionFactoryBean配置

      <!--SqlSessionFactoryBean配置-->
      <bean class="org.mybatis.spring.SqlSessionFactoryBean">
          <!--注入数据源-->
          <property name="dataSource" ref="dataSource"/>
          <!--指定mybatis核心配置文件-->
          <property name="configLocation" value="mybatis-config.xml"/>
          <!--指定别名包-->
          <property name="typeAliasesPackage" value="com.klein.bank.pojo"/>
      </bean>
      
      • 注入mybatis核心配置文件
      • 指定别名包
      • 注入数据源
    • Mapper扫描配置器

      <!--Mapper扫描配置器-->
      <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
          <!--指定扫描的包-->
          <property name="basePackage" value="com.klein.bank.mapper"/>
      </bean>
      
      • 指定扫描的包
    • 事务管理器DataSourceTransactionManager

      <!--事务管理器DataSourceTransactionManager-->
      <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <!--注入数据源-->
          <property name="dataSource" ref="dataSource"/>
      </bean>
      
      • 注入数据源
    • 启用事务注解

      <!--启用事务注解-->
      <!--注入事务管理器-->
      <tx:annotation-driven transaction-manager="txManager"/>
      
      • 注入事务管理器
    • 第十一步:编写测试程序,并添加事务,进行测试

Spring中的八大模式

1 简单工厂模式

BeanFactory的getBean()方法。通过唯一标识获取Bean对象。是典型的简单工厂模式

2 工厂方法模式

FactoryBean是典型的工厂方法模式。在配置文件中通过factory-method属性来指定工厂方法,该方法是一个实例方法。

3 单例模式

Spring用的是双重判断加锁的单例模式。

4 代理模式

Spring的AOP就是使用了动态代理实现的

5 装饰器模式

JavaSE中的IO流是非常典型的装饰器模式。

Spring配置DataSource的时候,DataSource可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySql等,也可能是不同的数据源:比如apache提供的org.apache.commons.dbcp.BasicDataSource、spring提供的org.springframework.jndi,JndiObjectFactoryBean等

这时,能否在尽可能少的修改原有代码的情况下,做到动态切换不同的数据源?此时可以用到装饰器模式。

Spring中类名中带有:Decorator和Wrapper单词的类,都是装饰器模式

6 观察者模式

定义对象之间一对多的关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并自动更新。Spring中观察者模式一般用在listener实现。

Spring中的事件编程模式就是观察者模式的实现。在Spring中定义了一个ApplicationListener接口,用来监听Application的事件,Application其实就是ApplicationContextApplicationContext中内置了几个事件,其中比较容易理解的是:ContextRefreshedEventContextStartedEventContextStartedEventContextStoppedEventContextClosedEvent

7 策略模式

策略模式是行为性模式,调用不同的方法,适应行为的变化,强调父类调用子类的特性。

getHandlerHandlerMapping接口中唯一的方法,用于根据请求找到匹配的处理器。

比如我写的接口AccountDao,然后这个接口有不同的实现类:AccountDaoImplForMySqlAccountDaoImplForOracle。对于service来说不需要关心底层的具体实现,只需要向AccountDao接口调用,底层可以灵活切换实现,这就是策略模式

8 模板方法模式

Spring中的JdbcTemplate类就是一个模板类。他就是一个模板方法设计模式的体现。在模板类的模板方法execute中编写核心算法,具体实现步骤在子类中完成。
SpringJUnit4Test {

@Autowired
private User user;

@Test
public void testUser(){
	// ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
	// User user = context.getBean("user", User.class);
	System.out.println(user);
}

}


## 2 Spring对JUnit5的支持

引入junit和spring对junit支持的相关依赖依赖

```xml
<!--spring对junit支持的依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <!--spring6即支持junit4又支持junit5-->
    <version>6.1.4</version>
</dependency>

<!--junit5的依赖-->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
</dependency>

使用测试程序

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SpringConfig.class)
public class SpringJUnit5Test {
    
    @Autowired
    private User user;

    @Test
    public void testUser(){
        System.out.println(user);
    }
}

Spring6集成MyBatis3.5

1 实现步骤

  • 第一步:准备数据库

    • 使用t_act表(账户表)
  • 第二步:IDEA中创建模块,并引入依赖

    • spring-context

    • spring-jdbc

    • mysql驱动

    • mybatis

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.15</version>
      </dependency>
      
    • mybatis-spring:mybatis提供的与spring框架集成的依赖

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>3.0.3</version>
      </dependency>
      
    • 德鲁伊连接池(druid)

    • junit

  • 第三步:基于三层架构实现

    • com.klein.bank.mapper
    • com.klein.bank.service
    • com.klein.bank.service.impl
    • com.klein.bank.pojo
  • 第四步:编写pojo

    • Account,属性私有化,提供公开的setter、getter、toString方法
  • 第五步:编写mapper接口

    • AccountMapper接口,定义方法
  • 第六步:编写mapper配置文件

    • 在配置文件中配置命名空间,以及每一个方法对应的sql
  • 第七步:编写service接口

    • AccountService
    • AccountServiceImpl
  • 第八步:编写jdbc.properties配置文件

    • 数据库连接池相关信息
  • 第九步:编写mybatis-config.xml配置文件

    • 该文件可以没有,大部分配置可以转移到spring配置文件中

    • 如果遇到mybatis相关的系统及配置,还是需要这个文件

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration
              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>
      
          <!--帮助我们打印mybatis的日志信息-->
          <settings>
              <setting name="logImpl" value="STDOUT_LOGGING"/>
          </settings>
      
      </configuration>
      
  • 第十步:编写spring.xml配置文件(也可以使用配置类)

    • 组件扫描

    • 引入外部属性文件

    • 数据源

    • SqlSessionFactoryBean配置

      <!--SqlSessionFactoryBean配置-->
      <bean class="org.mybatis.spring.SqlSessionFactoryBean">
          <!--注入数据源-->
          <property name="dataSource" ref="dataSource"/>
          <!--指定mybatis核心配置文件-->
          <property name="configLocation" value="mybatis-config.xml"/>
          <!--指定别名包-->
          <property name="typeAliasesPackage" value="com.klein.bank.pojo"/>
      </bean>
      
      • 注入mybatis核心配置文件
      • 指定别名包
      • 注入数据源
    • Mapper扫描配置器

      <!--Mapper扫描配置器-->
      <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
          <!--指定扫描的包-->
          <property name="basePackage" value="com.klein.bank.mapper"/>
      </bean>
      
      • 指定扫描的包
    • 事务管理器DataSourceTransactionManager

      <!--事务管理器DataSourceTransactionManager-->
      <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <!--注入数据源-->
          <property name="dataSource" ref="dataSource"/>
      </bean>
      
      • 注入数据源
    • 启用事务注解

      <!--启用事务注解-->
      <!--注入事务管理器-->
      <tx:annotation-driven transaction-manager="txManager"/>
      
      • 注入事务管理器
    • 第十一步:编写测试程序,并添加事务,进行测试

Spring中的八大模式

1 简单工厂模式

BeanFactory的getBean()方法。通过唯一标识获取Bean对象。是典型的简单工厂模式

2 工厂方法模式

FactoryBean是典型的工厂方法模式。在配置文件中通过factory-method属性来指定工厂方法,该方法是一个实例方法。

3 单例模式

Spring用的是双重判断加锁的单例模式。

4 代理模式

Spring的AOP就是使用了动态代理实现的

5 装饰器模式

JavaSE中的IO流是非常典型的装饰器模式。

Spring配置DataSource的时候,DataSource可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySql等,也可能是不同的数据源:比如apache提供的org.apache.commons.dbcp.BasicDataSource、spring提供的org.springframework.jndi,JndiObjectFactoryBean等

这时,能否在尽可能少的修改原有代码的情况下,做到动态切换不同的数据源?此时可以用到装饰器模式。

Spring中类名中带有:Decorator和Wrapper单词的类,都是装饰器模式

6 观察者模式

定义对象之间一对多的关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并自动更新。Spring中观察者模式一般用在listener实现。

Spring中的事件编程模式就是观察者模式的实现。在Spring中定义了一个ApplicationListener接口,用来监听Application的事件,Application其实就是ApplicationContextApplicationContext中内置了几个事件,其中比较容易理解的是:ContextRefreshedEventContextStartedEventContextStartedEventContextStoppedEventContextClosedEvent

7 策略模式

策略模式是行为性模式,调用不同的方法,适应行为的变化,强调父类调用子类的特性。

getHandlerHandlerMapping接口中唯一的方法,用于根据请求找到匹配的处理器。

比如我写的接口AccountDao,然后这个接口有不同的实现类:AccountDaoImplForMySqlAccountDaoImplForOracle。对于service来说不需要关心底层的具体实现,只需要向AccountDao接口调用,底层可以灵活切换实现,这就是策略模式

8 模板方法模式

Spring中的JdbcTemplate类就是一个模板类。他就是一个模板方法设计模式的体现。在模板类的模板方法execute中编写核心算法,具体实现步骤在子类中完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值