Spring6笔记

1. Spring简介

Spring的两个最核心模块

IOC:Inverse of control控制反转。将创建对象的过程交给Spring

AOP:Aspect Oriented Programming,面向切面编程。AOP用来封装多个类的公共行为,将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,减少系统的重复代码,降低模块之间的耦合度。另外,AOP还解决一些系统层面的问题,比如日志、事务、权限等。

Spring Framework的特点

  • 非侵入式:Spring对应用程序本身的结构影响非常小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会破坏原有结构,反而能将组件结构进一步简化。
  • 控制反转Inversion of Control 翻转资源获取方向。把自己创建资源、像环境索取资源变成环境将资源准备好,我们享受资源注入
  • 面向切面编程:Aspect Oriented Programming 在不修改源代码的基础上增强代码功能。
  • 容器:Spring IOC是一个容器,它包含并且管理组件对象的生命周期。组件享受到了容器化的管理,替程序员屏蔽了组件创建过程中的大量细节,极大降低了使用门槛,大幅提高了开发效率
  • 组件化:Spring实现了使用简单的组件配置组合成一个复杂的应用。在Spring中可以使用XML和Java注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊地搭建超大型复杂应用系统。
  • 一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。并且Spring旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在Spring Framework的基础上全部使用Spring来实现。

Spring的核心组成部分

image-20231002100242621 image-20231002100258154

Spring Core(核心容器)

Spring Core提供了IOC,DI,Bean配置装在创建的核心实现。核心概念:Beans、BeanFactory、BeanDefinitions、ApplicationContext。

  • spring-core:IOC和DI的基本实现
  • spring-beans:BeanFactory和Bean的装配管理
  • spring-context:spring context上下文,即IOC容器(ApplicationContext)
  • spring-expression:spring表达式语言

Spring AOP

  • spring-aop:面向切面编程的应用模块,整合ASM,CGLib,JDK Proxy
  • spring-aspects:集成AspectJ,AOP应用框架
  • spring-instrument:动态Class Loading模块

Spring Data Access

  • spring-jdbc:spring对JDBC的封装,用于简化jdbc操作
  • spring-orm:java对象与数据库数据的映射框架
  • spring-oxm:对象与xml文件的映射框架
  • spring-jms: Spring对Java Message Service(java消息服务)的封装,用于服务之间相互通信
  • spring-tx:spring jdbc事务管理

Spring Web

  • spring-web:最基础的web支持,建立于spring-context之上,通过servlet或listener来初始化IOC容器
  • spring-webmvc:实现web mvc
  • spring-websocket:与前端的全双工通信协议
  • spring-webflux:Spring 5.0提供的,用于取代传统java servlet,非阻塞式Reactive Web框架,异步,非阻塞,事件驱动的服务

Spring Message

  • Spring-messaging:spring 4.0提供的,为Spring集成一些基础的报文传送服务

Spring test

  • spring-test:集成测试支持,主要是对junit的封装

2. 入门

2.1 程序开发过程

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

    <!--junit5测试-->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.3.1</version>
    </dependency>
</dependencies>
  1. 创建Java类,定义属性和方法;
  2. 在resources文件夹中创建spring.xml文件。在beans中添加bean,指定id(bean的唯一表示)和class(要创建对象所在类的全路径:package.class_name)
<?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">
    <!--完成user对象创建
    bean 标签
        id属性: 唯一表示
        class属性:要创建对象所在类的全路径(包名.类名)
    -->
    <bean id="user" class="com.atguigu.spring6.User"></bean>
</beans>
  1. 创建测试类
public class TestUser {
    @Test
    public void testUserObject(){
        // 加载spring配置文件,对象创建
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        // 获取创建的对象
        User user = (User) context.getBean("user");
        System.out.println(user);
        //
        user.add();
    }
}

2.2 程序分析

Spring通过反射机制调用无参构造器创建对象

// dom4j解析beans.xml文件,从中获取class属性值,类的全类名
 // 通过反射机制调用无参数构造方法创建对象
 Class clazz = Class.forName("com.atguigu.spring6.bean.HelloWorld");
 //Object obj = clazz.newInstance();
 Object object = clazz.getDeclaredConstructor().newInstance();

创建好的对象存储在Spring容器中,在Spring源码底层就是一个Map集合,存储bean的Map在DefaultListableBeanFactory类中:

private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

Spring容器加载Bean类时,会把这个类的描述信息,以包名+类名的方式存储到beanDefinitionMap中,Map<String, BeanDefinition>其中String是key,默认是类名首字母小写,BeanDefinition,存的是类的定义(描述信息),通常叫BeanDefinition接口为:bean的定义对象。


如何使用反射创建的对象?

  1. 加载bean.xml配置文件
  2. 对xml文件进行解析操作
  3. 获取bean标签属性值:id属性值和class属性值
  4. 使用反射根据类的全路径创建对象

(Spring并不是一开始就创建所有的Bean,而是在需要使用它们之前创建,创建实际受到是否启用延迟加载是否标记为预初始化的影响)

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>

加入日志配置文件,在resoureces文件夹中添加log4j2.xml文件并添加一下配置信息

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <loggers>
        <!--
            level指定日志级别,从低到高的优先级:
                TRACE < DEBUG < INFO < WARN < ERROR < FATAL
                trace:追踪,是最低的日志级别,相当于追踪程序的执行
                debug:调试,一般在开发中,都将其设置为最低的日志级别
                info:信息,输出重要的信息,使用较多
                warn:警告,输出警告的信息
                error:错误,输出错误信息
                fatal:严重错误
        -->
        <root level="DEBUG">
            <appender-ref ref="spring6log"/>
            <appender-ref ref="RollingFile"/>
            <appender-ref ref="log"/>
        </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>

        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
        <File name="log" fileName="d:/spring6_log/test.log" append="false">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </File>

        <!-- 这个会打印出所有的信息,
            每次大小超过size,
            则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,
            作为存档-->
        <RollingFile name="RollingFile" fileName="d:/spring6_log/app.log"
                     filePattern="log/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
            <SizeBasedTriggeringPolicy size="50MB"/>
            <!-- DefaultRolloverStrategy属性如不设置,
            则默认为最多同一文件夹下7个文件,这里设置了20 -->
            <DefaultRolloverStrategy max="20"/>
        </RollingFile>
    </appenders>
</configuration>

运行测试程序


使用日志

在测试类中添加logger(import org.slf4j.Logger; import org.slf4j.LoggerFactory;)

public class TestUser {
    // 创建logger对象
    private Logger logger = LoggerFactory.getLogger(TestUser.class);

    @Test
    public void testUserObject(){
        // 加载spring配置文件,对象创建
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        // 获取创建的对象
        User user = (User) context.getBean("user");
        System.out.println(user);
        //
        user.add();
        //手动写入日志
        logger.info("-------执行调用成功了....");
    }
}

3. 10.02 - IOC容器

任务12

image-20231002134621200

image-20231002134524122

基于XML管理Bean

获取bean

获取bean对象的三种方式、

  • 根据id获取bean
  • 根据类型获取bean
  • 根据id类型获取bean。
  1. 根据bean的唯一标识id标签获取对象
@Test
public void test1(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    Student student = context.getBean("student");
    student.method();
}
  1. 根据类型获取
@Test
public void test2(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    Student student = context.getBean(Student.class);
    student.method();
}
  1. 根据id和类型
@Test
public void test3(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    Student student = context.getBean("student", Student.class);
    student.method();
}

当根据类型获取bean时,要求IOC容器中指定类型的bean只能有一个:

<bean id="helloworldOne" class="com.atguigu.spring6.bean.HelloWorld"></bean>
<bean id="helloworldTwo" class="com.atguigu.spring6.bean.HelloWorld"></bean>

根据类型获取时,会出现异常:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.atguigu.spring6.bean.HelloWorld' available: expected single matching bean but found 2: helloworldOne,helloworldTwo

扩展:如果组件类实现了接口,根据接口类型可以获取bean吗?

可以,前提是bean唯一。

如果一个接口有多个实现类,这些实现类都配置了bean,根据接口类型可以获取bean吗?

不可以。因为bean不唯一。

<!--获取bean 的 user对象创建-->
<bean id="user" class="com.atguigu.spring.iocxml.User"></bean>

<!--  一个接口实现类获取过程  -->
<bean id="UserDaoImpl" class="com.atguigu.spring.iocxml.bean.UserDaoImpl"></bean>
<bean id="PersonDaoImpl" class="com.atguigu.spring.iocxml.bean.PersonDaoImpl"></bean>

在测试类中根据类型获取接口对应bean

public class TestUserDao {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        // 根据类型获取接口对应bean
        UserDao userDao = context.getBean(UserDao.class);
        System.out.println(userDao);
        userDao.run();
    }
}

报错:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.atguigu.spring.iocxml.bean.UserDao' available: expected single matching bean but found 2: UserDaoImpl,PersonDaoImpl

依赖注入

  • set注入;
  • constructor注入;
<!--setter方法注入-->
    <bean id="book" class="com.atguigu.spring.iocxml.di.Book">
        <property name="bname" value="web"></property>
        <property name="author" value="atguigu"></property>
    </bean>

<!--constructor方法注入-->
    <bean id="bookCon" class="com.atguigu.spring.iocxml.di.Book">
        <constructor-arg name="bname" value="spring"></constructor-arg>
        <constructor-arg name="author" value="atguigu"></constructor-arg>
    </bean>

特殊值处理

  • 字面量赋值
  • null值
  • xml实体
  • CDATA节

  1. 字面量赋值

字面量:就是看到的字面上的数据本身,没有引申含义。

<property name="others" value="others"/>
  1. null值
<property name="others">
	<null/>
</property>

注:<property name="null"></property>这种写法所附的值是字符串null

  1. xml实体
<!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 -->
<!-- 解决方案一:使用XML实体来代替 -->
<property name="expression" value="a &lt; b"/>
  1. CDATA节
<property name="others">
    <!-- 解决方案二:使用CDATA节 -->
    <!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 -->
    <!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 -->
    <!-- 所以CDATA节中写什么符号都随意 -->
    <value><![CDATA[a < b]]></value>
</property>

对象类型属性赋值

  1. 引用外部bean
<bean id="department" class="com.atguigu.spring.iocxml.ditest.Department">
    <property name="dname" value="安保部"></property>
</bean>

<bean id="employee" class="com.atguigu.spring.iocxml.ditest.Employee">
    <!-- 注入对象属性 -->
    <property name="department" ref="department"></property> <!-- 错误写法: value="department" -->
    <!-- 普通属性注入 -->
    <property name="ename" value="Tom"></property>
    <property name="age" value="30"></property>
</bean>

image-20231002161645358

  1. 内部bean
<!-- 内部bean注入 -->

<bean id="employee2" class="com.atguigu.spring.iocxml.ditest.Employee">
    <!-- 注入对象属性 -->
    <property name="department">
        <bean id="department2" class="com.atguigu.spring.iocxml.ditest.Department">
            <property name="dname" value="财务部"></property>
        </bean>
    </property>
    <!-- 普通属性注入 -->
    <property name="ename" value="Jerry"></property>
    <property name="age" value="20"></property>
</bean>
  1. 级联属性赋值
<!-- 级联赋值 -->
<bean id="department3" class="com.atguigu.spring.iocxml.ditest.Department">
    <property name="dname" value="技术研发部"></property>
</bean>
<bean id="employee3" class="com.atguigu.spring.iocxml.ditest.Employee">
    <property name="ename" value="Saul"></property>
    <property name="age" value="40"></property>
    <property name="department" ref="department3"></property>
    <property name="department.dname" value="测试部"></property>   <!-- 通过department.dname的方式来进行赋值 -->
</bean>

数组类型属性赋值

<!-- 数组类型属性 -->
<bean id="department" class="com.atguigu.spring.iocxml.ditest.Department"></bean>
<bean id="employee" class="com.atguigu.spring.iocxml.ditest.Employee">
    <!-- 普通属性 -->
    <property name="ename" value="Lucy"></property>
    <property name="age" value="20"></property>
    <!-- 对象属性 -->
    <property name="department" ref="department"></property>
    <!-- 数组类型属性 -->
    <property name="loves">
        <array>
            <value>抽烟</value>
            <value>喝酒</value>
            <value>烫头</value>
        </array>
    </property>
</bean>

集合类型属性赋值

List类型属性赋值

<bean id="department" class="com.atguigu.spring.iocxml.ditest.Department">
    <property name="dname" value="技术部"></property>
    <property name="empList">
        <list>
            <ref bean="employee1"></ref>
            <ref bean="employee2"></ref>
        </list>
    </property>
</bean>

<bean id="employee1" class="com.atguigu.spring.iocxml.ditest.Employee">
    <property name="ename" value="Tom"></property>
    <property name="age" value="20"></property>
</bean>
<bean id="employee2" class="com.atguigu.spring.iocxml.ditest.Employee">
    <property name="ename" value="Jerry"></property>
    <property name="age" value="20"></property>
</bean>

若为Set集合类型属性赋值,只需要将其中的list标签改为set标签即可。

Map类型属性赋值

<bean id="teacher1" class="com.atguigu.spring.iocxml.diMap.Teacher">
    <property name="teacherId" value="1001"></property>
    <property name="teacherName" value="Tom"></property>
</bean>
<bean id="teacher2" class="com.atguigu.spring.iocxml.diMap.Teacher">
    <property name="teacherId" value="1002"></property>
    <property name="teacherName" value="Rose"></property>
</bean>

<bean id="student" class="com.atguigu.spring.iocxml.diMap.Student">
    <property name="id" value="20231001"></property>
    <property name="name" value="Jerry"></property>
    <property name="teacherMap">
        <map>
            <entry>
                <key>
                    <value>10010</value>
                </key>
                <ref bean="teacher1"></ref>
            </entry>
            <entry>
                <key>
                    <value>10086</value>
                </key>
                <ref bean="teacher2"></ref>
            </entry>
        </map>
    </property>
</bean>
//在Student类中的run()输出学生信息和teacherMap信息
public class TestStudent {
    @Test
    public void testStu(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-diMap.xml");
        Student student = context.getBean("student", Student.class);
        student.run();
    }
}
学生编号:20231001 学生姓名Jerry
{10010=Teacher{teacherId='1001', teacherName='Tom'}, 10086=Teacher{teacherId='1002', teacherName='Rose'}}

其中key 10010对应的valueTeacher{teacherId='1001', teacherName='Tom'}key 10086同理。

引用集合类型的bean

  1. 直接在<beans>中添加<util:list><util:map>会报错,需要现在头文件中添加util相关的配置
  2. 分别使用<util:list><util:map>添加list和Map类型属性
  3. 在student的bean中添加list和Map类型的引用
<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">		<!-- 添加 -->
    <!--
    1. 创建三个对象
    2. 注入普通类型的属性
    3. 使用util:类型 定义
    4. 在学生的bean里边引入util: 类型定义的bean,完成list、map属性的注入
     -->
    <bean id="student" class="com.atguigu.spring.iocxml.diMap.Student">
        <property name="id" value="10000"></property>
        <property name="name" value="Lucy"></property>
        <property name="lessonList" ref="lessonList"></property>
        <property name="teacherMap" ref="teacherMap"></property>
    </bean>
	<!-- 注入list和Map类型属性 -->
    <util:list id="lessonList">
        <ref bean="lesson1"></ref>
        <ref bean="lesson2"></ref>
    </util:list>
    <util:map id="teacherMap">
        <entry>
            <key>
                <value>10010</value>
            </key>
            <ref bean="teacher1"></ref>
        </entry>
        <entry>
            <key>
                <value>10086</value>
            </key>
            <ref bean="teacher2"></ref>
        </entry>
    </util:map>

    <bean id="lesson1" class="com.atguigu.spring.iocxml.diMap.Lesson">
        <property name="lessonName" value="java开发"></property>
    </bean>
    <bean id="lesson2" class="com.atguigu.spring.iocxml.diMap.Lesson">
        <property name="lessonName" value="web开发"></property>
    </bean>
    <bean id="teacher1" class="com.atguigu.spring.iocxml.diMap.Teacher">
        <property name="teacherId" value="100"></property>
        <property name="teacherName" value="Rose"></property>
    </bean>
    <bean id="teacher2" class="com.atguigu.spring.iocxml.diMap.Teacher">
        <property name="teacherId" value="200"></property>
        <property name="teacherName" value="Jack"></property>
    </bean>
</beans>

p命名空间

<beans>标签头中添加以下信息:

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

进行p命名空间注入:

<!-- p命名空间注入 -->
<bean id="studentp" class="com.atguigu.spring.iocxml.diMap.Student"
      p:id="10000" p:name="Lucy" p:lessonList-ref="lessonList" p:teacherMap-ref="teacherMap">

</bean>

引入外部属性文件

实现步骤:

  1. 在模块的pom.xml文件中引入数据库相关依赖;
 <!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>

<!-- 数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.15</version>
</dependency>
  1. resources文件夹中创建一个外部的属性文件,properties格式,定义数据信息:用户名、密码、地址等;
jdbc.user=root
jdbc.password=atguigu
jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
jdbc.driver=com.mysql.cj.jdbc.Driver
  1. 引入外部属性文件:创建spring配置文件,引入context命名空间引入属性文件,使用表达式完成注入
  2. 配置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">	<!-- 添加这行 -->
    <!-- 引入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <!-- 完成数据库信息注入 -->
    <!-- 配置bean -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
    </bean>


</beans>

注意:在使用 <context:property-placeholder> 元素加载外包配置文件功能前,首先需要在 XML 配置的一级标签 <beans> 中添加 context 相关的约束。

bean的作用域

在Spring中可以通过配置bean标签中的scope属性来指定bean的作用域范围:

取值 含义 创建对象的时机
singleton 在IOC容器中,这个bean的对象始终为单实例 IOC容器初始化时
prototype 这个bean在IOC容器中有多个实例 获取bean时
<!-- 通过scope属性配置单实例 多实例 -->
<bean id="orders" class="com.atguigu.spring.iocxml.scope.Orders"
      scope="singleton">
</bean>

单实例:bean在IOC容器中只有一个实例,IOC容器初始化时创建对象

public class TestOrders {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-scope.xml");
        Orders orders = context.getBean("orders", Orders.class);
        System.out.println(orders);
        Orders orders1 = context.getBean("orders", Orders.class);
        System.out.println(orders1);
    }
}
com.atguigu.spring.iocxml.scope.Orders@32811494
com.atguigu.spring.iocxml.scope.Orders@32811494		// 地址值相同

多实例:bean在IOC容器中可以有多个实例,getBean()时创建对象

<!-- 通过scope属性配置单实例 多实例 -->
<bean id="orders" class="com.atguigu.spring.iocxml.scope.Orders"
      scope="prototype">
</bean>
public class TestOrders {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-scope.xml");
        Orders orders = context.getBean("orders", Orders.class);
        System.out.println(orders);
        Orders orders1 = context.getBean("orders", Orders.class);
        System.out.println(orders1);
    }
}
com.atguigu.spring.iocxml.scope.Orders@19553973
com.atguigu.spring.iocxml.scope.Orders@7bb6ab3a		// 地址之不同

10.03 - bean的生命周期

  1. bean对象创建(调用无参构造器)
  2. 给bean设置相关属性(set方法)
  3. bean后置处理器(初始化之前执行)
  4. bean对象初始化(调用指定的初始化方法)
  5. bean后置处理器(初始化之前执行)
  6. bean对象创建完成,可以使用了
  7. bean对象进行销毁(配置指定销毁的方法)
  8. IOC容器关闭

创建User类并在控制台打印相关信息:

public class User {
    private String name;
    
    public User(){
        System.out.println("1. 调用无参构造创建bean对象");
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        System.out.println("2. 给bean对象设置属性值");
        this.name = name;
    }
    
    // 初始化方法
    public void initMethod(){
        System.out.println("4. 调用指定方法初始化bean对象");
    }
    // 销毁方法
    public void destoryMethod(){
        System.out.println("7. 调用指定方法销毁bean对象");
    }
}

添加配置信息

    <bean id="user" class="com.atguigu.spring.iocxml.life.User"
          scope="singleton" init-method="initMethod" destroy-method="destoryMethod">
        <property name="name" value="lucy"></property>
    </bean>

创建bean的后置处理器:

bean的后置处理器,会在生命周期的初始化前添加额外的操作,需要实现BeanPostProcessor接口 且 配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中的所有bean都会执行

public class MyBeanPost implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("3. bean后置处理器(初始化之前执行)");
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("5. bean后置处理器(初始化之后执行)");
        return bean;
    }
}

在IOC容器中添加后置处理器

    <bean id="myBeanPost" class="com.atguigu.spring.iocxml.life.MyBeanPost"></bean>

测试:

@Test
public void test1(){
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean-life.xml");
    User user = (User) context.getBean("user");
    System.out.println("6. bean对象创建完成");
    System.out.println(user);
    context.close();    // 销毁bean对象
}

测试结果:

image-20231003174935770

FactoryBean

  1. 创建一个MyFactoryBean类,并实现FactoryBean接口。实现接口中的方法:
public class MyFactoryBean implements FactoryBean<User> {
    @Override
    public User getObject() throws Exception {			// 返回创建的对象
        return new User();
    }

    @Override
    public Class<?> getObjectType() {					// 创建对象的class
        return User.class;
    }

    @Override
    public boolean isSingleton() {						// 对象是否是单例
        return true;									// true 每次获取对象都是同一个对象
    }													// false 每次获取都是新的对象
}
  1. 配置bean文件
    <bean id="user" class="com.atguigu.spring.iocxml.factoryBean.MyFactoryBean"></bean>
  1. 测试
@Test
public void test1(){
    ApplicationContext context = new ClassPathXmlApplicationContext("bean-factorybean.xml");
    User user = (User) context.getBean("user");
    System.out.println(user);
}

基于XML自动装配

自动装配:根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型属性赋值

创建UserController类

public class UserController {
    private UserService userService;

    public UserService getUserService() {
        return userService;
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void addUser(){
        System.out.println("controller 方法执行了....");
        // 用接口new它的实现类,创建它的对象
        userService.addUserService();
    }
}

创建接口UserService

public interface UserService {
    public void addUserService();
}

创建UserService接口的实现类

public class UserServiceImpl implements UserService{
    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

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

    @Override
    public void addUserService() {
        System.out.println("userService方法执行了....");
        //UserDao userDao = new UserDaoImpl();
        //userDao.addUserDao();
        userDao.addUserDao();
    }
}

创建UserDao接口

public interface UserDao {
    public void addUserDao();
}

创建UserDao接口的实现类

public class UserDaoImpl implements UserDao{
    @Override
    public void addUserDao() {
        System.out.println("userDao方法执行了....");
    }
}

配置bean。使用bean标签的autowire属性设置自动装配效果:

  • 根据类型自动装配:根据类型匹配IOC容器中的某个兼容类型的bean为属性自动赋值。

    若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不进行装配,即指为默认值null

    若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException.

    <!-- 根据类型自动装配 -->
    <bean id="userController" class="com.atguigu.spring.iocxml.auto.controller.UserController"
          autowire="byType">
    </bean>
    <bean id="userService" class="com.atguigu.spring.iocxml.auto.service.UserServiceImpl"
          autowire="byType">
    </bean>
    <bean id="userDao" class="com.atguigu.spring.iocxml.auto.dao.UserDaoImpl"></bean>
  • 根据名称自动装配:自动装配属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值。(属性名字要和id名字保持一致)
    <!-- 根据名称自动装配 -->
    <bean id="userController" class="com.atguigu.spring.iocxml.auto.controller.UserController"
          autowire="byName">
    </bean>
    <bean id="userService" class="com.atguigu.spring.iocxml.auto.service.UserServiceImpl"
          autowire="byName">
    </bean>
    <bean id="userDao" class="com.atguigu.spring.iocxml.auto.dao.UserDaoImpl"></bean>

测试

@Test
public void test1(){
    ApplicationContext context = new ClassPathXmlApplicationContext("bean-auto.xml");
    UserController userController = context.getBean("userController", UserController.class);
    userController.addUser();
}

10.04 - 基于注解管理Bean ★

Spring通过注解是按自动装配的步骤如下:

  1. 引入依赖(pom.xml),添加日志

  2. 开启组件扫描。开启<context:component-scan>元素开启Spring Beans的自动扫描功能。开启此功能后,Spring会自动从指定的包及其子包下扫描所有类,如果类上使用@Component注解,就将该类装配到容器中。

    三种扫描方式:

    方式一:最基本的扫描方式

<?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.atguigu.spring6"></context:component-scan>
</beans>

​ 方式二:指定要排除的组件

<context:component-scan base-package="com.atguigu.spring6">
    <!-- context:exclude-filter标签:指定排除规则 -->
    <!-- 
 		type:设置排除或包含的依据
		type="annotation",根据注解排除,expression中设置要排除的注解的全类名
		type="assignable",根据类型排除,expression中设置要排除的类型的全类名
	-->
    <context:exclude-filter type="annotation" 
                            expression="org.springframework.stereotype.Controller"/>
    <!--<context:exclude-filter type="assignable" 			
              				expression="com.atguigu.spring6.controller.UserController"/>-->
</context:component-scan>

​ 方式三:仅扫描指定组件

<context:component-scan base-package="com.atguigu" use-default-filters="false">
    <!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
    <!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
    <!-- 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类 -->
    <!-- 
 		type:设置排除或包含的依据
		type="annotation",根据注解排除,expression中设置要排除的注解的全类名
		type="assignable",根据类型排除,expression中设置要排除的类型的全类名
	-->
    <context:include-filter type="annotation" 			
                            expression="org.springframework.stereotype.Controller"/>
	<!--<context:include-filter type="assignable"
							expression="com.atguigu.spring6.controller.UserController"/>-->
</context:component-scan>
  1. 使用注解定义Bean
注解 说明
@Component 可以作用在应用的任何层次。用于描述Spring中的Bean,是一个泛化的概念,仅仅表示容器中的一个组件。
@Repository 用于将Dao层(数据访问层)的类标识为Spring中的Bean,其功能与@Component相同。
@Service 用于将Service层(业务层)的类标识为Spring中的Bean,其功能与@Component相同。
@Controller 用于将Controller层(控制层)的类标识为Spring中的Bean,,其功能与@Component相同。

案例一 @Autowired注入

@Autowired可以标注在哪些地方:

  • 属性注入
  • set注入
  • 构造方法
  • 形参
  • 只有一个构造函数,无注解
  • @Autowired和@Qualifier注解共同注入

①属性注入

创建UserDao接口

package com.atguigu.spring6.autowire.dao;

public interface UserDao {
    public void add();
}

创建UserDao接口的实现类UserDaoImpl

package com.atguigu.spring6.autowire.dao;

import org.springframework.stereotype.Repository;

@Repository
public class UserDaoImpl implements UserDao {
    @Override
    public void add() {
        System.out.println("dao add()......");
    }
}

创建UserService接口

package com.atguigu.spring6.autowire.service;

public interface UserService {
    public void add();
}

创建UserService接口的实现类

package com.atguigu.spring6.autowire.service;

import com.atguigu.spring6.autowire.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    @Override
    public void add() {
        userDao.add();
        System.out.println("service add()......");
    }
}

创建UserController0类

package com.atguigu.spring6.autowire.controller;

import com.atguigu.spring6.autowire.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;

@Controller
public class UserController0 {
    @Autowired
    private UserService userService;

    public void add(){
        userService.add();
        System.out.println("controller add()......");
    }
}

测试:

package com.atguigu.spring6.autowire;

import com.atguigu.spring6.autowire.controller.UserController0;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestUserController {
    @Test
    public void test1(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        UserController0 bean = context.getBean(UserController0.class);
        bean.add();

    }
}
image-20231004162902931

②set注入

修改UserServiceImpl类

import com.atguigu.spring6.autowire.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    private UserDao userDao;

    @Autowired												// 添加userDao的set方法
    public void setUserDao(UserDao userDao) {				// 并对set方法进行标注
        this.userDao = userDao;								
    }
    @Override
    public void add() {
        userDao.add();
        System.out.println("service add()......");
    }
}

修改UserController0类

import com.atguigu.spring6.autowire.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController0 {
    private UserService userService;
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void add(){
        userService.add();
        System.out.println("controller add()......");
    }
}

测试

③构造方法注入

修改UserServiceImpl

import com.atguigu.spring6.autowire.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    private UserDao userDao;

    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    @Override
    public void add() {
        userDao.add();
        System.out.println("service add()......");
    }
}

修改UserController0

package com.atguigu.spring6.autowire.controller;

import com.atguigu.spring6.autowire.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController0 {
    private UserService userService;

    @Autowired
    public UserController0(UserService userService) {
        this.userService = userService;
    }
    public void add(){
        userService.add();
        System.out.println("controller add()......");
    }
}

④形参上注入

将③中标注在构造方法上的注解添加到构造方法的形参列表之前

package com.atguigu.spring6.autowire.service;

import com.atguigu.spring6.autowire.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service()
public class UserServiceImpl implements UserService {
    private UserDao userDao;
    
    public UserServiceImpl(@Autowired UserDao userDao) {
        this.userDao = userDao;
    }
    @Override
    public void add() {
        userDao.add();
        System.out.println("service add()......");
    }
}
package com.atguigu.spring6.autowire.controller;

import com.atguigu.spring6.autowire.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller()
public class UserController0 {
    private UserService userService;

    public UserController0(@Autowired UserService userService) {
        this.userService = userService;
    }
    public void add(){
        userService.add();
        System.out.println("controller add()......");
    }
}

测试

⑤只有一个构造函数,无注解

将③中的在构造方法上的注解去掉

import com.atguigu.spring6.autowire.dao.UserDao;
import org.springframework.stereotype.Service;

@Service()
public class UserServiceImpl implements UserService {
    private UserDao userDao;
    
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    @Override
    public void add() {
        userDao.add();
        System.out.println("service add()......");
    }
}
package com.atguigu.spring6.autowire.controller;

import com.atguigu.spring6.autowire.service.UserService;
import org.springframework.stereotype.Controller;

@Controller()
public class UserController0 {
    private UserService userService;
    
    public UserController0(UserService userService) {
        this.userService = userService;
    }
    public void add(){
        userService.add();
        System.out.println("controller add()......");
    }
}

测试

⑥使用@Autowired和Qualifier

在dao层添加一个UserDao接口的实现类UserRedisDaoImpl

package com.atguigu.spring6.autowire.dao;

import org.springframework.stereotype.Repository;

@Repository()
public class UserRedisDaoImpl implements UserDao {
    @Override
    public void add() {
        System.out.println("redis dao add()......");
    }
}

此时进行测试,报错提示,UserDao接口类型有两个bean

在UserServiceImpl类中使用@Autowired@Qualifier来进行注入

package com.atguigu.spring6.autowire.service;

import com.atguigu.spring6.autowire.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service()
public class UserServiceImpl implements UserService {
    @Autowired
    @Qualifier("userDaoImpl")
    //@Qualifier("userRedisDaoImpl")
    private UserDao userDao;

    @Override
    public void add() {
        userDao.add();
        System.out.println("service add()......");
    }
}

测试:分别使用连个实现类,结果如下。

image-20231004165533825image-20231004165537929

案例二 @Resource注入

①根据name注入

修改UserDaoImpl实现类,在类上的注解中给它起一个名字

package com.atguigu.spring6.resource.dao;

import org.springframework.stereotype.Repository;

@Repository(value = "myUserDao")
public class UserDaoImpl implements UserDao{
    @Override
    public void add() {
        System.out.println("dao add()......");
    }
}

修改UserServiceImpl类,在类上的注解中给它起一个名字,同时在注入属性时,使用@Resource(name="myUserDao")来进行注入

package com.atguigu.spring6.resource.service;

import com.atguigu.spring6.resource.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

@Service(value = "myUserService")
public class UserServiceImpl implements UserService{

    // 根据名称进行注入
    @Resource(name = "myUserDao")
    private UserDao userDao;
    @Override
    public void add() {
        userDao.add();
        System.out.println("service add()......");
    }
}

修改UserController类,在类上的注解中给它起一个名字,同时在注入属性时,使用@Resource(name="myUserService")来进行注入。

package com.atguigu.spring6.resource.controller;

import com.atguigu.spring6.resource.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;

@Controller("myUserController")		
public class UserController {

     // 根据名称进行注入
    @Resource(name = "myUserService")
    private UserService userService;

    public void add(){
        userService.add();
        System.out.println("controller add()......");
    }
}

测试通过

package com.atguigu.spring6.resource;

import com.atguigu.spring6.resource.controller.UserController;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestUserController {
    @Test
    public void test1(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        UserController bean = context.getBean("myUserController", UserController.class);//指定名字
        bean.add();
    }
}

②name未知时注入

修改UserServiceImpl类

package com.atguigu.spring6.resource.service;

import com.atguigu.spring6.resource.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service(value = "myUserService")
public class UserServiceImpl implements UserService{

    // 不指定名称,就根据属性名称进行注入
    @Resource
    private UserDao myUserDao;		// UserDaoImpl的名字是myUserDao
    @Override
    public void add() {
        myUserDao.add();
        System.out.println("service add()......");
    }
}

修改UserController类

package com.atguigu.spring6.resource.controller;

import com.atguigu.spring6.resource.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;

@Controller("myUserController")
public class UserController {

     // 不指定名称,就根据属性名称进行注入
    @Resource
    private UserService myUserService;		// UserServiceImpl的名字是myUserService

    public void add(){
        myUserService.add();
        System.out.println("controller add()......");
    }
}

测试通过

当通过name找不到的时候,就会启动byType进行注入。byType进行注入时,某种类型的Bean只能有一个!

package com.atguigu.spring6.resource.service;

import com.atguigu.spring6.resource.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service(value = "myUserService")
public class UserServiceImpl implements UserService{

    @Resource
    private UserDao myUserDao1;		// 根据名称注入没找到,通过类型注入,但是UserDao接口有两个实现类
    @Override
    public void add() {
        myUserDao1.add();
        System.out.println("service add()......");
    }
}

③ 根据name进行set注入

先按照名称,如果没有名称或者名称不匹配则使用类型进行匹配,使用类型匹配时,如果有多个实现类,需要使用@Primary指定主要候选者,否则报错NoUniqueBeanDefinitionException

在UserServiceImpl类中添加set方法,并在set方法上添加@Resource(name = "myUserDaoImpl")来指定UserDao接口的其中一个实现类(如果@Resource()中没有指定名字,则必须保证UserDao接口的实现类唯一,否则报错NoUniqueBeanDefinitionException

package com.atguigu.spring6.resource.service;

import com.atguigu.spring6.resource.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

@Service(value = "myUserService")
public class UserServiceImpl implements UserService{
    private UserDao userDao111;

    @Resource(name = "myUserDaoImpl")
    public void setMyUserDao(UserDao myUserDao) {
        this.userDao111 = myUserDao;
    }

    @Override
    public void add() {
        userDao111.add();
        System.out.println("service add()......");
    }
}

修改UserController类

package com.atguigu.spring6.resource.controller;

import com.atguigu.spring6.resource.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;

@Controller("myUserController")
public class UserController {

    private UserService myUserService;

    @Resource
    public void setMyUserService(UserService myUserService) {
        this.myUserService = myUserService;
    }

    public void add(){
        myUserService.add();
        System.out.println("controller add()......");
    }
}

测试通过

全注解开发

不再使用Spring配置文件了,而是写一个配置类来代替配置文件。

package com.atguigu.spring6.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration  // 配置类
@ComponentScan("com.atguigu.spring6") // 开启组件扫描
public class SpringConfig {
}

测试类中使用AnnotationConfigApplicationContext(SpringConfig.class)来加载配置类。

package com.atguigu.spring6.resource;

import com.atguigu.spring6.config.SpringConfig;
import com.atguigu.spring6.resource.controller.UserController;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TestUserControllerAnno {
    @Test
    public void test1(){
        // 加载配置类
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserController bean = context.getBean(UserController.class);
        bean.add();
    }
}

4. 原理 手写IOC

反射机制

Java的反射机制是在运行状态中,对任意一个类,都能知道这个类的所有属性和方法;对于任一对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能成为Java的反射机制。

要想使用反射机制,就必须要获取到一个类的Class对象

对于自定义类

package com.atguigu.reflect;

public class Car {

    private String name;
    private int age;
    private String color;

    public Car() {
    }

    public Car(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }
    private void run(){
        System.out.println("This is a private method run()......");
    }
    
    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 String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    
    @Override
    public String toString() {
        return "Car{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", color='" + color + '\'' +
                '}';
    }
}

测试类

package com.atguigu.reflect;

import org.junit.jupiter.api.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TestCar {

    // 1. 获取Class对象的方式
    @Test
    public void test01() throws Exception {
        // 1.1 通过类名.class
        Class clazz1 = Car.class;
        // 1.2 对象.getClass()
        Class clazz2 = new Car().getClass();
        //1.3 Class.forName("path of class")
        Class clazz3 = Class.forName("com.atguigu.reflect.Car");

        // 实例化
        Car car = (Car) clazz3.getDeclaredConstructor().newInstance();
        System.out.println(car);
    }

    // 2. 获取构造方法
    @Test
    public void test02() throws Exception{
        Class clazz = Car.class;

        // getConstructors() 获取所有public的构造方法
        //Constructor[] constructors = clazz.getConstructors();

        // getDeclaredConstructors() 获取所有构造方法,包括public和private
        Constructor[] constructors = clazz.getDeclaredConstructors();
        for (Constructor c: constructors) {
            System.out.println("method name: " + c.getName() + 
                               "\targs: " + c.getParameterCount());
        }

        // 指定有参数构造器创建对象
        
        // 1. public constructor
        //Constructor c1 = clazz.getConstructor(String.class, int.class, String.class);
        //Car car1 = (Car) c1.newInstance("迪子", 10, "red");
        //System.out.println(car1);

        // 2. private constructor
        Constructor c2 = clazz.getDeclaredConstructor(String.class, int.class, String.class);
        c2.setAccessible(true);
        Car car2 = (Car) c2.newInstance("五菱", 10, "gray");
        System.out.println(car2);
    }

    // 3. 获取类中的属性
    @Test
    public void test03() throws Exception{
        Class clazz = Car.class;
        Car car = (Car) clazz.getDeclaredConstructor().newInstance();
        // 获取所有public 属性
        Field[] fields = clazz.getFields();
        // 获取所有属性
        Field[] fields1 = clazz.getDeclaredFields();
        for (Field field: fields1) {
            if (field.getName().equals("name")){
                field.setAccessible(true);
                field.set(car, "Tesla");
            }
            System.out.println(field.getName());
        }
        System.out.println(car);
    }

    // 4. 获取类中的所有的方法(public private)
    @Test
    public void test04() throws Exception {
        Car car = new Car("BMW", 10, "Black");
        Class clazz = car.getClass();
        // 1. public 方法
        Method[] methods = clazz.getMethods();
        for (Method m :
                methods) {
            //System.out.println(m.getName());
            // 指定某个方法 toString
            if (m.getName().equals("toString")){
                String invoke = (String) m.invoke(car);
                System.out.println("toString method: " + invoke);
            }
        }

        // 2. private 方法
        Method[] methods1 = clazz.getDeclaredMethods();
        for (Method m :
                methods1) {
            if (m.getName().equals("run")){
                m.setAccessible(true);
                m.invoke(car);
            }
        }
    }
}

10.05 - 手写IOC

  1. 搭建子模块guigu.spring
  2. 添加单元测试相关依赖
  3. 分别创建UserDaoUserService接口,并分别添加方法
  4. 分别创建UserDaoUserService接口的实现类,UserDaoImplUserServiceImpl,并是接口中的方法
package com.atguigu.dao.impl;

import com.atguigu.annotation.Bean;
import com.atguigu.dao.UserDao;

@Bean		// 我们自定义的bean注解
public class UserDaoImpl implements UserDao {
    @Override
    public void add() {
        System.out.println("dao add()......");
    }
}


// -------------------------
package com.atguigu.service.Impl;

import com.atguigu.annotation.Bean;
import com.atguigu.annotation.Di;
import com.atguigu.dao.UserDao;
import com.atguigu.service.UserService;

@Bean		// 我们自定义的bean注解
public class UserServiceImpl implements UserService {
    @Di		// 自定义的依赖注入的注解
    private UserDao userDao;

    @Override
    public void add() {
        userDao.add();
        System.out.println("service add().....");
    }
}
  1. 定义注解
package com.atguigu.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)						// 指定能够使用该注解的类型
@Retention(RetentionPolicy.RUNTIME)				// 指定该注解的生命周期
public @interface Bean {
}
package com.atguigu.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)						// 指定能够使用该注解的类型
@Retention(RetentionPolicy.RUNTIME)				// 指定该注解的生命周期
public @interface Di {
}
  1. 定义bean容器接口
package com.atguigu.bean;

public interface ApplicationContext_ {
    Object getBean(Class clazz);
}
  1. 创建bean容器接口的实现类,并分别实现扫描bean对象loadBean()属性注入loadDi()的功能
package com.atguigu.bean;

import com.atguigu.annotation.Bean;
import com.atguigu.annotation.Di;

import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class AnnotationApplicationContext_ implements ApplicationContext_{

    // 创建一个MAP集合 放bean对象
    private Map<Class, Object> beanFactory = new HashMap<>();
    private static String rootPath;

    // 返回创建的对象
    @Override
    public Object getBean(Class clazz) {
        return beanFactory.get(clazz);
    }

    // 创建有参构造,传递包路径 设置包的扫描的规则
    // 当前包及其子包,哪个类有@Bean注解,就把这个类通过反射进行实例化
    public AnnotationApplicationContext_(String basePackage){
    //public static void pathDemo1(String basePackage){
        // 处理 com.atguigu
        try {
            // 1. 将 . 替换称 \
            String packagePath = basePackage.replaceAll("\\.", "\\\\");
            // 2. 获取包的绝对路径
            Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(packagePath);
            while(urls.hasMoreElements()){
                URL url = urls.nextElement();
                String filePath = URLDecoder.decode(url.getFile(), "utf-8");
                System.out.println(filePath);

                // 获取包前面的路径部分,进行字符串截取
                rootPath = filePath.substring(0, filePath.length() - packagePath.length());
                // 包的扫描
                loadBean(new File(filePath));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        loadDi();

    }

    // 包扫描过程,实例化
    private void loadBean(File file) throws Exception{
        // 1. 判断当前是否是文件夹
        if(file.isDirectory()){
            // 2. 获取问价夹里的所有内容
            File[] childrenFiles = file.listFiles();
            // 3. 判断文件夹里面内容为空,直接返回
            if(childrenFiles == null || childrenFiles.length == 0){
                return;
            }
            // 4. 如果不为空,遍历这个文件夹里的所有内容
            for (File child : childrenFiles) {
                // 4.1 遍历得到每个File对象,继续判断,如果还是文件夹,进行递归
                if(child.isDirectory()){
                    loadBean(child);
                }else{
                    // 4.2 遍历得到的File对象不是一个文件夹,则是文件
                    // 4.3 得到包的路径 + 类名称部分(是一个字符串的截取过程)
                    String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1);
                    // 4.4 判断当前文件的类型是否是 .class
                    if (pathWithClass.contains(".class")){
                        // 4.5 如果是 .class类型,把路径中的 \ 替换成 . 并且把 .class去掉
                        // 例如 com.atguigu.service.UserServiceImpl
                        String allName = pathWithClass.replaceAll("\\\\", ".").replace(".class", "");
                        // 4.6 判断类上是否有@Bean注解,有的话进行实例化
                        // 4.6.1 获取类的Class
                        Class<?> clazz = Class.forName(allName);
                        // 4.6.2 判断不是接口
                        if(!clazz.isInterface()){
                            // 4.6.3 获取类上的注解
                            Bean annotation = clazz.getAnnotation(Bean.class);
                            if (annotation != null){
                                // 4.6.4 如果类上的注解不为空,获取其构造器
                                Object instance = clazz.getDeclaredConstructor().newInstance();
                                // 4.7 把对象实例化之后,放到Map集合beanFactory中
                                // 4.7.1 判断当前类是否有接口,如果有让接口类型作为key,如果没有就是自己作为key
                                if (clazz.getInterfaces().length > 0){
                                    beanFactory.put(clazz.getInterfaces()[0], instance);
                                }else {
                                    beanFactory.put(clazz, instance);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // 完成属性注入
    private void loadDi(){
        // 实例化的对象都放在beanFactory的Map集合里面
        // 1. 遍历beanFactory的Map集合
        Set<Map.Entry<Class, Object>> entries = beanFactory.entrySet();
        for (Map.Entry<Class, Object> entry : entries) {
            // 2. 获取Map集合的每个对象(value),每个对象属性获取到
            Object obj = entry.getValue();
            // 2.1 获取对象Class
            Class<?> clazz = obj.getClass();
            // 2.2 获取每个对象的属性
            Field[] declaredFields = clazz.getDeclaredFields();
            // 3. 遍历得到的每个对象属性的数组,得到每个属性
            for (Field field: declaredFields){
                // 4. 判断属性上面是否有@Di的注解
                Di annotation = field.getAnnotation(Di.class);
                if(annotation != null){
                    // 如果私有属性,设置setAccessible为true
                    field.setAccessible(true);
                    // 5. 如果有@Di注解,把对象进行设置(注入)
                    try {
                        field.set(obj, beanFactory.get(field.getType()));   // getType得到类型
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

测试

package com.atguigu;

import com.atguigu.bean.AnnotationApplicationContext_;
import com.atguigu.bean.ApplicationContext_;
import com.atguigu.service.UserService;
import org.junit.jupiter.api.Test;

public class TestUser {
    @Test
    public void test01(){
        ApplicationContext_ context_ = new AnnotationApplicationContext_("com.atguigu");
        UserService userService = (UserService) context_.getBean(UserService.class);
        System.out.println(userService);
        userService.add();
    }
}

测试成功

5. 面向切面 AOP

5.1 场景模拟

声明一个Calculator接口,包含加减乘除的抽象方法,并创建实现类

package com.atguigu.example;

public interface Calculator {
    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);
}

在实现类中的每个方法中添加日志功能。分别在操作数据前,操作数据后记录日志

package com.atguigu.example;

public class CalculatorLogImpl implements Calculator{
    @Override
    public int add(int i, int j) {
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
        int result = i + j;
        System.out.println("方法内部 result = " + result);
        System.out.println("[日志] add 方法结束了,结果是:" + result);
        return result;
    }
    @Override
    public int sub(int i, int j) {
        System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
        int result = i - j;
        System.out.println("方法内部 result = " + result);
        System.out.println("[日志] sub 方法结束了,结果是:" + result);
        return result;
    }
    @Override
    public int mul(int i, int j) {
        System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
        int result = i * j;
        System.out.println("方法内部 result = " + result);
        System.out.println("[日志] mul 方法结束了,结果是:" + result);
        return result;
    }
    @Override
    public int div(int i, int j) {
        System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
        int result = i / j;
        System.out.println("方法内部 result = " + result);
        System.out.println("[日志] div 方法结束了,结果是:" + result);
        return result;
    }
}

上述代码中存在的问题:

  • 对核心业务功能有干扰,程序员在开发核心业务功能时会分散注意力
  • 附加功能分散在各个业务功能方法中,不利于统一维护

解决思路:

  • 解耦。将附加功能从也业务功能代码中抽取出来

技术难点:

  • 要抽取的代码在方法内部,靠以前把父类中的重复代码抽取到父类的方式无法结局,因此引入代理模式。

5.2 代理模式

代理模式,属于结构型模式。它的作用是通过提供一个代理类,让我们在调用方法时,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能集中在一起,同时利于统一维护。

相关术语:

  • 代理:将非核心逻辑剥离出来后,封装这些非核心逻辑的类、对象、方法。
  • 目标:被代理“套用”了非核心逻辑代码的类、对象、方法

5.2.1 静态代理

创建静态代理类:

package com.atguigu.example;

public class CalculatorStaticProxy implements Calculator{
    // 要将被代理目标对象传递进来
    private Calculator target;
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    @Override
    public int add(int i, int j) {
        // 输出日志
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
        // 调用目标对象的方法实现核心业务
        int result = target.add(i, j);
        // 输出日志
        System.out.println("[日志] add 方法结束了,结果是:" + result);
        return result;
    }
}

静态代理虽然实现了解耦,但由于代码都写死了,不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

进一步使用动态代理:将日志功能集中到一个代理类中,奖励啊有任何日志需求,都通过这一个代理类来实现。

5.2.2 动态代理

image-20231005204804004

生产代理对象的工厂类:

package com.atguigu.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class ProxyFactory {
    // 目标对象
    private Object target;
    public ProxyFactory(Object target) {
        this.target = target;
    }

    // 返回代理对象
    public Object getProxy(){
        /**
         * Proxy.newProxyInstance()有三个参数
         * 1. ClassLoader:加载动态生成代理类的类加载器
         * 2. Class[] interfaces:目标对象实现的所有接口类型的Class数组
         * 3. InvocationHandler:设置代理对象实现目标对象方法的过程
         */
        // 1. ClassLoader:加载动态生成代理类的类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        // 2. Class[] interfaces:目标对象实现的所有接口类型的Class数组
        Class<?>[] interfaces = target.getClass().getInterfaces();
        // 3. InvocationHandler:设置代理对象实现目标对象方法的过程
        InvocationHandler invocationHandler = new InvocationHandler() {		// 创建匿名对象并实现接口中的方法
            // 参数列表:代理对象, 需要重写目标对象中的方法, method中的参数
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 方法调用之前输出
                 System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
                // 调用目标方法
                Object result = method.invoke(target, args);
                // 方法调用之后输出
                System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
                return result;
            }
        };
        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}

测试

package com.atguigu.example;

import org.junit.jupiter.api.Test;

public class TestCal {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
        Calculator proxy = (Calculator) proxyFactory.getProxy();
        proxy.add(1, 2);
        proxy.mul(3, 2);
    }
}

5.3 AOP

5.3.1 AOP概述

AOP(Aspect Oriented Programming)是一种设计思想,通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。

5.3.2 相关术语

①横切关注点

分散在各个模块中解决同一问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有多少附加功能就有多少横切关注点。

②通知(增强)

增强。通俗来说就是想要增强的功能,比如安全、日志、事务等。

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法叫做通知方法

  • 前置通知:在被代理的目标方法前执行
  • 后置通知:在被代理的目标方法最终结束后执行
  • 返回通知:在被代理的目标方法成功返回后执行
  • 异常通知:在被代理的目标方法异常结束后执行
  • 环绕通知:使用try-catch-finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
image-20231005205307497
③切面:封装通知方法的类
image-20231005210304726
④目标

别代理的目标对象

⑤代理

向目标对象应用通知之后创建的代理对象。

⑥连接点

各个模块中的可执行方法和切面相交的地方。通俗来讲,就是Spring允许你使用通知的地方。

image-20231005210520988
⑦切入点

定位连接点的方式。

每个类中的方法中都包含了多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)

如果把连接点看作是数据库中的记录,那么切入点就是查询记录的SQL语句

Spring的AOP技术可以通过切入点定位到特定的连接点。通俗说就是要实际去增强的方法

切入点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。

5.3.3 作用

  • 简化代码:将方法中固定位置的重复代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内举性。
  • 代码增强:把特定的功能封装到切面类中,看哪里需要就用到哪里,被套用了切面逻辑的方法就被切面给增强了。

5.4 基于注解的AOP

image-20231005211444939 image-20231005211509611
  • 动态代理分为:JDK动态代理,CGLib动态代理
  • 当目标类有接口时,可以使用JDK动态代理或CGLib动态代理;没有接口时只能使用CGLib动态代理
  • JDK动态代理动态生成的代理类会在com.sum.proxy包下,类名为$proxy1,和目标类实现相同的接口(类名为$proxy1
  • CGLib动态代理动态生成的代理类火鹤目标在相同的包下,会继承目标类
  • 动态代理(InvocationHandler):JDK原生的实现方式。需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口
  • CGLib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。
  • AspectJ:是AOP思想的一种体现。本质是静态代理,将代理类逻辑织入被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入机。Spring只是借用了AspectJ中的注解。

5.4.2 准备工作

①添加依赖

在IOC所需依赖基础上添加aop依赖和aspect依赖

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

②准备被代理的目标资源

接口:

package com.atguigu.annotationAOP;

public interface Calculator {

    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);

}

实现类:

package com.atguigu.annotationAOP;

import org.springframework.stereotype.Component;

@Component
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        System.out.println("方法内部 result = " + result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        System.out.println("方法内部 result = " + result);
        //int a = 1 / 0;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    @Override
    public int div(int i, int j) {
        int result = i / j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
}

5.4.3 创建切面类并配置

package com.atguigu.annotationAOP;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Arrays;

// 切面类
@Component
@Aspect // 表示它是一个切面类
public class LogAspect {
    // 设置切入点 和 通知类型

    // 切入点表达式
    // execution( 访问修饰符 增强方法返回类型 方法所在类的全类名.方法名(args ... )  )

    // 前置 @Before(value = "切入点表达式配置切入点")
    //@Before(value = "execution(public int com.atguigu.annotationAOP.CalculatorImpl.add(int, int))")
    //@Before(value = "execution(* com.atguigu.annotationAOP.*.*(..))")
    //@Before(value = "com.atguigu.annotationAOP.LogAspect.pointCut()")          // 重用切入点表达式
    @Before(value = "pointCut()")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName + ", 参数:"+args);
    }
    // 后置 @After()
    @After(value = "execution(* com.atguigu.annotationAOP.*.*(..))")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->后置通知,方法名:"+methodName);
    }
    // 返回 @AfterReturning
    @AfterReturning(value = "execution(* com.atguigu.annotationAOP.*.*(..))", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名:"+methodName + ", 方法返回结果" + result);
    }
    // 异常 @AfterThrowing 获取到目标方法异常信息
    // 目标方法出现异常,这个通知才会执行(在除法操作方法中添加一行除0操作)
    @AfterThrowing(value = "execution(* com.atguigu.annotationAOP.*.*(..))", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名:"+methodName + ", 异常:" + ex);
    }
    // 环绕 @Around()
    @Around(value = "execution(* com.atguigu.annotationAOP.*.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        Object result = null;
        try{
            System.out.println("环绕通知 == 目标方法之前执行");
            // 调用目标方法
            result = joinPoint.proceed();
            System.out.println("环绕通知 == 目标方法返回值之后执行");
        }catch(Throwable throwable){
            throwable.printStackTrace();
            System.out.println("环绕通知 == 目标方法出现异常时执行");
        }finally {
            System.out.println("环绕通知 == 目标方法执行完毕后执行");
        }
        return result;
    }

    // 重用切入点表达式
    @Pointcut(value = "execution(* com.atguigu.annotationAOP.*.*(..))")
    public void pointCut(){}

}

在Spring的配置文件中进行配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns: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.atguigu.annotationAOP"></context:component-scan>

    <!-- 开启aspectj自动代理,为目标对象生成代理 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

测试

package com.atguigu.annotationAOP;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestCal {
    @Test
    public void testAdd(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        Calculator calculator = context.getBean(Calculator.class);
        calculator.sub(2, 3);
    }
}

5.4.5 切入表达式语法

image-20231005213343915

  • 方法参数列表部分:
    • 使用(int, ...)表示参数列表以一个int类型的参数开头
    • 方法参数列表部分:基本数据类型和对应的包装类是不一样的
  • 方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
    • 例如:execution(public int ..Service.(.., int)) 正确
      例如:execution( int ..Service.(.., int)) 错误

5.4.6 重用切入点表达式

首先声明一个切入点表达式

@Pointcut(value = "execution(* com.atguigu.annotationAOP.*.*(..))")
public void pointCut(){}

在同一个切面中使用

@Before(value = "pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName + ", 参数:"+args);
}

在不同切面中使用需要表明切入点表达式的完整路径

@Before("com.atguigu.aop.CommonPointCut.pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}

5.4.7 获取通知的相关信息

获取连接点信息

获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参。

@Before(value = "pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    // 获取连接点的签名信息
    String methodName = joinPoint.getSignature().getName();
    // 获取目标方法中的实参信息
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName + ", 参数:"+args);
}
获取目标方法的返回值

@AfterReturning中的returning属性,用来将通知方法的某个形参,接受目标方法的返回值

@AfterReturning(value = "execution(* com.atguigu.annotationAOP.*.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->返回通知,方法名:"+methodName + ", 方法返回结果" + result);
}

@AfterThrowing中的throwing属性,用来将通知方法的某个形参,接受目标方法的异常

@AfterThrowing(value = "execution(* com.atguigu.annotationAOP.*.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->异常通知,方法名:"+methodName + ", 异常:" + ex);
}

5.4.8 环绕通知

@Around(value = "execution(* com.atguigu.annotationAOP.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    Object result = null;
    try{
        System.out.println("环绕通知 == 目标方法之前执行");
        // 调用目标方法
        result = joinPoint.proceed();

        System.out.println("环绕通知 == 目标方法返回值之后执行");
    }catch(Throwable throwable){
        throwable.printStackTrace();
        System.out.println("环绕通知 == 目标方法出现异常时执行");
    }finally {
        System.out.println("环绕通知 == 目标方法执行完毕后执行");
    }
    return result;
}

5.4.9 切面的优先级

相同目标方法生同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

5.5 基于XML的AOP

5.5.1 准备工作

参考上一节 基于注解的AOP

5.5.2 实现

配置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.atguigu.xmlAOP"></context:component-scan>
    
    <!-- 配置AOP五种通知类型 -->
    <aop:config>
        <!-- 配置切面类 -->
        <aop:aspect ref="logAspect">
            <!-- 配置切入点 -->
            <aop:pointcut id="pointcut" expression="execution(* com.atguigu.xmlAOP.*.*(..))"/>
            
            <!-- 配置五种通知类型 -->
            <!-- ① 前置通知-->
            <aop:before method="beforeMethod" pointcut-ref="pointcut">
            </aop:before>

            <!-- ② 后置通知-->
            <aop:after method="afterMethod" pointcut-ref="pointcut">
            </aop:after>

            <!-- ③ 返回通知-->
            <aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointcut">
            </aop:after-returning>

            <!-- ④ 异常通知-->
            <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointcut">
            </aop:after-throwing>

            <!-- ⑤ 环绕通知-->
            <aop:around method="aroundMethod" pointcut-ref="pointcut">
            </aop:around>
            
        </aop:aspect>
    </aop:config>
</beans>

6. 单元测试 JUnit

在之前的测试方法中,几乎都能看到以下的两行代码:

ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");
Xxxx xxx = context.getBean(Xxxx.class);

这两行代码的作用是创建Spring容器,最终获取到对象,但是每次测试都需要重复编写。

针对上述问题,我们需要程序能自动帮助我们创建容器。Spring提供了一个运行器,可以读取配置文件(或注解)来创建容器。我们只需要告诉它配置文件位置就可以了。这样一来我们通过Spring整合JUnit可以使程序创建Spring容器了。

6.1 整合JUnit5

① 搭建模块spring-junit

② 引入相关依赖。在父工程的pom.xml文件中添加相关依赖

<!--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.3.1</version>
</dependency>

③ 添加配置文件,bing复制日志文件log4j2.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.atguigu.spring6.junit5"></context:component-scan>
</beans>

④ 添加Java类并添加@Component注解

package com.atguigu.spring6.junit5;

import org.springframework.stereotype.Component;

@Component
public class User {
    public void run(){
        System.out.println("user...");
    }
}

⑤ 测试

package com.atguigu.spring6.junit5;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

//@ExtendWith(SpringExtension.class)					// 方式一:同时使用两个注解
//@ContextConfiguration("classpath:bean.xml")

@SpringJUnitConfig(locations = "classpath:bean.xml")	// 方式二
public class SpringJunitTest5 {
    @Autowired
    private User user;

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

6.2 整合JUnit4

添加依赖

<!-- junit4 测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
</dependency>

测试

package com.atguigu.spring6.junit4;

import com.atguigu.spring6.junit5.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:bean.xml")
public class SpringTestJunit4 {
    @Autowired
    private User user;

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

10.06 - 7. 事务

7.1 JdbcTemplate

Spring框架对JDBC进行封装,使用JdbcTemplate方便实现对数据库操作。

7.1.1 准备工作

① 搭建子模块

搭建子模块:spring-jdbc-tx

② 添加相关依赖

<!--spring jdbc  Spring 持久化层支持jar包-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>6.0.2</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>
<!-- 数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.15</version>
</dependency>

③ 创建jdbc.properties文件

jdbc.user=root
jdbc.password=wang0806
jdbc.url=jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
jdbc.driver=com.mysql.cj.jdbc.Driver

④ 配置Spring的配置文件

bean.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.atguigu.spring6"></context:component-scan>

    <!-- 导入外部属性文件,创建数据源对象 -->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <!-- 配置数据源 -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 创建 JdbcTemplate,并注入数据源 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
</beans>

⑤ 准备数据库和测试表

CREATE DATABASE `spring`;

use `spring`;

CREATE TABLE `t_emp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `sex` varchar(2) DEFAULT NULL COMMENT '性别',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

7.1.2 实现CRUD

package com.atguigu.spring6.jdbc;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import java.util.List;

@SpringJUnitConfig(locations = "classpath:bean.xml")
public class JdbcTemplateTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 添加、修改、删除
    @Test
    public void testInsert(){
        // 添加操作
        // 1. 编写sql语句
        String sql1 = "insert into t_emp values(?, ?, ?, ?);";
        // 2. 调用jdbcTemplate
        // int rows = jdbcTemplate.update(sql1, 1, "Walt", "52", "男");
        // int rows = jdbcTemplate.update(sql1, 2, "Jessie", "22", "男");
        int rows = jdbcTemplate.update(sql1, 3, "Jane", "21", "女");			// 调用jdbcTemplate的update()方法
        System.out.println(rows);
    }
    @Test
    public void testModify(){
        // 修改操作
        String sql = "update t_emp set name=? where id=?;";
        int rows = jdbcTemplate.update(sql, "Jessie Pinkman", "2");
        System.out.println(rows);
    }
    @Test
    public void testDelete(){
        // 删除操作
        String sql = "delete from t_emp where id=?;";
        int rows = jdbcTemplate.update(sql, 3);
        System.out.println(rows);
    }
    
    // 查询 返回对象
    @Test
    public void testSelect(){
        // 查询操作 方法①
        String sql = "select * from t_emp where id=?;";
        Emp empResult = jdbcTemplate.queryForObject(sql,
            (rs, rowNum) -> {
                Emp emp = new Emp();
                emp.setId(rs.getInt("id"));
                emp.setName(rs.getString("name"));
                emp.setAge(rs.getInt("age"));
                emp.setSex(rs.getString("sex"));
                return emp;
            }, 1);
        System.out.println(empResult);

        // 方法②
        String sql1 = "select * from t_emp where id=?;";
        Emp emp = jdbcTemplate.queryForObject(sql1, new BeanPropertyRowMapper<>(Emp.class), 2);
        System.out.println(emp);
    }

    // 查询 返回list集合
    @Test
    public void testSelectList(){
        String sql = "select * from t_emp;";
        List<Emp> empList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class));	// 查询操作
        System.out.println(empList);
    }

    // 查询 返回单个值
    @Test
    public void testSelectValue(){
        String sql = "select count(*) from t_emp;";
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class);	// 
        System.out.println(count);
    }
}

7.2 声明式事务

7.2.1 事务

① 什么是事务

数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位,事务由事务开始与事务结束之间执行的全部数据库操作组成。

② 事务的特性

  • A:原子性(Atomicity)一个事务中的所有操作,要么全部执行,要么全部不执行,不会结束在中间的某个环节。事务的执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就好像这个事务从来没有被执行过一样。
  • C:一致性(Consistency)指一个事务执行之前和执行之后都必须处于一致性状态。如果事务成功完成,那么系统中所有变化将正确地运用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
  • I:隔离性(Isolation)指在并发环境中,当不同事务同时操纵相同的数据时,每个事务都有各自完整的数据空间。由并发事务所作的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
  • D:持久性(Duaability)只要事务成功结束,他对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

7.2.2 编程式事务

事务功能的相关操作全部是通过自己编写代码来实现。

这一方法存在缺陷:

  • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
  • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

7.2.3 声明式事务

既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。

封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。

  • 好处1:提高开发效率
  • 好处2:消除了冗余的代码
  • 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化

7.3 基于注解的声明式事务

7.3.1 准备工作

①添加配置

在xml文件中的beans标签中添加context相关配置,并添加扫描组件。

<!--扫描组件-->
<context:component-scan base-package="com.atguigu.spring6"></context:component-scan>

② 创建表

CREATE TABLE `t_book` (
  `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
  `price` int(11) DEFAULT NULL COMMENT '价格',
  `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
  PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert  into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'呐喊',80,100),(2,'彷徨',10,100);
CREATE TABLE `t_user` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(20) DEFAULT NULL COMMENT '用户名',
  `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert  into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);

③ 创建相关接口和实现类

BookDao接口

package com.atguigu.spring6.tx.dao;

public interface BookDao {
    
    public Integer getPriceByBookId(Integer bookId);

    void updateStock(Integer bookId);

    void updateBalance(Integer userId, Integer price);
}

BookDao接口实现类BookDaoImpl

package com.atguigu.spring6.tx.dao;

@Repository
public class BookDaoImpl implements BookDao{
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sql = "select price from t_book where book_id=?";
        Integer price = jdbcTemplate.queryForObject(sql, Integer.class, bookId);
        return price;
    }

    @Override
    public void updateStock(Integer bookId) {
        String sql = "update t_book set stock=stock-1 where book_id=?";
        jdbcTemplate.update(sql, bookId);
    }

    @Override
    public void updateBalance(Integer userId, Integer price) {
        String sql = "update t_user set balance=balance-? where user_id=?";
        jdbcTemplate.update(sql, price, userId);
    }
}

BookService接口

package com.atguigu.spring6.tx.service;

public interface BookService {
    public void buyBook(Integer id, Integer userId);
}

BookService接口实现类

package com.atguigu.spring6.tx.service;

import java.util.concurrent.TimeUnit;

@Service
public class BookServiceImpl implements BookService{

    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer bookId, Integer userId) {

        Integer priceByBookId = bookDao.getPriceByBookId(bookId);

        bookDao.updateStock(bookId);

        bookDao.updateBalance(userId, priceByBookId);
    }
}

BookController

package com.atguigu.spring6.tx;

import com.atguigu.spring6.tx.controller.BookController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:bean.xml")
public class TestBookTX {
    @Autowired
    private BookController bookController;

    @Test
    public void testBuyBook(){
        bookController.buyBook(1, 1);
    }
}

7.3.2 无事务的情况

① 创建测试类

package com.atguigu.spring6.tx;

@SpringJUnitConfig(locations = "classpath:bean.xml")
public class TestBookTX {
    @Autowired
    private BookController bookController;

    @Test
    public void testBuyBook(){
        bookController.buyBook(1, 1);
    }
}

② 模拟场景

用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额

假设用户id为1的用户,购买id为1的图书

用户余额为50,而图书价格为80

购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段

此时执行sql语句会抛出SQLException

③ 观察结果

因为没有添加事务,图书的库存更新了,但是用户的余额没有更新

显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败

7.3.3 加入事务

① 添加事务配置

在Spring配置文件中引入tx命名空间,并添加相关配置:

<?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.atguigu.spring6"></context:component-scan>

    <!-- 导入外部属性文件,创建数据源对象 -->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <!-- 配置数据源 -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 创建 JdbcTemplate,并注入数据源 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>

    <!--
        开启事务的注解驱动
        通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
    -->
    <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

</beans>

② 添加事务注解

因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理

BookServiceImpl类上添加@Transactional注解

③ 测试

由于使用了Spring的声明式事务,更新库存和更新余额都没有执行(用户余额不足以支付书的费用,且书的库存没有发生变化)

@Transactional注解标识的位置:

  • 方法上,只会影响该方法;
  • 类上,影响该类中的所有方法。

7.3.4 事务属性:只读

对于一个查询操作来说,如果我们把它设置成只读,就能明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作进行优化。

使用方法:添加@Transactional(readOnly = true)到方法上

@Transactional(readOnly = true)
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
}

注:对于增删改操作,设置只读会抛出以下异常:Connection is read-only

org.springframework.dao.TransientDataAccessResourceException: PreparedStatementCallback; SQL [update t_book set stock=stock-1 where book_id=?]; Connection is read-only. Queries leading to data modification are not allowed
org.springframework.dao.TransientDataAccessResourceException: PreparedStatementCallback; SQL [update t_book set stock=stock-1 where book_id=?]; Connection is read-only. Queries leading to data modification are not allowed

7.3.5 事务属性:超时

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

概括来说就是一句话:超时回滚,释放资源。

使用方式:修改BookServiceImpl中的buyBook方法

//超时时间单位秒
@Transactional(timeout = 3)
public void buyBook(Integer bookId, Integer userId) {
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
}

执行过程中抛出以下异常:

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Oct 06 23:13:56 CST 2023

7.3.6 事务属性:回滚策略

声明式事务默认只针对运行时异常回滚,编译时异常不回滚。

可以通过@Transactional中相关属性设置回滚策略:

  • rollbackFor属性:需要设置一个Class类型的对象

  • rollbackForClassName属性:需要设置一个字符串类型的全类名

  • noRollbackFor属性:需要设置一个Class类型的对象

  • rollbackFor属性:需要设置一个字符串类型的全类名

使用方式:

@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    System.out.println(1/0);
}

虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行。

7.3.7 事务属性:隔离级别

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

隔离级别一共有四种:

  • 读未提交:READ UNCOMMITTED

    允许Transaction01读取Transaction02未提交的修改。

  • 读已提交:READ COMMITTED

    要求Transaction01只能读取Transaction02已提交的修改。

  • 可重复读:REPEATABLE READ

    确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。

  • 串行化:SERIALIZABLE

    确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

各个隔离级别解决并发问题的能力见下表:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

各种数据库产品对事务隔离级别的支持程度:

隔离级别 Oracle MySQL
READ UNCOMMITTED ×
READ COMMITTED √(默认)
REPEATABLE READ × √(默认)
SERIALIZABLE

使用方法:

@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

7.3.8 事务属性:传播行为

什么是事务的传播行为?

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

一共有七种传播行为:

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

测试

创建接口:CheckOutService

package com.atguigu.spring6.tx.service;

public interface CheckOutService {
    // 买多本书的方法
    public void checkOut(Integer[] bookIds, Integer userId);
}

创建接口实现类:

package com.atguigu.spring6.tx.service;

@Service
public class CheckOutServiceImpl implements CheckOutService{
    @Autowired
    private BookService bookService;

    @Transactional()
    @Override
    public void checkOut(Integer[] bookIds, Integer userId) {
        for (Integer bookId : bookIds) {
           bookService.buyBook(bookId, userId);
        }
    }
}

在BookController类中添加以下内容:

@Autowired
private CheckOutService checkOutService;

@Transactional()
public void checkOut(Integer[] bookIds, Integer userId){
    checkOutService.checkOut(bookIds, userId);
}

将用户表中的账户余额修改为101,将两本书的价格别改为100和10元

在测试类中进行测试:

@Test
public void testBuyBook1(){
    Integer[] bookIds = {1, 2};
    bookController.checkOut(bookIds, 1);
}

可以通过@Transactional中的propagation属性设置事务传播行为

修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性

@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了

@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。

7.3.9 全注解配置事务

① 添加配置类

package com.atguigu.spring6.tx.config;

import javax.sql.DataSource;

@Configuration
@ComponentScan("com.atguigu.spring6.tx")
@EnableTransactionManagement    //  开启事务管理
public class SpringConfig{

    @Bean
    public DataSource getDateSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("wang0806");
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true");
        return dataSource;
    }

    @Bean(name = "jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return  dataSourceTransactionManager;
    }
}

② 测试

package com.atguigu.spring6.tx;

public class TestAnno {

    @Test
    public void testTxAllAnnotation(){
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookController accountService = (BookController) context.getBean("bookController");
        Integer[] bookIds = {1, 2};
        accountService.checkOut(bookIds, 1);
    }
}

成功购买第一本书,购买第二本书失败。符合我们的预期。

7.4 基于XML的声明式事务

实现步骤:

  • 环境准备;
  • 创建Spring配置文件:
    1. 开启组件扫描:<context:component-scan>
    2. 创建数据源:<context:property-placeholder>引入外部属性文件,添加<bean>并设置<property>来创建数据源对象;
    3. 创建JdbcTemplate对象:注入数据源,指定druidDataSource
    4. 创建事务管理器,注入数据源;
    5. 配置事务通知,设置事务相关属性;
    6. 配置切入点表达式和通知使用的方法。

7.4.1 场景

创建相关接口和实现类,并将Service层中的@Transational注解去掉:

// BookDao接口
package com.atguigu.spring6.xmltx.dao;

public interface BookDao {
    public Integer getPriceByBookId(Integer bookId);

    void updateStock(Integer bookId);

    void updateBalance(Integer userId, Integer price);
}


// BookDao接口 实现类
package com.atguigu.spring6.xmltx.dao;

@Repository
public class BookDaoImpl implements BookDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sql = "select price from t_book where book_id=?";
        Integer price = jdbcTemplate.queryForObject(sql, Integer.class, bookId);
        return price;
    }

    @Override
    public void updateStock(Integer bookId) {
        String sql = "update t_book set stock=stock-1 where book_id=?";
        jdbcTemplate.update(sql, bookId);
    }

    @Override
    public void updateBalance(Integer userId, Integer price) {
        String sql = "update t_user set balance=balance-? where user_id=?";
        jdbcTemplate.update(sql, price, userId);
    }
}

// BookService接口
package com.atguigu.spring6.xmltx.service;

public interface BookService {
    public void buyBook(Integer id, Integer userId);
}

// BookService接口实现类
package com.atguigu.spring6.xmltx.service;

@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer bookId, Integer userId) {

        Integer priceByBookId = bookDao.getPriceByBookId(bookId);

        bookDao.updateStock(bookId);

        bookDao.updateBalance(userId, priceByBookId);
    }
}

// BookController
package com.atguigu.spring6.xmltx.controller;

@Controller
public class BookController {
    @Autowired
    private BookService bookService;

    // 买书的方法: bookId, userId
    public void buyBook(Integer bookId, Integer userId){
        bookService.buyBook(bookId, userId);
    }
}

7.4.2 创建配置文件

bean_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: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 https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- ① 开启组件扫描 -->
    <context:component-scan base-package="com.atguigu.spring6.xmltx"></context:component-scan>

    <!-- ② 数据源对象 -->
    <!-- 引入外部属性文件,创建数据源对象 -->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="username" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!-- ③ JdbcTemplate对象 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>

    <!--④ 创建事务管理器-->
    <bean id="transcationManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>

    <!-- ⑤ 配置事务增强 -->
    <tx:advice id="txAdvice" transaction-manager="transcationManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="update*" read-only="false" propagation="REQUIRED"/>
            <tx:method name="buy*" read-only="false" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    <!-- ⑥ 配置切入点和通知使用的方法 -->
    <!-- 切入点在service层生效,因此在配置事务增强时,需要添加service层中的方法,buy*() -->
    <aop:config>
        <aop:pointcut id="pt" expression="execution(* com.atguigu.spring6.xmltx.service.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
    </aop:config>

</beans>

基于xml实现的声明式事务,必须引入aspactJ的依赖(下面依赖在父工程中已经引入)

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

测试:将用户余额修改为99(图书的价格为100),并修改要购买图书的库存。

package com.atguigu.spring6.xmltx;

@SpringJUnitConfig(locations = "classpath:bean_xml.xml")
public class TestBookTX {
    @Autowired
    private BookController bookController;

    @Test
    public void testBuyBook(){
        bookController.buyBook(1, 1);
    }
}

测试结果:购买图书时,图书的库存以及用户的余额未发生变化(用户余额不足以购买第一本图书)。符合预期结果

org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user set balance=balance-? where user_id=?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`spring`.`t_user`.`balance` - 100)'

10.07 - 8. Resources

8.1 概述

Java的标准java.net.URL类和各种URL前缀的标准处理程序无法满足所有对low-level资源的访问,比如:没有标准化的 URL 实现可用于访问需要从类路径或相对于 ServletContext 获取的资源。并且缺少某些Spring所需要的功能,例如检测某资源是否存在等。而Spring的Resource声明了访问low-level资源的能力。

8.2 Resource接口

Spring的Resource接口位于org.springframework.core.io中。旨在成为一个更强大的接口,用于抽象对低级资源的访问。Resource接口继承了InputStreamSource接口,提供了很多InputStreamSource没有的方法,InputStreamSource接口只有一个方法:

public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
}

Resource接口中的一些重要方法:

  • getInputStream(): 找到并打开资源,返回一个InputStream以从资源中读取。预计每次调用都会返回一个新的InputStream(),调用者有责任关闭每个流;
  • exists():返回一个布尔值,表明某个资源是否以物理形式存在
  • isOpen(): 返回一个布尔值,指示此资源是否具有开放流的句柄。如果为trueInputStream就不能够多次读取,只能够读取一次并且及时关闭以避免内存泄漏。对于所有常规资源实现,返回false,但是InputStreamResource除外。
  • getDescription(): 返回资源的描述,用来输出错误的日志。这通常是完全限定的文件名或资源的实际URL。

其他方法:

  • isReadable(): 表明资源的目录读取是否通过getInputStream()进行读取。
  • isFile(): 表明这个资源是否代表了一个文件系统的文件
  • getURL(): 返回一个URL句柄,如果资源不能够被解析为URL,将抛出IOException
  • getURI(): 返回一个资源的URI句柄
  • getFile(): 返回某个文件,如果资源不能够被解析称为绝对路径,将会抛出FileNotFoundException
  • lastModified(): 资源最后一次修改的时间戳
  • createRelative(): 创建此资源的相关资源
  • getFilename(): 资源的文件名是什么

8.3 Resource的实现类

Resource接口是Spring资源访问策略的抽象,它本身并不提供任何资源访问实现,具体的资源访问由该接口的实现类完成——每个实现类代表一种资源访问策略。Resource一般包括这些实现类:UrlResource, ClassPathResource, FileSystemResource, ServletContextResource, InputStreanResource, ByteArrayResource

8.3.1 UrlResource访问网络资源

UrlResource是Resource接口的一个实现类,用来访问网络资源,支持URL的绝对路径。

  • http:------该前缀用于访问基于HTTP协议的网络资源
  • ftp:------该前缀用于访问基于FTP协议的网络资源
  • file: ------该前缀用于从文件系统中读取资源

案例

创建子模块spring6-resources,配置相关依赖。

分别进行以下测试:

  • 访问网络资源;
  • 从文件系统中读取资源(文件放置在父工程spring6的文件夹下);
package com.atguigu.spring6.resource;

// 演示 UrlResource 访问网络资源
public class UrlResourceDemo {
    public static void main(String[] args) {
        // http 前缀开头
        loadUrlResource("http://www.google.com");

        // file 前缀
        // 文件放置在父工程的根目录下 (spring6文件夹下)
        loadUrlResource("file:atguigu.txt");
    }

    // 访问前缀是http, file
    public static void loadUrlResource(String path){
        try {
            // 1. 创建resource接口的实现类的对象 UrlResource
            UrlResource url = new UrlResource(path);

            // 2. 获取资源相关信息
            System.out.println(url.getFilename());
            System.out.println(url.getURL());
            System.out.println(url.getDescription());
            System.out.println(url.getInputStream().read());

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

8.3.2 ClassPathResource 访问类路径下资源

ClassPathResource用来访问类加载路径下的资源,相对于其他Resource实现类,其主要优势是方便访问类加载路径里的资源,尤其对于Web应用,ClassPathResource可自动搜索位于classes下的资源文件,无需使用绝对路径访问。

案例

创建atguigu.txt,使用ClassPathResource访问

image-20231007164154804
package com.atguigu.spring6.resource;

// 访问类路径下的资源
public class ClassPathDemo {
    public static void loadClassPathResource(String path){
        // 1. 创建ClassPathResource 对象
        ClassPathResource resource = new ClassPathResource(path);

        System.out.println(resource.getFilename());
        System.out.println(resource.getDescription());
        try {
            InputStream inputStream = resource.getInputStream();
            byte[] b = new byte[1024];
            while(inputStream.read(b) != -1){
                System.out.println(new String(b));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        loadClassPathResource("atguigu.txt");
    }
}

ClassPathResource实例可使用ClassPathResource构造器显式地创建,但更多的时候它都是隐式地创建的。当执行Spring的某个方法时,该方法接受一个代表资源路径的字符串参数,当Spring识别该字符串参数中包含classpath:前缀后,系统会自动创建ClassPathResource对象。

8.3.3 FileSystemResource访问文件系统资源

Spring提供的FileSystemResource类用于访问文件系统资源,使用FileSystemResource类来访问文件系统资源并没有太大的优势,因为Java提供的File类也可用于访问文件系统资源。

案例

package com.atguigu.spring6.resource;

// 访问系统中的资源
public class FileSystemResourceDemo {
    public static void main(String[] args) {
        // 绝对路径
        //loadFileResource("d:\\atguigu.txt");
        // 相对路径
        loadFileResource("atguigu.txt");
    }
    public static void loadFileResource(String path){
        FileSystemResource resource = new FileSystemResource(path);
        System.out.println(resource.getFilename());
        System.out.println(resource.getDescription());
        try {
            InputStream inputStream = resource.getInputStream();
            byte[] b = new byte[1024];
            while(inputStream.read(b) != -1){
                System.out.println(new String(b));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
}

FileSystemResource实例可使用FileSystemResource构造器显示地创建,但更多的时候它都是隐式创建。执行Spring的某个方法时,该方法接受一个代表资源路径的字符串参数,当Spring识别该字符串参数中包含file:前缀后,系统将会自动创建FileSystemResource对象。

8.3.4 ServletContextResource

这是ServletContext资源的Resource实现,它解释相关Web应用程序根目录中的相对路径。它始终支持流(stream)访问和URL访问,但只有在扩展Web应用程序存档且资源实际位于文件系统上时才允许java.io.File访问。无论它是在文件系统上扩展还是直接从JAR或其他地方(如数据库)访问,实际上都依赖于Servlet容器。

8.3.5 InputStreamResource

InputStreamResource 是给定的输入流(InputStream)的Resource实现。它的使用场景在没有特定的资源实现的时候使用(感觉和@Component 的适用场景很相似)。与其他Resource实现相比,这是已打开资源的描述符。 因此,它的isOpen()方法返回true。如果需要将资源描述符保留在某处或者需要多次读取流,请不要使用它。

8.3.6 ByteArrayResource

字节数组的Resource实现类。通过给定的数组创建了一个ByteArrayInputStream。它对于从任何给定的字节数组加载内容非常有用,而无需求助于单次使用的InputStreamResource。

8.4 ResourceLoader 接口

8.4.1 ResourceLoader 概述

Spring提供如下两个标志性接口:

  • ResourceLoader:该接口实现类的实例可以获得一个Resource实例
  • ResourceLoaderAware:该接口实现类的实例可以获得一个ResourceLoader的引用

ResourceLoader接口中有如下方法:Resource getResource(String location),该方法仅有这个方法,用于返回一个Resource实例。ApplicationContext的实现类都实现了ResourceLoader接口,因此ApplicationContext可直接获取Resource实例。

image-20231007145055869

案例使用:

  • ClassPathXmlApplicationContext获取Resource实例
  • FileSystemApplicationContext获取Resource实例
package com.atguigu.spring6.resourceLoader;

public class ResourceLoaderDemo {
    @Test
    public void test01(){
        ApplicationContext context = new ClassPathXmlApplicationContext();
        Resource resource = context.getResource("atguigu.txt");
        System.out.println(resource);
    }
    @Test
    public void test02(){
        ApplicationContext context = new FileSystemXmlApplicationContext();
        Resource resource = context.getResource("atguigu.txt");
        System.out.println(resource);
    }
}

8.4.3 ResourceLoader 总结

Spring将采用和ApplicationContext相同的策略来访问资源。也就是说,如果ApplicationContextFileSystemXmlApplicationContextresource就是FileSystemResource实例;如果ApplicationContextClassPathXmlApplicationContextresource就是ClassPathResource实例

当Spring应用需要进行资源访问时,实际上并不需要直接使用Resource实现类,而是调用ResourceLoader实例getResource()方法来获得资源,ReosurceLoader将会负责选择Reosurce实现类,也就是确定具体的资源访问策略,从而将应用程序和具体的资源访问策略分离开来

另外,使用ApplicationContext访问资源时,可通过不同前缀指定强制使用指定的ClassPathResourceFileSystemResource等实现类

@Test
public void test01(){
    ApplicationContext context = new ClassPathXmlApplicationContext();
    Resource resource = context.getResource("classpath:atguigu.txt");
    //Resource resource = context.getResource("file:atguigu.txt");
    //Resource resource = context.getResource("http://localhost:8080/atguigu.txt");
    System.out.println(resource);
}

8.5 ResourceLoaderAware 接口

ResourceLoaderAware接口实现类的实例将获得一个ResourceLoader的引用,ResourceLoaderAware接口也提供了一个setResourceLoader()方法,该方法将由Spring容器负责调用,Spring容器会将一个ResourceLoader对象作为该方法的参数传入。

如果把实现ResourceLoaderAware接口的Bean类部署在Spring容器中,Spring容器会将自身当成ResourceLoader并作为setResourceLoader()方法的参数传入。由于ApplicationContext的实现类都实现了ResourceLoader接口,Spring容器自身完全可作为ResorceLoader使用。

案例演示

创建ResourceLoaderAware接口的实现类

package com.atguigu.spring6.resourceLoaderAware;

public class TestBean implements ResourceLoaderAware {

    private ResourceLoader resourceLoader;
    
    //实现ResourceLoaderAware接口必须实现的方法
	//如果把该Bean部署在Spring容器中,该方法将会有Spring容器负责调用。
	//Spring容器调用该方法时,Spring会将自身作为参数传给该方法。
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
	
    // 返回ResourceLoader对象的应用
    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }
}

创建bean.xml,配置TestBean

<?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="testBean" class="com.atguigu.spring6.resourceLoaderAware.TestBean"></bean>
</beans>

测试

package com.atguigu.spring6.resourceLoaderAware;

public class TestDemo {
    public static void main(String[] args) {
        // Spring容器会将一个ResourceLoader对昂作为该方法的参数传入
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        TestBean testBean = context.getBean("testBean", TestBean.class);
        // 获取ResourceLoader对象
        ResourceLoader resourceLoader = testBean.getResourceLoader();
        // 验证Spring容器是否将自身注入到ResourceLoaderAware Bean种
        System.out.println(context == resourceLoader);
        // 加载其他资源
        Resource resource = resourceLoader.getResource("atguigu.txt");
        System.out.println(resource.getFilename());
        System.out.println(resource.getDescription());
    }
}

8.6 使用Resource作为属性

前面介绍了 Spring 提供的资源访问策略,但这些依赖访问策略要么需要使用 Resource 实现类,要么需要使用 ApplicationContext 来获取资源。实际上,当应用程序中的 Bean 实例需要访问资源时,Spring 有更好的解决方法:直接利用依赖注入。从这个意义上来看,Spring 框架不仅充分利用了策略模式来简化资源访问,而且还将策略模式和 IoC 进行充分地结合,最大程度地简化了 Spring 资源访问。

归纳起来,如果 Bean 实例需要访问资源,有如下两种解决方案:

  • 代码中获取 Resource 实例。
  • 使用依赖注入。

对于第一种方式,当程序获取Resource实例时,总需要提供Resource所在的位置。不管通过FileSystemResource创建实例,还是通过ClassPathResource创建实例,或者通过ApplicationContextgetResource()方法获取实例,都需要提供资源位置。这意味着:资源所在的物理位置将被耦合到代码中,如果资源位置发生变化,则必须改写程序。因此通常建议采用第二种方法,让Spring为Bean实例依赖注入资源。

案例 让Spring为Bean实例依赖注入资源

创建依赖注入类,定义属性和方法:

package com.atguigu.spring6.di;

public class ResourceBean {
    private Resource resource;

    public void setResource(Resource resource) {
        this.resource = resource;
    }

    public Resource getResource() {
        return resource;
    }

    public void parse(){
        System.out.println(resource.getFilename());
        System.out.println(resource.getDescription());
    }
}

创建spring配置文件,配置依赖注入

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

    <bean id="resourceBean" class="com.atguigu.spring6.di.ResourceBean">
        <property name="resource" value="classpath:atguigu.txt"/>
    </bean>

    <bean id="user" class="com.atguigu.spring6.prefix.User"></bean>
</beans>

测试

package com.atguigu.spring6.di;

public class TestBean {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-di.xml");
        ResourceBean resourceBean = (ResourceBean) context.getBean("resourceBean");
        resourceBean.parse();
    }
}

8.7 应用程序上下文和资源路径

8.7.1 概述

不管以怎样的方式创建ApplicationContext实例,都需要为ApplicationContext指定配置文件,Spring允许使用一份或多分XML配置文件。当程序创建ApplicationContext实例时,通常也是以Resource的方式来访问配置文件的,所以ApplicationContext完全支持ClassPathResourceFileSystemResourceServletContextResource等资源访问方式。

ApplicationContext确定资源访问策略通常有两种方法:

(1)使用ApplicationContext实现类指定访问策略。

(2)使用前缀指定访问策略。

8.7.2 ApplicationContext实现类指定访问策略

创建ApplicationContext对象时,通常可以使用如下实现类:

(1)ClassPathXMLApplicationContext: 对应使用ClassPathResource进行资源访问。

(2)FileSystemXmlApplicationContext : 对应使用FileSystemResource进行资源访问。

(3)XmlWebApplicationContext : 对应使用ServletContextResource进行资源访问。

当使用ApplicationContext的不同实现类时,就意味着Spring使用响应的资源访问策略。

8.7.3 使用前缀指定访问策略

classpath前缀使用

package com.atguigu.spring6.prefix;

public class TestDemo {
    public static void main(String[] args) {
        /*
         * 通过搜索文件系统路径下的xml文件创建ApplicationContext,
         * 但通过指定classpath:前缀强制搜索类加载路径
         * classpath:bean.xml
         * */
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:bean*.xml");
        Resource resource = context.getResource("atguigu.txt");
        System.out.println(resource.getDescription());

        User user = context.getBean("user", User.class);
        System.out.println(user);
    }
}

classpath通配符使用

classpath*:前缀提供了加载多个XML配置文件的能力,当使用classpath*:前缀来指定XML配置文件时,系统将搜索类加载路径,找到所有与文件名匹配的文件,分别加载文件中的配置定义,最后合并成一个ApplicationContext

ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:bean.xml");

当使用classpath * :前缀时,Spring将会搜索类加载路径下所有满足该规则的配置文件。

如果不是采用classpath * :前缀,而是改为使用classpath:前缀,Spring则只加载第一个符合条件的XML文件

注意 :

classpath * : 前缀仅对ApplicationContext有效。实际情况是,创建ApplicationContext时,分别访问多个配置文件(通过ClassLoadergetResource()方法实现)。因此,classpath * :前缀不可用于Resource

通配符其他使用方法

一次性加载多个配置文件的方式:指定配置文件时使用*通配符

ApplicationContext context = new ClassPathXmlApplicationContext("classpath:bean*.xml");

Spring允许将classpath*:前缀和*通配符结合使用

ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:bean*.xml");

9 国际化:i18n

由于软件发行可能面向多个国家,对于不同国家的用户,软件显示不同语言的过程就是国际化。通常来讲,软件中的国际化是通过配置文件来实现的,假设要支撑两种语言,那么就需要两个版本的配置文件。

Java国际化

Java种的java.util.Locale用于指定用户所属的语言环境等信息,java.util.ResourceBundle用于查找绑定对应的资源文件。Locale包含了language信息和country信息,Locale创建默认locale对象时所使用的静态方法:

/**
     * This method must be called only for creating the Locale.*
     * constants due to making shortcuts.
     */
private static Locale createConstant(String lang, String country) {
    BaseLocale base = BaseLocale.createInstance(lang, country);
    return getInstance(base, null);
}

配置文件明明规则:basename_language_country.properties

必须遵循以上的命名规则,Java才会识别。其中,basename是必须的,语言和国家是可选的。这里存在一个优先级概念,如果同时提供了messages.properties和messages_zh_CN.propertes两个配置文件,如果提供的locale符合zh_CN,那么优先查找messages_zh_CN.propertes配置文件,如果没查找到,再查找messages.properties配置文件。最后,提示下,所有的配置文件必须放在classpath中,一般放在resources目录下。

案例演示

创建子模块spring6-i18n,引入spring依赖

在resource目录下创建两个配置文件:messages_zh_CN.properties, messages_en_GB.properties

image-20231007203044366

分别在两个文件种添加以下内容:

test = GB test
test = China test

测试:

package com.atguigu.spring6.javai18n;

import java.util.Locale;
import java.util.ResourceBundle;

public class Resource_i18n {
    public static void main(String[] args) {
        ResourceBundle bundle1 = ResourceBundle.getBundle("messages",
                new Locale("zh", "CN"));
        String value1 = bundle1.getString("test");
        System.out.println(value1);

        ResourceBundle bundle2 = ResourceBundle.getBundle("messages",
                new Locale("en", "GB"));
        String value2 = bundle2.getString("test");
        System.out.println(value2);
    }
}

Spring国际化

MessageSource接口

Spring种国际化是通过MessageSource这个接口来实现的

常见实现类:

  • ResourceBundleMessageSource:这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源
  • ReloadableResourceBundleMessageSource:和第一个类功能相似,多了定时刷新功能,在不重启系统的情况下,更新资源的信息
  • StaticMessageSource:它允许通过编程的方式提供国际化信息,

使用Spring6国际化

① 创建资源文件

image-20231007203659962

分别添加以下内容:

www.atguigu.com=welcome {0}, time:{1}
www.atguigu.com=欢迎 {0}, 时间:{1}

将IDEA种的编码方式更换为UTF-8

Settings - Editor - File Encodings - Default encoding for properties files

image-20231007203951609

② 创建Spring配置文件,配置MesageSource

<?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="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>atguigu</value>
            </list>
        </property>

        <property name="defaultEncoding">
            <value>utf-8</value>
        </property>

    </bean>
</beans>

③ 创建测试类

package com.atguigu.spring6.springi18n;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;

public class Resource_i18n {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        
        // 传递动态参数,使用数组形式对应 {0} {1}顺序
        Object[] objects = new Object[]{"atguigu", new Date().toString()};
        
        // www.atguigu.com为资源文件的key值
        // objects是资源文件value值所需要的参数,Locale.UK为
        String message = context.getMessage("www.atguigu.com", objects, Locale.UK);
        System.out.println(message);
    }
}

10 数据校验 Validation

10.1 概述

在开发中,我们经常遇到参数校验的需求,比如用户注册的时候,要校验用户名不能为空、用户名长度不超过20个字符、手机号是合法的手机号格式等等。如果使用普通方式,我们会把校验的代码和真正的业务处理逻辑耦合在一起,而且如果未来要新增一种校验逻辑也需要在修改多个地方。而spring validation允许通过注解的方式来定义对象校验规则,把校验和业务逻辑分离开,让代码编写更加方便。Spring Validation其实就是对Hibernate Validator进一步的封装,方便在Spring中使用。

在Spring中有多种校验的方式:

  • 第一种是通过实现org.springframework.validation.Validator接口,然后在代码中调用这个类
  • 第二种是按照Bean Validation方式来进行校验,即通过注解的方式。
  • 第三种是基于方法实现校验
  • 除此之外,还可以实现自定义校验

10.2 案例一:通过Validator接口实现

① 创建子模块

com.atguigu.validator

② 在子模块种引入相关依赖

<dependencies>
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>7.0.5.Final</version>
    </dependency>

    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>jakarta.el</artifactId>
        <version>4.0.1</version>
    </dependency>
</dependencies>

③ 创建实体类,定义属性和方法

package com.atguigu.validator.one;

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

    public Person(String name, int age) {
        this.name = name;
        this.age = 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;}
}

④ 创建类实现Validatior接口,实现接口方法指定校验规则

package com.atguigu.validator.one;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class PersonValidator implements Validator {
    
    // 表示此校验用在哪个类型上
    @Override
    public boolean supports(Class<?> clazz) {
        return Person.class.equals(clazz);
    }

    // 设置校验规则
    @Override
    public void validate(Object target, Errors errors) {
        // name不能为空
        ValidationUtils.rejectIfEmpty(errors, "name", "name is empty");
        Person person = (Person) target;
        // age 不能小于0,不能大于200
        if (person.getAge() < 0){
            errors.rejectValue("age", "error value < 0");
        }else if (person.getAge() > 200){
            errors.rejectValue("age", "error value too old");
        }
    }
}

上面定义的类,其实就是实现接口中对应的方法,supports()表示此校验用在那个类型上,validate()是设置校验逻辑的地点,其中ValidationUtils,是Spring封装的校验工具类,帮助快速实现校验。

⑤ 使用上述Validator进行测试

package com.atguigu.validator.one;

import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;

public class TestMethod1 {
    public static void main(String[] args) {
        // 创建Person对象
        Person person = new Person("Jane", 26);

        // 创建Person对应的DataBinder
        DataBinder binder = new DataBinder(person);

        // 设置校验
        binder.setValidator(new PersonValidator());

        // 调用方法执行校验
        binder.validate();

        // 输出结果
        BindingResult result = binder.getBindingResult();
        System.out.println(result.getAllErrors());
    }
}

10.3 案例二:Bean Validation注解实现

使用Bean Validation校验方式,就是如何将Bean Validation需要使用的javax.validation.ValidatorFactoryjavax.validation.Validator注入到容器中。spring默认有一个实现类LocalValidatorFactoryBean,它实现了上面Bean Validation中的接口,并且也实现了org.springframework.validation.Validator接口。

① 创建配置类

package com.atguigu.validator.two;

@Configuration
@ComponentScan("com.atguigu.validator.two")
public class ValidatorConfig {

    @Bean
    public LocalValidatorFactoryBean validator(){
        return new LocalValidatorFactoryBean();
    }
}

② 创建实体类,使用直接定义校验规则

package com.atguigu.validator.two;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

public class User {
    @NotNull
    private String name;

    @Min(0)
    @Max(150)
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = 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; }
}

常用注解说明:

  • @NotNull 限制必须不为null
  • @NotEmpty 只作用于字符串类型,字符串不为空,并且长度不为0
  • @NotBlank 只作用于字符串类型,字符串不为空,并且trim()后不为空串
  • @DecimalMax(value) 限制必须为一个不大于指定值的数字
  • @DecimalMin(value) 限制必须为一个不小于指定值的数字
  • @Max(value) 限制必须为一个不大于指定值的数字
  • @Min(value) 限制必须为一个不小于指定值的数字
  • @Pattern(value) 限制必须符合指定的正则表达式
  • @Size(max,min) 限制字符长度必须在min到max之间
  • @Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

③ 使用两种不同的校验器实现

image-20231007224926274

(1)使用jakarta.validation.Validator校验

package com.atguigu.validator.two;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
public class MyValidation_1 {

    @Autowired
    private Validator validator;

    public boolean validatorByUserOne(User user){
        Set<ConstraintViolation<User>> validate = validator.validate(user);
        return validate.isEmpty();
    }
}

(2)使用org.springframework.validation.Validator校验

package com.atguigu.validator.two;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.validation.Validator;

import java.util.List;

@Component
public class MyValidation_2 {
    @Autowired
    private Validator validator;
    public boolean validatorByUserTwo(User user){
        BindException bindException = new BindException(user, user.getName());
        validator.validate(user, bindException);
        List<ObjectError> allErrors = bindException.getAllErrors();
        System.out.println(allErrors);
        return bindException.hasErrors();
    }
}

④ 测试

package com.atguigu.validator.two;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestUser {
    @Test
    public void testValidationOne(){
        ApplicationContext context = new AnnotationConfigApplicationContext(ValidatorConfig.class);
        MyValidation_1 validation1 = context.getBean(MyValidation_1.class);

        User user = new User("Jane", 20);
        boolean b = validation1.validatorByUserOne(user);
        System.out.println(b);
    }

    @Test
    public void testValidationTwo(){
        ApplicationContext context = new AnnotationConfigApplicationContext(ValidatorConfig.class);
        MyValidation_2 validation2 = context.getBean(MyValidation_2.class);

        User user = new User("Jane", 200);
        boolean b = validation2.validatorByUserTwo(user);
        System.out.println(b);
    }
}

10.4 案例三:基于方法实现校验

① 创建配置类:

package com.atguigu.validator.three;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration
@ComponentScan("com.atguigu.validator.three")
public class ValidationConfig {
    @Bean
    public MethodValidationPostProcessor validationPostProcessor(){
        return new MethodValidationPostProcessor();
    }
}

② 创建实体类

package com.atguigu.validator.three;

import com.atguigu.validator.four.CannotBlank;
import jakarta.validation.constraints.*;

public class User {
    @NotNull
    private String name;

    @Min(0)
    @Max(150)
    private int age;

    public User() {
    }

    public User(@NotNull String name, int age, String message, String phone) {
        this.name = name;
        this.age = age;
        this.message = message;
        this.phone = phone;
    }

    @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
    @NotBlank(message = "手机号码不能为空")
    private String phone;

    public String getMessage() { return message; }

    public void setMessage(String message) { this.message = message; }

    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 String getPhone() { return phone; }

    public void setPhone(String phone) { this.phone = phone; }

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

③ 定义Service类,通过注解操作对象

package com.atguigu.validator.three;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

@Component
@Validated
public class MyService {

    public String testMethod(@NotNull @Valid User user){
       return user.toString();
    }
}

④ 测试

package com.atguigu.validator.three;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TestUser {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
        MyService bean = context.getBean(MyService.class);
        User user = new User("Jane", 23, "test a t guigu", "13278922342");
        bean.testMethod(user);
    }
}

10.5 案例四:实现自定义校验

① 自定义校验注解

package com.atguigu.validator.four;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {CannotBlankValidation.class})
public @interface CannotBlank {

    // 默认错误信息
    String message() default "{cannot contains blank!}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        CannotBlank[] value();
    }
}

② 编写校验类

package com.atguigu.validator.four;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class CannotBlankValidation implements ConstraintValidator<CannotBlank, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value != null && value.contains(" ")){
            //获取默认提示信息
            String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
            System.out.println("default message :" + defaultConstraintMessageTemplate);
            //禁用默认提示信息
            context.disableDefaultConstraintViolation();
            //设置提示语
            context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
            return false;
        }
        return false;
    }
}

③ 在com.atguigu.validator.three.User类中添加一个message属性并设置getset方法,在注解上设置@CannotBlank,修改相关的构造方法

@CannotBlank
private String message;

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}

④ 使用com.atguigu.validator.three.TestUser进行测试

package com.atguigu.validator.three;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TestUser {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
        MyService bean = context.getBean(MyService.class);
        User user = new User("Jane", 23, "test a t guigu", "13278922342");
        bean.testMethod(user);
    }
}

11 AOT

11.1 AOT概述

11.1.1 JIT与AOT的区别

JIT和AOT 这个名词是指两种不同的编译方式,这两种编译方式的主要区别在于是否在“运行时”进行编译

(1)JIT, Just-in-time,动态(即时)编译,边运行边编译;

在程序运行时,根据算法计算出热点代码,然后进行 JIT 实时编译,这种方式吞吐量高,有运行时性能加成,可以跑得更快,并可以做到动态生成代码等,但是相对启动速度较慢,并需要一定时间和调用频率才能触发 JIT 的分层机制。JIT 缺点就是编译需要占用运行时资源,会导致进程卡顿。

(2)AOT,Ahead Of Time,指运行前编译,预先编译。

AOT 编译能直接将源代码转化为机器码,内存占用低,启动速度快,可以无需 runtime 运行,直接将 runtime 静态链接至最终的程序中,但是无运行时性能加成,不能根据程序运行情况做进一步的优化,AOT 缺点就是在程序运行前编译会使程序安装的时间增加。

简单来讲:JIT即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而 AOT 编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。

.java -> .class -> (使用jaotc编译工具) -> .so(程序函数库,即编译好的可以供其他程序使用的代码和数据)

image-20231008104309938

(3)AOT的优点

简单来讲,Java 虚拟机加载已经预编译成二进制库,可以直接执行。不必等待及时编译器的预热,减少 Java 应用给人带来“第一次运行慢” 的不良体验。

在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗可以在程序运行初期就达到最高性能,程序启动速度快运行产物只有机器码,打包体积小

(4)AOT的缺点

由于是静态提前编译,不能根据硬件情况或程序运行情况择优选择机器指令序列,理论峰值性能不如JIT没有动态能力,同一份产物不能跨平台运行。

第一种即时编译 (JIT) 是默认模式,Java Hotspot 虚拟机使用它在运行时将字节码转换为机器码。后者提前编译 (AOT)由新颖的 GraalVM 编译器支持,并允许在构建时将字节码直接静态编译为机器码。

现在正处于云原生,降本增效的时代,Java 相比于 Go、Rust 等其他编程语言非常大的弊端就是启动编译和启动进程非常慢,这对于根据实时计算资源,弹性扩缩容的云原生技术相冲突,Spring6 借助 AOT 技术在运行时内存占用低,启动速度快,逐渐的来满足 Java 在云原生时代的需求,对于大规模使用 Java 应用的商业公司可以考虑尽早调研使用 JDK17,通过云原生技术为公司实现降本增效。

Spring6 支持的 AOT 技术,这个 GraalVM 就是底层的支持,Spring 也对 GraalVM 本机映像提供了一流的支持。GraalVM 是一种高性能 JDK,旨在加速用 Java 和其他 JVM 语言编写的应用程序的执行,同时还为 JavaScript、Python 和许多其他流行语言提供运行时。 GraalVM 提供两种运行 Java 应用程序的方法:在 HotSpot JVM 上使用 Graal 即时 (JIT) 编译器或作为提前 (AOT) 编译的本机可执行文件。 GraalVM 的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外语调用成本。GraalVM 向 HotSpot Java 虚拟机添加了一个用 Java 编写的高级即时 (JIT) 优化编译器。

11.1.2 Graalvm

Spring6 支持的 AOT 技术,这个 GraalVM 就是底层的支持,Spring 也对 GraalVM 本机映像提供了一流的支持。GraalVM 是一种高性能 JDK,旨在加速用 Java 和其他 JVM 语言编写的应用程序的执行,同时还为 JavaScript、Python 和许多其他流行语言提供运行时。 GraalVM 提供两种运行 Java 应用程序的方法:在 HotSpot JVM 上使用 Graal 即时 (JIT) 编译器或作为提前 (AOT) 编译的本机可执行文件。 GraalVM 的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外语调用成本。GraalVM 向 HotSpot Java 虚拟机添加了一个用 Java 编写的高级即时 (JIT) 优化编译器。

GraalVM 具有以下特性:

(1)一种高级优化编译器,它生成更快、更精简的代码,需要更少的计算资源

(2)AOT 本机图像编译提前将 Java 应用程序编译为本机二进制文件,立即启动,无需预热即可实现最高性能

(3)Polyglot 编程在单个应用程序中利用流行语言的最佳功能和库,无需额外开销

(4)高级工具在 Java 和多种语言中调试、监视、分析和优化资源消耗

总的来说对云原生的要求不算高短期内可以继续使用 2.7.X 的版本和 JDK8,不过 Spring 官方已经对 Spring6 进行了正式版发布。

11.2 演示Native Image构建过程

11.2.1 GraalVM安装

① 下载GraalVM

进入Github下载:https://github.com/graalvm/graalvm-ce-builds/releases?page=2

image-20231008105543877

② 配置环境变量

image-20231008105648598

image-20231008105703225

image-20231008105738994

使用命令行查看是否安装成功

image-20231008105819269

③ 安装native-image插件

image-20231008110053093

11.2.2 安装C++的编译环境

① 下载Visual Studio安装软件

https://visualstudio.microsoft.com/zh-hans/downloads/

image-20231008110247083

选择使用C++的桌面开发

image-20231008110319243

② 配置环境变量

在用户变量的path中添加Visual Studio的环境变量:

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.37.32822\bin\Hostx64\x64
image-20231008114945225

随后在用户变量中新建INCLUDE,并添加以下路径,结尾要添加分号。随后点击确定

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.37.32822\include;

重新打开INCLUDE分别添加以下内容:

C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\cppwinrt
C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\sahred
C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\ucrt
C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\um
C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\winrt
image-20231008115012562

在用户变量中新建LIB,添加下面的路径,结尾添加分号,随后点击确定

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.37.32822\bin\Hostx64\x64

重新打开LIB,分别添加以下路径

C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\ucrt\x64
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\ucrt_enclave\x64
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\um\x64
image-20231008115034449

③ 验证

打开命令行,输入cl

image-20231008115147826

11.2.3 编写代码,构建Native Image

创建Hello.java文件,添加以下内容:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

复制文件到以下路径

C:\Program Files\Microsoft Visual Studio\2022\Community

Ctrl+S查找,并以管理员身份打开

image-20231008115313408

定位到Hello.java所在的文件夹,查看该文件夹中的文件目录,编译Hello.java,随后使用native-image进行构建

image-20231008120815252

查看构建的文件

image-20231008120848479

执行构建的文件

image-20231008121029827

可以看到这个Hello最终打包产出的二进制文件大小为11M,这是包含了SVM和JDK各种库后的大小,虽然相比C/C++的二进制文件来说体积偏大,但是对比完整JVM来说,可以说是已经是非常小了。相比于使用JVM运行,Native Image的速度要快上不少,cpu占用也更低一些。

从官方提供的各类实验数据也可以看出Native Image对于启动速度和内存占用带来的提升是非常显著的:

image-20231008121248942 image-20231008121300277
posted @ 2023-10-16 21:37  Wang_Peicheng  阅读(24)  评论(0编辑  收藏  举报