Spring学习笔记

目录

1.spring

1.1 简介

1.2 优点 

1.3 组成

1.4 拓展

2. IOC(控制反转)

2.1 IOC理论推导

2.2 IOC的本质

3.Hello Spring 

3.1 导入相关依赖

3.2 编写代码 

4. IOC创建对象的方式

5. Spring配置 

5.1 别名

5.2 Bean配置

5.3 import 

6. 依赖注入(DI)

6.1 构造器注入

6.2 set注入【重点】

编写pojo测试类

6.2.1 常量注入

6.2.2 Bean注入

6.2.3 数组注入

6.2.4 List注入

6.2.5 Map注入

6.2.6 Set注入

6.2.7 Null注入

6.2.8 Properties注入

6.3 拓展注入方式

6.4 Bean的作用域​

7. Bean的自动装配

7.1 测试类

7.2 ByName自动装配

7.3 ByType自动装配

7.3 使用注解自动装配

8.使用注解开发 

9.使用java的方式配置Spring 

10.代理模式

10.1 静态代理

10.2 动态代理 

11. AOP

11.1 什么是AOP

11.2 AOP在Spring中的作用

11.3 Spring实现AOP

12.Spring整合Mybatis

12.1 回忆Mybatis

12.2 Mybatis-Spring

13.声明式事务

13.1 回顾事务

13.2 spring中的事务管理

13.3 声明式事务测试步骤

待补充...


1.spring

1.1 简介

  • 官网:Spring | Projects
  • 它是为了解决企业应用开发的复杂性而创建的
  • Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架
  • 2002年首次推出Spring框架的雏形(interface21框架)
  • Spring框架即以interface21框架为基础,经过重新设计,不断丰富其内涵,于2004年3月24日,发布了1.0正式版
  • Rod Johnson Spring Framework创始人,著名作者。 Rod在悉尼大学不仅获得了计算机学位,同时还获得了音乐学位。更令人吃惊的是在回到软件开发领域之前,他还获得了音乐学的博士学位
  • Spring理念:使现有的技术更加容易使用,本身是一个大杂烩,整合了现有的技术框架
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.12</version>
</dependency>

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

1.2 优点 

  • Spring是一个开源的免费的框架(容器) 
  • Spring是一个轻量级的,非入侵式的框架
  • 控制反转(IOC),切面编程(AOP)
  • 支持事务的处理,对框架的整合

总结:Spring是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架

1.3 组成

组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下:

  • 核心容器:核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转(IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开
  • Spring 上下文:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能
  • Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向切面的编程功能 , 集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理任何支持 AOP的对象。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中
  • Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构
  • Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构
  • Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作
  • Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI

1.4 拓展

Spring Boot与Spring Cloud

  • Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务
  • Spring Cloud是基于Spring Boot实现的
  • Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架
  • Spring Boot使用了约束优于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置 , Spring Cloud很大的一部分是基于Spring Boot来实现,Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系
  • SpringBoot在SpringClound中起到了承上启下的作用,如果要学习SpringCloud必须要学习SpringBoot

2. IOC(控制反转)

2.1 IOC理论推导

新建一个maven项目

1. 编写UserDao接口

public interface UserDao {
     // 得到用户
     void getUser();
}

2. 编写UserDaoImpl实现类

public class UserDaoImpl implements UserDao{
    @Override
    public void getUser() {
        System.out.println("默认getUser");
    }
}

3. 编写UserService业务接口

public interface UserService {
    // 得到用户
    void getUser();
}

4. 编写UserServiceImpl业务实现类

public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();
    @Override
    public void getUser() {
        userDao.getUser();
    }
}
  • 如果我们把UserDao的实现类在增加一个或者多个,我们紧接着要去对应的Service实现类中修改对应的实现类 
  • 假设我们的这种需求非常大 , 这种方式就根本不适用了, 甚至反人类对吧 , 每次变动 , 都需要修改大量代码 . 这种设计的耦合性太高了, 牵一发而动全身

我们可以使用Set接口实现

  • 他们已经发生了根本性的变化 , 很多地方都不一样了 . 仔细去思考一下 , 以前所有东西都是由程序去进行控制创建 , 而现在是由我们自行控制创建对象 , 把主动权交给了调用者 . 程序不用去管怎么创建,怎么实现了 . 它只负责提供一个接口 
  • 这种思想 , 从本质上解决了问题 , 我们程序员不再去管理对象的创建了 , 更多的去关注业务的实现 . 耦合性大大降低 . 这也就是IOC的原型
    private UserDao userDao;
    
    // 利用set进行动态实现值的注入
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

2.2 IOC的本质

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

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

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

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

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

3.Hello Spring 

3.1 导入相关依赖

spring 需要导入commons-logging进行日志记录 . 我们利用maven , 他会自动下载对应的依赖项 

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

3.2 编写代码 

创建一个Hello实体类

/**
 * @author xiaow
 * @data 2021/10/12
 */
public class Hello implements Serializable {

    private String str;

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

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

编写Spring文件(applicationContext.xml),这里起名为beans.xml 

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

    <!-- bean就是java对象, 由spring创建和管理 -->
    <bean id="hello" class="com.xiaow.pojo.Hello">
        <property name="str" value="hello spring"/>
    </bean>

</beans>

测试

 @Test
    public void testHelloBean() {
        // 解析bean.xml 文件, 生成管理相应的Bean对象
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        // getBean, 参数为spring配置文件中的id
        Hello hello = (Hello) context.getBean("hello");
        System.out.println(hello.toString());
    }

 测试结果

 总结:所谓IOC就是对象由Spring来创建,管理,装配

4. IOC创建对象的方式

例子的pojo是HelloSpring中的Hello对象

1. 使用无参构造创建对象(默认)

2. 使用有参构造创建对象

        2.1 下表赋值

     <!-- 第一种:下标赋值 -->
     <bean id="User" class="com.xiaow.pojo.User">
        <constructor-arg index="0" value="下标赋值:xiaow"/>
     </bean>

        2.2 参数类型赋值

     <!-- 第二种:参数类型赋值 -->
     <bean id="User" class="com.xiaow.pojo.User">
        <constructor-arg type="java.lang.String" value="参数类型:xiaow"/>
     </bean>

        2.3 通过参数名赋值

    <!-- 第三种:通过参数名赋值 -->
    <bean id="User" class="com.xiaow.pojo.User">
        <constructor-arg name="name" value="参数名赋值:xiaow"/>
    </bean>

总结:在配置文件加载的时候,spring中管理的对象就已经初始化了

5. Spring配置 

5.1 别名

alias 设置别名 , 为bean设置别名 , 可以设置多个别名

    <!-- 给User这个Bean起了一个别名myUser -->
    <alias name="User" alias="myUser"/>

5.2 Bean配置

<!--bean就是java对象,由Spring创建和管理-->
<!--
    id 是bean的标识符,要唯一,如果没有配置id,name就是默认标识符
    如果配置id,又配置了name,那么name是别名
    name可以设置多个别名,可以用逗号,分号,空格隔开
    如果不配置id和name,可以根据applicationContext.getBean(.class)获取对象;
    class是bean的全限定名=包名+类名
-->
<bean id="User" name="user1 user2,u3;u4" class="com.xiaow.pojo.User">
    <property name="name" value="Spring"/>
</bean>

5.3 import 

import 一般多用于团队开发,它可以将多个配置文件导入合并为一个

假设现在项目中有多人开发,有三个人负责不同类的开发,不同的类在不同的Bean中注册,我们可以使用import将所有人的applicationContext.xml合并为一个总的

  • 小魏
  • 小小魏
  • 小小小魏
  • applicationContext.xml
<import resource="小魏.xml"/>
<import resource="小小魏.xml"/>
<import resource="小小小魏.xml"/>

6. 依赖注入(DI)

  • 依赖注入(Dependency Injection,DI)

  • 依赖 : 指Bean对象的创建依赖于容器 . Bean对象的依赖资源 

  • 注入 : 指Bean对象所依赖的资源 , 由容器来设置和装配

6.1 构造器注入

6.2 set注入【重点】

注意:要求被注入的属性 , 必须有set方法 , set方法的方法名由set + 属性首字母大写 , 如果属性是boolean类型 , 没有set方法 , 是 is

搭建测试环境

编写pojo测试类

Student类

/**
 * @author xiaow
 * @data 2021/10/13
 */
public class Student implements Serializable {

    private String name;
    private Address address;
    private String[] books;
    private List<String> hobbys;
    private Map<String,String> card;
    private Set<String> games;
    private String wife;
    private Properties info;

    public String getName() {
        return name;
    }

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

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public String[] getBooks() {
        return books;
    }

    public void setBooks(String[] books) {
        this.books = books;
    }

    public List<String> getHobbys() {
        return hobbys;
    }

    public void setHobbys(List<String> hobbys) {
        this.hobbys = hobbys;
    }

    public Map<String, String> getCard() {
        return card;
    }

    public void setCard(Map<String, String> card) {
        this.card = card;
    }

    public Set<String> getGames() {
        return games;
    }

    public void setGames(Set<String> games) {
        this.games = games;
    }

    public String getWife() {
        return wife;
    }

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

    public Properties getInfo() {
        return info;
    }

    public void setInfo(Properties info) {
        this.info = info;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", address=" + address +
                ", books=" + Arrays.toString(books) +
                ", hobbys=" + hobbys +
                ", card=" + card +
                ", games=" + games +
                ", wife='" + wife + '\'' +
                ", info=" + info +
                '}';
    }
}

Address类

/**
 * @author xiaow
 * @data 2021/10/13
 */
public class Address implements Serializable {

    private String addressName;

    public String getAddressName() {
        return addressName;
    }

    public void setAddressName(String addressName) {
        this.addressName = addressName;
    }

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

6.2.1 常量注入

<bean id="student" class="com.xiaow.pojo.Student">
        <!-- 普通注入 value -->
       <property name="name" value="xiaow"/>
</bean>

测试

    @Test
    public void testStudent() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Student student = (Student) context.getBean("Student");
        System.out.println(student);
    }

6.2.2 Bean注入

注意:这里的ref是一个引用值

<bean id="address" class="com.xiaow.pojo.Address">
        <property name="addressName" value="江西科技学院"/>
</bean>

<bean id="student" class="com.xiaow.pojo.Student">
    <!-- 普通注入 value -->
    <property name="name" value="xiaow"/>

    <!-- Bean注入 ref -->
    <property name="address" ref="address"/>
</bean>

6.2.3 数组注入

        <!-- 数组 -->
        <property name="books">
            <array>
                <value>西游记</value>
                <value>红楼梦</value>
                <value>水浒传</value>
                <value>三国演义</value>
            </array>
        </property>

6.2.4 List注入

        <!-- list -->
        <property name="hobbys">
            <list>
                <value>篮球</value>
                <value>看电影</value>
                <value>学习</value>
            </list>
        </property>

6.2.5 Map注入

        <!-- map -->
        <property name="card">
            <map>
                <entry key="身份证" value="429006222222222222"/>
                <entry key="学号" value="202099000437"/>
            </map>
        </property>

6.2.6 Set注入

        <!-- set -->
        <property name="games">
            <set>
                <value>英雄联盟</value>
                <value>王者荣耀</value>
            </set>
        </property>

6.2.7 Null注入

        <!-- null -->
        <property name="wife"><null/></property>

6.2.8 Properties注入

        <!-- Properties -->
        <property name="info">
            <props>
                <prop key="学号">202099000437</prop>
                <prop key="姓名">小魏</prop>
                <prop key="性别">男</prop>
            </props>
        </property>

6.3 拓展注入方式

  • p命名空间注入
  • c命名空间注入

导入p命名空间文件约束

xmlns:p="http://www.springframework.org/schema/p"
<!-- p: 命名空间,可以直接注入属性的值:property -->
<bean id="user" class="com.xiaow.pojo.User" p:name="xiaow"/>

导入c命名空间约束

xmlns:c="http://www.springframework.org/schema/c"
<!-- c:命名空间, 通过有参构造注入:construct-args -->
<bean id="user" class="com.xiaow.pojo.User" c:name="xiaow"/>

测试

    @Test
    public void testUser() {
        // 解析beans.xml文件, 生成相应的对象
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        // getBean
        User user = (User) context.getBean("user1");
        System.out.println(user.toString());
    }

注意:使用c/p命名空间注入,需要导入空间文件约束

6.4 Bean的作用域

1. singleton 单例模式 (Spring的默认机制)

<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

2. prototype 原型模式

  • 每一次从容器中get的时候,都会产生一个新的对象
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

3.其他的 request session application 这个只能在web开发中使用 

7. Bean的自动装配

  • 自动装配是Spring满足bean依赖的一种方式
  • Spring会在上下文中自动寻找,并自动给bean装配属性 

在Spring中的三种装配方式

  • 在Xml中显示配置
  • 在java中显示配置
  • 隐式的自动装配Bean【重要】

7.1 测试类

一个人有一条猫和一条狗

public class Person implements Serializable {

    private String name;
    private Cat cat;
    private Dog dog;

    public String getName() {
        return name;
    }

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

    public Cat getCat() {
        return cat;
    }

    public void setCat(Cat cat) {
        this.cat = cat;
    }

    public Dog getDog() {
        return dog;
    }

    public void setDog(Dog dog) {
        this.dog = dog;
    }
}
public class Dog implements Serializable {

    public void shout() {
        System.out.println("wang~");
    }

}
public class Cat implements Serializable {

    public void shout() {
        System.out.println("miao~");
    }

}

7.2 ByName自动装配

    <bean id="cat" class="com.xiaow.pojo.Cat"/>
    <bean id="dog" class="com.xiaow.pojo.Dog"/>    
    <!--
        byName:会自动在容器上下文中查找, 和自己对象set方法后面的值对应的beanid
        byType:会自动在容器上下文中查找, 和自己对象类型相同的bean
    -->    
    <bean id="Person" class="com.xiaow.pojo.Person" autowire="byName">
        <property name="name" value="xiaow"/>
    </bean>

 7.3 ByType自动装配

    <bean id="cat" class="com.xiaow.pojo.Cat"/>
    <bean id="dog" class="com.xiaow.pojo.Dog"/>   
    <!--
        byName:会自动在容器上下文中查找, 和自己对象set方法后面的值对应的beanid
        byType:会自动在容器上下文中查找, 和自己对象类型相同的bean
    -->
    <bean id="Person" class="com.xiaow.pojo.Person" autowire="byType">
        <property name="name" value="xiaow"/>
    </bean>

小结

  • 使用ByName的时候,需要保证BeanId的唯一,并且这个Bean需要和自动注入的属性的set方法的值一致
  • 使用ByType的时候,需要保证Bean的class唯一,并且这个Bean要跟自动注入的属性类型一致

7.3 使用注解自动装配

准备工作: 利用注解的方式注入属性。

  1. 在spring配置文件中引入context文件头

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

开启属性注解支持

<context:annotation-config/>

@Autowired

  • 直接在属性上使用即可,也可以在set方法上使用
  • 使用Autowired我们可以不用编写set方法,前提是你这个自动装配的属性在IOC(Spring)容器中存在,并复合名字ByName
@Nullable 如果字段使用了这个注解,这个字段可以为null
public @interface Autowired {
    boolean required() default true;
}
public class Person implements Serializable {

    private String name;
    // 如果显示定义了Autowired的属性required为false,说明这个对象可以为null,否则允许为null
    @Autowired(required = false)
    private Cat cat;
    @Autowired
    private Dog dog;
}

如果@Autowired自动装配的环境比较复杂,自动装配无法通过一个注解【@Autowired】完成装配的时候,我们可以使用@Qualifier(value="xxx")去配合@Autowired进行使用,指定一个唯一的bean对象注入

public class Person implements Serializable {

    private String name;
    @Autowired
    @Qualifier(value = "cat2")
    private Cat cat;
    @Autowired
    @Qualifier(value = "dog2")
    private Dog dog;
}

@Resource注解

public class Person implements Serializable {

    private String name;
    @Resource(name = "cat2")
    private Cat cat;
    @Resource(name = "dog2")
    private Dog dog;
}

小结

@AutoWired和@Resource

  • 都是用来的自动装配的,都可以放在字段上
  • @AutoWired通过ByType的方式实现装配,而且必须要求这个对象存在(@Nullable)解决不了根本问题
  • @Resource默认通过ByName的方式实现,如果找不到名字,则通过ByType的方式实现,如果两个都找不到的情况下就报错

8.使用注解开发 

在spring4之后,想要使用注解形式,必须得要引入aop的包

jdk1.5开始支持注解,spring2.5开始全面支持注解

使用注解

  • 导入约束:context约束
  • 配置注解的支持: <context:annotation-config/>
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

</beans>

1.bean

2.属性如何注入

@Component
public class User implements Serializable {
    // 相当于 <property name="name" value="xiaow"/>
    @Value("xiaow")
    private String name;

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

3.衍生注解

@Componet有几个衍生注解,我们在web开发中,会按照MVC三层架构分层

  • dao 【@Repository】
  • service 【@Service】
  • controller 【@Controller】

这四个注解的功能都是一样的,都代表将某个类注册到Spring中,装配Bean

4.自动装配

- @Autowired 默认是通过byType进行装配,而却必须要求这个对象存在【常用】
    如果@Autowired不能唯一自动装配上属性,则需要通过@Qualifier(value = "xxx")
- @Resource 默认通过byName进行装配,如果找不到name则通过byType装配,如果两者都找不到则报错【常用】
- @Nullable 字段标记了这个注解,说明这个字段可以为null

5. 作用域

@Component
@Scope("singleton")
public class User implements Serializable {
    // 相当于 <property name="name" value="xiaow"/>
    @Value("xiaow")
    private String name;

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

6.小结

 xml与注解

  • xml更加万能,适用于任何场合,维护简单方便
  • 注解,不是自己的类使用不了,维护相对复杂

xml与注解的最佳实践

  • xml用来管理bean
  • 注解只负责属性的注入
  • 在使用的过程中,只需要注意一个问题:必须要让注解生效,就需要开启注解的支持
    <!-- 开启注解支持 -->
    <context:annotation-config/>
    <!-- 指定扫描的包, 包下的注解生效 -->
    <context:component-scan base-package="com.xiaow"/>

9.使用java的方式配置Spring 

 完全不使用Spring的xml文件配置,全权交给java来做

javaConfig是Spring的一个子项目,在Spring4之后,它成为了一个核心功能

实体类

@Component // 将此类标注为spring组件,注入bean
public class User implements Serializable {

    private String name;

    public String getName() {
        return name;
    }

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

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

配置类

@Configuration // 代表这是一个配置类
public class MyConfig {

    @Bean // 通过方法注册bean,返回值就是class,方法名就是id
    public User getUser() {
        return new User();
    }

}

测试类

    @Test
    public void test1() {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
        User user = context.getBean("getUser", User.class);
        System.out.println(user.getName());
    }

10.代理模式

学习代理模式,是因为SpringAOP的底层就是代理模式【SpringAOP 和 SpringMVC】

代理模式的分类

  • 静态代理
  • 动态代理

10.1 静态代理

角色分析

  • 抽象角色:一般会使用接口或抽象类来创建
  • 真实角色:被代理的角色
  • 代理角色:代理真实角色,代理真实角色并且有一些附属操作
  • 客户:访问代理角色的人

代码步骤

1. 接口

// 抽象角色:看房
public interface Rent {

    void rent();

}

2. 真实角色

// 真实角色:房东,房东要出租房子
public class Host implements Rent {

    @Override
    public void rent() {
        System.out.println("房东房屋出租");
    }

}

3. 代理角色

// 代理对象:中介
public class Proxy implements Rent {
    private Host host;

    public Proxy(){}

    public Proxy(Host host) {
        this.host = host;
    }

    @Override
    public void rent() {
        seeHouse();
        host.rent();
        contract();
        free();
    }

    // 看房
    public void seeHouse() {
        System.out.println("中介带你来看房子");
    }

    // 签租赁合同
    public void contract() {
        System.out.println("签租赁合同");
    }

    // 收中介费
    public void free() {
        System.out.println("收中介费");
    }
}

4. 客户访问代理角色

// 真实对象:客户
public class Client {
    public static void main(String[] args) {
        Host host = new Host();

        Proxy proxy = new Proxy(host);
        proxy.rent();
    }
}

代理模式的好处

  • 可以使真实角色的操作更加干净纯粹,不用去关注一些公共业务
  • 公共业务交给代理角色,实现了业务的分工
  • 公共业务发生扩展的时候,方便集中管理

缺点

  • 一个真实角色就产生一个代理角色,代码量会翻倍,开发效率会变低

10.2 动态代理 

  • 动态代理和静态代理的角色一样
  • 动态代理的代理类是动态生成的,不是直接写好的
  • 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
    • 基于接口--jdk 动态代理
    • 基于类:cglib
    • java字节码实现:javasist

需要了解两个类:Proxy:代理,invocationHandler:调用处理程序

动态代理的好处

  • 可以使真实角色的操作更加的纯粹,不用去关注一些公共业务
  • 公共也就交给代理角色,实现了业务的分工
  • 公共业务发生扩展的时候,方便集中管理
  • 一个动态代理类代理的是一个接口,一般就是对应一类业务

11. AOP

11.1 什么是AOP

AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

11.2 AOP在Spring中的作用

提供声明式事务;允许用户自定义切面

  • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 ….

  • 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类

  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法

  • 目标(Target):被通知对象

  • 代理(Proxy):向目标对象应用通知之后创建的对象

  • 切入点(PointCut):切面通知 执行的 “地点”的定义

  • 连接点(JointPoint):与切入点匹配的执行点

 SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice

即 Aop 在 不改变原有代码的情况下 , 去增加新的功能

11.3 Spring实现AOP

注意:要使用AOP织入,需要导入依赖
 

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

方式一:通过Spring API接口实现

首先编写业务接口和实现类

public interface UserService {
    // 增
    void add();

    // 删
    void delete();

    // 改
    void update();

    // 查
    void select();
}
public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("添加用户");
    }

    @Override
    public void delete() {
        System.out.println("删除用户");
    }

    @Override
    public void update() {
        System.out.println("修改用户");
    }

    @Override
    public void select() {
        System.out.println("查询用户");
    }
}

编写增强类,编写两个,一个前置增强,一个后置增强

public class BeforeLog implements MethodBeforeAdvice {
    // method : 要执行目标对象的方法
    // objects : 被调用方法的参数
    // o : 目标对象
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("执行了" + o.getClass().getName() + "的" + method.getName() + "方法!");
    }
}
public class AfterLog implements AfterReturningAdvice {
    // o : 返回值
    // method : 被调用的方法
    // objects : 被调用方法的参数
    // o1 : 目标对象
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("执行了" + o1.getClass().getName()
                + "的" + method.getName() + "方法,"
                + "返回值:" + o);
    }
}

去spring核心配置文件中注册bean,然后aop切入实现,要导入约束

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns: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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">   
     <!-- 注册bean -->
    <bean id="userService" class="com.xiaow.service.UserServiceImpl"/>
    <bean id="afterLog" class="com.xiaow.log.AfterLog"/>
    <bean id="beforeLog" class="com.xiaow.log.BeforeLog"/>

    <!-- 方式一:使用原生spring api接口 -->
    <!-- aop的配置 需要导入aop约束 -->
    <aop:config>
        <!-- 切入点 expression:表达式匹配要执行的方法 -->
        <aop:pointcut id="pointcut" expression="execution(* com.xiaow.service.UserServiceImpl.*(..))"/>
        <!-- 执行环绕 advice-ref:执行方法 pointcut-ref:切入点 -->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

测试

public class MyTest {
    @Test
    public void testAop1() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) context.getBean("userService");
        userService.add();
    }
}

 

 方式二:通过diy来实现AOP【主要是切面定义】

编写增强类

public class DiyPointCut {

    public void before() {
        System.out.println("=============before============");
    }


    public void after() {
        System.out.println("=============after============");
    }

}

去spring核心配置文件中注册bean,然后aop切入实现,要导入约束

    <!-- 方式二:自定义类 -->
    <!-- 注册bean -->
    <bean id="diy" class="com.xiaow.diy.DiyPointCut"/>
    <aop:config>
        <!-- 自定义切面 ref:要引用的类 -->
        <aop:aspect ref="diy">
            <!-- 切入点 expression:表达式匹配要执行的方法 -->
            <aop:pointcut id="point" expression="execution(* com.xiaow.service.UserServiceImpl.*(..))"/>
            <aop:before method="before" pointcut-ref="point"/>
            <aop:after method="after" pointcut-ref="point"/>
        </aop:aspect>
    </aop:config>

测试

 方式三:使用注解实现

编写一个注解实现的增强类

@Aspect // 标注这个类是切面
public class AnnotationPointCut {
    @Before("execution(* com.xiaow.service.UserServiceImpl.*(..))")
    public void before() {
        System.out.println("=============执行前============");
    }

    @After("execution(* com.xiaow.service.UserServiceImpl.*(..))")
    public void after() {
        System.out.println("=============执行后============");
    }

    // 在环绕增强中,我们可以给定一个参数,代表我们要处理切入的点
    @Around("execution(* com.xiaow.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("环绕前");
        System.out.println("签名:"+jp.getSignature());
        //执行目标方法proceed
        Object proceed = jp.proceed();
        System.out.println("环绕后");
        System.out.println(proceed);
    }
}

去spring核心配置文件中注册bean,然后增加支持注解的配置

    <!-- 方式三:注解 -->
    <bean id="annotationPointcut" class="com.xiaow.diy.AnnotationPointCut"/>
    <!-- 开启注解支持 -->
    <aop:aspectj-autoproxy/>

12.Spring整合Mybatis

相关依赖

junit

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

mybatis

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>

mysql-connector

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>

spring 相关

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

        <!-- spring 操作数据库需要导入spring-jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.16.RELEASE</version>
        </dependency>

aspectJ AOP 织入器

        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.7</version>
        </dependency>

mybatis-spring整合包【重点】

        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
        </dependency>

Maven静态资源过滤问题

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>

12.1 回忆Mybatis

编写pojo实体类

@Data
public class User implements Serializable {
    private int id;
    private String userName;
    private String passWord;
}

编写Mybatis-config.xml配置文件(其中导入外部文件db.properties)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 引入外部文件配置 -->
    <properties resource="db.properties"/>

    <!-- mybatis调整设置 -->
    <settings>
        <!-- 标准的日志工厂实现 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <!--  可以给实体类起别名  -->
    <typeAliases>
        <package name="com.xiaow.pojo"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>

    <!--每个mapper.xml文件都要在mybatis核心配置文件中注册-->
    <mappers>
        <mapper class="com.xiaow.mapper.UserMapper"/>
    </mappers>
</configuration>
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/ssm?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username=root
password=root

UserMapper接口实现

public interface UserMapper {
    // 得到所有用户信息
    List<User> getAllUser();
}

对应的UserMapper.xml实现

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace=绑定一个对应的mapper/mapper接口-->
<mapper namespace="com.xiaow.mapper.UserMapper">
    <select id="getAllUser" resultType="user">
        select * from user
    </select>
</mapper>

编写测试类

    @Test
    public void test1() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sessionFactory.openSession();

        UserMapper mapper = session.getMapper(UserMapper.class);
        List<User> allUser = mapper.getAllUser();
        for (User user : allUser) {
            System.out.println(user);
        }

        session.close();
    }

测试结果

12.2 Mybatis-Spring

 整合方式一

编写Spring配置文件applicationContext.xml

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

 在核心配置文件配置Spring中的数据源替换MyBatis的数据源

    <!-- Datasource:使用Spring的数据源替换Mybatis的配置 c3p0 dbcp druid 
    这里使用Spring提供的JDBC:org.springFramework.jdbc.datasource
    -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="url" value="jdbc:mysql://localhost:3306/ssm?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=Asia/Shanghai"/>
    </bean>

配置SqlSessionFactory,关联MyBatis

注意:这里使用mapperLocations绑定了接口,如果在mybatis的配置文件mybatis-config.xml再次绑定的话,就会报错 Mapped Statements collection already contains value for xxxx

    <!-- sqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 绑定Mybatis配置文件 -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath:com/xiaow/mapper/*.xml"/>
    </bean>

注册sqlSessionTemplate,关联sqlSessionFactory

    <!-- SqlSessionTemplate 也就是我们使用的sqlSession-->
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <!-- 只能使用构造器注入sqlSessionFactory 因为它没有set方法 -->
        <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>

增加Dao接口的实现类;私有化sqlSessionTemplate

public class UserMapperImpl implements UserMapper{
    // 我们的所有操作,在原来都使用SqlSession来执行,现在使用SqlSessionTemplate
    private SqlSessionTemplate sqlSession;

    public void setSqlSession(SqlSessionTemplate sqlSession) {
        this.sqlSession = sqlSession;
    }

    @Override
    public List<User> queryAllUser() {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        return mapper.queryAllUser();
    }
}

注册bean实现

    <bean id="userMapper" class="com.xiaow.mapper.UserMapperImpl">
        <property name="sqlSession" ref="sqlSession"/>
    </bean>

测试

    @Test
    public void test1() throws IOException {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
        for (User user : userMapper.queryAllUser()) {
            System.out.println(user);
        }
    }

结果

 小结:Mybatis配置文件都可以被Spring整合

整合方式二

和方式一环境一样,实现类改变

public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {
    @Override
    public List<User> queryAllUser() {
        return getSqlSession().getMapper(UserMapper.class).queryAllUser();
    }
}

注入bean(这里跟方式一不一样,需要的是SqlSessionFactory)

    <bean id="userMapper2" class="com.xiaow.mapper.UserMapperImpl2">
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>

13.声明式事务

13.1 回顾事务

  • 把一组业务当成一个业务来做,要么都成功,要么都失败
  • 事务在项目开发中,十分的重要,涉及到数据的一致性问题,不能大意
  • 确保完整性和一致性

事务ACID原则

  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
    • 多个业务可能操作同一个资源,防止数据损坏
  • 持久性(Durability)
    • 事务一旦提交,无论系统发生什么问题,结果都不会再被影响,被持久化的写到储存器中

13.2 spring中的事务管理

Spring在不同的事务管理API之上定义了一个抽象层,使得开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。Spring支持编程式事务管理和声明式的事务管理

声明式事务:AOP

  • 一般情况下比编程式事务好用。
  • 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
  • 将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理

编程式事务:需要再代码中进行事务的管理

  • 将事务管理代码嵌到业务方法中来控制事务的提交和回滚
  • 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码

13.3 声明式事务测试步骤

pojo

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private int id;
    private String userName;
    private String passWord;
}

编写UserMapper接口,增加两个方法,一个add,一个delete

public interface UserMapper {

    // 增加
    int addUser(User user);
    // 删除
    int deleteUser(int  id);

    List<User> selectUser();

}

实现UserMapper.xml

这里故意写错delete语句(deletes)方便测试

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaow.mapper.UserMapper">

    <insert id="addUser" parameterType="user">
        insert into user (id, user_name, pass_word) values(#{id}, #{userName}, #{passWord})
    </insert>

    <delete id="deleteUser" parameterType="int">
        deletes from user where id = #{id}
    </delete>

    <select id="selectUser" resultType="user">
        select * from user
    </select>
</mapper>

编写UserMapperImpl实现类

在调用selectUser中先后调用了addUser和deleteUser

public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {
    @Override
    public int addUser(User user) {
        return getSqlSession().getMapper(UserMapper.class).addUser(user);
    }

    @Override
    public int deleteUser(int id) {
        return getSqlSession().getMapper(UserMapper.class).deleteUser(id);
    }

    @Override
    public List<User> selectUser() {
        UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
        mapper.addUser(new User(4, "xiaohei", "200206"));
        mapper.deleteUser(4);
        return mapper.selectUser();
    }
}

编写测试类并测试

控制台报错:sql错误(deletes)

结果:插入new User id4 的记录成功

不符合ACID原则,要么都成功,要么都失败,所以我们需要使用事务

    @Test
    public void test1() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
        List<User> users = userMapper.selectUser();
        for (User user : users) {
            System.out.println(user);
        }
    }

开启声明式事务管理

配置JDBC事务

    <!-- 配置声明试事务 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

配置好事务管理器后我们需要去配置事务的通知 

    <!-- 结合AOP织入事务 -->
    <!-- 配置事务通知 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 给哪些事务配置方法 -->
        <!-- 配置事务的传播特性:new propagation -->
        <tx:attributes>
            <tx:method name="add" propagation="REQUIRED"/>
            <tx:method name="delete" propagation="REQUIRED"/>
            <tx:method name="update" propagation="REQUIRED"/>
            <tx:method name="query" read-only="true"/>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

spring事务传播特性:

事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为:

  • propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
  • propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
  • propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
  • propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
  • propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
  • propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作

Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。

假设 ServiveX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。

就好比,我们刚才的几个方法存在调用,所以会被放在一组事务当中

织入事务

    <!-- 配置事务切入 -->
    <aop:config>
        <aop:pointcut id="txPointcut" expression="execution(* com.xiaow.mapper.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>

再次测试

控制台报错:sql错误(deletes)

结果:没有插入数据,符合ACID原则

 小结

  • 如果不配置事务,我们需要手动提交事务
  • 如果不配置事务,会影响到数据的一致性!!!

待补充...

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值