Spring6 框架学习

Spring6 框架学习

1 Spring介绍

image-20230311171748148

1.1 简介

2002年,Rod Jahnson 首次推出了 Spring 框架雏形interface21 框架。

2004年3月24日,Spring 框架以 interface21 框架为基础,经过重新设计,发布了1.0正式版。

Spring 是一个开源框架,它由 [Rod Johnson](https://baike.baidu.com/item/Rod Johnson) 创建。它是为了解决企业应用开发的复杂性而创建的。Spring 使用基本的 JavaBean 来完成以前只可能由 EJB 完成的事情。然而,Spring 的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 中受益。

1.2 优点

  • Spring 是一个开源的免费的框架(容器)!
  • Spring 是一个轻量级的、非入侵式的框架!
  • 控制反转(IoC),面向切面编程 (AOP)!
  • 支持事务的处理,对框架整合的支持!
  • 总结一句话: Spring 就是一个轻量级的控制反转(IoC)和面向切面编程(AOP)的框架!

1.3 Maven 导入

<!--https://mvnrepository.com/artifact/org.springframework/spring-webmvc-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.17</version>
</dependency>

1.4 Spring模块组成

image-2097896352

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

    • Spring-core :IOC 和 DI 的基本实现

    • Spring-Beans:BeanFactory 和 Bean 的装配管理(BeanFactory)

    • Spring-context:Spring context 上下文,即 IoC 容器 (AppliactionContext)

    • 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 的事务管理

  • 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:Spring4.0 提供的,为 Spring 集成一些基础的报文传送服务
  • Spring test

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

2 IoC思想 ▲

以前的程序:DAO—>DaoImpl—>Service—>ServiceImpl

当业务需求改变时,需要在 ServiceImpl 中手动改变 DaoImpl 对象。当代码体量十分庞大时,修改一次的代价十分昂贵,此时我们用一个 set 接口:

private UserDao userDao;
public viod setUserDao(UserDao userDao){
    this.userDao = userDao;
}
  • 以前的程序,控制权在程序员手里,现在程序不再具有主动性,而是变成了被动的接受者.系统耦合性大大降低
  • 这就是IOC的原型

2.1 IoC本质

控制反转 IoC(Inversion of Control),是一种设计思想,DI(依赖注入) 是实现 IoC 的一种方法,也有人认为 DI 只是 IoC 的另一种说法。没有 IoC 的程序中 , 我们使用面向对象编程 , 对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。

image-20221231153844424

IoC 是 Spring 框架的核心内容,使用多种方式完美的实现了 IoC,我们可以使用 XML 配置,也可以使用注解,新版本的 Spring 也可以零配置实现 IoC。

Spring 容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从 Ioc 容器中取出需要的对象。

下图显示了 Spring 的工作原理的高级视图。在创建和初始化 ApplicationContext 之后,我们将具有完全配置且可执行的系统或应用程序。

采用 XML 方式配置 Bean 的时候,Bean 的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean 的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。

控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在 Spring 中实现控制反转的是 IoC 容器,其实现方法是依赖注入(Dependency Injection,DI)。

image-20221231154404184

2.2 IoC 在 Spring 中的实现

Spring 的 IoC 容器就是 IoC 思想的一个落地的产品实现。IoC容器中管理的组件也叫做 Bean。在创建 Bean 之前,首先需要创建IoC 容器。Spring 提供了 IoC 容器的两种实现方式:

  • BeanFactory

    这是 IoC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。

  • ApplicationContext

    BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。

ApplicationContext的主要实现类

iamges

类型名简介
ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IoC 容器对象
FileSystemXmlApplicationContext通过文件系统路径读取 XML 格式的配置文件创建 IoC 容器对象
ConfigurableApplicationContextApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力
WebApplicationContext专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中

2.3 使用 xml 管理 Bean

2.3.1 测试准备

编写 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"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="user" class="***.User">  (1.) (2.)
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
        <property name="..." value="..."/>   (3.)
        <property name="..." ref="..."/>   (4.)
    </bean>

    <!-- more bean definitions go here -->

</beans>
  1. id 属性是标识单个 Bean 定义的字符串。
  2. class 属性定义 Bean 的类型并使用完全限定的类名。
  3. value 属性定义基本数据类型
  4. ref 属性引用 Spring 容器中创建好的的对象

获取配置文件

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

获取对象

  1. 根据id获取bena

    User user = (User)context.getBean("user");
    
  2. 根据类型获取Bean(要求同类型只有一个)

    User user = (User)context.getBean(User.class);
    
  3. 根据id和类型获取

    User user = (User)context.getBean("user", User.class);
    
2.3.2 基于 Setter 方法注入
<bean id="..." class="...">
    <!-- collaborators and configuration for this bean go here -->
    <property name="..." value="..."/>   
    <property name="..." ref="..."/> 
</bean>

property 标签使用 Setter 方法进行注入

2.3.3 基于构造器方法注入
  1. 默认使用无参构造来创建对象

  2. 使用有参构造创建对象时有三种方法

    <!-- 第一种根据参数名字设置 -->
    <bean id="userT" class="com.pojo.User">
       <!-- name指参数名 -->
       <constructor-arg name="name" value="test1"/>
    </bean>
    
    <!-- 第二种根据 index 参数下标设置 -->
    <bean id="userT" class="com.pojo.User">
       <!-- index指构造方法 , 下标从0开始 -->
       <constructor-arg index="0" value="test2"/>
    </bean>
    
    <!-- 第三种根据参数类型设置 -->
    <bean id="userT" class="com.pojo.User">
       <constructor-arg type="java.lang.String" value="test3"/>
    </bean>
    
  3. 在xml配置文件加载时容器中的对象就已经初始化完成了

2.3.4 特殊值处理

字面量赋值

什么是字面量?

int a = 10;

声明一个变量 a,初始化为10,此时 a 就不代表字母 a 了,而是作为一个变量的名字。当我们引用 a 的时候,我们实际上拿到的值是 10。

而如果 a 是带引号的:‘a’,那么它现在不是一个变量,它就是代表 a 这个字母本身,这就是字面量。所以字面量没有引申含义,就是我们看到的这个数据本身。

<!-- 使用 value 属性给 Bean 的属性赋值时,Spring 会把 value 属性的值看做字面量 -->
<property name="name" value="张三"/>

null值

<property name="name">
    <null />
</property>

注意:

<property name="name" value="null"></property>

以上写法,为name所赋的值是字符串null

特殊符号

  1. xml 实体

    <!-- 小于号在 XML 文档中用来定义标签的开始,不能随便使用 -->
    <!-- 解决方案一:使用XML实体来代替 -->
    <property name="expression" value="a &lt; b"/>
    
  2. CDATA 节

    <property name="expression">
        <!-- 解决方案二:使用CDATA节 -->
        <!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 -->
        <!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 -->
        <!-- 所以CDATA节中写什么符号都随意 -->
        <value><![CDATA[a < b]]></value>
    </property>
    
2.3.5 对象类型注入
  1. 引用外部 Bean

    <property name="..." ref="外部 Bean 的 id"/> 
    
  2. 内部 Bean

    <property name="...">
        <!-- 在一个 Bean 中再声明一个 Bean 就是内部 Bean -->
        <!-- 内部 Bean 只能用于给属性赋值,不能在外部通过 IoC 容器获取,因此可以省略 id 属性 -->
        <bean class="...">
            <property name="..." value="..."></property>
            <property name="..." value="..."></property>
        </bean>
    </property>
    
  3. 级联属性赋值

    <bean id="..." class="...">
        <property name="..." value="..."></property>
        
        <property name="clazz" ref="..."></property>
        <!-- 级联(通过du)属性赋值--> 
        <property name="clazz.clazzId" value="..."></property>
    </bean>
    
2.3.6 Array、List、Map 类型注入
Array
<bean id="..." class="...">
    <property name="..." value="..."></property>
    <property name="array">
        <array>
            <value>抽烟</value>
            <value>喝酒</value>
            <value>烫头</value>
        </array>
</bean>
List
<bean id="listOne" class="..."></bean>
<bean id="listTwo" class="..."></bean>

<bean id="..." class="...">
    <property name="..." value="..."></property>
    <property name="List">
        <list>
            <ref bean="listOne"></ref>
            <ref bean="listTwo"></ref>
        </list>
    </property>
</bean>
Map
<bean id="mapOne" class="..."></bean>
<bean id="mapTwo" class="..."></bean>

<bean id="..." class="...">
    <property name="..." value="..."></property>
    <property name="Map">
        <map>
            <entry>
                <key>
                    <value>10010</value>
                </key>
                <ref bean="mapOne"></ref>
            </entry>
            <entry>
                <key>
                    <value>10086</value>
                </key>
                <ref bean="mapTwo"></ref>
            </entry>
        </map>
    </property>
</bean>
命名空间引入
  1. 引入相应的命名空间

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

    <bean id="listOne" class="..."></bean>
    <bean id="listTwo" class="..."></bean>
    <bean id="mapOne" class="..."></bean>
    <bean id="mapTwo" class="..."></bean>
    
    <!--list 集合类型的 Bean-->
    <util:list id="List">
        <ref bean="listOne"></ref>
        <ref bean="listTwo"></ref>
    </util:list>
    <!--map 集合类型的 Bean-->
    <util:map id="Map">
        <entry>
            <key>
                <value>10010</value>
            </key>
            <ref bean="mapOne"></ref>
        </entry>
        <entry>
            <key>
                <value>10086</value>
            </key>
            <ref bean="mapTwo"></ref>
        </entry>
    </util:map>
    <bean id="..." class="...">
        <property name="..." value="..."></property>
        <!-- ref 属性:引用 IoC 容器中某个 Bean 的 id,将所对应的 Bean 为属性赋值 -->
        <property name="list" ref="List"></property>
        <property name="map" ref="Map">
    </bean>
    
2.3.7 p命名空间

引入p命名空间

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:p="http://www.springframework.org/schema/p">

引入p命名空间后,可以通过以下方式为Bean的各个属性赋值

<bean id="..." class="..."
    p:id="..." p:name="..." p:clazz-ref="clazzOne" p:teacherMap-ref="teacherMap"></bean>
2.3.8 引入外部属性文件

以数据库配置为例

  1. 创建 jdbc.properties 文件

    jdbc.user=root
    jdbc.password=root
    jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
    jdbc.driver=com.mysql.cj.jdbc.Driver
    
  2. 在 xml 中引入属性文件

    引入 context 名称空间

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">
    
    </beans>
    
    <!-- 引入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    

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

  3. 配置Bean

    <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>
    
2.3.9 Bean 的作用域

在 Spring 中可以通过配置 bean 标签的 scope 属性来指定 Bean 的作用域范围,各取值含义参加下表:

取值含义创建对象的时机
singleton(默认)在 IoC 容器中,这个 Bean 的对象始终为单实例IoC 容器初始化时
prototype这个 Bean 在 IoC 容器中为多实例获取 Bean 时

如果是在 WebApplicationContext 环境下还会有另外几个作用域(但不常用):

取值含义
request在一个请求范围内有效
session在一个会话范围内有效
2.3.10 Bean 生命周期
具体的生命周期过程
  1. Bean 对象创建(调用无参构造器)
  2. 给 Bean 对象设置属性
  3. Bean 的后置处理器(初始化之前)
  4. Bean 对象初始化(需在配置 Bean 时指定初始化方法)
  5. Bean 的后置处理器(初始化之后)
  6. Bean 对象就绪可以使用
  7. Bean 对象销毁(需在配置 Bean 时指定销毁方法)
测试类 User
public class User {

    private Integer id;

    public User() {
        System.out.println("生命周期:1、创建对象");
    }

    public void setId(Integer id) {
        System.out.println("生命周期:2、依赖注入");
        this.id = id;
    }

    public void initMethod(){
        System.out.println("生命周期:4、初始化");
    }

    public void destroyMethod(){
        System.out.println("生命周期:7、销毁");
    }
}
配置 Bean
<!-- 使用 init-method 属性指定初始化方法 -->
<!-- 使用 destroy-method 属性指定销毁方法 -->
<bean class="User" scope="prototype" init-method="initMethod" destroy-method="destroyMethod">
    <property name="id" value="1001"></property>
</bean>
测试
@Test
public void testLife(){
    ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-lifecycle.xml");
    User bean = ac.getBean(User.class);
    System.out.println("生命周期:6、通过 IoC 容器获取 Bean 并使用");
    ac.close();//生命周期:7、销毁
}
Bean 的后置处理器

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

创建 Bean 的后置处理器

public class MyBeanProcessor implements BeanPostProcessor {
    
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("生命周期:3、后置处理器,初始化之前");
        return bean;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("生命周期:5、后置处理器,初始化之后");
        return bean;
    }
}

在 IoC 容器中配置后置处理器

<!-- Bean 的后置处理器要放入 IoC 容器才能生效 -->
<bean id="myBeanProcessor" class="com.oizys.spring6.process.MyBeanProcessor"/>
2.3.11 FactoryBean

FactoryBean 是 Spring 提供的一种整合第三方框架的常用机制。和普通的 Bean 不同,配置一个 FactoryBean 类型的 Bean,在获取 Bean 的时候得到的并不是 class 属性中配置的这个类的对象,而是 getObject() 方法的返回值。通过这种机制,Spring 可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。

2.3.12 基于 xml 自动装配

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

2.4 使用注解管理 Bean

2.4.1 开启组件扫描

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

<context:component-scan base-package="要扫描的路径">
</context:component-scan>

情况二:指定要排除的组件

<context:component-scan base-package="要扫描的路径">
    <!-- 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="***.UserController"/>-->
</context:component-scan>

情况三:仅扫描指定组件

<context:component-scan base-package="要扫描的路径" 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="***.UserController"/>-->
</context:component-scan>
2.4.2 使用注解定义 Bean

Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean

注解说明
@Component该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。
@Repository该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Service该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Controller该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
2.4.3 依赖注入
@Autowired 注入

单独使用 @Autowired 注解,默认根据类型装配

查看源码:

package org.springframework.beans.factory.annotation;

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

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

源码中有两处需要注意:

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

注入方式

场景一:属性注入

在 UserServiceImpl 类中注入 UserDao

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

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

场景二:set注入

在 UserServiceImpl 类中注入 UserDao

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

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

场景三:构造方法注入

在 UserServiceImpl 类中注入 UserDao

@Service
public class UserServiceImpl implements UserService {
	@Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
}

场景四:形参上注入

在 UserServiceImpl 类中注入 UserDao

@Service
public class UserServiceImpl implements UserService {
    public UserServiceImpl(@Autowired UserDao userDao) {
        this.userDao = userDao;
    }
}

场景五:@Autowired注解和@Qualifier注解联合

在 UserServiceImpl 类中注入 UserDao

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    @Qualifier("userDaoImpl") // 指定bean的名字
    private UserDao userDao;
}

总结

  • @Autowired 注解可以出现在:属性上、构造方法上、构造方法的参数上、setter 方法上。
  • @Autowired 注解默认根据类型注入。如果要根据名称注入的话,需要配合 @Qualifier 注解一起使用。
@Resource 注入(推荐)

@Resource 注解也可以完成属性注入。那它和 @Autowired 注解有什么区别?

  • @Resource 注解是 JDK 扩展包中的,也就是说属于 JDK 的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250 标准中制定的注解类型。 JSR 是 Java 规范提案。)而

    @Autowired 注解是 Spring 框架自己的。

  • @Resource 注解默认根据名称装配 byName,未指定 name 时,用属性名作为 name。通过 name 找不到的话会自动启动通过类型 byType 装配。

    @Autowired 注解默认根据类型装配 byType ,如果想根据名称装配,需要配合 @Qualifier 注解一起用。

  • @Resource 注解用在属性上、setter 方法上。

    @Autowired 注解用在属性上、setter 方法上、构造方法上、构造方法参数上。

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

<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>2.1.1</version>
</dependency>

3 AOP思想 ▲

3.1 AOP 的概念

3.1.1 为什么要用 AOP
使用日志时缺陷

针对带日志功能的实现类,我们发现有如下缺陷:

  • 对核心业务功能有干扰,导致在开发核心业务功能时分散了精力
  • 附加功能分散在各个业务功能方法中,不利于统一维护
解决思路

解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

困难

解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术即 AOP 技术。

3.1.2 概念

面向切面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。在OOP中,模块化的关键单元是类,而在 AOP 中,模块化的单元是切面。切面支持跨多个类型和对象的横切关注点(例如事务管理)的模块化。它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

3.1.3 相关术语
横切关注点

分散在每个各个模块中解决同一样的问题,如用户验证、日志管理、事务处理、数据缓存等从每个方法中抽取出来的同一类非核心业务,都属于横切关注点。

在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

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

images

通知(增强)

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

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

  • 前置通知:在被代理的目标方法执行
  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝
  • 异常通知:在被代理的目标方法异常结束后执行(死于非命
  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论
  • 环绕通知:使用 try…catch…finally 结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

各种通知的执行顺序:

  • Spring 版本5.3.x以前:
    • 前置通知
    • 目标操作
    • 后置通知
    • 返回通知或异常通知
  • Spring 版本5.3.x以后:
    • 前置通知
    • 目标操作
    • 返回通知或异常通知
    • 后置通知

images

切面

封装通知方法的类。

images

目标

被代理的目标对象。

代理

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

连接点

这也是一个纯逻辑概念,不是语法定义的。

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。通俗说,就是 Spring 允许你使用通知的地方

images

切入点

定位连接点的方式。

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

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

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

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

3.2 代理模式

3.2.1 概念

代理模式就是通过一个代理类来帮助我们调用目标方法,原始使用:

image-20230418162557749

使用代理之后:

image-20230418163049577

其核心逻辑是:让不属于目标方法核心逻辑的代码从目标方法中解耦出来放在代理类中,让附加功能集中在一起方便管理。

3.2.2 静态代理的实现方式

写一个静态代理类实现目标方法的接口,通过构造方法进行注入,然后在用注入的对象直接调用核心方法,在调用的前后写附加功能。

静态代理对应目标对象确实实现了解耦,但是代码都写死了,不具备灵活性,如果有其他的目标对象,则需要写一个新的静态代理来实现解耦,且相同的附加功能难以复用

3.2.3 动态代理的实现方式日志输出
动态代理类的实现
public class ProxyFactory {

    private Object target;

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

    public Object getProxy(){

        /**
         * newProxyInstance():创建一个代理实例
         * 其中有三个参数:
         * 1、classLoader:加载动态生成的代理类的类加载器
         * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
         * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
         */
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                 * proxy:被代理的对象
                 * method:代理对象需要实现的方法,即其中需要重写的方法
                 * args:method所对应方法的参数
                 */
                Object result = null;
                try {
                    System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
                    result = method.invoke(target, args);
                    System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
                } finally {
                    System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
                }
                return result;
            }
        };

        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}
使用
ProxyFactory proxyFactory = new ProxyFactory(new 目标方法对象);
目标方法对象 proxy = (目标方法对象)proxyFactory.getProxy();
proxy.目标方法();

3.3 Spring 实现 AOP 的两种方式

3.3.1 JDK 和 CGLib 动态代理的区别

image-20230419102008969

  1. 当目标类有接口的情况使用 JDK 动态代理和 CGLib 动态代理,没有接口时只能使用 CGLib 动态代理。根本原因是通过 JDK 动态代理生成的类已经继承了 Proxy 类,所以无法再使用继承的方式去对类实现代理。
  2. JDK 动态代理本质上是实现了被代理对象的接口,而 CGLib 本质上是继承了被代理对象,覆盖其中的方法。
  3. 因为 CGLib 使用继承实现,所以 CGLib 无法代理被 final 修饰的方法或类。
  4. 在调用代理方法上,JDK 是通过反射机制调用,CGLib 是通过 FastClass 机制直接调用。FastClass 简单的理解,就是使用 index 作为入参,可以直接定位到要调用的方法直接进行调用。
  5. 在性能上,JDK1.7 之前,由于使用了 FastClass 机制,CGLib 在执行效率上比 JDK 快,但是随着 JDK 动态代理的不断优化,从 JDK 1.7 开始,JDK 动态代理已经明显比 CGLib 更快了。

3.4 准备工作

3.4.1 创建目标资源

以计算器和日志功能为列

接口
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);
    
}
实现类
@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);
    
        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;
    }
}
3.5.3 创建切面类
// @Component 注解保证这个切面类能够放入 IoC 容器
@Component
public class LogAspect {
    
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }

    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->后置通知,方法名:"+methodName);
    }

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

    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
    }

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

3.5 基于 xml 的 AOP

3.5.1 在 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.oizys.aop.xmlAop"></context:component-scan>

<aop:config>
    <!-- 配置切面类 -->
    <aop:aspect ref="loggerAspect">
        <!-- 配置切入点 -->
        <aop:pointcut id="pointCut" 
                   expression="execution(* com.oizys.aop.xml.CalculatorImpl.*(..))"/>
        <!-- 配置五种通知类型,并指定切入点 -->
        <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>

3.6 基于注解的 AOP

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

    <!--Springaop 依赖-->
    <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>
</dependencies>
3.6.2 在切面类添加注解
// @Aspect 表示这个类是一个切面类
@Aspect
// @Component 注解保证这个切面类能够放入 IoC 容器
@Component
public class LogAspect {
    
    // 设置切入点和通知类型
    
    @Before("execution(public int com.oizys.aop.annotation.CalculatorImpl.*(..))")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }

    @After("execution(* com.oizys.aop.annotation.CalculatorImpl.*(..))")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->后置通知,方法名:"+methodName);
    }

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

    @AfterThrowing(value = "execution(* com.oizys.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
    }
    
    @Around("execution(* com.oizys.aop.annotation.CalculatorImpl.*(..))")
    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;
    }
    
}
3.6.3 在 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">
    <!--
        基于注解的AOP的实现:
        1、将目标对象和切面交给IOC容器管理(注解+扫描)
        2、开启AspectJ的自动代理,为目标对象自动生成代理
        3、将切面类通过注解@Aspect标识
    -->
    <!-- 开启组件扫描 -->
    <context:component-scan base-package="com.oizys.aop.annotation"></context:component-scan>
	<!-- 开启aspectJ自动代理,为目标对象生成代理 -->
    <aop:aspectj-autoproxy />
</beans>
3.6.4 切入点表达式

image-20230419104956946

3.6.5 复用切入点表达式

①声明

@Pointcut("execution(* com.oizys.aop.annotation.*.*(..))")
public void pointCut(){}

②在同一个切面中使用

@Before("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.oizys.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);
}
3.6.6 切面优先级

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

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

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

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

4 事务

4.1 事务的概念

4.1.1 基本概念

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

4.1.2 事务的特性
A:原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据都必须处于一致性状态,即操作前后数据的总量不变。

如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。

如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

D:持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

4.1.3 编程式事务与声明式事务
编程式事务

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

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

编程式的实现方式存在缺陷:

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

由于事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。

  • 优点

    • 提高开发效率

    • 消除了冗余的代码

所以,我们可以总结下面两个概念:

  • 编程式自己写代码实现功能
  • 声明式:通过配置框架实现功能

4.2 准备工作

以用户购买商品为例

4.2.1 创建表
用户表商品表
用户 id商品 id
用户名商品名
用户余额商品价格
商品库存
4.2.2 希望的实现

在购买商品时对 用户余额商品价格商品库存 的操作要求是一个事务,当修改库存失败(库存不足)或者修改余额失败(余额不足)时都要进行事务的回滚

4.3 基于 XML 的声明式事务

4.3.1 修改 Spring 配置文件

将 Spring 配置文件中去掉 tx:annotation-driven 标签,并添加配置:

<aop:config>
    <!-- 配置事务通知和切入点表达式 -->
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.oizys.spring.tx.xml.service.impl.*.*(..))"></aop:advisor>
</aop:config>
<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- tx:method标签:配置具体的事务方法 -->
        <!-- name属性:指定方法名,可以使用星号代表多个字符 -->
        <tx:method name="get*" read-only="true"/>
        <tx:method name="query*" read-only="true"/>
        <tx:method name="find*" read-only="true"/>
    
        <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
    </tx:attributes>
</tx:advice>

注意:基于xml实现的声明式事务,必须引入aspectJ的依赖

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

4.4 基于注解的声明式事务

4.4.1 在 Spring 配置文件中加入事务的配置
名称空间
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>
配置
<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" />
4.4.2 使用事务

在我们需要使用事务的方法或类上加上 @Transactional 注解即开始使用事务

@Transactional
@Service
public class GoodsServiceImpl implements GoodsService{
    
}
4.4.3 @Transactional 注解的重要属性
  1. readOnly = true:只读,如果为 true 只能查询,不能修改、添加、删除

  2. timeout = 3:超时,默认为 -1 表示永不超时,在设置的事件内操作没有完成,则抛出异常并进行回滚

  3. 回滚策略,声明式事务默认只针对运行时异常回滚,编译时异常不回滚,可以设置哪些异常不回滚

    • rollbackFor:需要设置一个Class类型的对象,表示出现指定异常时进行回滚

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

    • noRollbackFor:需要设置一个Class类型的对象,表示出现指定异常时不进行回滚

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

  4. isolation = Isolation.READ_UNCOMMITTED:隔离级别,多进程的读问题

    • 读未提交:READ UNCOMMITTED

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

    • 读已提交:READ COMMITTED、

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

    • 可重复读:REPEATABLE READ

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

    • 串行化:SERIALIZABLE

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

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

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

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

    隔离级别OracleMySQL
    READ UNCOMMITTED×
    READ COMMITTED√(默认)
    REPEATABLE READ×√(默认)
    SERIALIZABLE
  5. propagation = Propagation.REQUIRED:传播行为,事物之间调用,事务如何使用

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

使用配置类代替配置文件

@Configuration
@ComponentScan("com.oizys.spring6")
@EnableTransactionManagement
public class SpringConfig {

    // 配置数据库相关
    @Bean
    public DataSource getDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }

    // 配置 set 数据源相关
    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

5 资源操作

5.1 Spring Resources 概述

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

5.2 Resource 接口

Spring 的 Resource 接口位于 org.springframework.core.io 中。 旨在成为一个更强大的接口,用于抽象对低级资源的访问。以下显示了 Resource 接口定义的方法

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isReadable();

    boolean isOpen();

    boolean isFile();

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    ReadableByteChannel readableChannel() throws IOException;

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();
}

Resource 接口继承了 InputStreamSource 接口,提供了很多 InputStreamSource 所没有的方法。InputStreamSource 接口,只有一个方法:

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}

其中一些重要的方法:

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

其他方法:

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

5.3 Resource 的实现类

ClassPathResource

5.3.1 UrlResource 访问网络资源

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

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

创建一个 maven 子模块 spring6-resources,配置 Spring 依赖

public class UrlResourceDemo {

    public static void loadAndReadUrlResource(String path){
        // 创建一个 Resource 对象
        UrlResource url = null;
        try {
            url = new UrlResource(path);
            // 获取资源名
            System.out.println(url.getFilename());
            System.out.println(url.getURI());
            // 获取资源描述
            System.out.println(url.getDescription());
            //获取资源内容
            System.out.println(url.getInputStream().read());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    public static void main(String[] args) {
        //访问网络资源
        loadAndReadUrlResource("http://www.baidu.com");
    }
}
在项目根路径下创建文件,从文件系统中读取资源

方法不变,修改调用传递路径

public static void main(String[] args) {
    //1 访问网络资源
	//loadAndReadUrlResource("http://www.baidu.com");
    
    //2 访问文件系统资源
    loadAndReadUrlResource("file:hello.txt");
}
5.3.2、ClassPathResource 访问类路径下资源

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

在 resources 下创建文件 hello.txt,使用 ClassPathResource 访问

image-20230423214321945

public class ClassPathResourceDemo {

    public static void loadAndReadUrlResource(String path) throws Exception{
        // 创建一个 Resource 对象
        ClassPathResource resource = new ClassPathResource(path);
        // 获取文件名
        System.out.println("resource.getFileName = " + resource.getFilename());
        // 获取文件描述
        System.out.println("resource.getDescription = "+ resource.getDescription());
        //获取文件内容
        InputStream in = resource.getInputStream();
        byte[] b = new byte[1024];
        while(in.read(b)!=-1) {
            System.out.println(new String(b));
        }
    }

    public static void main(String[] args) throws Exception {
        loadAndReadUrlResource("hello.txt");
    }
}

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

5.3.3、FileSystemResource 访问文件系统资源

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

使用FileSystemResource 访问文件系统资源
public class FileSystemResourceDemo {

    public static void loadAndReadUrlResource(String path) throws Exception{
        //相对路径
        FileSystemResource resource = new FileSystemResource("hello.txt");
        //绝对路径
        //FileSystemResource resource = new FileSystemResource("C:\\hello.txt");
        // 获取文件名
        System.out.println("resource.getFileName = " + resource.getFilename());
        // 获取文件描述
        System.out.println("resource.getDescription = "+ resource.getDescription());
        //获取文件内容
        InputStream in = resource.getInputStream();
        byte[] b = new byte[1024];
        while(in.read(b)!=-1) {
            System.out.println(new String(b));
        }
    }

    public static void main(String[] args) throws Exception {
        loadAndReadUrlResource("hello.txt");
    }
}

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

5.3.4、ServletContextResource

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

5.3.5、InputStreamResource

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

5.3.6、ByteArrayResource

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

5.4 ResourceLoader 接口

5.4.1 ResourceLoader 概述

该接口实现类的实例可以获得一个 Resource 实例。

在 ResourceLoader 接口里仅有 Resource getResource(String location) 这个方法,用于返回一个 Resource 实例。

ApplicationContext 实现类都实现 ResourceLoader 接口,因此 ApplicationContext 可直接获取相应的 Resource 实例。例如,如果使用 ClassPathXmlApplicationContext 实现类则返回 ClassPathResource 实例。

5.4.2 使用演示

ClassPathXmlApplicationContext 获取 ClassPathResource 实例

public class Demo1 {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext();
		// 通过 ApplicationContext 访问资源
		// ApplicationContext 实例获取 Resource 实例时,
		// 默认采用与 ApplicationContext 相同的资源访问策略
        Resource res = ctx.getResource("hello.txt");
        System.out.println(res.getFilename());
    }
}

FileSystemApplicationContext 获取 FileSystemResource 实例

public class Demo2 {

    public static void main(String[] args) {
        ApplicationContext ctx = new FileSystemXmlApplicationContext();
        Resource res = ctx.getResource("hello.txt");
        System.out.println(res.getFilename());
    }
}
5.4.3 ResourceLoader 总结

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

另外,使用 ApplicationContext 访问资源时,可通过不同前缀指定强制使用指定的 ClassPathResource、FileSystemResource 等实现类,例如:

Resource res = ctx.getResource("calsspath:bean.xml");
Resrouce res = ctx.getResource("file:bean.xml");
Resource res = ctx.getResource("http://localhost:8080/beans.xml");

5.5 ResourceLoaderAware 接口

该接口实现类的实例将获得一个 ResourceLoader 的引用。

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

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

使用 ResourceLoaderAware 得到 ResourceLoader 实例

创建类,实现ResourceLoaderAware接口

public class TestBean implements ResourceLoaderAware {

    private ResourceLoader resourceLoader;

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

    // 返回 ResourceLoader 对象的应用
    public ResourceLoader getResourceLoader(){
        return this.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.oizys.spring6.resouceloader.TestBean"></bean>
</beans>

使用测试

public class Demo3 {

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

5.6 使用 Resource 作为属性

前面介绍了 Spring 提供的资源访问策略,但这些依赖访问策略要么需要使用 Resource 实现类,要么需要使用 ApplicationContext 来获取资源。

并且当我们要修改资源的路径时会很麻烦,当程序获取 Resource 实例时,总需要提供 Resource 所在的位置,不管通过 FileSystemResource 创建实例,还是通过 ClassPathResource 创建实例,或者通过 ApplicationContext 的 getResource() 方法获取实例,都需要提供资源位置。这意味着:资源所在的物理位置将被耦合到代码中,如果资源位置发生改变,则必须改写程序。

实际上,当应用程序中的 Bean 实例需要访问资源时,Spring 有更好的解决方法:直接利用依赖注入这样做的好处是代码里面只用写逻辑,而资源的位置在配置文件中维护。

5.6.1 依赖注入使用 Resource

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

public class ResourceBean {
    
    private Resource res;
    
    public void setRes(Resource res) {
        this.res = res;
    }
    public Resource getRes() {
        return res;
    }
    
    public void parse(){
        System.out.println(res.getFilename());
        System.out.println(res.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.oizys.spring6.resouceloader.ResourceBean" >
      <!-- 可以使用 file:、http:、ftp: 等前缀强制 Spring 采用对应的资源访问策略 -->
      <!-- 如果不采用任何前缀,则 Spring 将采用与该 ApplicationContext 相同的资源访问策略来访问资源 -->
        <property name="res" value="classpath:hello.txt"/>
    </bean>
</beans>

使用测试

public class Demo4 {

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

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

5.7.1、概述

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

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

  • 使用ApplicationContext实现类指定访问策略。

  • 使用前缀指定访问策略。

5.7.2、ApplicationContext实现类指定访问策略

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

  • ClassPathXMLApplicationContext:对应使用 ClassPathResource 进行资源访问。
  • FileSystemXmlApplicationContext:对应使用 FileSystemResource 进行资源访问。
  • XmlWebApplicationContext:对应使用 ServletContextResource 进行资源访问。
5.7.3、使用前缀指定访问策略
classpath前缀使用
public class Demo1 {

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

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

ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:bean.xml");
System.out.println(ctx);

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

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

注意 :

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

通配符其他使用

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

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

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

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

6 国际化

6.1 Java 实现国际化

6.1.1 Locale 包

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 符合 en_GB,那么优先查找 messages_en_GB.propertes 配置文件,如果没查找到,再查找 messages.properties 配置文件。

所有的配置文件必须放在 classpath 中,一般放在 resources 目录下

image-20230423175336603

6.1.2 使用
public static void main(String[] args) {
    // getBundle 两个参数 指定前缀, 指定 Locale 对象(语言,地区)
    System.out.println(ResourceBundle.getBundle("messages",new Locale("en","GB"))
                       // 要读取的内容
                       .getString("test"));

    System.out.println(ResourceBundle.getBundle("messages",new Locale("zh","CN"))
                       .getString("test"));
}

6.2 Spring 实现国际化

6.2.1、MessageSource 接口

Spring 中国际化是通过 MessageSource 这个接口来支持的

常见实现类

ResourceBundleMessageSource

这个是基于 Java 的 ResourceBundle 基础类实现,允许仅通过资源名加载国际化资源

ReloadableResourceBundleMessageSource

这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新资源的信息

StaticMessageSource

它允许通过编程的方式提供国际化信息,一会我们可以通过这个来实现db中存储国际化信息的功能。

6.2.2、使用Spring6国际化

第一步 创建资源文件

国际化文件命名格式:基本名称 _ 语言 _ 国家.properties

{0},{1}这样内容,就是动态参数

(1)创建messages_en_GB.properties

test=welcome {0},时间:{1}

(2)创建messages_zh_CN.properties

test=欢迎 {0},时间:{1}

第二步 创建spring配置文件,配置MessageSource

<?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>oizys</value>
            </list>
        </property>
        <property name="defaultEncoding">
            <value>utf-8</value>
        </property>
    </bean>
</beans>
6.2.3 使用
public static void main(String[] args) {

    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

    // 传递动态参数,使用数组形式对应{0} {1}顺序
    Object[] objs = new Object[]{"oizys",new Date().toString()};

    // messages 为资源文件的key值,
    // objs 为资源文件 value 值所需要的参数,Local.CHINA 为国际化为语言
    String str=context.getMessage("messages", objs, Locale.CHINA);
    System.out.println(str);
}

7 数据校验

7.1 Spring Validation 概述

在开发中,我们经常遇到参数校验的需求。如果使用普通方式,我们会把校验的代码和真正的业务处理逻辑耦合在一起,而且如果未来要新增一种校验逻辑也需要在修改多个地方。而 Spring Validation 允许通过注解的方式来定义对象校验规则,把校验和业务逻辑分离开,让代码编写更加方便。Spring Validation 其实就是对 Hibernate Validator 进一步的封装,方便在 Spring 中使用。

7.2 通过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>

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

@Data
public class Person {
    private String name;
    private int age;
}

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

public class PersonValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Person.class.equals(clazz);
    }

    @Override
    public void validate(Object object, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty","name 不能为空");
        Person p = (Person) object;
        if (p.getAge() < 0) {
            errors.rejectValue("age", "age.error", "error value < 0");
        } else if (p.getAge() > 110) {
            errors.rejectValue("age", "age.error", "error value too old");
        }
    }
}
  • supports 方法用来表示此校验用在哪个类型上

  • validate 是设置校验逻辑的地点,其中 ValidationUtils,是 Spring 封装的校验工具类,帮助快速实现校验

7.3 Bean Validation 注解实现

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

创建配置类,配置 LocalValidatorFactoryBean

@Configuration
@ComponentScan("com.oizys.spring6.validation.method2")
public class ValidationConfig {

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

创建实体类,使用注解定义校验规则

@Data
public class User {

    @NotNull
    private String name;

    @Min(0)
    @Max(120)
    private int 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格式

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

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

@Service
public class MyService1 {

    @Autowired
    private Validator validator;

    public  boolean validator(User user){
        Set<ConstraintViolation<User>> sets =  validator.validate(user);
        // 表示需要校验的属性是否为通过,true 表示为通过
        return sets.isEmpty();
    }

}

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

package com.oizys.spring6.validation.method2;

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

@Service
public class MyService2 {

    @Autowired
    private Validator validator;

    public boolean validaPersonByValidator(User user) {
        BindException bindException = new BindException(user, user.getName());
        validator.validate(user, bindException);
        // 获取校验失败的所有错误信息
        List<ObjectError> allErrors = bindException.getAllErrors();
        System.out.println(allErrors);
        // 表明是否有错误信息,true 表示有
        return bindException.hasErrors();
    }
}

7.4 基于方法实现校验

创建配置类,配置 MethodValidationPostProcessor

@Configuration
@ComponentScan("com.oizys.spring6.validation.method3")
public class ValidationConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

创建实体类,使用注解设置校验规则

@Data
public class User {

    @NotNull
    private String name;

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

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

定义 Service 类,通过 @Validated 注解表明通过方法进行校验

@Service
@Validated
public class MyService {
    
    public String testParams(@NotNull @Valid User user) {
        return user.toString();
    }

}

7.5 实现自定义校验

即写一个自定义的注解,使用它来实现我们规定的校验规则

第一步 自定义校验注解

// 表明我们的注解可以用到哪些地方
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
// 表明我们的注解在运行时生效
@Retention(RetentionPolicy.RUNTIME)
// 表示生成文档时会显示改自定义注解的名字
@Documented
// 指定该注解的实现类
@Constraint(validatedBy = {CannotBlankValidator.class})
public @interface CannotBlank {
    //默认错误消息
    String message() default "不能包含空格";

    //分组
    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
    @interface List {
        CannotBlank[] value();
    }
}

第二步 编写真正的校验类,实现 ConstraintValidator 接口

package com.oizys.spring6.validation.method4;

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

public class CannotBlankValidator implements ConstraintValidator<CannotBlank, String> {

        @Override
        public void initialize(CannotBlank constraintAnnotation) {
        }

        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
                //null时不进行校验
                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 true;
        }
}

8 提前编译

image-20221218154841001

8.1 AOT概述

8.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-20221207113544080

(3)AOT的优点

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

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

AOT的缺点

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

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

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

8.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 进行了正式版发布。

8.1.3 Native Image

目前业界除了这种在JVM中进行AOT的方案,还有另外一种实现Java AOT的思路,那就是直接摒弃JVM,和C/C++一样通过编译器直接将代码编译成机器代码,然后运行。这无疑是一种直接颠覆Java语言设计的思路,那就是GraalVM Native Image。它通过C语言实现了一个超微缩的运行时组件 —— Substrate VM,基本实现了JVM的各种特性,但足够轻量、可以被轻松内嵌,这就让Java语言和工程摆脱JVM的限制,能够真正意义上实现和C/C++一样的AOT编译。这一方案在经过长时间的优化和积累后,已经拥有非常不错的效果,基本上成为Oracle官方首推的Java AOT解决方案。
Native Image 是一项创新技术,可将 Java 代码编译成独立的本机可执行文件或本机共享库。在构建本机可执行文件期间处理的 Java 字节码包括所有应用程序类、依赖项、第三方依赖库和任何所需的 JDK 类。生成的自包含本机可执行文件特定于不需要 JVM 的每个单独的操作系统和机器体系结构。

8.2 演示Native Image构建过程

8.2.1 GraalVM安装
(1)下载GraalVM

进入官网下载:https://www.graalvm.org/downloads/

image-20221207153944132

image-20221207152841304

(2)配置环境变量

添加GRAALVM_HOME

image-20221207110539954

把JAVA_HOME修改为graalvm的位置

image-20221207153724340

把Path修改位graalvm的bin位置

image-20221207153755732

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

image-20221207153642253

(3)安装native-image插件

使用命令 gu install native-image下载安装

image-20221207155009832

8.2.2 安装C++的编译环境
(1)下载Visual Studio安装软件

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

image-20221219112426052

(2)安装Visual Studio

image-20221207155726572

image-20221207155756512

(3)添加Visual Studio环境变量

配置INCLUDE、LIB和Path

image-20221207110947997

image-20221207111012582

image-20221207111105569

(4)打开工具,在工具中操作

image-20221207111206279

8.2.3 编写代码,构建Native Image
(1)编写Java代码
public class Hello {

    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
(2)复制文件到目录,执行编译

image-20221207111420056

(3)Native Image 进行构建

image-20221207111509837

image-20221207111609878

(4)查看构建的文件

image-20221207111644950

(5)执行构建的文件

image-20221207111731150

可以看到这个Hello最终打包产出的二进制文件大小为11M,这是包含了SVM和JDK各种库后的大小,虽然相比C/C++的二进制文件来说体积偏大,但是对比完整JVM来说,可以说是已经是非常小了。

相比于使用JVM运行,Native Image的速度要快上不少,cpu占用也更低一些,从官方提供的各类实验数据也可以看出Native Image对于启动速度和内存占用带来的提升是非常显著的:

image-20221207111947283

image-20221207112009852

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值