Spring启示录

持久层(dao层)impl类

public class UserDaoImplForMySQL implements UserDao {
    @Override
    public void deleteById() {
        System.out.println("MySQL数据库正在删除用户信息....");
    }
}

业务层(service层)impl类

public class UserServiceImpl implements UserService {
    //调用Dao的增删改查方法
    private UserDao userDao = new UserDaoImplForMySQL();

    @Override
    public void deleteUser() {
        userDao.deleteById();
    }
}

表示层(web/action层)类

public class UserAction {

    //调用service业务层里面的方法
    private UserService userService = new UserServiceImpl();

    /**
     * 删除用户信息的请求
     */
    public void deleteRequest(){
        userService.deleteUser();
    }
}

可以看出,UserDaoImplForMySQL中主要是连接MySQL数据库进行操作。如果更换到Oracle数据库上,则需要再提供一个UserDaoImplForOracle,如下:

public class UserDaoImplForOracle implements UserDao {
    public User selectByUsernameAndPassword(String username, String password) {
        // 连接Oracle数据库,根据用户名和密码查询用户信息
        return null;
    }
}

很明显,以上的操作正在进行功能的扩展,添加了一个新的类UserDaoImplForOracle来应付数据库的变化,这里的变化会引起连锁反应,如果想要切换到Oracle数据库上,UserServiceImpl类代码就需要修改,这样会导致整个系统程序改变,很容易出错。

OCP开闭原则

开闭原则是这样说的:在软件开发过程中应当对扩展开放,对修改关闭。也就是说,如果在进行功能扩展的时候,添加额外的类是没问题的,但因为功能扩展而修改之前运行正常的程序,这是忌讳的,不被允许的。因为一旦修改之前运行正常的程序,就会导致项目整体要进行全方位的重新测试。这是相当麻烦的过程。导致以上问题的主要原因是:代码和代码之间的耦合度太高。

表示层依赖业务层依赖持久层,拓展功能改变持久层代码,则业务层和表示层都需要改代码,这样就会导致 下面只要改动,上面必然也要改动

image.png

依赖倒置原则DIP

依赖倒置原则(Dependence Inversion Principle),简称DIP,主要倡导面向抽象编程,面向接口编程,不要面向具体编程,让上层不再依赖下层,下面改动了,上面的代码不会受到牵连。这样可以大大降低程序的耦合度,耦合度低了,扩展力就强了,同时代码复用性也会增强。(软件七大开发原则都是在为解耦合服务

image-20230502125039378

确实已经面向接口编程了,但对象的创建是:new UserServiceImpl() 显然并没有完全面向接口编程,还是使用到了具体的接口实现类。

完全面向接口编程为:

image-20230502125319230

如果代码是这样编写的,才算是完全面向接口编程,才符合依赖倒置原则。那你可能会问,这样userDao是null,在执行的时候就会出现空指针异常呀。你说的有道理,确实是这样的,所以我们要解决这个问题。解决空指针异常的问题,其实就是解决两个核心的问题:

  • 第一个问题:谁来负责对象的创建。【也就是说谁来:new UserDaoImplForOracle()/new UserDaoImplForMySQL()】
  • 第二个问题:谁来负责把创建的对象赋到这个属性上。【也就是说谁来把上面创建的对象赋给userDao属性】

如果我们把以上两个核心问题解决了,就可以做到既符合OCP开闭原则,又符合依赖倒置原则。

在Spring框架中,它可以帮助我们new对象,并且它还可以将new出来的对象赋到属性上。换句话说,Spring框架可以帮助我们创建对象,并且可以帮助我们维护对象和对象之间的关系。比如:

image.png

很显然,这种方式是将对象的创建权/管理权交出去了,不再使用硬编码的方式了。同时也把对象关系的管理权交出去了,也不再使用硬编码的方式了。像这种把对象的创建权交出去,把对象关系的管理权交出去,被称为控制反转。

控制反转loC

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计思想,可以用来降低代码之间的耦合度,符合依赖倒置原则。

控制反转的核心是:将对象的创建权交出去,将对象和对象之间关系的管理权交出去,由第三方容器来负责创建与维护

控制反转常见的实现方式:依赖注入(Dependency Injection,简称DI)

通常,依赖注入的实现由包括两种方式:

  • set方法注入

  • 构造方法注入

而Spring框架就是一个实现了IoC思想的框架。

IoC可以认为是一种全新的设计模式,但是理论和时间成熟相对较晚,并没有包含在GoF中。(GoF指的是23种设计模式)

Spring底层的IOC容器是通过:XML解析 + 工厂模式 + 反射机制 实现的

Spring概述

Spring简介

pring是一个开源框架,它由Rod Johnson创建。它是为了解决企业应用开发的复杂性而创建的。

从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。

Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。

Spring最初的出现是为了解决EJB臃肿的设计,以及难以测试等问题。

Spring为简化开发而生,让程序员只需关注核心业务的实现,尽可能的不再关注非业务逻辑代码(事务控制,安全日志等)。

Spring8大模块

image.png
  1. Spring Core模块

    这是Spring框架最基础的部分,它提供了依赖注入(DependencyInjection)特征来实现容器对Bean的管理。核心容器的主要组件是 BeanFactory,BeanFactory是工厂模式的一个实现,是任何Spring应用的核心。它使用IoC将应用配置和依赖从实际的应用代码中分离出来。

  2. Spring Context模块

    如果说核心模块中的BeanFactory使Spring成为容器的话,那么上下文模块就是Spring成为框架的原因。

    这个模块扩展了BeanFactory,增加了对国际化(I18N)消息、事件传播、验证的支持。另外提供了许多企业服务,例如电子邮件、JNDI访问、EJB集成、远程以及时序调度(scheduling)服务。也包括了对模版框架例如Velocity和FreeMarker集成的支持

  3. Spring AOP模块

    Spring在它的AOP模块中提供了对面向切面编程的丰富支持,Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中,可以自定义拦截器、切点、日志等操作。

  4. Spring DAO模块

    提供了一个JDBC的抽象层和异常层次结构,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析,用于简化JDBC。

  5. Spring ORM模块

    Spring提供了ORM模块。Spring并不试图实现它自己的ORM解决方案,而是为几种流行的ORM框架提供了集成方案,包括Hibernate、JDO和iBATIS SQL映射,这些都遵从 Spring 的通用事务和 DAO 异常层次结构。

  6. Spring Web MVC模块

    Spring为构建Web应用提供了一个功能全面的MVC框架。虽然Spring可以很容易地与其它MVC框架集成,例如Struts,但Spring的MVC框架使用IoC对控制逻辑和业务对象提供了完全的分离。

  7. Spring WebFlux模块

    Spring Framework 中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版的后期添加的。它是完全非阻塞的,支持反应式流(Reactive Stream)背压,并在Netty,Undertow和Servlet 3.1+容器等服务器上运行。

  8. Spring Web模块

    Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文,提供了Spring和其它Web框架的集成,比如Struts、WebWork。还提供了一些面向服务支持,例如:实现文件上传的multipart请求。

image.png

Spring特点

  • 轻量

    • 从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。

    • Spring是非侵入式的:Spring应用中的对象不依赖于Spring的特定类。

      • 侵入式:需要依赖别人的

        非侵入式:不依赖别人的

        也就是说,我们自己创建的对象不依赖spring容器

  • 控制反转

    • Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
  • 面向切面

    • Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
  • 容器

    • Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
  • 框架

    • Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。

所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。

入门

Spring的jar包

JAR文件 描述
spring-aop-5.3.9.jar 这个jar 文件包含在应用中使用Spring 的AOP 特性时所需的类
spring-aspects-5.3.9.jar 提供对AspectJ的支持,以便可以方便的将面向切面的功能集成进IDE中
spring-beans-5.3.9.jar 这个jar 文件是所有应用都要用到的,它包含访问配置文件、创建和管理bean 以及进行Inversion ofControl / Dependency Injection(IoC/DI)操作相关的所有类。如果应用只需基本的IoC/DI 支持,引入spring-core.jar 及spring-beans.jar 文件就可以了。
spring-context-5.3.9.jar 这个jar 文件为Spring 核心提供了大量扩展。可以找到使用Spring ApplicationContext特性时所需的全部类,JDNI 所需的全部类,instrumentation组件以及校验Validation 方面的相关类。
spring-context-indexer-5.3.9.jar 虽然类路径扫描非常快,但是Spring内部存在大量的类,添加此依赖,可以通过在编译时创建候选对象的静态列表来提高大型应用程序的启动性能。
spring-context-support-5.3.9.jar 用来提供Spring上下文的一些扩展模块,例如实现邮件服务、视图解析、缓存、定时任务调度等
spring-core-5.3.9.jar Spring 框架基本的核心工具类。Spring 其它组件要都要使用到这个包里的类,是其它组件的基本核心,当然你也可以在自己的应用系统中使用这些工具类。
spring-expression-5.3.9.jar Spring表达式语言。
spring-instrument-5.3.9.jar Spring3.0对服务器的代理接口。
spring-jcl-5.3.9.jar Spring的日志模块。JCL,全称为”Jakarta Commons Logging”,也可称为”Apache Commons Logging”。
spring-jdbc-5.3.9.jar Spring对JDBC的支持。
spring-jms-5.3.9.jar 这个jar包提供了对JMS 1.0.2/1.1的支持类。JMS是Java消息服务。属于JavaEE规范之一。
spring-messaging-5.3.9.jar 为集成messaging api和消息协议提供支持
spring-orm-5.3.9.jar Spring集成ORM框架的支持,比如集成hibernate,mybatis等。
spring-oxm-5.3.9.jar 为主流O/X Mapping组件提供了统一层抽象和封装,OXM是Object Xml Mapping。对象和XML之间的相互转换。
spring-r2dbc-5.3.9.jar Reactive Relational Database Connectivity (关系型数据库的响应式连接) 的缩写。这个jar文件是Spring对r2dbc的支持。
spring-test-5.3.9.jar 对Junit等测试框架的简单封装。
spring-tx-5.3.9.jar 为JDBC、Hibernate、JDO、JPA、Beans等提供的一致的声明式和编程式事务管理支持。
spring-web-5.3.9.jar Spring集成MVC框架的支持,比如集成Struts等。
spring-webflux-5.3.9.jar WebFlux是 Spring5 添加的新模块,用于 web 的开发,功能和 SpringMVC 类似的,Webflux 使用当前一种比较流程响应式编程出现的框架。
spring-webmvc-5.3.9.jar SpringMVC框架的类库
spring-websocket-5.3.9.jar Spring集成WebSocket框架时使用

注意:

如果你只是想用Spring的IoC功能,仅需要引入:spring-context即可。将这个jar包添加到classpath当中。

如果采用maven只需要引入context的依赖即可。

程序开发

引入依赖

https://spring.io/projects/spring-framework#learn

添加依赖:

<dependencies>
    <!--spring context依赖-->
    <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.0.2</version>
    </dependency>

    <!--junit5测试-->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.3.1</version>
    </dependency>
</dependencies>

也可以用 junit4 依赖

<!--junit4测试-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

创建java类

创建对象时调用了无参数构造方法,spring是通过调用无参数构造方法来创建对象的,所以必须保证有无参数构造方法

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

创建配置文件

在resources目录创建一个Spring配置文件 spring.xml(配置文件名称可随意命名)

Spring配置文件可以右键resources目录,选中

image-20230502214822398 image-20230502214659060

Spring的配置文件:

这个文件名可以是其他名字
这个文件最好是放在类路径当中,方便后期的移植,放在resources根目录下,就相当于是放到了类的根路径下
配置bean,这样spring才可以帮助我们管理这个对象

bean标签的两个重要属性:
id : 唯一的标识,不能重复
class : 必须填写类的全路径,全限定类名(带包名的类名)

<?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="userBean" class="com.byxl8112.spring6.bean.User"/>

    <bean id="userDaoBean" class="com.byxl8112.spring6.dao.UserDaoImplForMySQL"/>

    <bean id="nowTime" class="java.util.Date"/>
</beans>

创建测试类测试

@Test
public void testFirstSpringCode(){
    // 第一步:获取Spring容器对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    // 第二步:根据bean的id从Spring容器中获取这个对象,如果id不存在则会报错
    User userBean = applicationContext.getBean("userBean", User.class);
    System.out.println(userBean);
}

ApplicationContext 接口下有很多实现类,其中有一个实现类叫做:ClassPathXmlApplicationContext(),括号里面是spring配置文件的路径

只要这行new ClassPathXmlApplicationContext() 代码执行,就相当于启动了Spring容器,解析spring.xml文件,并且实例化所有的bean对象,放到spring容器当中

applicationContext 里面有个获取bean的方法getBean(),括号里是要获取这个bean的id值,如果bean的id不存在,不会返回null,而是出现异常

第二步中的,User.class,表示强转为User类

在配置文件中的类也可以使用JDK中的类,例如:java.util.Date

/**
   * 格式化输出日期,需要强制类型转换为Date,不想强制类型转换,可以使用以下代码
   * 通过第二个参数来指定返回的bean的类型,即类.class
 */
Date nowTime = applicationContext.getBean("nowTime", Date.class);
//自定义格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strNowTime = sdf.format(nowTime);
System.out.println(strNowTime);

Spring是创建对象是通过反射机制调用无参数构造方法创建对象

Class clazz = Class.forName("com.byxl8112.spring6.bean.User");
// Object obj = clazz.newInstance();
Object object = clazz.getDeclaredConstructor().newInstance();

创建好的对象存储到了一个Map集合的数据结构中

image.png

spring的配置文件可以有多个,在ClassPathXmlApplicationContext构造方法的参数上传递文件路径即可,中间用逗号隔开

image-20230503092612350

因为在源码中:

image.png

getBean()方法返回的类型是Object,如果访问子类的特有属性和方法时,还需要向下转型

可以用到getBean()方法的第二个参数 类.class

User user = applicationContext.getBean("userBean", User.class);

ClassPathXmlApplicationContext是从类路径中加载配置文件,如果没有在类路径当中,又应该如何加载配置文件呢?

可以用绝对路径

ApplicationContext applicationContext2 = new FileSystemXmlApplicationContext("D:/spring6.xml");
User user = applicationContext2.getBean("userBean2", User.class);
System.out.println(user);

ApplicationContext的顶级父接口是BeanFactory

BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring.xml");
User userBean = =beanFactory.getBean("userBean", User.class);
System.out.println(userBean);

启用Log4j2日志框架

从Spring5之后,Spring框架支持集成的日志框架是Log4j2

Log4j2主要由几个重要的组件构成:

(1)日志信息的优先级,日志信息的优先级从高到低有TRACE < DEBUG < INFO < WARN < ERROR < FATAL
TRACE:追踪,是最低的日志级别,相当于追踪程序的执行
DEBUG:调试,一般在开发中,都将其设置为最低的日志级别
INFO:信息,输出重要的信息,使用较多
WARN:警告,输出警告的信息
ERROR:错误,输出错误信息
FATAL:严重错误

这些级别分别用来指定这条日志信息的重要程度;级别高的会自动屏蔽级别低的日志,也就是说,设置了WARN的日志,则INFO、DEBUG的日志级别的日志不会显示

(2)日志信息的输出目的地,日志信息的输出目的地指定了日志将打印到控制台还是文件中

(3)日志信息的输出格式,而输出格式则控制了日志信息的显示内容。

第一步:引入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篇日志文件

文件名固定为:log4j2.xml,文件必须放到类根路径下(resources)

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

第三步:使用日志框架

Logger logger = LoggerFactory.getLogger(FirstSpringTest.class); 
//此FirstSpringTest.class 指的是这个类使用日志框架
logger.info("消息...");
logger.debug("调试....");
logger.error("错误...");

Spring对IoC容器的实现

IoC控制反转

  • 控制反转是一种思想。

  • 控制反转是为了降低程序耦合度,提高程序扩展力,达到OCP原则,达到DIP原则。

  • 控制反转,反转的是什么?

    • 将对象的创建权利交出去,交给第三方容器负责。
    • 将对象和对象之间关系的维护权交出去,交给第三方容器负责。
  • 控制反转这种思想如何实现呢?

    • DI(Dependency Injection):依赖注入

依赖注入

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

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

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

依赖注入:

  • 依赖指的是对象和对象之间的关联关系。
  • 注入指的是一种数据传递行为,通过注入行为来让对象和对象产生关系。

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

  • 第一种:set注入
  • 第二种:构造注入

新建模块:spring6-002-dependency-injection

set注入

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

pom.xml 文件中依赖注入:

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

    <!--junit4测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

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

dao包下的UserDao:

public class UserDao {
    public void insert(){
        System.out.println("正在保存用户数据。");
    }
}

service包下的UserService:

public class UserService {

    //不实例化对象,通过set方法或构造方法注入
    private UserDao userDao;

    // 使用set方式注入,必须提供set方法。
    // 反射机制要调用这个方法给属性赋值的。
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        userDao.insert();
    }
}

spring配置文件spring.xml:

  • 想让Spring调用对应的set方法,需要配置property标签

    • name : set方法的方法名,去掉set,然后把剩下的单词首字母变小写,写到这里

    • ref : 后面指定的是要注入的bean对象的id

<!-- 配置dao -->
<bean id="userDaoBean" class="com.byxl8112.spring6.dao.UserDao"/>

<!-- 配置service -->
<bean id="userServiceBean" class="com.byxl8112.spring6.service.UserService">
    <property name="userDao" ref="userDaoBean"/>
</bean>

property标签的name是:setUserDao()方法名演变得到的。演变的规律是:

  • setUsername() 演变为 username
  • setPassword() 演变为 password
  • setUserDao() 演变为 userDao
  • setUserService() 演变为 userService

测试程序:

 @Test
public void testSetDI(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    UserService userService = applicationContext.getBean("userServiceBean", UserService.class);
    userService.save();
}

实现原理:

通过property标签获取到属性名:userDao

通过属性名推断出set方法名:setUserDao

通过反射机制调用setUserDao()方法给属性赋值

构造注入

核心原理:通过调用构造方法来给属性赋值。

set注入和构造注入的时机是不同的。

  • set注入是调用set方法,此时对象早已创建出来了。
  • 构造注入调用的是构造方法,是在对象创建的时刻进行注入。

dao包下的UserDao类 和 VipDao类:

public class UserDao {
    public void insert(){
        System.out.println("正在保存用户数据。");
    }
}
public class VipDao {
    public void insert(){
        System.out.println("正在保存用户数据。");
    }
}

Service包下的CustomerService类:

public class CustomerService {
    
    private UserDao userDao;
    private VipDao vipDao;

    //有参构造方法
    public CustomerService(UserDao userDao, VipDao vipDao) {
        this.userDao = userDao;
        this.vipDao = vipDao;
    }

    public void save(){
        userDao.insert();
        vipDao.insert();
    }
}

spring配置文件spring.xml:

方法一:

  • index 属性指定下标,第一个参数是0,第二个参数是1,第三个是2,以此类推
  • ref 属性 :指定注入的bean对象的id
<bean id="userDaoBean" class="com.byxl8112.spring6.dao.UserDao"/>

<bean id="vipDaoBean" class="com.byxl8112.spring6.dao.VipDao"/>

<bean id="csBean" class="com.byxl8112.spring6.service.CustomerService">
    <!--构造注入-->
    <!--指定构造方法的第一个参数,下标是0-->
    <constructor-arg index="0" ref="userDaoBean"/>
    <!--指定构造方法的第二个参数,下标是1-->
    <constructor-arg index="1" ref="vipDaoBean"/>
</bean>

方法二:

  • name :构造方法参数的名字
  • ref :指定注入的bean对象的id
<bean id="userDaoBean" class="com.byxl8112.spring6.dao.UserDao"/>

<bean id="vipDaoBean" class="com.byxl8112.spring6.dao.VipDao"/>

<bean id="csBean2" class="com.byxl8112.spring6.service.CustomerService">
    <constructor-arg name="vipDao" ref="vipDaoBean"/>
    <constructor-arg name="userDao" ref="userDaoBean"/>
</bean>

方法三:

可以不指定下标,也不指定参数名,实际上是根据类型注入的

<bean id="csBean3" class="com.byxl8112.spring6.service.CustomerService">
    <constructor-arg ref="vipDaoBean"/>
    <constructor-arg ref="userDaoBean"/>
</bean>

set注入专题

注入外部Bean

之前的案例就是注入外部Bean的方式

spring.xml

<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>

<bean id="userServiceBean" class="com.byxl8112.spring6.service.UserService">
    <property name="userDao" ref="userDaoBean"/>
</bean>

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

注入内部Bean(了解)

内部Bean的方式:在bean标签中嵌套bean标签。

spring-inner-bean.xml

<bean id="userServiceBean" class="com.byxl8112.spring6.service.UserService">
    <property name="userDao">
        <bean class="com.byxl8112.spring6.dao.UserDao"/>
    </property>
</bean>

注入简单类型

简单类型包括

  • 基本数据类型
  • 基本数据类型对应的包装类
  • String或其他的CharSequence子类
  • Number子类
  • Date子类 (一般看作非简单类型)
  • Enum子类 (枚举类)
  • URI
  • URL
  • Temporal子类
  • Locale
  • Class
  • 另外还包括以上简单值类型对应的数组类型。

注意:

  • 如果把Date当做简单类型的话,日期字符串格式不能随便写。格式必须符合Date的toString()方法格式。显然这就比较鸡肋了。如果我们提供一个这样的日期字符串:2010-10-11,在这里是无法赋值给Date类型的属性的。
  • spring6之后,当注入的是URL,那么这个url字符串是会进行有效性检测的。如果是一个存在的url,那就没问题。如果不存在则报错。

之前都是注入的是非简单类型,即对象的属性是另一个对象

public class UserService{  
    private UserDao userDao;
    
    public void setUserDao(UserDao userDao){
        this.userDao = userDao;
    }
}

如果是简单类型的话,例如int类型,也可以调用set方法给属性赋值

Dao包下的User类

public class User {
    private int age;

    //set方法
    public void setAge(int age) {
        this.age = age;
    }
    //toString方法
    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                '}';
    }
}

spring配置文件:spring-simple-type.xml

简单类型在注入的时候要使用 value 属性,不能使用ref

<bean id="userBean" class="com.powernode.spring6.beans.User">
    <!--如果像这种int类型的属性,我们称为简单类型,这种简单类型在注入的时候要使用value属性,不能使用ref-->
    <!--<property name="age" value="20"/>-->
    <property name="age">
        <value>20</value>
    </property>
</bean>

测试

@Test
public void testSimpleType(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-simple-type.xml");
    User user = applicationContext.getBean("userBean", User.class);
    System.out.println(user);
}
1664444974497(1).png

需要特别注意:如果给简单类型赋值,使用value属性或value标签。而不是ref。

案例:给数据源的属性注入值

jdbc包下的MyDataSource类实现DataSource接口

public class MyDataSource implements DataSource {  //可以把数据源交给spring容器来管理

    private String driver;
    private String url;
    private String username;
    private String password;

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public Connection getConnection() throws SQLException {
        //获取数据库连接对象的时候需要4个信息:driver url username password
        return null;
    }

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

    @Override
    public Connection getConnection(String s, String s1) throws SQLException {
        return null;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter printWriter) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int i) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    @Override
    public <T> T unwrap(Class<T> aClass) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> aClass) throws SQLException {
        return false;
    }
}

我们给driver、url、username、password四个属性分别提供了setter方法,我们可以使用spring的依赖注入完成数据源对象的创建和属性的赋值

spring配置文件 spring-datasource.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">
  
    <bean id="dataSource" class="com.byxl8112.spring6.beans.MyDataSource">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"/>
        <property name="username" value="root"/>
        <property name="password" value="abc123"/>
    </bean>
  
</beans>

测试:

@Test
public void testDataSource(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-datasource.xml");
    MyDataSource dataSource = applicationContext.getBean("dataSource", MyDataSource.class);
    System.out.println(dataSource);
}

image.png

级联属性赋值(了解)

Bean包下的Clazz类

public class Clazz {
    //班级名称
    private String name;
	//set方法
    public void setName(String name){
        this.name = name;
    }
	//toString方法
    @Override
    public String toString() {
        return "Clazz{" +
                "name='" + name + '\'' +
                '}';
    }
}

Bean包下的Student类

public class Student {

    private String name;

    private Clazz clazz;

    public void setClazz(Clazz clazz) {
        this.clazz = clazz;
    }

    // 使用级联属性赋值,需要使用get方法来获取clazz对象里面的name属性进行赋值
    public Clazz getClazz() {
        return clazz;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", clazz=" + clazz +
                '}';
    }
}

spring配置文件 cascade.xml

  1. 未使用级联属性赋值时,先给clazz对象中name属性赋值,然后传入到studentBean里面

    <bean id="clazzBean" class="com.byxl8112.spring6.bean.Clazz">
         <property name="name" value="高三一班"/>
    </bean>
    
    <bean id="studentBean" class="com.byxl8112.spring6.bean.Student">
        <!--简单类型使用value-->
        <property name="name" value="张三"/>
        <!--非简单类型使用ref-->
        <property name="clazz" ref="clazzBean"/>
    </bean>
  2. 使用级联属性赋值,需要在Student类中有getClazz()方法来获取clazz对象进行赋值

    <bean id="clazzBean" class="com.byxl8112.spring6.bean.Clazz"/>
    
    <bean id="studentBean" class="com.byxl8112.spring6.bean.Student">
        <!--简单类型使用value-->
        <property name="name" value="张三"/>
        <!--非简单类型使用ref-->
        <property name="clazz" ref="clazzBean"/>
        <!--级联属性赋值,相当于先拿到clazz对象然后调用其中的name属性进行赋值,即getClazz().name-->
        <property name="clazz.name" value="高三一班"/>
    </bean>

    注意:

    • 配置的顺序不能颠倒,必须如下顺序
    • clazz属性必须提供getter方法

测试程序:

@Test
public void testCascade(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-cascade.xml");
    Student student = applicationContext.getBean("student", Student.class);
    System.out.println(student);
}
image.png

注入数组

数组中的元素是简单类型

bean包下的Person类

public class Person {
    private String[] favariteFoods;
	//set方法
    public void setFavariteFoods(String[] favariteFoods) {
        this.favariteFoods = favariteFoods;
    }
	//toString方法
    @Override
    public String toString() {
        return "Person{" +
                "favariteFoods=" + Arrays.toString(favariteFoods) +
                '}';
    }
}

spring配置文件 spring-array-simple.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">

    <bean id="person" class="com.byxl8112.spring6.beans.Person">
        <property name="favariteFoods">
            <array>
                <value>鸡排</value>
                <value>汉堡</value>
                <value>鹅肝</value>
            </array>
        </property>
    </bean>
</beans>

测试:

@Test
public void testArraySimple(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-array-simple.xml");
    Person person = applicationContext.getBean("person", Person.class);
    System.out.println(person);
}

数组中的元素是非简单类型

一个订单中包含多个商品。

bean包下的 Goods类

public class Goods {
    private String name;

    public Goods() {
    }

    public Goods(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                '}';
    }
}

bean包下的Order类

public class Order {
    // 一个订单中有多个商品
    private Goods[] goods;

    public Order() {
    }

    public Order(Goods[] goods) {
        this.goods = goods;
    }

    public void setGoods(Goods[] goods) {
        this.goods = goods;
    }

    @Override
    public String toString() {
        return "Order{" +
                "goods=" + Arrays.toString(goods) +
                '}';
    }
}

spring配置文件 spring-array.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">

    <bean id="goods1" class="com.byxl8112.spring6.beans.Goods">
        <property name="name" value="西瓜"/>
    </bean>

    <bean id="goods2" class="com.byxl8112.spring6.beans.Goods">
        <property name="name" value="苹果"/>
    </bean>

    <bean id="order" class="com.byxl8112.spring6.beans.Order">
        <property name="goods">
            <array>
                <!--这里使用ref标签-->
                <ref bean="goods1"/>
                <ref bean="goods2"/>
            </array>
        </property>
    </bean>
</beans>

测试:

@Test
public void testArray(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-array.xml");
    Order order = applicationContext.getBean("order", Order.class);
    System.out.println(order);
}
image.png

注意:

  • 如果数组中是简单类型,使用value标签。
  • 如果数组中是非简单类型,使用ref标签。

注入List集合

list集合:有序可重复

bean包下的People.java类

public class People {
    // 一个人有多个名字
    private List<String> names;
	//set方法
    public void setNames(List<String> names) {
        this.names = names;
    }

    @Override
    public String toString() {
        return "People{" +
                "names=" + names +
                '}';
    }
}

spring配置文件 spring-collection.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">

    <bean id="peopleBean" class="com.byxl8112.spring6.beans.People">
        <property name="names">
            <list>
                <value>铁锤</value>
                <value>张三</value>
                <value>张三</value>
                <value>张三</value>
                <value></value>
            </list>
        </property>
    </bean>
</beans>

测试:

@Test
public void testCollection(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-collection.xml");
    People peopleBean = applicationContext.getBean("peopleBean", People.class);
    System.out.println(peopleBean);
}
image.png

注意:注入List集合的时候使用list标签,如果List集合中是简单类型使用value标签,反之使用ref标签。

注入Set集合

Set集合:无序不重复

bean包下的People.java类

public class People {
    // 一个人有多个电话,且电话不重复
    private Set<String> phones;

    public void setPhones(Set<String> phones) {
        this.phones = phones;
    }
    
    //......
    
    @Override
    public String toString() {
        return "People{" +
                "phones=" + phones +
                ", names=" + names +
                '}';
    }
}

spring配置文件 spring-collection.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">

    <bean id="peopleBean" class="com.byxl8112.spring6.beans.People">
        <property name="phones">
            <set>
                <!--非简单类型可以使用ref,简单类型使用value-->
                <value>110</value>
                <value>110</value>
                <value>120</value>
                <value>120</value>
                <value>119</value>
                <value>119</value>
            </set>
        </property>
    </bean>
</beans>

执行结果:

image.png

要点:

  • 使用 标签
  • set集合中元素是简单类型的使用value标签,反之使用ref标签。

注入Map集合

bean包下的People.java类

public class People {
    // 一个人有多个住址
    private Map<Integer, String> addrs;

    public void setAddrs(Map<Integer, String> addrs) {
        this.addrs = addrs;
    }
    
    //......
    
    @Override
    public String toString() {
        return "People{" +
                "addrs=" + addrs +
                ", phones=" + phones +
                ", names=" + names +
                '}';
    }
}

spring的配置文件 spring-collection.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">

    <bean id="peopleBean" class="com.byxl8112.spring6.beans.People">
        <property name="addrs">
            <map>
                <!--如果key不是简单类型,使用 key-ref 属性-->
                <!--如果value不是简单类型,使用 value-ref 属性-->
                <entry key="1" value="北京大兴区"/>
                <entry key="2" value="上海浦东区"/>
                <entry key="3" value="深圳宝安区"/>
            </map>
        </property>
    </bean>
</beans>

执行结果:

image.png

要点:

  • 使用 标签
  • 如果key是简单类型,使用 key 属性,反之使用 key-ref 属性。
  • 如果value是简单类型,使用 value 属性,反之使用 value-ref 属性。

注入 Properties

java.util.Properties 继承 java.util.Hashtable,所以 Properties 也是一个Map集合。

bean包下的People.java类:

public class People {

    private Properties properties;
	// set方法
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
    
    //......

    @Override
    public String toString() {
        return "People{" +
                "properties=" + properties +
                ", addrs=" + addrs +
                ", phones=" + phones +
                ", names=" + names +
                '}';
    }
}

spring配置文件 spring-collection.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">

    <bean id="peopleBean" class="com.byxl8112.spring6.beans.People">
        <property name="properties">
            <props>
                <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
                <prop key="url">jdbc:mysql://localhost:3306/spring</prop>
                <prop key="username">root</prop>
                <prop key="password">123456</prop>
            </props>
        </property>
    </bean>
</beans>

执行测试程序: image.png

要点:

  • 使用 标签嵌套 标签完成。

注入null和空字符串

注入空字符串使用: 或者 value=””

注入null使用: 或者 不为该属性赋值

  • 注入空字符串

    bean包下的Vip.java类

    public class Vip {
        private String email;
    	// set方法
        public void setEmail(String email) {
            this.email = email;
        }
    
        @Override
        public String toString() {
            return "Vip{" +
                    "email='" + email + '\'' +
                    '}';
        }
    }

    spring配置文件 spring-null.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">
    
        <bean id="vipBean" class="com.byxl8112.spring6.beans.Vip">
            <!--空串的第一种方式-->
            <!-- <property name="email" value=""/> -->
            <!--空串的第二种方式-->
            <property name="email">
                <value/>
            </property>
        </bean>
    </beans>

    测试:

    @Test
    public void testNull(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-null.xml");
        Vip vipBean = applicationContext.getBean("vipBean", Vip.class);
        System.out.println(vipBean);
    }

    执行结果:

    image.png
  • 注入null

    • 方式一:不给属性赋值

      <?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="vipBean" class="com.byxl8112.spring6.beans.Vip" />
      
      </beans>

      执行结果:

      image.png
    • 方式二:使用

      <?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="vipBean" class="com.powernode.spring6.beans.Vip">
              <property name="email">
                  <null/>
              </property>
          </bean>
      
      </beans>

      执行结果:

      image.png

注入的值中含有特殊符号

XML中有5个特殊字符,分别是:<、>、’、”、&

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

image.png

解决方案包括两种:

  • 第一种:特殊符号使用转义字符代替。
  • 第二种:将含有特殊符号的字符串放到: 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。

5个特殊字符对应的转义字符分别是:

特殊字符 转义字符
> >
< <
&apos;
"
& &
  • 先使用转义字符来代替:

    bean包下的Math.java类:

    public class Math {
        private String result;
    	//set方法
        public void setResult(String result) {
            this.result = result;
        }
    
        @Override
        public String toString() {
            return "Math{" +
                    "result='" + result + '\'' +
                    '}';
        }
    }
    

    spring配置文件 spring-special.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">
        
        <bean id="mathBean" class="com.byxl8112.spring6.beans.Math">
            <property name="result" value="2 &lt; 3"/>
        </bean>
    </beans>

    测试程序:

    @Test
    public void testSpecial(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-special.xml");
        Math mathBean = applicationContext.getBean("mathBean", Math.class);
        System.out.println(mathBean);
    }

    执行结果:

    image.png
  • 使用 CDATA 方式:

    spring-special.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">
    
        <bean id="mathBean" class="com.byxl8112.spring6.beans.Math">
            <property name="result">
                <!--只能使用value标签-->
                <value><![CDATA[2 < 3]]></value>
            </property>
        </bean>
    
    </beans>

    注意:使用CDATA时,不能使用value属性,只能使用value标签。

p命名空间注入

目的:简化配置。对set方法注入的简化

使用p命名空间注入的前提条件包括两个:

bean包中的Customer.java类

public class Customer {
    private String name;
    private int age;

    //set方法
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

spring配置文件 spring-p.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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="customerBean" class="com.byxl8112.spring6.beans.Customer" p:name="zhangsan" p:age="20"/>

</beans>

测试程序:

@Test
public void testP(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-p.xml");
    Customer customerBean = applicationContext.getBean("customerBean", Customer.class);
    System.out.println(customerBean);
}

执行结果:

image.png

注意:p命名空间实际上是对set注入的简化,去掉set方法程序会出错!!!

c命名空间注入

c命名空间是简化构造方法注入的。

使用c命名空间的两个前提条件:

bean包下的People.java类

public class People {

    private String name;
    private int age;
    private boolean sex;
	
    //构造方法
    public People(String name, int age, boolean sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                '}';
    }
}

spring配置文件中的 spring-c.xml

  • c_0 , c_1 ,c_2 ….. :下标方式
  • c_name,c_age , c_sex …. :参数名方式
<?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">

<!-- <bean id="peopleBean" class="com.byxl8112.spring6.bean.People" c:_0="张三" c:_1="3" c:_2="true"/> -->
    <bean id="peopleBean" class="com.byxl8112.spring6.bean.People" c:name="张三" c:age="4" c:sex="false"/>

</beans>

测试程序:

@Test
public void testC(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-c.xml");
    MyTime myTimeBean = applicationContext.getBean("myTimeBean", MyTime.class);
    System.out.println(myTimeBean);
}

执行结果:

image.png

注意:不管是p命名空间还是c命名空间,注入的时候都可以注入简单类型以及非简单类型。

util命名空间

使用util命名空间可以让配置复用。主要是对于集合。底层调用的是set方法。

使用util命名空间的前提是:在spring配置文件头部添加配置信息。

xmlns:util="http://www.springframework.org/schema/util"

http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd

image-20230504125526830

utils包下的MyDataSource1.java类

public class MyDataSource1 {
    private Properties properties;
	//set方法
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public String toString() {
        return "MyDataSource1{" +
                "properties=" + properties +
                '}';
    }
}

utils包下的MyDataSource2.java类

public class MyDataSource2 {
    private Properties properties;

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public String toString() {
        return "MyDataSource2{" +
                "properties=" + properties +
                '}';
    }
}

spring配置文件中的 spring-util.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:util="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:properties id="prop">
        <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
        <prop key="url">jdbc:mysql://localhost:3306/spring</prop>
        <prop key="username">root</prop>
        <prop key="password">123456</prop>
    </util:properties>

    <bean id="dataSource1" class="com.byxl8112.spring6.beans.MyDataSource1">
        <property name="properties" ref="prop"/>
    </bean>

    <bean id="dataSource2" class="com.byxl8112.spring6.beans.MyDataSource2">
        <property name="properties" ref="prop"/>
    </bean>
</beans>

测试程序:

@Test
public void testUtil(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-util.xml");

    MyDataSource1 dataSource1 = applicationContext.getBean("dataSource1", MyDataSource1.class);
    System.out.println(dataSource1);

    MyDataSource2 dataSource2 = applicationContext.getBean("dataSource2", MyDataSource2.class);
    System.out.println(dataSource2);
}

image.png

基于XML的自动装配

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

根据名称自动装配

dao包下的UserDao.java类

public class UserDao {

    public void insert(){
        System.out.println("正在保存用户数据。");
    }
}

service包下的UserService.java类

public class UserService {

    private UserDao userDao;

    // 这个set方法非常关键
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        aaa.insert();
    }
}

spring配置文件中的spring-autowire.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">

    <bean id="userService" class="com.byxl8112.spring6.service.UserService" autowire="byName"/>
    
    <!--此id="userDao"指的是setUserDao去掉set,后面首字母变小写-->
    <bean id="userDao" class="com.byxl8112.spring6.dao.UserDao"/>

</beans>

这个配置起到关键作用:

  • UserService Bean中需要添加autowire=”byName”,表示通过名称进行装配。
  • UserService类中有一个UserDao属性,而UserDao属性的名字是userDao,**对应的set方法是setUserDao()**,正好和UserDao Bean的id是一样的。这就是根据名称自动装配。

测试程序:

@Test
public void testAutowireByName(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowire.xml");
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.save();
}

执行结果:

image.png

这说明,如果根据名称装配(byName),底层会调用set方法进行注入。

例如:setAge() 对应的名字是age,setPassword()对应的名字是password,setEmail()对应的名字是email。

相当于为某个类创建了bean对象后,bean有id,那么在容器中这个类就只会以id被其他人识别,所以才会有a类能被b类找到,且b类有以a类id命名的set方法时,它们两者之间建立连接

根据类型自动装配

dao包下的 AccountDao.java类

public class AccountDao {
    public void insert(){
        System.out.println("正在保存账户信息");
    }
}

service包下的 AccountService.java类

public class AccountService {
    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public void save(){
        accountDao.insert();
    }
}

spring配置文件 spring-autowire.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">

    <!--byType表示根据类型自动装配-->
    <bean id="accountService" class="com.byxl8112.spring6.service.AccountService" autowire="byType"/>

    <bean class="com.byxl8112.spring6.dao.AccountDao"/>

</beans>

测试程序:

@Test
public void testAutowireByType(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowire.xml");
    AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
    accountService.save();
}

执行结果:

image.png

无论是byName还是byType,在装配的时候都是基于set方法的。所以set方法是必须要提供的,只提供构造方法是不行的

当byType进行自动装配的时候,配置文件中某种类型的Bean必须是唯一的,不能出现多个类型一样的bean,即下面这样会报错

<bean id="accountService" class="com.byxl8112.spring6.service.AccountService" autowire="byType"/>

<bean class="com.byxl8112.spring6.dao.AccountDao"/>
<bean class="com.byxl8112.spring6.dao.AccountDao"/>

Spring引入外部属性配置文件

我们都知道编写数据源的时候是需要连接数据库的信息的,例如:driver url username password等信息。这些信息可以单独写到一个属性配置文件中吗,这样用户修改起来会更加的方便。当然可以。

第一步:写一个数据源类,提供相关属性。

utils包下的 MyDataSource 类,实现DataSource接口

public class MyDataSource implements DataSource {

    private String driver;
    private String url;
    private String username;
    private String password;

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

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

第二步:在类路径下新建jdbc.properties文件,并配置信息。

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

第三步:在spring配置文件中引入context命名空间。

xmlns:context="http://www.springframework.org/schema/context"

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"

image-20230504132412889

第四步:在spring中配置使用jdbc.properties文件。

<?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:property-placeholder location="jdbc.properties"/>
    
    <bean id="dataSource" class="com.byxl8112.spring6.beans.MyDataSource">
        <property name="driver" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

测试程序:

@Test
public void testProperties(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-properties.xml");
    MyDataSource dataSource = applicationContext.getBean("dataSource", MyDataSource.class);
    System.out.println(dataSource);
}

执行结果:

image.png

Bean的作用域

singleton

默认情况下,Spring的IoC容器创建的Bean对象是单例的。来测试一下:

bean包下的 SpringBean.java类

public class SpringBean {
}

spring配置文件中 spring-scope.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">

    <bean id="sb" class="com.byxl8112.spring6.beans.SpringBean" />
    
</beans>

测试程序:

@Test
public void testScope(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");

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

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

执行结果:

image.png

通过测试得知:Spring的IoC容器中,默认情况下,Bean对象是单例的。

这个对象在什么时候创建的呢?可以为SpringBean提供一个无参数构造方法,测试一下,如下:

bean目录下 SpringBean.java类

public class SpringBean {
    public SpringBean() {
        System.out.println("SpringBean的无参数构造方法执行。");
    }
}

在测试的时候不执行getBean() 方法,先初始化Spring

@Test
public void testScope(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
}

执行结果:

image.png

通过测试得知,默认情况下,Bean对象的创建是在初始化Spring上下文的时候就完成的。

默认情况下,构造方法仅仅执行一次,创建一个对象。每getBean(“sb”, SpringBean.class);一次,就获取创建的那个对象(同一个)

prototype

如果想让Spring的Bean对象以多例的形式存在,可以在bean标签中指定scope属性的值为:prototype,这样Spring会在每一次执行getBean()方法的时候创建Bean对象,调用几次则创建几次。

spring的配置文件 spring-scope.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">

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

</beans>

测试程序:

@Test
public void testScope(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");

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

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

执行结果:

image.png

去掉测试中的getBean() 方法

@Test
public void testScope(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
}

执行结果:

image.png

这一次在初始化Spring上下文的时候,并没有创建Bean对象。

可以看到,prototype模式下,spring容器启动的时候,并不创建对象。在每次getBean(“sb”, SpringBean.class);的时候才会每次再创建对象

注意:没有指定scope属性时,默认是singleton单例的。

其他scope

scope属性的值不止两个,它一共包括8个选项:

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

接下来咱们自定义一个Scope,线程级别的Scope,在同一个线程中,获取的Bean都是同一个。跨线程则是不同的对象:(以下内容作为了解)

  • 第一步:自定义Scope。(实现Scope接口)

    • spring内置了线程范围的类:org.springframework.context.support.SimpleThreadScope,可以直接用。
  • 第二步:将自定义的Scope注册到Spring容器中。

<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
  <property name="scopes">
    <map>
      <entry key="myThread">
        <bean class="org.springframework.context.support.SimpleThreadScope"/>
      </entry>
    </map>
  </property>
</bean>
  • 第三步:使用Scope。
<bean id="sb" class="com.byxl8112.spring6.beans.SpringBean" scope="myThread" />

编写测试程序:

@Test
public void testCustomScope(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
    SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
    SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
    System.out.println(sb1);
    System.out.println(sb2);
    // 启动线程
    new Thread(new Runnable() {
        @Override
        public void run() {
            SpringBean a = applicationContext.getBean("sb", SpringBean.class);
            SpringBean b = applicationContext.getBean("sb", SpringBean.class);
            System.out.println(a);
            System.out.println(b);
        }
    }).start();
}

执行结果:

img

GoF之工厂模式

  • 设计模式:一种可以被重复利用的解决方案。

  • GoF(Gang of Four),中文名——四人组。

  • 《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为”四人组(Gang of Four)”。

  • 该书中描述了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。

  • 不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。

  • GoF23种设计模式可分为三大类:

    • 创建型(5个):解决对象创建问题。
      • 单例模式
      • 工厂方法模式
      • 抽象工厂模式
      • 建造者模式
      • 原型模式
    • 结构型(7个):一些类或对象组合在一起的经典结构。
      • 代理模式
      • 装饰模式
      • 适配器模式
      • 组合模式
      • 享元模式
      • 外观模式
      • 桥接模式
    • 行为型(11个):解决类或对象之间的交互问题。
      • 策略模式
      • 模板方法模式
      • 责任链模式
      • 观察者模式
      • 迭代子模式
      • 命令模式
      • 备忘录模式
      • 状态模式
      • 访问者模式
      • 中介者模式
      • 解释器模式
  • 工厂模式是解决对象创建问题的,所以工厂模式属于创建型设计模式。这里为什么学习工厂模式呢?这是因为Spring框架底层使用了大量的工厂模式。

工厂模式的三种形态

工厂模式通常有三种形态:

  • 第一种:简单工厂模式(Simple Factory):不属于23种设计模式之一。简单工厂模式又叫做:静态 工厂方法模式。简单工厂模式是工厂方法模式的一种特殊实现。
  • 第二种:工厂方法模式(Factory Method):是23种设计模式之一。
  • 第三种:抽象工厂模式(Abstract Factory):是23种设计模式之一。

简单工厂模式

简单工厂模式的角色包括三个:

  • 抽象产品 角色
  • 具体产品 角色
  • 工厂类 角色

简单工厂模式的代码如下:

抽象产品角色:提供抽象类Weapon

//武器(抽象产品角色)
public abstract class Weapon {
    /**
     * 所有的武器都有攻击行为
     */
    public abstract void attack();
}

具体产品角色:

//坦克继承武器抽象类
public class Tank extends Weapon{
    @Override
    public void attack() {
        System.out.println("坦克开炮!");
    }
}
//战斗机继承武器抽象类
public class Fighter extends Weapon{
    @Override
    public void attack() {
        System.out.println("战斗机投下原子弹!");
    }
}
//匕首继承武器抽象类
public class Dagger extends Weapon{
    @Override
    public void attack() {
        System.out.println("砍他丫的!");
    }
}

工厂类角色:

public class WeaponFactory {
    /**
     * 根据不同的武器类型生产武器,必须是静态方法
     * @param weaponType 武器类型
     * @return 武器对象
     */
    public static Weapon get(String weaponType){
        if (weaponType == null || weaponType.trim().length() == 0) {
            return null;
        }
        Weapon weapon = null;
        if ("TANK".equals(weaponType)) {
            weapon = new Tank();
        } else if ("FIGHTER".equals(weaponType)) {
            weapon = new Fighter();
        } else if ("DAGGER".equals(weaponType)) {
            weapon = new Dagger();
        } else {
            throw new RuntimeException("不支持该武器!");
        }
        return weapon;
    }
}

测试程序:

public class Client {
    public static void main(String[] args) {
        Weapon weapon1 = WeaponFactory.get("TANK");
        weapon1.attack();

        Weapon weapon2 = WeaponFactory.get("FIGHTER");
        weapon2.attack();

        Weapon weapon3 = WeaponFactory.get("DAGGER");
        weapon3.attack();
    }
}

执行结果:

image.png

简单工厂模式的优点:

  • 客户端程序不需要关心对象的创建细节,需要哪个对象时,只需要向工厂索要即可,初步实现了责任的分离。客户端只负责“消费”,工厂负责“生产”。生产和消费分离。

简单工厂模式的缺点:

  • 缺点1:工厂类集中了所有产品的创造逻辑,形成一个无所不知的全能类,有人把它叫做上帝类。显然工厂类非常关键,不能出问题,一旦出问题,整个系统瘫痪。
  • 缺点2:不符合OCP开闭原则,在进行系统扩展时,需要修改工厂类。

Spring中的BeanFactory就使用了简单工厂模式。

工厂方法模式

工厂方法模式既保留了简单工厂模式的优点,同时又解决了简单工厂模式的缺点。

工厂方法模式的角色包括:

  • 抽象工厂角色
  • 具体工厂角色
  • 抽象产品角色
  • 具体产品角色

抽象产品角色:

public abstract class Weapon {
    /**
     * 所有武器都有攻击行为
     */
    public abstract void attack();
}

具体产品角色:

public class Gun extends Weapon{
    @Override
    public void attack() {
        System.out.println("开枪射击!");
    }
}
public class Fighter extends Weapon{
    @Override
    public void attack() {
        System.out.println("战斗机发射核弹!");
    }
}

抽象工厂角色:

public interface WeaponFactory {
    Weapon get();
}

具体工厂角色:

public class GunFactory implements WeaponFactory{
    @Override
    public Weapon get() {
        return new Gun();
    }
}
public class FighterFactory implements WeaponFactory{
    @Override
    public Weapon get() {
        return new Fighter();
    }
}

客户端程序:

public class Client {
    public static void main(String[] args) {
        WeaponFactory factory = new GunFactory();
        Weapon weapon = factory.get();
        weapon.attack();

        WeaponFactory factory1 = new FighterFactory();
        Weapon weapon1 = factory1.get();
        weapon1.attack();
    }
}

执行客户端程序:

image.png

如果想扩展一个新的产品,只要新增一个产品类,再新增一个该产品对应的工厂即可,例如新增:匕首

增加:具体产品角色

public class Dagger extends Weapon{
    @Override
    public void attack() {
        System.out.println("砍丫的!");
    }
}

增加:具体工厂角色

public class DaggerFactory implements WeaponFactory{
    @Override
    public Weapon get() {
        return new Dagger();
    }
}

客户端程序:

public class Client {
    public static void main(String[] args) {
        WeaponFactory factory = new GunFactory();
        Weapon weapon = factory.get();
        weapon.attack();

        WeaponFactory factory1 = new FighterFactory();
        Weapon weapon1 = factory1.get();
        weapon1.attack();

        WeaponFactory factory2 = new DaggerFactory();
        Weapon weapon2 = factory2.get();
        weapon2.attack();
    }
}

执行结果:

image.png

我们可以看到在进行功能扩展的时候,不需要修改之前的源代码,显然工厂方法模式符合OCP原则。

工厂方法模式的优点:

  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
  • 屏蔽产品的具体实现,调用者只关心产品的接口。

工厂方法模式的缺点:

  • 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加(类爆炸),在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

抽象工厂模式(了解)

抽象工厂模式相对于工厂方法模式来说,就是工厂方法模式是针对一个产品系列的,而抽象工厂模式是针对多个产品系列的,即工厂方法模式是一个产品系列一个工厂类,而抽象工厂模式是多个产品系列一个工厂类。

抽象工厂模式特点:

抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。
抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。
抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。
它有多个抽象产品类,每个抽象产品类可以派生出多个具体产品类,一个抽象工厂类,可以派生出多个具体工厂类,每个具体工厂类可以创建多个具体产品类的实例。
每一个模式都是针对一定问题的解决方案,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式针对的是多个产品等级结果。

抽象工厂中包含4个角色:

  • 抽象工厂角色
  • 具体工厂角色
  • 抽象产品角色
  • 具体产品角色

抽象工厂模式的类图如下:

image.png

抽象工厂模式代码如下:

第一部分:武器产品族

public abstract class Weapon {
    public abstract void attack();
}
public class Gun extends Weapon{
    @Override
    public void attack() {
        System.out.println("开枪射击!");
    }
}
public class Dagger extends Weapon{
    @Override
    public void attack() {
        System.out.println("砍丫的!");
    }
}

第二部分:水果产品族

public abstract class Fruit {
    /**
     * 所有果实都有一个成熟周期。
     */
    public abstract void ripeCycle();
}
public class Orange extends Fruit{
    @Override
    public void ripeCycle() {
        System.out.println("橘子的成熟周期是10个月");
    }
}
public class Apple extends Fruit{
    @Override
    public void ripeCycle() {
        System.out.println("苹果的成熟周期是8个月");
    }
}

第三部分:抽象工厂类

public abstract class AbstractFactory {
    public abstract Weapon getWeapon(String type);
    public abstract Fruit getFruit(String type);
}

第四部分:具体工厂类

//武器族工厂
public class WeaponFactory extends AbstractFactory{

    public Weapon getWeapon(String type){
        if (type == null || type.trim().length() == 0) {
            return null;
        }
        if ("Gun".equals(type)) {
            return new Gun();
        } else if ("Dagger".equals(type)) {
            return new Dagger();
        } else {
            throw new RuntimeException("无法生产该武器");
        }
    }

    @Override
    public Fruit getFruit(String type) {
        return null;
    }
}
//水果组工厂
public class FruitFactory extends AbstractFactory{
    @Override
    public Weapon getWeapon(String type) {
        return null;
    }

    public Fruit getFruit(String type){
        if (type == null || type.trim().length() == 0) {
            return null;
        }
        if ("Orange".equals(type)) {
            return new Orange();
        } else if ("Apple".equals(type)) {
            return new Apple();
        } else {
            throw new RuntimeException("我家果园不产这种水果");
        }
    }
}

第五部分:客户端程序

public class Client {
    public static void main(String[] args) {
        // 客户端调用方法时只面向AbstractFactory调用方法。
        AbstractFactory factory = new WeaponFactory(); // 注意:这里的new WeaponFactory()可以采用 简单工厂模式 进行隐藏。
        Weapon gun = factory.getWeapon("Gun");
        Weapon dagger = factory.getWeapon("Dagger");

        gun.attack();
        dagger.attack();

        AbstractFactory factory1 = new FruitFactory(); // 注意:这里的new FruitFactory()可以采用 简单工厂模式 进行隐藏。
        Fruit orange = factory1.getFruit("Orange");
        Fruit apple = factory1.getFruit("Apple");

        orange.ripeCycle();
        apple.ripeCycle();
    }
}

执行结果:

image.png

抽象工厂模式的优缺点:

  • 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
  • 缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在AbstractFactory里加代码,又要在具体的里面加代码。

Bean的实例化方式

Spring为Bean提供了多种实例化方式,通常包括4种方式。(也就是说在Spring中为Bean对象的创建准备了多种方案,目的是:更加灵活)

  • 第一种:通过构造方法实例化

  • 第二种:通过简单工厂模式实例化

  • 第三种:通过factory-bean实例化

  • 第四种:通过FactoryBean接口实例化

通过构造方法实例化

我们之前一直使用的就是这种方式。默认情况下,会调用Bean的无参数构造方法。

Bean包下的User.java

public class User {
    public User() {
        System.out.println("User类的无参数构造方法执行。");
    }
}

Spring配置文件的spring.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">

    <bean id="userBean" class="com.byxl8112.spring6.bean.User"/>

</beans>

测试程序:

@Test
public void testConstructor(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    User user = applicationContext.getBean("userBean", User.class);
    System.out.println(user);
}

执行结果:

image.png

通过简单工厂模式实例化

第一步:定义一个bean

bean目录下的Vip.java类

public class Vip {
}

第二步:编写简单工厂模式当中的工厂类

bean目录下的 VipFactory.java 类

public class VipFactory {
    public static Vip get(){
        return new Vip();
    }
}

第三步:Spring配置文件

在Spring配置文件中指定创建该Bean的方法(使用factory-method属性指定)

<bean id="vipBean" class="com.byxl8112.spring6.bean.VipFactory" factory-method="get"/>

测试程序

@Test
public void testSimpleFactory(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Vip vip = applicationContext.getBean("vipBean", Vip.class);
    System.out.println(vip);
}

执行结果:

image.png

通过factory-bean实例化

这种方式本质上是:通过工厂方法模式进行实例化。

通过factory-bean实例化bean和通过简单工厂模式实例化bean最大的区别是:前者中的方法是实例方法,后者中的方法是静态方法

第一步:定义一个bean

bean包下的Order.java类

public class Order {
}

第二步:定义具体工厂类

工厂类中定义实例方法

bean包下的 OrderFactory.java 类

public class OrderFactory {
    public Order get(){
        return new Order();
    }
}

第三步:Spring配置文件

在Spring配置文件中指定factory-bean以及factory-method

<bean id="orderFactory" class="com.byxl8112.spring6.bean.OrderFactory"/>
<bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>

第四步:测试

@Test
public void testSelfFactoryBean(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Order orderBean = applicationContext.getBean("orderBean", Order.class);
    System.out.println(orderBean);
}
image.png

通过FactoryBean接口实例化

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

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

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

第一步:定义一个Bean

bean目录下的Person.java类

public class Person {
}

第二步:编写一个类实现FactoryBean接口

public class PersonFactoryBean implements FactoryBean<Person> {

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

    @Override
    public Class<?> getObjectType() {
        return null;
    }

    @Override
    public boolean isSingleton() {
        // true表示单例
        // false表示原型
        return true;
    }
}

第三步:Spring配置文件

在Spring配置文件中配置FactoryBean

<bean id="personBean" class="com.byxl8112.spring6.bean.PersonFactoryBean"/>

第四步:测试

@Test
public void testFactoryBean(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Person personBean = applicationContext.getBean("personBean", Person.class);
    System.out.println(personBean);

    Person personBean2 = applicationContext.getBean("personBean", Person.class);
    System.out.println(personBean2);
}
image.png

FactoryBean在Spring中是一个接口。被称为“工厂Bean”。“工厂Bean”是一种特殊的Bean。所有的“工厂Bean”都是用来协助Spring框架来创建其他Bean对象的。

BeanFactory和FactoryBean的区别

BeanFactory

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

BeanFactory是工厂。

FactoryBean

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

在Spring中,Bean可以分为两类:

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

注入自定义的Date

我们前面说过,java.util.Date在Spring中被当做简单类型,简单类型在注入的时候可以直接使用value属性或value标签来完成。但我们之前已经测试过了,对于Date类型来说,采用value属性或value标签赋值的时候,对日期字符串的格式要求非常严格,必须是这种格式的:Mon Oct 10 14:30:26 CST 2022。其他格式是不会被识别的。如以下代码:

原来的程序

格式是固定的

public class Student {
    private Date birth;

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

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

Spring配置文件 spring.xml

<bean id="studentBean" class="com.byxl8112.spring6.bean.Student">
  <property name="birth" value="Mon Oct 10 14:30:26 CST 2002"/>
</bean>

测试:

@Test
public void testDate(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Student studentBean = applicationContext.getBean("studentBean", Student.class);
    System.out.println(studentBean);
}
image.png

改动后程序

格式自定义

Student类和原来的一样,不同的是要编写DateFactoryBean来实现FactoryBean接口,Spring配置文件也要进行调用修改

编写 DateFactoryBean 实现 FactoryBean 接口:

public class DateFactoryBean implements FactoryBean<Date> {

    // 定义属性接收日期字符串
    private String date;

    // 通过构造方法给日期字符串属性赋值
    public DateFactoryBean(String date) {
        this.date = date;
    }

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

    @Override
    public Class<?> getObjectType() {
        return null;
    }
}

编写spring配置文件:

<bean id="dateBean" class="com.byxl8112.spring6.bean.DateFactoryBean">
  <constructor-arg name="date" value="1999-10-11"/>
</bean>

<bean id="studentBean" class="com.powernode.spring6.bean.Student">
  <property name="birth" ref="dateBean"/>
</bean>

测试程序:

@Test
public void testStudentBean(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Student student = applicationContext.getBean("student", Student.class);
    System.out.println(student);
}
image.png

Bean的生命周期

什么是Bean的生命周期

Spring其实就是一个管理Bean对象的工厂。它负责对象的创建,对象的销毁等。

所谓的生命周期就是:对象从创建开始到最终销毁的整个过程。

什么时候创建Bean对象?

创建Bean对象的前后会调用什么方法?

Bean对象什么时候销毁?

Bean对象的销毁前后调用什么方法?

Bean生命周期之5步

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

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

  • 第一步:实例化Bean
  • 第二步:Bean属性赋值
  • 第三步:初始化Bean
  • 第四步:使用Bean
  • 第五步:销毁Bean

image.png

编写测试程序:

Bean包下的User.java类

public class User {
    private String name;

    public User() {
        System.out.println("1.实例化Bean");
    }

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

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

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

}

Spring 配置文件 spring.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">

    <!--需要手动指定初始化方法和销毁方法-->
    <!--
		init-method : 指定初始化方法
		destory-method : 指定销毁方法
	-->
    <bean id="user" class="com.byxl8112.spring6.bean.User"
          init-method="initBean" destroy-method="destroyBean">
        <property name="name" value="zhangsan"/>
    </bean>
</beans>

测试程序:

@Test
    public void testLifecycle(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User userBean = applicationContext.getBean("userBean", User.class);
        System.out.println("4.使用Bean");
        // 只有正常关闭spring容器才会执行销毁方法
        ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
        context.close();
    }

执行结果:

image.png

需要注意的:

  1. 只有正常关闭spring容器,bean的销毁方法才会被调用。
  2. ClassPathXmlApplicationContext类才有close()方法。
  3. 配置文件中的init-method指定初始化方法。destroy-method指定销毁方法。

Bean生命周期之七步(重点)

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

编写一个类实现BeanPostProcessor类(额外的一个类),并且重写before和after方法:

bean包下的BeanPostProcessor类:

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);
    }

    @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.powernode.spring6.bean.LogBeanPostProcessor"/>

即 Spring配置文件 spring.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">

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

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

</beans>

一定要注意:在spring.xml文件中配置的Bean后处理器将作用于当前配置文件中所有的Bean。

执行测试程序:

image.png

如果加上Bean后处理器的话,Bean的生命周期就是7步了:

image.png

Bean生命周期之10步

如果根据源码跟踪,可以划分更细粒度的步骤,10步:

image.png

上图中检查Bean是否实现了Aware的相关接口是什么意思?

Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware

  • 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
  • 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
  • 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。

测试以上10步,可以让User类实现5个接口,并实现所有方法:

  • BeanNameAware
  • BeanClassLoaderAware
  • BeanFactoryAware
  • InitializingBean
  • DisposableBean

代码如下:

bean目录下的User.java类:

public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
    private String name;

    public User() {
        System.out.println("1.实例化Bean");
    }

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

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

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

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

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("3.Bean工厂:" + beanFactory);
    }

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

    @Override
    public void destroy() throws Exception {
        System.out.println("9.DisposableBean destroy");
    }

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

bean包下的 LogBeanPostProcessor.java 类

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

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("7.Bean后处理器的after方法执行,已完成初始化");
        return bean;
    }
}

执行结果:

image.png

通过测试可以看出来:

  • InitializingBean的方法早于init-method的执行。
  • DisposableBean的方法早于destroy-method的执行。

对于SpringBean的生命周期,掌握之前的7步即可。够用。

Bean的作用域不同,管理方式不同

回顾Bean的作用域:

scope:有 singleton 和 prototype 两个参数,singleton 指的是单例,prototype 指的是多例

Spring 容器只对 singleton 的Bean进行完整的生命周期

如果是prototype 作用域的Bean,Spring容器只负责将Bean初始化完毕,等客户端程序一旦获取到该Bean之后,Spring容器就不再管理该对象的生命周期了

Spring 根据Bean的作用域来选择管理方式。(默认为singleton)

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

我们把之前User类的spring.xml文件中的配置scope设置为prototype:

<?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">

    <!--
    init-method属性指定初始化方法。
    destroy-method属性指定销毁方法。
    -->
    <bean id="userBean" class="com.byxl8112.spring6.bean.User" init-method="initBean" destroy-method="destroyBean" scope="prototype">
        <property name="name" value="zhangsan"/>
    </bean>

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

</beans>

执行测试程序

image.png

通过测试一目了然。只执行了前8步,第9和10都没有执行。

自己new的对象如何让Spring管理

有些时候可能会遇到这样的需求,某个java对象是我们自己new的,然后我们希望这个对象被Spring容器管理,怎么实现?

bean包下的User.java类

public class User{  
}

测试程序

@Test
public void testBeanRegister(){
    // 自己new的对象
    User user = new User();
    System.out.println(user);

    // 创建 默认列表BeanFactory 对象
    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    // 注册Bean
    factory.registerSingleton("userBean", user);
    // 从spring容器中获取bean
    User userBean = factory.getBean("userBean", User.class);
    System.out.println(userBean);
}

执行结果

image.png

Bean的循环依赖问题

什么是Bean的循环依赖

A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。

比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。

image.png

bean包下的Husband.java类

public class Husband {
    private String name;
    private Wife wife;
}

bean包下的Wife.java类

public class Wife {
    private String name;
    private Husband husband;
}

singleton下的set注入产生的循环依赖

我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?

bean包下的Husband.java类

public class Husband {
    private String name;
    private Wife wife;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setWife(Wife wife) {
        this.wife = wife;
    }

    // toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。
    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}

bean包下的Wife.java类

public class Wife {
    private String name;
    private Husband husband;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setHusband(Husband husband) {
        this.husband = husband;
    }

    // toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband.getName() +
                '}';
    }
}

Spring配置文件 spring.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">

    <bean id="husbandBean" class="com.byxl8112.spring6.bean.Husband" scope="singleton">
        <property name="name" value="张三"/>
        <property name="wife" ref="wifeBean"/>
    </bean>
    
    <bean id="wifeBean" class="com.byxl8112.spring6.bean.Wife" scope="singleton">
        <property name="name" value="小花"/>
        <property name="husband" ref="husbandBean"/>
    </bean>
</beans>

测试程序

@Test
public void testSingletonAndSet(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
    Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
    System.out.println(husbandBean);
    System.out.println(wifeBean);
}

执行结果:

image.png

通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题

根据bean的生命周期可以知道,spring容器当中的bean是先被实例化的 实例化之后才是属性初始化,所以当husband需要给wife属性赋值的时候 wife这个bean已经提前曝光了而且还是单例的,所以可以解决循环依赖问题

prototype下的set注入产生的循环依赖

我们再来测试一下:prototype+set注入的方式下,循环依赖会不会出现问题?

bean包下的 Husband.java类 和 Wife.husband.java类 不变

Spring的配置文件 spring.xml 中的scope参数改为 prototype

<?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="husbandBean" class="com.byxl8112.spring6.bean.Husband" scope="prototype">
        <property name="name" value="张三"/>
        <property name="wife" ref="wifeBean"/>
    </bean>
    <bean id="wifeBean" class="com.byxl8112.spring6.bean.Wife" scope="prototype">
        <property name="name" value="小花"/>
        <property name="husband" ref="husbandBean"/>
    </bean>
</beans>

执行测试程序后发生了异常,异常信息如下:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'husbandBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
	... 44 more

翻译为:创建名为“husbandBean”的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用?

通过测试得知,当循环依赖的所有Bean的scope=”prototype”的时候,产生的循环依赖,Spring是无法解决的,会出现BeanCurrentlyInCreationException异常。

大家可以测试一下,以上两个Bean,如果其中一个是singleton,另一个是prototype,是没有问题的。

为什么两个Bean都是prototype时会出错呢?

image.png

singleton下的构造注入产生的循环依赖

我们再来测试一下 singleton + 构造注入的方式下,spring是否能够解决这种循环依赖。

bean包下的Husband.java类

public class Husband {
    private String name;
    private Wife wife;

    public Husband(String name, Wife wife) {
        this.name = name;
        this.wife = wife;
    }

    // -----------------------分割线--------------------------------
    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife +
                '}';
    }
}

bean包下的Wife.java类

public class Wife {
    private String name;
    private Husband husband;

    public Wife(String name, Husband husband) {
        this.name = name;
        this.husband = husband;
    }

    // -------------------------分割线--------------------------------
    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband +
                '}';
    }
}

Spring配置文件 spring2.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">

    <bean id="hBean" class="com.byxl8112.spring6.bean2.Husband" scope="singleton">
        <constructor-arg name="name" value="张三"/>
        <constructor-arg name="wife" ref="wBean"/>
    </bean>

    <bean id="wBean" class="com.byxl8112.spring6.bean2.Wife" scope="singleton">
        <constructor-arg name="name" value="小花"/>
        <constructor-arg name="husband" ref="hBean"/>
    </bean>
</beans>

测试程序:

@Test
public void testSingletonAndConstructor(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
    Husband hBean = applicationContext.getBean("hBean", Husband.class);
    Wife wBean = applicationContext.getBean("wBean", Wife.class);
    System.out.println(hBean);
    System.out.println(wBean);
}

执行结果:发生异常

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'hBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
	... 56 more

和上一个测试结果相同,都是提示产生了循环依赖,并且Spring是无法解决这种循环依赖的。

主要原因是因为通过构造方法注入导致的:

因为构造方法注入会导致 实例化对象的过程 和 对象属性赋值的过程 没有分离开,必须在一起完成导致的。

Spring解决循环依赖的机理

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

根本的原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。

  • 实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。

  • 给Bean属性赋值的时候:调用setter方法来完成。

实例化对象 和 对象属性赋值这两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。

也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。

那么在Spring框架底层源码级别上是如何实现的呢?

image.png

在以上类中包含三个重要的属性:

Cache of singleton objects: bean name to bean instance.单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】

Cache of early singleton objects: bean name to bean instance.早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】

Cache of singleton factories: bean name to ObjectFactory. 单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】

这三个缓存其实本质上是三个Map集合。

我们再来看,在该类中有这样一个方法addSingletonFactory(),这个方法的作用是:将创建Bean对象的ObjectFactory对象提前曝光。

image.png

再分析下面的源码:

image.png

从源码中可以看到,spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。

总结:

Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。

回顾反射机制

分析方法四要素

我们先来看一下,不使用反射机制调用一个方法需要几个要素的参与。

有一个这样类:

systemService.java类

public class SystemService {
    
    public void logout(){
        System.out.println("退出系统");
    }

    public boolean login(String username, String password){
        if ("admin".equals(username) && "admin123".equals(password)) {
            return true;
        }
        return false;
    }
}

编写程序调用方法:

测试类:

public class ReflectTest01 {
    public static void main(String[] args) {

        // 创建对象
        SystemService systemService = new SystemService();

        // 调用方法并接收方法的返回值
        boolean success = systemService.login("admin", "admin123");

        System.out.println(success ? "登录成功" : "登录失败");
    }
}

通过以上代码可以看出,调用一个方法,一般涉及到4个要素:

  • 调用哪个对象的(systemService)
  • 哪个方法(login)
  • 传什么参数(”admin”, “admin123”)
  • 返回什么值(success)

获取Method

要使用反射机制调用一个方法,首先要获取到这个方法

在反射机制中Method实例代表的是一个方法,那么怎么获取Method实例呢

SystemService.java类

public class SystemService {

    public void logout(){
        System.out.println("退出系统");
    }

    public boolean login(String username, String password){
        if ("admin".equals(username) && "admin123".equals(password)) {
            return true;
        }
        return false;
    }
    
    public boolean login(String password){
        if("110".equals(password)){
            return true;
        }
        return false;
    }
}

如何获取到 logout() 、login(String, String) 、login(String) 这三个方法呢?

要获取方法Method,首先需要获取这个类Class。

Class clazz = Class.forName("com.byxl8112.reflect.SystemService");

当拿到Class之后,调用getDeclaredMethod() 方法可以获取到方法,第一个参数是方法名,后面参数是此方法中的参数类型.class

获取方法:login(String username, String password)

Method loginMethod = clazz.getDeclaredMethod("login", String.calss, String.class);

获取方法:login(String password)

Method loginMethod = clazz.getDeclaredMethod("login", String.class);

获取一个方法,需要告诉Java程序,你要获取的方法的名字是什么,这个方法上每个形参的类型是什么。这样Java程序才能给你拿到对应的方法。

这样的设计也非常合理,因为在同一个类当中,方法是支持重载的,也就是说方法名可以一样,但参数列表一定是不一样的,所以获取一个方法需要提供方法名以及每个形参的类型。

有一个方法:

public void setAge(int age){
    this.age = age;
}

获取这个方法的话,代码应该这样写:

Method setAgeMethod = clazz.getDeclaredMethod("setAge", int.class);

其中setAge是方法名,int.class是形参的类型。

获取上面的logout方法:

Method logoutMethod = clazz.getDeclaredMethod("logout");

因为这个方法形参的个数是0个,所以只需要提供方法名就行了。

调用Method

要让一个方法调用的话,就关联到四要素了:

  • 调用哪个对象的
  • 哪个方法
  • 传什么参数
  • 返回什么值

SystemService.java类

public class SystemService {

    public void logout(){
        System.out.println("退出系统");
    }

    public boolean login(String username, String password){
        if ("admin".equals(username) && "admin123".equals(password)) {
            return true;
        }
        return false;
    }

    public boolean login(String password){
        if("110".equals(password)){
            return true;
        }
        return false;
    }
}

假如我们要调用的方法是:login(String, String)

第一步:创建(实例化)对象(四要素之首:调用哪个对象的)

Class clazz = Class.forName("com.byxl8112.reflect.SystemService");
Object obj = clazz.getConstructor().newInstance();

第二步:获取方法login(String,String)(四要素之一:哪个方法)

Method loginMethod = clazz.getDeclaredMethod("login", String.class, String.class);

第三步:调用方法

Method对象的invoke()方法可以调用方法

Object retValue = loginMethod.invoke(obj, "admin", "admin123");

解说四要素:

  • 哪个对象:obj
  • 哪个方法:loginMethod
  • 传什么参数:”admin”, “admin123”
  • 返回什么值:retValue

测试程序:

public class ReflectTest02 {
    public static void main(String[] args) throws Exception{
        Class clazz = Class.forName("com.byxl8112.reflect.SystemService");
        Object obj = clazz.getConstructor().newInstance();
        Method loginMethod = clazz.getDeclaredMethod("login", String.class, String.class);
        Object retValue = loginMethod.invoke(obj, "admin", "admin123");
        System.out.println(retValue);
    }
}

执行结果:true

那如果调用既没有参数,又没有返回值的logout方法,应该怎么做?

public class ReflectTest03 {
    public static void main(String[] args) throws Exception{
       	Class clazz = Class.forName("com.byxl8112.reflect.SystemService");
        Class clazz = clazz.getConstructor().newInstance();
        Method logoutMethod = clazz.getDeclaredMethod("logout");
        logoutMethod.invoke(obj);
    }
}

执行结果:退出系统

已知属性名给其反射赋值

User.java类

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

知道以下这几条信息:

  • 类名是:com.powernode.reflect.User

  • 该类中有String类型的name属性和int类型的age属性。

  • 另外你也知道该类的设计符合javabean规范。(也就是说属性私有化,对外提供setter和getter方法)

通过反射机制给User对象的name属性赋值zhangsan,给age属性赋值20岁。(调用age的set方法)

主类:

public class UserTest {
    public static void main(String[] args) throws Exception{
        //已知类名
        String className = "com.byxl8112.reflect.User";
        //已知属性名
        String propertyName = "age";
        
        //通过反射机制给User对象的age属性赋值20岁
        Class<?> clazz = Class.forName(className);
        Class obj = clazz.getConstructor().newInstance();  //创建对象/实例化构造器
        
        //根据属性名获取setter方法
        String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
       
        //获取Method
        Method setMethod = clazz.getDeclaredMethod(setMethodName, int.class);
        
        //调用Method
        setMethod.invoke(obj, 20);
        
        System.out.println(obj);
    }
}

执行结果:

image.png

手写Spring框架

Spring IoC容器的实现原理:工厂模式 + 解析XML + 反射机制。

1.创建模块myspring

采用Maven方式新建Module:myspring

image.png

打包方式采用jar,并且引入dom4j和jaxen的依赖,因为要使用它解析XML文件,还有junit依赖。

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>org.myspringframework</groupId>
    <artifactId>myspring</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <dependencies>
        <!--dom4j是一个能够解析XML文件的java组件-->
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.2.0</version>
        </dependency>

        <!--单元测试依赖-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

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

    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
</project>

2.准备好要管理的Bean

准备好我们要管理的Bean(这些Bean在将来开发完框架之后是要删除的

注意包名,不要用org.myspringframework包,因为这些Bean不是框架内置的。是将来使用我们框架的程序员提供的。

com.byxl8112.myspring.bean包下的 Adress.java 类

public class Address {
    private String city;
    private String street;
    private String zipcode;

    public Address() {
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }

    @Override
    public String toString() {
        return "Address{" +
                "city='" + city + '\'' +
                ", street='" + street + '\'' +
                ", zipcode='" + zipcode + '\'' +
                '}';
    }
}

com.byxl8112.myspring.bean包下的 User.java类

public class User {
    private String name;
    private int age;
    private Address addr;

    public User() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Address getAddr() {
        return addr;
    }

    public void setAddr(Address addr) {
        this.addr = addr;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", addr=" + addr +
                '}';
    }
}

3.准备myspring.xml配置文件

将来在框架开发完毕之后,这个文件也是要删除的。因为这个配置文件的提供者应该是使用这个框架的程序员。

文件名随意,我们这里叫做:myspring.xml

文件放在类路径当中即可,我们这里把文件放到类的根路径下。(即resources目录下)

spring配置文件 myspring.xml

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

    <bean id="userBean" class="com.byxl8112.myspring.bean.User">
        <property name="name" value="张三"/>
        <property name="age" value="20"/>
        <property name="addr" ref="addrBean"/>
    </bean>
    
    <bean id="addrBean" class="com.byxl8112.myspring.bean.Address">
        <property name="city" value="北京"/>
        <property name="street" value="大兴区"/>
        <property name="zipcode" value="1000001"/>
    </bean>

</beans>

使用value给简单属性赋值。使用ref给非简单属性赋值。

4.编写ApplicationContext接口

ApplicationContext接口中提供一个getBean()方法,通过该方法可以获取Bean对象。

注意包名:这个接口就是myspring框架中的一员了。

org.myspringframework.core包下的ApplicationContext.java 接口类

public interface ApplicationContext {
    /**
     * 根据bean的id获取bean实例。
     * @param beanId bean的id
     * @return bean实例
     */
    Object getBean(String beanId);
}

5.编写ClassPathoXmlApplicationContext

ClassPathXmlApplicationContext是ApplicationContext接口的实现类。该类从类路径当中加载myspring.xml配置文件。

org.myspringframework.core包下的ClassXmlApplicationContext.java类

public class ClassPathXmlApplicationContext implements ApplicationContext{
    @Override
    public Object getBean(String beanId) {
        return null;
    }

6.确定采用Map集合存储Bean

确定采用Map集合存储Bean实例。Map集合的key存储beanId,value存储Bean实例。Map<String,Object>

在ClassPathXmlApplicationContext类中添加Map<String,Object>属性。

并且在ClassPathXmlApplicationContext类中添加构造方法,该构造方法的参数接收myspring.xml文件。

同时实现getBean方法。

org.myspringframework.core包下的ClassXmlApplicationContext.java类

public class ClassPathXmlApplicationContext implements ApplicationContext{
    /**
     * 存储bean的Map集合
     */
    private Map<String,Object> beanMap = new HashMap<>();

    /**
     * 在该构造方法中,解析myspring.xml文件,创建所有的Bean实例,并将Bean实例存放到Map集合中。
     * @param resource 配置文件路径(要求在类路径当中)
     */
    public ClassPathXmlApplicationContext(String resource) {

    }

    @Override
    public Object getBean(String beanId) {
        return beanMap.get(beanId);
    }
}

7.解析配置文件实例化所有Bean

在ClassPathXmlApplicationContext的构造方法中解析配置文件,获取所有bean的类名,通过反射机制调用无参数构造方法创建Bean。并且将Bean对象存放到Map集合中。

org.myspringframework.core包下的ClassXmlApplicationContext.java类中的 public ClassPathoXmlApplicationContext(String resource) 构造方法:

public ClassPathXmlApplicationContext(String resource) {
    try {
        //解析myspring.xml文件,然后实例化Bean,将Bean存放到singletonObject集合当中
        //这是dom4j解析XML文件的核心对象
        SAXReader reader = new SAXReader();
        //获取一个流,指向配置文件
        InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(resource);
        //读文件
        Document document = reader.read(in);
        //获取所有的bean标签
        List<Node> nodes = document.selectNodes("//bean");
        //遍历bean标签
        nodes.forEach(node -> {
            try {
                System.out.println(node);
                //向下转型的目的是为了使用Element接口里更加丰富的方法
                Element beanElt = (Element) node;
                //获取id属性
                String id = beanElt.attributeValue("id");
                //获取class属性
                String className = beanElt.attributeValue("class");

                //log日志
                logger.info("beanName" + id);
                logger.info("beanClass" + className);

                //通过反射机制创建对象,将其放到Map集合中,提前曝光
                //获取class
                Class<?> clazz = Class.forName(className);
                //获取无参数构造方法
                Constructor<?> defaultCon = clazz.getDeclaredConstructor();
                //调用无参数构造方法实例化Bean
                Object bean = defaultCon.newInstance();
                //将Bean曝光,加入Map集合
                beanMap.put(id,bean);
                //记录日志
                logger.info(beanMap.toString());
            }catch(Exception e){
                e.printStackTrace();
            }
        });
    } catch (Exception e) {
        e.printStackTrace();
    }
}

8.测试获取到Bean

测试程序:

public class MySpringTest {
    @Test
    public void testMySpring(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");
        Object userBean = applicationContext.getBean("userBean");
        Object addrBean = applicationContext.getBean("addrBean");
        System.out.println(userBean);
        System.out.println(addrBean);
    }
}

执行结果:

image.png

通过测试Bean已经实例化成功了,属性的值是null,这是我们能够想到的,毕竟我们调用的是无参数构造方法,所以属性都是默认值。

9.给Bean的属性赋值

通过反射机制调用set方法,给Bean的属性赋值

在ClassPathoXmlApplicationContext构造方法中编写代码。

public ClassPathXmlApplicationContext(String resource) {
    try {
        //解析myspring.xml文件,然后实例化Bean,将Bean存放到singletonObject集合当中
        //这是dom4j解析XML文件的核心对象
        SAXReader reader = new SAXReader();
        //获取一个流,指向配置文件
        InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(resource);
        //读文件
        Document document = reader.read(in);
        //获取所有的bean标签
        List<Node> nodes = document.selectNodes("//bean");
        //遍历bean标签
        nodes.forEach(node -> {
            try {
                System.out.println(node);
                //向下转型的目的是为了使用Element接口里更加丰富的方法
                Element beanElt = (Element) node;
                //获取id属性
                String id = beanElt.attributeValue("id");
                //获取class属性
                String className = beanElt.attributeValue("class");

                //log日志
                logger.info("beanName" + id);
                logger.info("beanClass" + className);

                //通过反射机制创建对象,将其放到Map集合中,提前曝光
                //获取class
                Class<?> clazz = Class.forName(className);
                //获取无参数构造方法
                Constructor<?> defaultCon = clazz.getDeclaredConstructor();
                //调用无参数构造方法实例化Bean
                Object bean = defaultCon.newInstance();
                //将Bean曝光,加入Map集合
                beanMap.put(id,bean);
                //记录日志
                logger.info(beanMap.toString());
                
                
                //再次重新把所有的bean标签遍历一次,这一次主要是给对象的属性赋值
            nodes.forEach(node ->{
                try{
                    Element beanElt = (Element) node;
                    //获取id
                    String id = beanElt.attributeValue("id");
                    //获取className
                    String className = beanElt.attributeValue("class");
                    //获取class
                    Class<?> aClass = Class.forName(className);
                    //获取该bean标签下所有的属性property标签
                    List<Element> propertys = (List<Element>) beanElt.element("property");
                    //遍历所有的属性标签
                    propertys.forEach(property ->{
                        try{
                            //获取属性名 User
                            String propertyName = property.attributeValue("name");
                            //获取属性类型  
                            Field field = aClass.getDeclaredField(propertyName);
                            //获取属性set类型
                            String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
                            //获取set方法 field.getType() : 获取属性的类型 String.class/int.class 等等
                            Method setMethod = aClass.getDeclaredMethod(setMethodName, field.getType());
                            //获取具体的值
                            String value = property.attributeValue("value");
                            Object actualValue = null; //真值
                            String ref = property.attributeValue("ref");
                            if(value != null){
                                //说明这个值是简单类型
                                //调用set方法
                                //invoke第一个参数是 对象名 其中singletonObject.get(id) 获取到的value是class
                                //获取属性类型名 byte short int long ........ Byte Short Integer Long...
                                String propertyTypeSimpleName = field.getType().getSimpleName();
                                switch(propertyTypeSimpleName){
                                    case "byte":
                                        actualValue = Byte.parseByte(value);
                                        break;
                                    case "short":
                                        actualValue = Short.parseShort(value);
                                        break;
                                    case "int":
                                        actualValue = Integer.parseInt(value);
                                        break;
                                    case "Integer":
                                        actualValue = Integer.valueOf(value);
                                }
                                setMethod.invoke(beanMap.get(id), actualValue);
                            }
                            if(ref != null){
                                //说明这个值是非简单类型
                                //调用set方法
                                setMethod.invoke(beanMap.get(id), beanMap.get(ref));
                            }
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    });
                }catch(Exception e){
                    e.printStackTrace();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

重点:当property标签中是value 和 ref 的时候需要分别判断

执行测试程序

image.png

10.打包发布

将多余的类以及配置文件删除,使用maven打包发布。

image.png image.png

11.使用myspring框架

新建模块:myspring-test

image.png

引入myspring框架的依赖:

<?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.powernode</groupId>
    <artifactId>myspring-test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.myspringframework</groupId>
            <artifactId>myspring</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>

Bean包下的UserDao.java类

public class UserDao {
    public void insert(){
        System.out.println("UserDao正在插入数据");
    }
}

bean包下的UserService.java类

public class UserService {
    private UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        System.out.println("UserService开始执行save操作");
        userDao.insert();
        System.out.println("UserService执行save操作结束");
    }
}

Spring配置文件myspring.xml

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

<beans>

    <bean id="userServiceBean" class="com.byxl8112.myspring.bean.UserService">
        <property name="userDao" ref="userDaoBean"/>
    </bean>

    <bean id="userDaoBean" class="com.byxl8112.myspring.bean.UserDao"/>

</beans>

编写测试程序:

public class MySpringTest {

    @Test
    public void testMySpring(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");
        UserService userServiceBean = (UserService) applicationContext.getBean("userServiceBean");
        userServiceBean.save();
    }
}

执行结果:

image.png

Spring IoC注解开发

回顾注解

注解的存在主要是为了简化XML的配置。Spring6倡导全注解开发

我们来回顾一下:

  • 第一:注解怎么定义,注解中的属性怎么定义?
  • 第二:注解怎么使用?
  • 第三:通过反射机制怎么读取注解?

注解怎么定义,注解中的属性怎么定义?

@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Component {
    String value();
}

以上是自定义了一个注解:Component

该注解上面修饰的注解包括:Target注解和Retention注解,这两个注解被称为元注解。

Target注解用来设置Component注解可以出现的位置,以上代表表示Component注解只能用在类和接口上。

Retention注解用来设置Component注解的保持性策略,以上代表Component注解可以被反射机制读取。

String value(); 是Component注解中的一个属性。该属性类型String,属性名是value。写此注解的时候默认的值就是value

注解怎么使用?

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

用法简单,语法格式:@注解类型名(属性名=属性值, 属性名=属性值, 属性名=属性值……)

userBean为什么使用双引号括起来,因为value属性是String类型,字符串。

另外如果属性名是value,则在使用的时候可以省略属性名,例如:

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

通过反射机制怎么读取注解?

接下来,我们来写一段程序,当Bean类上有Component注解时,则实例化Bean对象,如果没有,则不实例化对象。

我们准备两个Bean,一个上面有注解,一个上面没有注解。

有注解的Bean

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

没有注解的Bean

public class Vip {
}

假设我们现在只知道包名:com.byxl8112.bean。至于这个包下有多少个Bean我们不知道。哪些Bean上有注解,哪些Bean上没有注解,这些我们都不知道,如何通过程序全自动化判断。

public class Test {
    public static void main(String[] args) throws Exception {
        // 存放Bean的Map集合。key存储beanId。value存储Bean。
        Map<String,Object> beanMap = new HashMap<>();

        String packageName = "com.powernode.bean";
        String path = packageName.replaceAll("\\.", "/");
        URL url = ClassLoader.getSystemClassLoader().getResource(path);
        File file = new File(url.getPath());
        File[] files = file.listFiles();
        Arrays.stream(files).forEach(f -> {
            String className = packageName + "." + f.getName().split("\\.")[0];
            try {
                Class<?> clazz = Class.forName(className);
                if (clazz.isAnnotationPresent(Component.class)) {
                    Component component = clazz.getAnnotation(Component.class);
                    String beanId = component.value();
                    Object bean = clazz.getConstructor().newInstance();
                    beanMap.put(beanId, bean);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        System.out.println(beanMap);
    }
}

执行结果:

image.png

声明Bean的注解

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

  • @Component
  • Controller
  • Service
  • Repository

源码如下:

@Component注解

@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Component {
    String value();
}

@Controller注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

@Service注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

@Repository注解

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

通过源码可以看到,@Controller、@Service、@Repository这三个注解都是@Component注解的别名。

也就是说:这四个注解的功能都一样。用哪个都可以。

只是为了增强程序的可读性,建议:

  • 控制器类上使用:Controller
  • service类上使用:Service
  • dao类上使用:Repository

他们都是只有一个value属性,value属性用来指定bean的id,也就是bean的名字

image.png

Spring注解的使用

如何使用以上的注解呢?

  • 第一步:加入aop的依赖
  • 第二步:在配置文件中添加context命名空间
  • 第三步:在配置文件中指定扫描的包
  • 第四步:在Bean类上使用注解

第一步:加入aop的依赖

我们可以看到当加入spring-context依赖之后,会关联加入aop的依赖,所以这一步不用做

image.png

第二步:在配置文件中添加context命名空间

spring.xml文件中添加 :

xmlns:context="http://www.springframework.org/schema/context"

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"

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

第三步:在配置文件中指定要扫描的包

<context:component-scan base-package="com.byxl8112.spring6.bean"/>

<?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">
    <!--扫描com.byxl8112.spring6.bean包-->
    <context:component-scan base-package="com.byxl8112.spring6.bean"/>
</beans>

第四步:在Bean类上使用注解

bean包下的User.java类

@Component(value = "userBean")
public void User(){
}

编写测试程序:

public class AnnotationTest {
    @Test
    public void testBean(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User userBean = applicationContext.getBean("userBean", User.class);
        System.out.println(userBean);
    }
}

执行结果:

image.png

如果注解的属性名是value,那么value是可以省略的。

@Component("vipBean")
public class Vip(){
}

测试程序:

public class AnnotationTest {
    @Test
    public void testBean(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Vip vipBean = applicationContext.getBean("vipBean", Vip.class);
        System.out.println(vipBean);
    }
}

执行结果:

image.png

如果把value属性彻底去掉,spring会被Bean自动取名吗?会的。并且默认名字的规律是:Bean类名首字母小写即可。

@Component
public class BankDao {
}

也就是说,这个BankDao的bean的名字为:bankDao

测试一下

public class AnnotationTest {
    @Test
    public void testBean(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        BankDao bankDao = applicationContext.getBean("bankDao", BankDao.class);
        System.out.println(bankDao);
    }
}

执行结果:

image.png

将@Component注解换成其他三个注解(@Controller、@Service、@Repository)也可以用,所以表明这四个注解都是一样的,只是名称不同,为了程序可读性

如果是多个包怎么办?有两种解决方案:

  • 第一种:在配置文件中指定多个包,用逗号隔开。
  • 第二种:指定多个包的共同父包。

创建一个新的包:bean2,定义一个Bean类

bean2包下的Order.java类

@Service
public class Order {
}

配置文件spring.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="com.byxl8112.spring6.bean, com.byxl8112.spring6.bean2"/>
</beans>

测试程序:

public class AnnotationTest {
    @Test
    public void testBean(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        BankDao bankDao = applicationContext.getBean("bankDao", BankDao.class);
        System.out.println(bankDao);
        Order order = applicationContext.getBean("order", Order.class);
        System.out.println(order);
    }
}

执行结果:

image.png

指定父包

spring.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">
    <!--这里是直接指定bean和bean2包的父包spring6-->
    <context:component-scan base-package="com.powernode.spring6"/>
</beans>

执行测试程序:

image.png

选择性实例化Bean

假设在某个包下有很多Bean,有的Bean上标注了Component,有的标注了Controller,有的标注了Service,有的标注了Repository,现在由于某种特殊业务的需要,只允许其中所有的Controller参与Bean管理,其他的都不实例化。这应该怎么办呢?

这里为了方便,将这几个类都定义到同一个java源文件中了。

@Component
public class A {
    public A() {
        System.out.println("A的无参数构造方法执行");
    }
}

@Controller
class B {
    public B() {
        System.out.println("B的无参数构造方法执行");
    }
}

@Service
class C {
    public C() {
        System.out.println("C的无参数构造方法执行");
    }
}

@Repository
class D {
    public D() {
        System.out.println("D的无参数构造方法执行");
    }
}

@Controller
class E {
    public E() {
        System.out.println("E的无参数构造方法执行");
    }
}

@Controller
class F {
    public F() {
        System.out.println("F的无参数构造方法执行");
    }
}

只想实例化bean3包下的Controller,配置这样写:

<?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="com.byxl8112.spring6.bean3" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    
</beans>

use-default-filters="true"表示:使用spring默认的规则,只要有@Component、@Controller、@Service、@Repository中的任意一个注解标注,则进行实例化。

use-default-filters="false" 表示:不再spring默认实例化规则,即使有@Component、@Controller、@Service、@Repository这些注解标注,也不再实例化。

<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> 表示只有@Controller进行实例化。

选择注解生效(选择性实例化Bean)

  • 方案一:use-default-filters="false" 如果这个属性是 false,表示com.byxl8112.spring6.bean2包下的所有带有Bean的注解全部失效,即 @Component、@Controller、@Service、@Repository 全部失效

    指定注解生效(参与实例化Bean):

    <context:component-scan base-package="com.byxl8112.spring6.bean2" use-default-filters="false">
            <!--只有@Repository @Service 被包含进来,生效-->
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
    </context:component-scan>
  • 方案二:user-default-filters="true" 如果这个属性是true,表示com.byxl8112.spring6.bean2包下的所有带有声明Bean的注解全部生效,user-default-filters的默认值就是true,如果是true的话不用写

    指定注解失效(不参与实例化Bean):

    <context:component-scan base-package="com.byxl8112.spring6.bean2">
            <!--@Controller注解失效-->
            <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

注意:exclude-filter方式排除不能排除@Component注解,因为这个注解代表其他三个注解,把这个排除了,就相当于把其他三个也都排除了,就不会创建对象了

负责注入的注解

@Component、@Controller、@Service、@Repository 这四个注解是用来声明Bean的,声明后这些Bean将被实例化。

给Bean属性赋值需要用到以下注解:

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

@Value

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

bean4包下的 User.java类

@Component
public class User {
    
    @Value(value = "zhangsan")
    private String name;
    
    @Value("20")
    private int age;

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Spring配置文件 spring.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="com.byxl8112.spring6.bean4"/>
</beans>

测试程序:

@Test
public void testValue(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-injection.xml");
    Object user = applicationContext.getBean("user");
    System.out.println(user);
}

执行结果:

image.png

通过以上代码可以发现,我们并没有给属性提供setter方法,但仍然可以完成属性赋值。

如果提供setter方法,并且在setter方法上添加@Value注解,可以完成注入吗?尝试一下:

bean4包下的User.java类:

@Component
public class User {
    
    private String name;

    private int age;

    @Value("李四")
    public void setName(String name) {
        this.name = name;
    }

    @Value("30")
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

执行结果:

image.png

通过测试可以得知,@Value注解可以直接使用在属性上,也可以使用在setter方法上。都是可以的。都可以完成属性的赋值。

为了简化代码,以后我们一般不提供setter方法,直接在属性上使用@Value注解完成属性赋值。

出于好奇,我们再来测试一下,是否能够通过构造方法完成注入:

bean4包下的User.java类:

@Component
public class User {

    private String name;

    private int age;

    public User(@Value("隔壁老王") String name, @Value("33") int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

执行结果:

image.png

通过测试得知:@Value注解可以出现在属性上、setter方法上、以及构造方法的形参上。可见Spring给我们提供了多样化的注入。太灵活了。

@Autowired与@Qualifier

@Autowired装配

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

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

byType进行装配缺点:当有1个类有两个实现类时,将会报错

解决上述问题:联合使用@Autowired与@Qualifier

  • @Autuwired:使用类型注入值,从整个Bean工厂中搜索同源类型的对象进行注入,同源类型也可注入
  • 什么是同源类型?
    • 被注入的类型(Student中的school)与注入的类型是完全相同的类型
    • 被注入的类型(Student中的school父)与注入的类型(子)是父子类
    • 被注入的类型(Student中的school接口)与注入的类型(实现类)是接口和实现类的类型

注意:在有父子类的情况下,使用按类型注入,就意味着有多个可注入的对象,此时按照名称进行二次筛选,选中与被注入对象相同名称的对象进行注入。

源码:

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

源码中有两处需要注意:

  • 第一处:该注解可以标注在哪里?

    • 构造方法上
    • 方法上
    • 形参上
    • 属性上
    • 注解上
  • 第二处:该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在就注入,不存在的话,也不报错。

先在属性上使用@Autowired注解:

Dao包下的UserDao 接口:

package com.powernode.spring6.dao;

public interface UserDao {
    void insert();
}

Dao包下的UserDao实现类:

@Repository //纳入bean管理
public class UserDaoForMySQL implements UserDao{
    @Override
    public void insert() {
        System.out.println("正在向mysql数据库插入User数据");
    }
}

Service包下的UserService类:

@Service // 纳入bean管理
public class UserService {

    @Autowired // 在属性上注入
    private UserDao userDao;
    
    // 没有提供构造方法和setter方法。

    public void save(){
        userDao.insert();
    }
}

spring配置文件spring.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="com.byxl8112.spring6.dao,com.byxl8112.spring6.service"/>
</beans>

测试程序:

@Test
public void testAutowired(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-injection.xml");
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.save();
}

执行结果:

image.png

以上构造方法和setter方法都没有提供,经过测试,仍然可以注入成功。

接下来,再来测试一下@Autowired注解出现在setter方法上:

UserService包下的UserService.java类

@Service
public class UserService {

    private UserDao userDao;

    @Autowired     //出现在setter方法上
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        userDao.insert();
    }
}

执行结果:

image.png

我们再来看看能不能出现在构造方法上:

UserService包下的UserService.java类:

@Service
public class UserService {

    private UserDao userDao;

    @Autowired
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        userDao.insert();
    }
}

执行结果:

image.png

再来看看,这个注解能不能只标注在构造方法的形参上:

UserService包下的UserService.java类:

@Service
public class UserService {

    private UserDao userDao;

    public UserService(@Autowired UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        userDao.insert();
    }
}

执行结果:

image.png

还有更劲爆的,当有参数的构造方法只有一个时,@Autowired注解可以省略。

UserService包下的UserService.java类:

@Service
public class UserService {

    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        userDao.insert();
    }
}

执行结果:

image.png

当然,如果有多个构造方法,@Autowired肯定是不能省略的。

UserService包下的UserService.java类:

@Service
public class UserService {

    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
    
    public UserService(){
        
    }

    public void save(){
        userDao.insert();
    }
}

执行结果:

image.png

到此为止,我们已经清楚@Autowired注解可以出现在哪些位置了。

@Autowired注解默认是byType进行注入的,也就是说根据类型注入的,如果以上程序中,UserDao接口还有另外一个实现类,会出现问题吗?

Dao包下的UserDaoForOracle.java类:

@Repository //纳入bean管理
public class UserDaoForOracle implements UserDao{
    @Override
    public void insert() {
        System.out.println("正在向Oracle数据库插入User数据");
    }
}

当写完这个新的实现类之后,此时IDEA工具已经提示错误信息了:

image.png

错误信息中说:不能装配,UserDao这个Bean的数量大于1.

怎么解决这个问题呢?当然要byName,根据名称进行装配了。

联合装配

@Autowired注解和@Qualifier注解联合起来才可以根据名称进行装配,在@Qualifier注解中指定Bean名称。

Dao包下的UserService.java类:

@Repository // 这里没有给bean起名,默认名字是:userDaoForOracle
public class UserDaoForOracle implements UserDao{
    @Override
    public void insert() {
        System.out.println("正在向Oracle数据库插入User数据");
    }
}

Service包下的UserService.java类:

@Service
public class UserService {

    private UserDao userDao;

    @Autowired
    @Qualifier("userDaoForOracle") // 这个是bean的名字。
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        userDao.insert();
    }
}

执行结果:

image.png

总结:

  • @Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
  • 当带参数的构造方法只有一个,@Autowired注解可以省略。
  • @Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用

@Resource

@Resource注解也可以完成非简单类型注入。那它和‘@Autowired注解有什么区别?

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

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

pom.xml配置依赖文件:

<dependency>
  <groupId>jakarta.annotation</groupId>
  <artifactId>jakarta.annotation-api</artifactId>
  <version>2.1.1</version>
</dependency>

一定要注意:如果你用Spring6,要知道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注解的源码如下:

image.png

测试:

例子1:

dao包下的Student.java接口:

public interface StudentDao{
    void deleteById();
}

dao.impl包下的StudentDaoImplForMySQL.java类:

@Repository("studentDaoImplForMySQL")  //可省略括号中内容
public class StudentDaoImplForMySQL implements StudentDao {   //实现了StudentDao接口
    @Override
    public void deleteById() {
        System.out.println("mysql数据库正在删除学生信息...");
    }
}

service包下的UserService.java类:

@Service
public void StudentService{
    @Resource("studentDaoImplForMySQL")  //注入StudentDaoImplForMySQL类
    private StudentDao studentDao;
    
    public void deleteStudent(){
        studentDao.deleteById();
    }
}

spring配置文件spring.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="cn"/>  

</beans>

测试类:

@Test
public void testResource(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    StudentService studentService = applicationContext.getBean("studentService", StudentService.class);
    studentService.deleteStudent();
}

执行结果:

image-20230518083859716

例子2:

dao包下的Student.java接口:

public interface StudentDao{
    void deleteById();
}

dao.impl包下的StudentDaoImplForMySQL.java类:

@Repository("studentDao")  
public class StudentDaoImplForMySQL implements StudentDao {   //实现了StudentDao接口
    @Override
    public void deleteById() {
        System.out.println("mysql数据库正在删除学生信息...");
    }
}

service包下的UserService.java类:

@Service
public void UserService{
    @Resource   //不指定名称,默认的是"studentDao"
    private StudentDao studentDao;
    
    public void deleteStudent(){
        studentDao.deleteById();
    }
}

测试类:

@Test
public void testResource(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    StudentService studentService = applicationContext.getBean("studentService", StudentService.class);
    studentService.deleteStudent();
}

执行结果:

image-20230518083859716

通过测试得知,当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。

接下来把StudentService类中的属性名修改一下:

将student

@Service
public class StudentService(){
    @Resource
    private StudentDao studentDao2;  //注意这里变成了studentDao2,根据name查找不到会根据类型进行查找
    
    public void deleteStudent(){
        studentDao.deleteById();
    }
}

执行结果:

image-20230518090731480

根据异常信息得知:显然当通过name找不到的时候,自然会启动byType进行注入。以上的错误是因为StudentDao接口下有两个实现类导致的。所以根据类型注入就会报错。

我们再来看@Resource注解使用在setter方法上可以吗?

Service包下的StudentService.java类:

@Service
public class StudentService{
    private StudentDao studentDao;
    
    @Resource
    public void setStudentDao(StudentDao studentDao){
        this.studentDao = studentDao;
    }
    public void deleteStudent(){
     	studentDao.deleteById();
    }
}

注意这个setter方法的方法名,setUserDao去掉set之后,将首字母变小写userDao,userDao就是name

执行结果:

image.png

当然,也可以指定name:

Service包下的StudentService.java类:

@Service
public class StudentService{
    private StudentDao studentDao;
    
    @Resource
    public void setStudentDao(StudentDao studentDao){
        this.studentDao = studentDao;
    }
    public void deleteStudent(){
     	studentDao.deleteById();
    }
}

执行结果:

image.png

一句话总结@Resource注解:默认byName注入,没有指定name时把属性名当做name,根据name找不到时,才会byType注入。byType注入时,某种类型的Bean只能有一个。

全注解式开发

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

配置类代替spring配置文件:

com.byxl.spring包下的Spring6Configuration.java类:

@Configuration
//扫描包
@ComponentScan({"com.byxl8112.spring6.dao", "com.byxl8112.spring6.service"})
public class Spring6Configuration {
}

编写测试程序:不再new ClassPathXmlApplicationContext()对象了。

@Test
public void testNoXml(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
    UserService userService = applicationContext.getBean("studentService", UserService.class);
    studentService.deleteStudent();
}

执行结果:

image.png

JdbcTemplate

JdbcTemplate是Spring提供的一个JDBC模板类,是对JDBC的封装,简化JDBC代码。

当然,你也可以不用,可以让Spring集成其它的ORM框架,例如:MyBatis、Hibernate等。

接下来我们简单来学习一下,使用JdbcTemplate完成增删改查。

环境准备

数据库表: t_user

image.png

创建Maven项目,引入相关依赖:

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.byxl8112</groupId>
    <artifactId>spring6-009-jdbc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>16</source>
                    <target>16</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

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

        <!--spring jdbc依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.0</version>
        </dependency>

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

        <!--junit4测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!--引入德鲁伊连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>
    </dependencies>

        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>

准备实体类:表 t_user 对应的实体类User

com.byxl8112.spring6.bean 包下的User.java类:

public class User {

    /**
     * 用包装类的原因:
     * 防止因为null报异常:因为数据库的默认值是null , java基本数据类型的默认值不是null,java的包装类默认值是null
     */
    private Integer id;
    private String realName;
    private Integer age;

    public User() {
    }

    public User(Integer id, String realName, Integer age) {
        this.id = id;
        this.realName = realName;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getRealName() {
        return realName;
    }

    public void setRealName(String realName) {
        this.realName = realName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", realName='" + realName + '\'' +
                ", age=" + age +
                '}';
    }
}

编写Spring配置文件:

JdbcTemplate是Spring提供好的类,这类的完整类名是:org.springframework.jdbc.core.JdbcTemplate

我们怎么使用这个类呢?new对象就可以了。怎么new对象,Spring最在行了。直接将这个类配置到Spring配置文件中,纳入Bean管理即可。

spring配置文件spring.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">
    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"></bean>
    
</beans>

我们来看一下这个JdbcTemplate源码:

image.png

image.png

可以看到JdbcTemplate中有一个DataSource属性,这个属性是数据源,我们都知道连接数据库需要Connection对象,而生成Connection对象是数据源负责的。所以我们需要给JdbcTemplate设置数据源属性。

所有的数据源都是要实现javax.sql.DataSource接口的。这个数据源可以自己写一个,也可以用写好的,比如:阿里巴巴的德鲁伊连接池,c3p0,dbcp等。我们这里自己先手写一个数据源。

自己写的数据源:

package com.powernode.spring6.jdbc;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class MyDataSource implements DataSource {  //实现DataSource接口
    // 添加4个属性
    private String driver;
    private String url;
    private String username;
    private String password;

    // 提供4个setter方法
    public void setDriver(String driver) {
        this.driver = driver;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    // 重点写怎么获取Connection对象就行。其他方法不用管。
    @Override
    public Connection getConnection() throws SQLException {
        try {
            Class.forName(driver);
            Connection conn = DriverManager.getConnection(url, username, password);
            return conn;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }
}

写完数据源,我们需要把我们创建的数据源传递给JdbcTemplate。因为JdbcTemplate中有一个DataSource属性:

Spring配置文件spring.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">

    <bean id="myDataSource" class="com.byxl8112.spring6.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="abc123"/>
    </bean>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    	<property name="dataSource" ref="myDataSource"/>
    </bean>
</beans>

到这里环境就准备好了

新增

编写测试程序:

测试insert:

public class JdbcTest {
    @Test
    public void testInsert(){
        // 获取JdbcTemplate对象
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        // 执行插入操作
        // 注意:insert delete update的sql语句,都是执行update方法。
        String sql = "insert into t_user(id,real_name,age) values(?,?,?)";
        int count = jdbcTemplate.update(sql, null, "张三", 30);
        System.out.println("插入的记录条数:" + count);
    }
}

update方法有两个参数:

  • 第一个参数:要执行的SQL语句。(SQL语句中可能会有占位符)
  • 第二个参数:可变长参数,参数的个数可以是0个,也可以是多个。一般是SQL语句中有几个问号,则对应几个参数。

修改

测试程序:

@Test
public void testUpdate(){
    //获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
    //执行更新操作
    String sql = "update t_user set real_name = ?, age = ? where id = ?";
    int count = jdbcTemplate.update(sql, "李四", 55, 1);
    System.out.println("更新的记录条数:" +count);
}

执行结果:

image.png

删除

测试程序:

@Test
public void testDelete(){
    //获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    JdbcTemplate jdbcTemplate = applcationContext.getBean("jdbcTemlate", JdbcTemplate.class);
    //执行delete
    String sql = "delete from t_user where id = ?";
    int count = jdbcTemplate.update(sql,1);  //删除id=1的数据
    System.out.pritln("删除了几条记录:" + count);
}

执行结果:

image.png

查询一个对象

测试程序:

@Test
public void testSelectOne(){
    // 获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
    // 执行select
    String sql = "select id, real_name, age from t_user where id = ?";
    User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 2);
    System.out.println(user);
}

执行结果:

image.png

queryForObject方法的三个参数:

  • 第一个参数:sql语句
  • 第二个参数:Bean属性值和数据库记录的映射对象。在构造方法中指定映射的对象类型。
  • 第三个参数:可变长参数,给sql语句的占位符问号传值。

查询多个对象

测试程序:

@Test
public void testSelectAll(){
    //获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    //执行select
    String sql = "select id, real_name, age from t_user";
    List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class), 2)
        System.out.println(users);
}

执行结果:

image.png

查询一个值

测试程序:

@Test
public void testSelcetOneValue(){
    //获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
    //执行select
    String sql = "select count(1) from t_user";
    Integer count = jdbcTemplate.queryForObject(sql, int.class);//这里用Integer.class也可以
    System.out.println("总记录条数:" + count);
}

执行结果:

image.png

批量添加

测试程序:

@Test
public void testAddBatch(){
    // 获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
    // 批量添加
    String sql = "insert into t_user(id,real_name,age) values(?,?,?)";

    Object[] objs1 = {null, "小花", 20};
    Object[] objs2 = {null, "小明", 21};
    Object[] objs3 = {null, "小刚", 22};
    List<Object[]> list = new ArrayList<>();
    list.add(objs1);
    list.add(objs2);
    list.add(objs3);

    int[] count = jdbcTemplate.batchUpdate(sql, list);
    System.out.println(Arrays.toString(count));
}

执行结果:

image.png

批量修改

测试程序:

@Test
public void testUpdateBatch(){
    //获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
    //批量修改
    String sql = "update t_user set real_name = ?, age = ? where id = ?";
    Object[] objs1 = {"小花", 10, 2};
    Object[] objs2 = {"小明", 12, 3};
    Object[] objs3 = {"小刚", 9, 4};
    
    List<Object[]> list = new ArrayList<>();
    list.add(objs1);
    list.add(objs2);
    list.add(objs3);
    
    int[] count = jdbcTemplate.batchUpdate(sql, list);
    System.out.println(Arrays.toString(count));
}

执行结果:

image.png

批量删除

测试程序:

@Test
public void testUpdateBatch(){
    //获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
    //批量删除
    String sql = "delete from t_user where id = ?";
    Object[] objs1 = {2};
    Object[] objs2 = {3};
    Object[] objs3 = {4};
    
    List<Object[]> list = new ArrayList<>();
    list.add(objs1);
    list.add(objs2);
    list.add(objs3);
    
    int[] count = jdbcTemplate.batchUpdate(sql, list);
    System.out.println(Arrays.toString(count));
}

执行结果:

image.png

使用回调函数

使用回调函数,可以参与的更多细节:

测试程序:

@Test
public void testCallback(){
    //获取JdbcTemplate对象
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
   	String sql = "select id, real_name, age from t_user where id = ?";
    
    User user = jdbcTemplate.execute(sql, new PreparedStatementCallback<User>(){
       @Override
        public User doInpreparedStatement(PreparedStatement ps) throws SQLException,DataAccessException{
            User user = null;
            ps.setInt(1, 5);
            ResultSet rs = ps.executeQuery();
            if(rs.next()){
                user = new User();
                user.setId(rs.getInt("id"));
                user.setRealName(rs.getString("real_name"));
                user.setAge(rs.getInt("age"));
            }
        }
    });
}

执行结果:

image.png

使用德鲁伊连接池

之前数据源是我们自己写的,也可以使用别人写好的,例如德鲁伊连接池

第一步:引入德鲁伊连接池的依赖。

pom.xml依赖文件:

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.1.8</version>
</dependency>

第二步:将德鲁伊中的数据源配置到spring配置文件中,和配置我们自己写的一样。

Spring配置文件spring.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">

    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
        <property name="username" value="root"/>
        <property name="password" value="abc123"/>
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
</beans>

测试结果:

image.png

GoF之代理模式

对代理模式的理解

生活场景1:牛村的牛二看上了隔壁村小花,牛二不好意思直接找小花,于是牛二找来了媒婆王妈妈。这里面就有一个非常典型的代理模式。牛二不能和小花直接对接,只能找一个中间人。其中王妈妈是代理类,牛二是目标类。王妈妈代替牛二和小花先见个面。(现实生活中的婚介所)【在程序中,对象A和对象B无法直接交互时。】

生活场景2:你刚到北京,要租房子,可以自己找,也可以找链家帮你找。其中链家是代理类,你是目标类。你们两个都有共同的行为:找房子。不过链家除了满足你找房子,另外会收取一些费用的。(现实生活中的房产中介)【在程序中,功能需要增强时。】

西游记场景:八戒和高小姐的故事。八戒要强抢民女高翠兰。悟空得知此事之后怎么做的?悟空幻化成高小姐的模样。代替高小姐与八戒会面。其中八戒是客户端程序。悟空是代理类。高小姐是目标类。那天夜里,在八戒眼里,眼前的就是高小姐,对于八戒来说,他是不知道眼前的高小姐是悟空幻化的,在他内心里这就是高小姐。所以悟空代替高小姐和八戒亲了嘴儿。这是非常典型的代理模式实现的保护机制。代理模式中有一个非常重要的特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。 【在程序中,目标需要被保护时】

业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】

代理模式是GoF23种设计模式之一。属于结构型设计模式。

代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。

代理模式中的角色:

  • 代理类(代理主题)
  • 目标类(真实主题)
  • 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。

代理模式的类图:

image.png

代理模式在代码实现上,包括两种形式:

  • 静态代理
  • 动态代理

静态代理

OrderService接口:

public interface OrderService {
    /**
     * 生成订单
     */
    void generate();

    /**
     * 查看订单详情
     */
    void detail();

    /**
     * 修改订单
     */
    void modify();
}

OrderService接口的实现类:目标对象

public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        System.out.println("订单已修改");
    }
}

其中Thread.sleep()方法的调用是为了模拟操作耗时。

项目已上线,并且运行正常,笔试客户反馈有一些地方运行较慢,要求项目负责人就下达了这个需求。首先要搞清楚是哪些业务方法耗时较长,于是让我们统计每个业务方法所耗费的时长。如果是你,你该怎么做呢?

第一种方案:直接修改Java源代码,在每个业务方法中添加统计逻辑,如下:

目标对象:OrderServiceImpl类

public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
       
        System.out.println("订单已生成");
        
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        
        System.out.println("订单信息如下:******");
        
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        
        System.out.println("订单已修改");
        
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }
}

需求可以满足,但是显然违背了OCP开闭原则,这种方案不可取。

第二种方案:编写一个子类继承OrderServiceImpl,在子类中重写每个方法,代码如下:

public class OrderServiceImplSub extends OrderServiceImpl{
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        super.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        super.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        super.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

这种方式可以解决,但是存在两个问题:

  • 假设系统中有100个这样的业务类,需要提供100个子类,并且之前写好的创建Service对象的代码,都要修改为创建子类对象。
  • 由于采用了继承的方式,导师代码之间的耦合度较高

这种方案也不可取。

第三种方案:使用代理模式(这里采用静态代理)

目标对象:OrderServiceImpl.java类

public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        System.out.println("订单已修改");
    }
}

可以为OrderService接口提供一个代理类:代理对象OrderServiceProxy

public class OrderServiceProxy implements OrderService{ // 代理对象

    // 目标对象
    private OrderService orderService;

    // 通过构造方法将目标对象传递给代理对象
    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

这种方式的优点:符合OCP开闭原则,同时采用的是关联关系,所以程序的耦合度较低。所以这种方案是被推荐的。

编写客户端程序:主类测试类

public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService proxy = new OrderServiceProxy(target);
        // 调用代理对象的代理方法
        proxy.generate();
        proxy.modify();
        proxy.detail();
    }
}

运行结果:

image.png

以上就是代理模式中的静态代理,其中OrderService 接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy 是代理类。

如果系统中业务接口很多,一个接口对应一个代理类,会导致类爆炸,显然也是不合理的。怎么解决这个问题呢?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了,类爆炸解决了,而且代码只需要写一次,代码也会得到复用。

动态代理

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

在内存当中动态生成类的技术常见的包括:

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

JDK动态代理

我们还是使用静态代理中的例子:一个接口和一个实现类。

OrderService接口: OrderService.java类

public interface OrderService {
    /**
     * 生成订单
     */
    void generate();

    /**
     * 查看订单详情
     */
    void detail();

    /**
     * 修改订单
     */
    void modify();
}

OrderService接口实现类:OrderServiceImpl.java类

public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        System.out.println("订单已修改");
    }
}

我们在静态代理的时候,除了以上一个接口和一个实现类之外,需要写一个代理类UserServiceProxy。在动态代理中UserServiceProxy 代理类是可以动态生成的。这个类不需要写。直接写客户端程序即可:

客户端程序:main类,测试类

public class Client{
    public void mian(String[] args){
        //第一步: 创建目标对象
        OrderService target = new OrderServiceImpl();
        //第二步: 创建代理对象
        OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),调用处理器对象);
        //第三步: 调用代理对象的代理方法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

以上第二步创建代理对象是需要大家理解的:

OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调用处理器对象);

这行代码做了两件事:

  • 第一件事:在内存中生成了代理类的字节码
  • 第二件事:创建代理对象

Proxy类全名:java.lang.reflect.Proxy。这是JDK提供的一个类(所以称为JDK动态代理)。主要是通过这个类在内存中生成代理类的字节码。

其中newProxyInstance()方法有三个参数:

  • 第一个参数:类加载器。在内存中生成了字节码,要想执行这个字节码,也是需要先把这个字节码加载到内存当中的。所以要指定使用哪个类加载器加载。
  • 第二个参数:接口类型。代理类和目标类实现相同的接口,所以要通过这个参数告诉JDK动态代理生成的类要实现哪些接口。
  • 第三个参数:调用处理器。这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。

所以接下来我们要写一下java.lang.reflect.InvocationHandler接口的实现类,并且实现接口中的方法,代码如下:

public class TimerInvocationHandler implements InvocationHandler {
    // 目标对象
    private Object target;

    // 通过构造方法来传目标对象
    public TimerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 目标执行之前增强。
        long begin = System.currentTimeMillis();
        
        // 调用目标对象的目标方法
        // 以下的这个方法必须是 invoke() ,因为JDK在底层调用 invoke() 方法的程序已经提前写好了
        // 注意:invoke方法不是我们程序员负责调用的,是JDK负责调用的
        Object retValue = method.invoke(target, args);
        
        // 目标执行之后增强。
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
        // 一定要记得返回哦。
        return retValue;
    }
}

InvocationHandler接口中有一个方法invoke,这个invoke方法上有三个参数:

  • 第一个参数:Object proxy。代理对象。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。
  • 第二个参数:Method method。目标方法。
  • 第三个参数:Object[] args。目标方法调用时要传的参数。

1.为什么强行要求必须实现InvocationHandler接口?
因为一个类实现接口就必须实现接口中的方法
2.invoke 方法什么时候被调用呢?
当代理对象调用代理方法的时候,注册在 InvocationHandler 调用处理器当中的invoke()方法被调用

到此为止,调用处理器就完成了。接下来,应该继续完善Client程序:

客户端程序:main类,测试程序

public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService orderServiceProxy = (OrderService)Proxy.newProxyInstance(
            target.getClass().getClassLoader(), 
            target.getClass().getInterfaces(), 
            new TimerInvocationHandler(target));
        
        // 调用代理对象的代理方法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

大家可能会比较好奇:那个InvocationHandler接口中的invoke()方法没看见在哪里调用呀?

注意:当你调用代理对象的代理方法的时候,注册在 InvocationHandler 接口中的 invoke() 方法会被调用。也就是上面代码第12, 13, 14 行,这三行代码中任意一行代码执行,注册在 InvocationHandler 接口中的 invoke() 方法都会被调用。

执行结果:

image.png

我们可以看到,不管你有多少个Service接口,多少个业务类,这个TimerInvocationHandler接口是不是只需要写一次就行了,代码是不是得到复用了!!!!

而且最重要的是,以后程序员只需要关注核心业务的编写了,像这种统计时间的代码根本不需要关注。因为这种统计时间的代码只需要在调用处理器中编写一次即可。

到这里,JDK动态代理的原理就结束了。

不过我们看以下这个代码确实有点繁琐,对于客户端来说,用起来不方便:

image.png

我们可以提供一个工具类:ProxyUtil,封装一个方法:

工具类:util.java类

public class ProxyUtil {
    public static Object newProxyInstance(Object target) {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(), 
                new TimerInvocationHandler(target));
    }
}

这样客户端就不需要写这么繁琐了,当然,也可以不用工具类,无伤大雅

客户端:main类,测试程序:

public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService orderServiceProxy = (OrderService) ProxyUtil.newProxyInstance(target);
        // 调用代理对象的代理方法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

执行结果:

image.png

CGLIB动态代理

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

使用CGLIB,需要引入它的依赖:pom.xml文件

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

我们准备一个没有实现接口的类,如下:UserService.java类

public class UserService {

    public void login(){
        System.out.println("用户正在登录系统....");
    }

    public void logout(){
        System.out.println("用户正在退出系统....");
    }
}

使用CGLIB在内存中为UserService类生成代理类,并创建对象:

客户端程序:main类、测试程序

public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器
        Enhancer enhancer = new Enhancer();
        // 告诉cglib要继承哪个类
        enhancer.setSuperclass(UserService.class);
        // 设置回调接口
        enhancer.setCallback(方法拦截器对象);
        // 生成源码,编译class,加载到JVM,并创建代理对象
        UserService userServiceProxy = (UserService)enhancer.create();

        userServiceProxy.login();
        userServiceProxy.logout();

    }
}

和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,而是:net.sf.cglib.proxy.MethodInterceptor

编写MethodInterceptor接口实现类:TimerMethodceptor.java类

MethodInterceptor接口中有一个方法intercept(),该方法有4个参数:

第一个参数:目标对象

第二个参数:目标方法

第三个参数:目标方法调用时的实参

第四个参数:代理方法

public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 前增强
        long begin = System.currentTimeMillis();
        
        // 调用目标
        Object retValue = methodProxy.invokeSuper(target, objects);
        
        // 后增强
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
        // 一定要返回
        return retValue;
    }
}

回调已经写完了,可以修改客户端程序了:修改main类、测试程序

public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器
        Enhancer enhancer = new Enhancer();
        // 告诉cglib要继承哪个类
        enhancer.setSuperclass(UserService.class);
        // 设置回调接口
        enhancer.setCallback(new TimerMethodInterceptor());
        // 生成源码,编译class,加载到JVM,并创建代理对象
        UserService userServiceProxy = (UserService)enhancer.create();

        userServiceProxy.login();
        userServiceProxy.logout();

    }
}

对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:

image.png
  • –add-opens java.base/java.lang=ALL-UNNAMED
  • –add-opens java.base/sun.net.util=ALL-UNNAMED

执行结果:

image.png

面向切面编程

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

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

AOP是对OOP的补充延伸。

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

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

AOP介绍

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

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

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

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

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

请看下图,可以帮助你快速理解AOP的思想:

image.png

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

AOP的优点:

  • 第一:代码复用性增强。
  • 第二:代码易维护。
  • 第三:使开发者更关注业务逻辑。

AOP的七大术语

  • 连接点 Joinpoint

    • 在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。
  • 切点 Pointcut

    • 在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点)
  • 通知 Advice

    • 通知又叫增强,就是具体你要织入的代码。
    • 通知包括:
      • 前置通知
      • 后置通知
      • 环绕通知
      • 异常通知
      • 最终通知
  • 切面 Aspect

    • 切点 + 通知就是切面。
  • 织入 Weaving

    • 把通知应用到目标对象上的过程。
  • 代理对象 Proxy

    • 一个目标对象被织入通知后产生的新对象。
  • 目标对象 Target

    • 被织入通知的对象。

通过下图,大家可以很好的理解AOP的相关术语:

image.png 图片.png

切点表达式

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

切入点表达式语法格式:

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

访问控制权限修饰符:

  • 可选项。
  • 没写,就是4个权限都包括。
  • 写public就表示只包括公开的方法。

返回值类型:

  • 必填项。
  • * 表示返回值类型任意。

全限定类名:

  • 可选项。
  • 两个点“..”代表当前包以及子包下的所有类。
  • 省略时表示所有的类。

方法名:

  • 必填项。
  • *表示所有方法。
  • set*表示所有的set方法。

形式参数列表:

  • 必填项

  • () 表示没有参数的方法

  • (..) 参数类型和个数随意的方法

  • (*) 只有一个参数的方法

  • (*, String) 第一个参数类型随意,第二个参数是String的。

异常:

  • 可选项。
  • 省略时表示任意异常类型。

理解以下的切点表达式:

execution(public * com.byxl8112.mall.service.*.delete*(..))//com.byxl8112.service包下的所有类下的delete*方法
execution(* com.byxl8112.mall..*(..))  //com.byxl8112.mall包下的所有包下的类的所有方法
execution(* *(..))  //这个工程的所有类的所有方法

使用Spring的AOP

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

  • 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式
  • 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式
  • 第三种方式:Spring框架自己实现的AOP,基于XML配置方式

实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第一种和第二种方式

什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)

准备工作

使用Spring+AspectJ的AOP需要引入的依赖如下:

pom.xml配置文件

<!--spring context依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>6.0.0-M2</version>
</dependency>
<!--spring aop依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>6.0.0-M2</version>
</dependency>
<!--spring aspects依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>6.0.0-M2</version>
</dependency>

Spring配置文件中添加context命名空间:spring.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"
       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>

基于AspectJ的AOP注解式开发

实现步骤

第一步:定义目标类以及目标方法

service包下的 OrderService类(目标类):

//目标类
public class OrderService{
    //目标方法
    public void generate(){
        System.out.println("订单已生成");
    }
}

第二步:定义切面类

service包下的LogAspect(切面类)

@Aspect   /切面类是需要使用@Aspect注解进行标注的
public class MyAspect{
    
}

第三步:目标类和切面类都纳入spring bean 管理

  • 在目标类OrderService上添加 @Component 注解
  • 在切面类LogAspect类上添加 @Component 注解

第四步:在spring配置文件中添加组建扫描

Spring配置文件 spring-aspectj-aop-annotation.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"
       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">
    <!--开启组件扫描-->
    <context:component-scan base-package="com.byxl8112.spring6.service"/>
</beans>

第五步:在切面类中添加通知

Service包下的LogAspect.java类(切面类)

//切面类
@Aspect
@Component("logAspect")
public class LogAspect{
    //这就是需要增强的代码(通知)
    public void advice(){
        System.out.println("通知");
    }
}

第六步:在通知上添加切点表达式

切面 = 通知 + 切点

Service包下的LogAspect.java类:

@Aspect
@Component
public class LogAspect{
    //切点表达式
    @Before("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    //这就是需要增强的代码(通知)
    public void adive(){
        System.out.println("通知")
    }
}

注解@Before表示前置通知。

第七步:在spring配置文件中启用自动代理

Spring配置文件spring-aspectj-aop-annotation.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"
       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">
    <!--开启组件扫描-->
    <context:component-scan base-package="com.byxl8112.spring6.service"/>
    <!--开启自动代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

<aop:aspectj-autoproxy proxy-target-class="true"/> 开启自动代理之后,凡事带有@Aspect注解的bean都会生成代理对象。

proxy-target-class="true" 表示采用cglib动态代理。

proxy-target-class="false" 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。

测试程序:

public class AOPTest {
    @Test
    public void testAOP(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj-aop-annotation.xml");
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
        orderService.generate();
    }
}

运行结果:

5F9597E7-7930-4384-95C2-CF64C9DDA9F3.png

通知类型

通知类型包括:

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

接下来,编写程序来测试这几个通知的执行顺序:

// 切面类
@Component
@Aspect
public class MyAspect {
	
    //测试环绕通知
    @Around("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    //测试前置通知
    @Before("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }

    //测试后置通知
    @AfterReturning("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void afterReturningAdvice(){
        System.out.println("后置通知");
    }

    //测试异常通知
    @AfterThrowing("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }

    //测试最终通知
    @After("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void afterAdvice(){
        System.out.println("最终通知");
    }
}

目标类和目标方法:

Service包下的OrderService.java类:

// 目标类
@Component
public class OrderService {
    // 目标方法
    public void generate(){
        System.out.println("订单已生成!");
    }
}

测试程序:

public class AOPTest{
    @Test
    public void testAOP(){
        ApplicationContext applicationCOntext = new ClassPathXmlApplicationContxt("spring-aspectj-aop-annotation.xml");
        OrderService orderService = applicationContext.getBean("orderService",Orderservice.class");
		orderService.generate();
    }
}

执行结果:

5F9597E7-7930-4384-95C2-CF64C9DDA9F3.png

通过上面的执行结果就可以判断他们的执行顺序了,这里不再赘述。

结果中没有异常通知,这是因为目标程序执行过程中没有发生异常。我们尝试让目标方法发生异常:

// 目标类
@Component
public class OrderService {
    // 目标方法
    public void generate(){
        System.out.println("订单已生成!");
        if (1 == 1) {
            throw new RuntimeException("模拟异常发生");
        }
    }
}

再次执行测试程序,结果如下:

5F9597E7-7930-4384-95C2-CF64C9DDA9F3.png

通过测试得知,当发生异常之后,最终通知也会执行,因为最终通知@After会出现在finally语句块中。

出现异常之后,后置通知环绕通知的结束部分不会执行

切面的先后顺序

我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高

再定义一个切面类,如下:设置切面类

@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {

    //环绕通知
    @Around("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("YourAspect环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("YourAspect环绕通知结束");
    }

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

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

    //异常通知
    @AfterThrowing("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("YourAspect异常通知");
    }

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

这个JoinPoint joinPoint,在Spring容器调用这个方法的时候自动传过来,我们可以直接用,用这个JoinPoint joinPoint作用是通过方法的签名可以获取到一个方法的具体信息,Signature signature = joinPoint.getSignature(); 获取目标方法的签名,即public void xxxx

//获取目标方法的方法名
System.out.println("目标方法的方法名: " + joinPoint.getSignature().getName());
//执行结果:目标方法的方法名:generate

设置切面类MyAspect的优先级:

// 切面类
@Component
@Aspect
@Order(2) //设置优先级
public class MyAspect {

    //环绕通知
    @Around("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

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

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

    //异常通知
    @AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }

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

执行测试程序:

5F9597E7-7930-4384-95C2-CF64C9DDA9F3.png

通过修改@Order注解的整数值来切换顺序,执行测试程序:

5F9597E7-7930-4384-95C2-CF64C9DDA9F3.png

优化使用切点表达式

以前的切点表达式:

@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {

    //环绕通知
    @Around("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("YourAspect环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("YourAspect环绕通知结束");
    }

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

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

    //异常通知
    @AfterThrowing("execution(* com.byxl8112.spring6.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("YourAspect异常通知");
    }

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

缺点是:

  • 第一:切点表达式重复写了多次,没有得到复用
  • 第二:如果要修改切点表达式,需要修改多处,难维护

可以这样做:将切点表达式单独的定义出来,在需要的位置引入即可。如下:

使用@Pointcut注解来定义独立的切点表达式。

注意这个@Pointcut注解标注的方法随意,只是起到一个能够让@Pointcut注解编写的位置。

// 切面类
@Component
@Aspect
@Order(2)
public class MyAspect {
    
    //定义独立的切点表达式
    @Pointcut("execution(* com.byxl8112.spring6.service.OrderService.*(..))")2
    public void pointcut(){}

    @Around("pointcut()")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

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

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

    @AfterThrowing("pointcut()")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }

    @After("pointcut()")
    public void afterAdvice(){
        System.out.println("最终通知");
    }
}

执行测试程序:

5F9597E7-7930-4384-95C2-CF64C9DDA9F3.png

全注解式开发AOP

就是编写一个类,在这个类上面使用大量注解来代替spring的配置文件,spring配置文件消失了,如下:

service包下的Spring6Configuration.java类:

@Configuration   //代替spring.xml文件
@ComponentScan("com.powernode.spring6.service")  //组件(包)扫描
@EnableAspectJAutoProxy(proxyTargetClass = true)  //启用aspectj的自动代理机制,(默认关闭)
public class Spring6Configuration {
}

<aop:aspectj-autoproxy proxy-target-class="true"/> 开启自动代理之后,凡事带有@Aspect注解的bean都会生成代理对象。

proxy-target-class="true" 表示采用cglib动态代理。

proxy-target-class="false" 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。

测试程序也变化了:

@Test
public void testAOPWithAllAnnotation(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
    OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
    orderService.generate();
}

执行结果如下:

5F9597E7-7930-4384-95C2-CF64C9DDA9F3.png

基于XML配置方式的AOP(了解)

第一步:编写目标类

service包下的UserService.java类,不添加 @Component 注解

//目标类
public class UserService {  //目标对象
    public void logout(){   //目标方法
        System.out.println("系统正在安全退出...");
    }
}

第二步:编写切面类,并且编写通知

service包下的TimerAspect.java类,不添加 @Component 注解

public class TimerAspect {

    //通知
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        //前环绕
        long begin = System.currentTimeMillis();
        //执行目标
        joinPoint.proceed();
        //后环绕
        long end = System.currentTimeMillis();

        System.out.println("耗时" + (end - begin) + "毫秒");
    }
}

第三步:编写spring配置文件

Spring配置文件:spring-aop-xml.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"
       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">

    <!--纳入spring IoC管理-->
    <bean id="userService" class="com.byxl8112.spring6.service.UserService"></bean>
    <bean id="timerAspect" class="com.byxl8112.spring6.service.TimerAspect"></bean>

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

</beans>

测试程序:

public class SpringAOPTest {
    @Test
    public void testXml(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.logout();
    }
}

执行结果:

image-20230523113803125

AOP的实际案例:事务处理

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

class 业务类1{
    public void 业务方法1(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
    public void 业务方法2(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
    public void 业务方法3(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
}

class 业务类2{
    public void 业务方法1(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
    public void 业务方法2(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
    public void 业务方法3(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            step1();
            step2();
            step3();
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
}
//......

可以看到,这些业务类中的每一个业务方法都是需要控制事务的,而控制事务的代码又是固定的格式,都是:

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

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

    // 提交事务
    commitTransaction();
}catch(Exception e){
    // 回滚事务
    rollbackTransaction();
}

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

service包下的AccountService.java类:

@Service
public class OrderService {  //目标对象

    //生成订单的业务方法
    public void generate(){   //目标方法
        System.out.println("正在生成订单...");
    }

    //取消订单的业务方法
    public void cancel(){  //目标方法
        System.out.println("订单已取消...");
    }

}

service包下的OrderService.java类:

@Service
public class OrderService {  //目标对象

    //生成订单的业务方法
    public void generate(){   //目标方法
        System.out.println("正在生成订单...");
    }

    //取消订单的业务方法
    public void cancel(){  //目标方法
        System.out.println("订单已取消...");
    }

}

注意,以上两个业务类已经纳入spring bean的管理,因为都添加了@Component注解。

接下来我们给以上两个业务类的4个方法添加事务控制代码,使用AOP来完成:

service包下的TransactionAspect.java类(切面类)

@Aspect
@Component
public class TransactionAspect {

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

你看,这个事务控制代码是不是只需要写一次就行了,并且修改起来也没有成本。编写测试程序:

test包下的AOPRealAppTest.java类

@Test
    public void testTransaction(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

        accountService.transfer();
        accountService.withdraw();
        orderService.generate();
        orderService.cancel();
    }

执行结果:

image-20230524215727687

通过测试可以看到,所有的业务方法都添加了事务控制的代码。

AOP的实际案例:安全日志

需求是这样的:项目开发结束了,已经上线了。运行正常。客户提出了新的需求:凡事在系统中进行修改操作的,删除操作的,新增操作的,都要把这个人记录下来。因为这几个操作是属于危险行为。例如有业务类和业务方法:

biz包下的UserService.java类:

@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("获取用户信息");
    }
}

biz包下的VipService.java类:

@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("获取会员信息");
    }

}

注意:已经添加了@Component注解。

接下来我们使用aop来解决上面的需求:编写一个负责安全的切面类

设置切面类:biz包下的 SecurityLogAspect.java类:

@Component
@Aspect
public class SecurityLogAspect {

    @Pointcut("execution(* com.byxl8112.spring6.biz..save*(..))")
    public void savePointcut(){}

    @Pointcut("execution(* com.byxl8112.spring6.biz..delete*(..))")
    public void deletePointcut(){}

    @Pointcut("execution(* com.byxl8112.spring6.biz..modify*(..))")
    public void modifyPointcut(){}


    //前绕通知,将saveXXX、deleteXXX、modifyXXX 方法设置切面
    @Before("savePointcut() || deletePointcut() || modifyPointcut()")
    public void beforeAdvice(JoinPoint joinPoint){
       //系统时间
       SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
       String nowTime = sdf.format(new Date());

       //输出日志信息
       System.out.println(nowTime + "zhangsan: " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
    }
}

测试程序:

@Test
public void testSecurityLog(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    UserService userService = applicationContext.getBean("userService", UserService.class);
    VipService vipService = applicationContext.getBean("vipService", VipService.class);

    userService.saveUser();
    userService.modifyUser();
    userService.deleteUser();
    userService.getUser();

    vipService.saveVip();
    vipService.modifyVip();
    vipService.deleteVip();
    vipService.getVip();
}

执行结果:

image-20230525081316211

Spring对事务的支持

事务概述

  • 什么是事务

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

    • 第一步:开启事务 (start transaction)
    • 第二步:执行核心业务代码
    • 第三步:提交事务(如果核心业务处理过程中没有出现异常)(commit transaction)
    • 第四步:回滚事务(如果核心业务处理过程中出现异常)(rollback transaction)
  • 事务的四个特性:

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

引入事务场景

以银行账户转账为例学习事务。两个账户act-001和act-002。act-001账户向act-002账户转账10000,必须同时成功,或者同时失败。(一个减成功,一个加成功, 这两条update语句必须同时成功,或同时失败。)

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

采用三层架构搭建:

image.png

依赖如下:

<?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.byxl8112</groupId>
    <artifactId>spring6-013-tx-bank</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>16</source>
                    <target>16</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

    <!--依赖-->
    <dependencies>
        <!--spring context依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.2</version>
        </dependency>
        <!--spring jdbc依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.0</version>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
        <!--德鲁伊连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</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.12</version>
            <scope>test</scope>
        </dependency>
        <!--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>
    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>

第一步:准备数据库表

表结构:

image.png

表数据:

image.png

第二步:创建包结构

com.byxl8112.bank.pojo

com.byxl8112.bank.service

com.byxl8112.bank.service.impl

com.byxl8112.bank.dao

com.byxl8112.bank.dao.impl

第三步:准备pojo类

com.byxl8112.bank.pojo包下的 Account.java类:

public class Account {

    private String actno;
    private Double 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;
    }

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

第四步:编写持久层

DAO中只执行SQL语句,没有任何业务逻辑,也就是说DAO不和业务挂钩

com.byxl8112.bank.dao下的 AccountDac.java类(接口):

public interface AccountDao {

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

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

    /**
     * 保存账户信息
     * @param act
     * @return
     */
    int insert(Account act);

}

dao层中的接口的实现类,执行所有的sql语句

com.byxl8112.bank.dao.impl包下的 AccountDaoImpl.java 类:

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

    @Resource(name = "jdbcTemplate")   // 从spring.xml自动注入配置
    private JdbcTemplate jdbcTemplate;


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

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

    @Override
    public int insert(Account act) {
        String sql = "insert into t_act values(?,?)";
        int count = jdbcTemplate.update(sql, act.getActno(), act.getBalance());
        return count;
    }
}

第五步:编写业务层

com.byxl8112.bank.service包下的 AccountService.java类:

public interface AccountService {

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

    /**
     * 保存账户信息
     * @param act
     */
    void save(Account act);

}

com.byxl8112.service.impl包下的AccountServiceImpl.java类

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

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

    //控制事务,因为在这个方法中要完成所有的转账业务
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED)  //隔离级别:读已提交
    public void transfer(String fromActno, String toActno, double money) {
        //查询转出账户的余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);
        if(fromAct.getBalance() < money){
            throw new RuntimeException("余额不足! ! !");
        }
        //余额不足那就是余额充足
        Account toAct = accountDao.selectByActno(toActno);

        //将内存中两个对象的余额先修改
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);

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

        count += accountDao.update(toAct);

        if(count != 2){
            throw new RuntimeException("转账失败,联系银行!");
        }
    }
}

第六步:编写Spring配置文件

Spring配置文件 spring.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"
       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.byxl8112.bank"/>

    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
        <property name="username" value="root"/>
        <property name="password" value="abc123"/>
    </bean>

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

</beans>

第七步:编写表示层

com.byxl8112.test包下的 BankeTest.java 类:

 @Test
public void testTransfer(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
    try {
        accountService.transfer("act-001", "act-002", 10000);
        System.out.println("转账成功");
    } catch (Exception e) {
        e.printStackTrace();
    }

}

执行结果:

image.png

数据变化:

image.png

模拟异常

com.byxl8112.bank.service.impl包下的 AccountService.java 类

@Service("accountService")
public class AccountServiceImpl implements AccountService{
    
    //注入accountDao
    @Resource(name = "accountDao")
    private AccountDao accountDao;
    
    @Override
    public void transfer(String fromActno, String toActno, double money){
        //查询账户余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);
        if(fromAct.getBalance() < money){
            throw new RuntimeException("账户余额不足");
        }
        //余额充足,开始转账
        Account toAct = accountDao.selectByActno(toActno);
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.sertBalance(toAct.getBalance() + money);
        int count = accountDao.update(fromAct);
        
        //模拟异常(空指针异常)
        String = null;
        s.toString();
        
        count += accountDao.update(toAct);
        if(count != 2){
            throw new RuntimeExcetion("转账失败,请联系银行");
        }
    }
}

执行结果:

image.png

数据库表中数据:

image.png

发现没开启事务控制时:丢了一万。

Spring对事务的支持

Spring实现事务的两种方式

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

Spring事务管理API

Spring对事务的管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装。所以Spring专门针对事务开发了一套API,API的核心如下:

image.png

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

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

如果有在Spring6中使用JdbcTemplate,就要使用DataSourceTransactionManager来管理事务。(Spring内置写好了,可以直接用。)

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

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

Spring配置文件spring.xml:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>
  • 第二步:在Spring配置文件中引入tx命名空间

Spring配置文件spring.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"
       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">
  • 第三步:在spring配置文件中配置 “事务注解驱动器”,开始注解的方式控制事务。

Spring配置文件spring.xml:

<tx:annotation-driven transaction-manager="transactionManager"/>
  • 第四步:在service类上或方法上添加@Transactional注解

service.impl包下的AccountServiceImpl.java类(实现了AccountService接口):

@Service("accountService")
public class AccountServiceImpl implements AccountService{
    
    @Resource(name = "accountDao")
    private AccountDao accountDao;
    
    //控制事务,因为在这个方法中要完成所有转账业务
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED)   //隔离级别: 读已提交
    public void transfer(String fromActno, String toActno, double money){
        
        //1.开启事务: @Transactional(isolation = Isolation.READ_COMMITTED)
        //2.执行核心业务逻辑
        //查询转出账户的余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);
        if(fromAct.getBalance() < money){
            throw new RuntimeExcetion("余额不足!!!");
        }
        //余额充足
        Account toAct = accountDao.selectByActno(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("转账失败,联系银行!");
        }
        
        //3.如果执行业务流程过程中,没有异常,提交事务
        //4.如果执行业务流程过程中,有异常,回滚事务
    }
}

当前数据库表中的数据:

image.png

执行测试程序:

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

执行结果:

image.png

虽然出现异常了,再次查看数据库表中数据:

image.png

通过测试,发现数据没有变化,事务起作用了。

事务属性

事务属性包括哪些

image.png

事务中的重点属性:

  • 事务传播行为
  • 事务隔离级别
  • 事务超时
  • 只读事务
  • 设置出现哪些异常回滚事务
  • 设置出现哪些异常不回滚事务

事务传播行为

什么是事务的传播行为?

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

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

image.png

一共有七种传播行为:

  • REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】 (REQUIRED表示增删改操作时必须添加的事务传播特性)

  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】

  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】

  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】

    REQUIRES_NEW:如果B正常执行,则B中的数据在A提交之前已经完成提交,其他线程已经可见其修改,这就意味着可能有脏数据的产生;同时,如果接下来A的其他逻辑发生了异常,A回滚,但是B已经完成提交,不会回滚了。当然,如果A接下来的逻辑没有相关要求,那就无所谓了

  • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】

  • NEVER:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】

  • NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】

    NESTED:如果B正常执行,此时B中的修改并不会立即提交,而是在A提交时一并提交,如果A下面的逻辑中发生异常,A回滚时,B中的修改也会回滚,就可以避免上述情况的发生

图片.png

在代码中设置事务的传播行为:

@Transactional(propagation = Propagation.REQUIRED)

测试传播行为:

service1:

@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {

    // 这里调用dao的insert方法。
    accountDao.insert(act); // 保存act-003账户

    // 创建账户对象
    Account act2 = new Account("act-004", 1000.0);
    try {
        accountService.save(act2); // 保存act-004账户
    } catch (Exception e) {

    }
    // 继续往后进行我当前1号事务自己的事儿。
}

service2:

@Override
//@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Account act) {
    accountDao.insert(act);
    // 模拟异常
    String s = null;
    s.toString();

    // 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
}

以上这种情况,执行结果是,虽然报了空指针异常,但是数据库中的数据仍然创建了

service2:

@Override
//@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.NESTED)
public void save(Account act) {
    accountDao.insert(act);
    // 模拟异常
    String s = null;
    s.toString();

    // 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
}

以上这种情况,执行结果是,虽然报了空指针异常,但是数据库中的数据仍然创建了

一定要集成Log4j2日志框架,在日志信息中可以看到更加详细的信息。

事务隔离级别

事务隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚。隔音效果越好。

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

  • 脏读:读取到没有提交到数据库的数据,叫做脏读。 (读到了缓存中的数据)

  • 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。

    事务A还没有结束的时候,事务B修改了数据库表,事务A还没结束就读到了修改后的数据。

  • 幻读:读到的数据是假的。

    只要是多事务并发,就一定存在幻读。

    幻读:事务A查询数据后,事务B修改了数据库中的数据,那么事务A之前查询的数据就不是最新的。

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

  • 读未提交:READ_UNCOMMITTED

    • 这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据。
  • 读已提交:READ_COMMITTED

    • 解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。
  • 可重复读:REPEATABLE_READ (MySQL数据库默认的隔离级别)

    • 解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题。
  • 序列化:SERIALIZABLE

    • 解决了幻读问题,事务排队执行。不支持并发。(你进入事务,我就不能进入,我进入你就不能进入)

大家可以通过一个表格来记忆:

隔离级别 脏读 不可重复读 幻读
读未提交
读提交
可重复读
序列化

在Spring代码中设置隔离级别:隔离级别在Spring中以枚举类型存在。

image.png

设置隔离级别:

@Transactional(isolation = Isolation.READ_COMMITTED)

测试事务隔离级别:READ_UNCOMMITTED 和 READ_COMMITTED

怎么测试:一个service负责插入,一个service负责查询。负责插入的service要模拟延迟。

service.impl包下的IsolationService1.java类:

@Service("i1")
public class IsolationService1 {

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

    //1号
    //负责查询
    //读未提交,当前事务可以读取到别的事务没有提交到的数据
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public void getByActno(String actno){
        Account account = accountDao.selectByActno(actno);
        System.out.println("查询到的账户信息:" + account);
    }
}

service.impl包下的IsolationService2.ajva类:

@Service("i2")
public class IsolationService2 {

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

    //2号
    //负责insert插入
    @Transactional
    public void save(Account act) {
        accountDao.insert(act);
        
        //睡眠一会儿
        try {
            Thread.sleep(1000 * 20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试程序:

@Test
public void testIsolation1(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    IsolationService1 i1 = applicationContext.getBean("i1", IsolationService1.class);
    i1.getByActno("act-004");
}

@Test
public void testIsolation2(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
    Account act = new Account("act-004", 1000.0);
    i2.save(act);
}

通过执行结果可以清晰的看出隔离级别不同,执行效果不同。

事务超时

代码如下:

@Transactional(timeout = 10) :设置事务超时时间为10秒,只是执行到最后的DML语句是否超过十秒,最后如果是睡眠则不是DML语句,不包括在这十秒内

@Transactional(timeout = 10)

以上代码表示设置事务的超时时间为10秒。

表示超过10秒如果该事务中所有的DML语句还没有执行完毕的话,最终结果会选择回滚。

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

这里有个坑,事务的超时时间指的是哪段时间?

在当前事务当中,最后一条DML语句执行之前的时间。如果最后一条DML语句后面很有很多业务逻辑,这些业务代码执行的时间不被计入超时时间。 (注意:这个时间只针对的是DML语句,只看执行完最后那个DML语句后的时间是否达到超时时间)

以下代码不会被计入超时时间:

@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
    accountDao.insert(act);
    // 睡眠一会
    try {
        Thread.sleep(1000 * 15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

以下代码超时时间会被计入超时时间:

@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
    // 睡眠一会
    try {
        Thread.sleep(1000 * 15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    accountDao.insert(act);
}

当然,如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的DML语句。 (比如查询语句)

只读事务

代码如下:

@Transactional(readOnly = true)

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

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

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

都没有增删改了,按理说设置这个干啥呀?有啥作用呢?

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

设置哪些异常回滚事务

代码如下:

@Transactional(rollbackFor = RuntimeException.class)

只有发生RuntimeException异常或该异常的子类异常才回滚。

设置哪些异常不回滚事务

代码如下:

@Transactional(noRollbackFor = NullPointerException.class)

表示发生NullPointerException或该异常的子类异常不回滚,其他异常则回滚。

@Transactional(noRollbackFor = NullPointerException.class)  
public void save(Account act){
    accountDao.insert(act);
    
    //空指针异常
    if(1=1){
        throw new NullPointerException();
    }
}

发现没有回滚。

事务的全注解式开发

编写一个类来代替配置文件,代码如下:

Spring6Config.java类:

@Configuration   //代替spring.xml配置文件,在这个类当中完成配置
@ComponentScan("com.byxl8112.bank")  //组件扫描
@EnableTransactionManagement    //开启事务注解
public class Spring6Config {

    // Spring框架,看到这个@Bean注解后,会调用这个被标注的方法,这个方法的返回值是一个java对象,这个java对象会自动纳入IoC容器管理
    // 返回的对象就是Spring容器当中的一个Bean了
    // 并且这个bean的名字是:dataSource
    @Bean(name = "dataSource")
    public DruidDataSource getDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring6");
        dataSource.setUsername("root");
        dataSource.setPassword("abc123");
        return dataSource;
    }

    @Bean(name = "jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean(name = "txManager")
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager txManager = new DataSourceTransactionManager();
        txManager.setDataSource(dataSource);
        return txManager;
    }
}

测试程序:

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

执行结果:

image.png

数据库表中的数据:

image.png

声明事务之XML实现方式

配置步骤:

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

记得添加aspectj的依赖(切面依赖):

pom.xml文件中:

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

Spring配置文件spring.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"
       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.byxl8112.bank"/>

    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
        <property name="username" value="root"/>
        <property name="password" value="abc123"/>
    </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>

    <!--配置通知:具体的增强代码-->
    <!--注意:在通知当中要关联事务管理器 transaction-manager -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!--配置通知的相关属性-->
        <tx:attributes>
            <!--之前所讲的所有的事务属性都可以在以下标签中配置。-->
            <tx:method name="transfer" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <!--可以采用模糊匹配方式 以xxx名字开头的方法-->
            <tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="modify" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="query*" read-only="true"/>  <!--read-only=true 表示只读-->
            <tx:method name="select*" read-only="true"/>
            <tx:method name="get*" read-only="true"/>
        </tx:attributes>
    </tx:advice>

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

将AccouontServiceImpl类上的@Transaction注解删除。

测试程序:

@Test
public void testTransferXml(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
    AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
    try {
        accountService.transfer("act-001", "act-002", 10000);
        System.out.println("转账成功");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

执行结果:

image.png

数据库表中记录:

image.png

通过测试可以看到配置XML已经起作用了。

Spring6整合JUnit5

Spring对JUnit4的支持

准备工作:

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.byxl8112</groupId>
    <artifactId>spring6-015-junit</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>16</source>
                    <target>16</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

    <dependencies>
        <!--spring context依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.2</version>
        </dependency>
        <!--spring对junit支持的依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.0.2</version>
        </dependency>
        <!--junit4测试依赖-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>

bean包下的User.java类:

package com.powernode.spring6.bean;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @author 动力节点
 * @version 1.0
 * @className User
 * @since 1.0
 **/
@Component
public class User {

    @Value("张三")
    private String name;

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User() {
    }

    public User(String name) {
        this.name = name;
    }
}

Spring配置文件spring.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="com.byxl8112.spring6.bean"/>
</beans>

单元测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit4Test {

    @Autowired
    private User user;

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

执行结果:

image.png

Spring提供的方便主要是这几个注解:

@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration("classpath:spring.xml")

在单元测试类上使用这两个注解之后,在单元测试类中的属性上可以使用@Autowired。比较方便。

Spring对JUnit5的支持

引入JUnit5的依赖,Spring对JUnit支持的依赖还是:spring-test,如下:

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.byxl8112</groupId>
    <artifactId>spring6-015-junit</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>16</source>
                    <target>16</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

    <dependencies>
        <!--spring context依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.2</version>
        </dependency>
        <!--spring对junit支持的依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.0.2</version>
        </dependency>
        <!--junit5测试依赖-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>

测试程序:

@ExtendWith(SpringExtension.class)
//@ContextConfiguration(classes = SpringConfig.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit5Test {

    @Autowired
    private User user;

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

在JUnit5当中,可以使用Spring提供的以下两个注解,标注到单元测试类上,这样在类当中就可以使用@Autowired注解了。

@ExtendWith(SpringExtension.class)

@ContextConfiguration("classpath:spring.xml")

Spring6集成MyBatis3.5

实现步骤

  • 第一步:准备数据库表

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

    • spring-context (context会关联引入aop依赖)
    • spring-jdbc (jdbc会关联引入事务的依赖)
    • mysql驱动
    • mybatis
    • mybatis-spring:mybatis提供的与spring框架集成的依赖
    • 德鲁伊连接池
    • junit
  • 第三步:基于三层架构实现,所以提前创建好所有的包

    在Mybatis中习惯将dao写成mapper,并且不需要写实现类,因为Mybatis会通过动态代理机制自动给我们生成实现类。

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

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

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

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

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

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

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

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

      Spring集成Mybatis之后,会将Mybatis的配置文件中的内容转移到Spring配置文件中。所以只要不进行系统级参数的配置,Mybatis配置文件可以省略。

  • 第十步:编写spring.xml配置文件

    • 组件扫描
    • 引入外部的属性文件
    • 数据源
    • SqlSessionFactoryBean配置
      • 注入mybatis核心配置文件路径
      • 指定别名包
      • 注入数据源
    • Mapper扫描配置器
      • 指定扫描的包
    • 事务管理器DataSourceTransactionManager
      • 注入数据源
    • 启用事务注解
      • 注入事务管理器
  • 第十一步:编写测试程序,并添加事务,进行测试

具体实现

  • 第一步:准备数据库表
image.png
  • 第二步:IDEA中创建一个模块,并引入依赖

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.byxl8112</groupId>
    <artifactId>spring6-016-sm</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>16</source>
                    <target>16</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

    <!--依赖-->
    <dependencies>
        <!--spring context依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.2</version>
        </dependency>
        <!--spring jdbc依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.0</version>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
        <!--mybatis依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!--mybatis-spring依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.1</version>
        </dependency>
        <!--德鲁伊连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>
        <!--junit4测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>
  • 第三步:基于三层架构实现,提前创建好所有的包
image-20230527144948543
  • 第四步:编写pojo

pojo包下的Account.java类:

public class Account {

    private String actno;
    private Double 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;
    }

    @Override
    public String toString() {
        return "Account{" +
                "actno='" + actno + '\'' +
                ", balance=" + balance +
                '}';
    }
}
  • 第五步:编写mapper接口

mapper包下的AccountMapper.java接口类:

public interface AccountMapper {

    /**
     * 保存账户
     * @param account
     * @return
     */
    int insert(Account account);

    /**
     * 根据账号删除账户
     * @param actno
     * @return
     */
    int deleteByActno(String actno);

    /**
     * 修改账户
     * @param account
     * @return
     */
    int update(Account account);

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

    /**
     * 获取所有账户
     * @return
     */
    List<Account> selectAll();
}
  • 第六步:编写mapper配置文件

一定要注意,按照下图提示创建这个目录。注意是斜杠不是点儿。在resources目录下新建。并且要和Mapper接口包对应上。

image.png

如果接口叫做AccountMapper,配置文件必须是AccountMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.byxl8112.bank.mapper.AccountMapper">

    <insert id="insert">
        insert into t_act values(#{actno}, #{balance})
    </insert>

    <delete id="deleteByActno">
        delete from t_act where actno = #{actno}
    </delete>

    <update id="update">
        update t_act set balance = #{balance} where actno = #{actno}
    </update>

    <select id="selectByActno" resultType="Account">
        select * from t_act where actno = #{actno}
    </select>

    <select id="selectAll" resultType="Account">
        select * from t_act
    </select>

</mapper>
  • 第七步:编写service接口和service接口实现类

注意编写的service实现类纳入IoC容器管理:

service包下的AccountService.java接口类:

public interface AccountService {

    /**
     * 开户
     * @param account
     * @return
     */
    int save(Account account);

    /**
     * 销户
     * @param actno
     * @return
     */
    int deleteByActno(String actno);

    /**
     * 修改账户
     * @param account
     * @return
     */
    int modify(Account account);

    /**
     * 查询账户
     * @param actno
     * @return
     */
    Account getByActno(String actno);

    /**
     * 获取所有账户
     * @return
     */
    List<Account> getAll();

    /**
     * 转账
     * @param fromActno
     * @param toActno
     * @param money
     */
    void transfer(String fromActno, String toActno, double money);

}

service.impl包下的AccountServiceImpl.java类(AccountService接口的实现类):

@Transactional     //开启事务
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public int save(Account account) {
        return accountMapper.insert(account);
    }

    @Override
    public int deleteByActno(String actno) {
        return accountMapper.deleteByActno(actno);
    }

    @Override
    public int modify(Account account) {
        return accountMapper.update(account);
    }

    @Override
    public Account getByActno(String actno) {
        return accountMapper.selectByActno(actno);
    }

    @Override
    public List<Account> getAll() {
        return accountMapper.selectAll();
    }

    @Override
    public void transfer(String fromActno, String toActno, double money) {
        Account fromAct = accountMapper.selectByActno(fromActno);
        if(fromAct.getBalance() < money){
            throw new RuntimeException("余额不足");
        }
        //余额充足
        Account toAct = accountMapper.selectByActno(toActno);
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);
        int count = accountMapper.update(fromAct);
        count += accountMapper.update(toAct);
        if(count != 2){
            throw new RuntimeException("转账失败");
        }
    }
}

第八步:编写jdbc.properties配置文件(放在类的根路径下)

resources资源包下的jdbc.properties配置文件:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring6
jdbc.username=root
jdbc.password=abc123
  • 第九步:编写mybatis-config.xml配置文件

    放在类的根路径下,只开启日志,其他配置到spring.xml中。

resources资源包下的mybatis-config.xml配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!--帮助我们打印mybatis的日志信息。sql语句等-->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

</configuration>
  • 第十步:编写spring.xml配置文件

    注意:当你在spring.xml文件中直接写标签内容时,IDEA会自动给你添加命名空间

Spring配置文件spring.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"
       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 https://www.springframework.org/schema/context/spring-context.xsd
                        http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd">

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

    <!--引入外部的属性配置文件-->
    <context:property-placeholder location="jdbc.properties"/>

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

    <!--配置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.byxl8112.bank.pojo"/>
    </bean>

    <!--Mapper扫描配置器,主要扫描Mapper接口,生成代理类-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.byxl8112.bank.mapper"/>
    </bean>

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

    <!--启用事务管理器(注解)-->
    <tx:annotation-driven transaction-manager="txManager"/>

</beans>

mapper.xml文件跟mapper接口就是在同一目录下的,所以spring在扫描mapper接口的时候,mapper.xml文件也连同被一起扫描到了。因此在配置 SqlSessionFactoryBean时就不需要指定mapper.xml配置文件的位置了。

image-20230527151318946
  • 第十一步:编写测试程序,并添加事务,进行测试

测试程序:

public class SMTest {

    @Test
    public void testSM(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
        try {
            accountService.transfer("act-001", "act-002", 10000.0);
            System.out.println("转账成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("转账失败");
        }
    }
}

Spring配置文件的import

spring配置文件有多个,并且可以在spring的核心配置文件中使用import进行引入,我们可以将组件扫描单独定义到一个配置文件中,如下:

common.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 https://www.springframework.org/schema/context/spring-context.xsd">

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

</beans>

然后在核心配置文件中引入:

Spring核心配置文件spring.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" 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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--引入其他的spring配置文件-->
    <import resource="common.xml"/>

</beans>

注意:在实际开发中,service单独配置到一个文件中,dao单独配置到一个文件中,然后在核心配置文件中引入,养成好习惯。

Spring中的八大模式

简单工厂模式

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

工厂方法模式

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

单例模式

Spring用的是双重判断加锁的单例模式。请看下面代码,我们之前讲解Bean的循环依赖的时候见过:

image.png

代理模式

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

装饰器模式

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

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

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

Spring根据每次请求的不同,将dataSource属性设置成不同的数据源,以到达切换数据源的目的。

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

观察者模式

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

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

策略模式

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

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

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

模板方法模式

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