Sping 5 详解!

注意:大部分内容均来自B站《遇见狂神说》,笔者也是基于视频做的笔记,方便日后复习与查看,有不懂的地方可观看视频讲解!
视频地址:https://www.bilibili.com/video/BV1WE411d7Dv

Spring 5 框架

1、Spring

1.1、Spring简介

Spring翻译过来就是春天,你也可以理解为给Java现代化软件开发带来了春天。Spring适用于任何Java应用!为降低软件开发的复杂性而诞生!

Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅仅限于服务器端的开发。从简单性、可测试性和松耦合性角度而言,绝大部分Java应用都可以从Spring中受益。

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

Spring相关知识:

  • 2002,首次推出了Spring框架的雏形: interface21框架!
  • Spring框架即以interface21框架为基础,经过重新设计,并不断丰富其内涵,于2004年3月24日,发布了1.0正式版。
  • 罗德·约翰逊(Rod Johnson) ,Spring Framework创始人!
  • Spring理念:使现有的技术更加容易使用!本身是一个大杂烩,整合了现有的技术框架!
  • Spring目的:解决企业应用开发的复杂性!

官方地址:https://spring.io/projects/spring-framework
官方下载地址:http://repo.spring.io/release/org/springframework/spring

maven坐标,导入spring-webmvc即可,maven会自动帮我们下载其他需要的依赖!

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

1.2、Spring的优点

Spring框架为任何类型的部署平台上基于Java的现代企业应用程序提供了一个全面的编程和配置模型。它的优点不言而喻!

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

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

1.3、Spring的组成

在这里插入图片描述

1.4、现代化Java开发体系

在Spring的官网有这个介绍:现代化的Java开发!说白就是基于Spring的开发。

现代化开发体系
SpringBoot

  • 一个快速开发的脚手架
  • 基于SpringBoot可快速开发单个微服务
  • 约定大于配置

SpringCloud

  • SpringCloud是基于SpringBoot实现的。
  • 利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等

因为现在大多数公司都在使用SpringBoot进行快速开发,学习SpringBoot的前提,需要完全掌握Spring及SpringMVC!

弊端:发展了太久之后,违背了原来的理念!配置十分繁琐,人称:“配置地狱!"

2、IOC理论推导

2.1、引子

在以前dao与service层我们都是基于这样实现:

  1. UserDao接口
  2. UserDaolmpl实现类
  3. UserService 业务接口
  4. UserServicelmpl 业务实现

在我们之前的业务中,我们可能需要书写很多实现类,去实现业务逻辑,但是随着用户的需求增加,用户的需求可能会影响我们原来的代码,我们需要根据用户的需求去修改原代码!如果程序代码量十分大,修改与维护的成本十分昂贵!

为了解决上面问题,我们可以使用set接口实现,让程序实现动态创建实现类!

private UserDao userDao;

// 利用set进行动态实现值的注入
public void setUserDao(UserDao userDaoImpl){
	this.userDao = userDaoImpl; 
	// 这里的userDaoImpl可以是不同的实现类,这样就可动态的创建接口实现类
}

set注入分析:

  • 在没有使用set注入之前,程序是主动创建对象!控制权在技术员手中!
  • 使用set注入后,程序不再具有主动性,而是变成了被动的接受对象!

这种思想,从本质上解决了问题,我们技术人员不用再去管理对象的创建了。系统耦合性大大降低,可以更加关注业务逻辑的实现!这就是IOC的原型!

2.2、IOC的本质

控制反转IOC(Inversion Of Control),是一种设计思想,DI(依赖注入)是实现IOC的一种方法,也有人认为DI只是IOC的另一种说法。

没有IOC的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方(工厂模式),个人认为所谓控制反转就是:获得依赖对象的方式反转了。

在这里插入图片描述
lOC是Spring框架的核心内容,使用多种方式完美的实现了loC,可以使用XML配置,也可以使用注解,新版本的Spring也可以零配置实现IOC。

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

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

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

总结IOC的底层实现原理:xml 解析、工厂模式、反射。

在这里插入图片描述

3、第一个Spring程序

上一期中我们理解了IOC的基本思想,我们现在来看下Spring的应用

3.1、实现步骤

1、新建maven普通项目,导入spring-webmvc的包,通过这个包maven会自动帮我们下载需要的依赖!

<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
<!--junit-->
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

2、编写实体类Hello

/**
 * @description: Hello实体类
 * @author: laizhenghua
 * @date: 2020/11/14 10:18
 */
public class Hello {
    private String message;
    public Hello() {
    }
    ...
}

3、编写xml配置文件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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置Hello对象,在Spring中称为bean,我们也可以使用property标签设置属性-->
    <bean id="hello" class="com.howie.pojo.Hello"/>
    
    <!--
    <bean id="hello" class="com.howie.pojo.Hello">
        <property name="message" value="Hello World"/>
    </bean>
    -->
</beans>

4、编写测试程序(实例化容器)

@Test
public void helloTest(){
    // 测试第一个spring程序
    // 1.加载Spring的配置文件即获取Spring的上下文对象
    String value = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(value);
    // 2.获取创建的对象hello
    Hello hello = context.getBean(Hello.class);
    hello.setMessage("Hello world");
    System.out.println(hello);
}

测试结果

在这里插入图片描述

3.2、来自第一个Spring程序的思考

1、Hello 对象是谁创建的?

  • hello对象是由Spring创建的

2、这个过程就叫控制反转:

  • 控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建的,使用Spring后,对象是由Spring来创建的
  • 反转:程序本身不创建对象,而变成被动的接收对象.
  • 依赖注入:就是利用set方法来进行注入的,要创建的对象没有set方法会报错。
    IOC是一种编程思想,由主动的编程变成被动的接收.

可以通过newClassPathXmlApplicationContext去浏览一下底层源码.
OK,到了现在,我们彻底不用再程序中去改动了,要实现不同的操作,只需要在xml配置文件中进行修改,所谓的lOC,一句话搞定:对象由Spring 来创建,管理,装配!

4、IOC创建对象的方式

1、使用无参构造器创建对象,默认!

2、有参构造器创建对象

  • 下标(Constructor argument index)
<!--构造器参数的下标-->
<bean id="user" class="com.howie.pojo.User">
	<!-- index指构造方法 , 下标从0开始 -->
    <constructor-arg index="0" value="7500000"/>
    <constructor-arg index="1" value="42"/>
</bean>
  • 参数类型(Constructor argument type matching)
<!--通过参数类型,给参数赋值,不推荐使用,如果参数类型都相同,就按照参数顺序赋值-->
<bean id="user" class="com.howie.pojo.User">
    <constructor-arg type="int" value="1001"/>
    <constructor-arg type="java.lang.String" value="Java"/>
</bean>
  • 通过参数名(Constructor argument name)
<bean id="user" class="com.howie.pojo.User">
	<!-- name指参数名 -->
    <constructor-arg name="id" value="1001"/>
    <constructor-arg name="name" value="Java"/>
</bean>

结论:在配置文件加载的时候。其中管理的对象都已经初始化了!并且默认是以单例的形式存在!

测试:

@Test
public void helloTest(){
    // 测试第一个spring程序
    // 1.加载Spring的配置文件即获取Spring的上下文对象
    String value = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(value);
    // 2.获取创建的对象hello
    Hello hello1 = context.getBean(Hello.class);
    Hello hello2 = context.getBean(Hello.class);
    
    System.out.println(hello1 == hello2); // true
}

5、Spring配置文件

5.1、别名alias标签

1、配置文件里添加别名

<!--别名,如果添加了别名我们也可以通过别名获取对象-->
<alias name="user" alias="userAAAA"/>

2、获取对象

String value = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(value);
// 获取创建的对象
User user1 = (User) context.getBean("user");
// 也可以使用别名获取对象
User user2 = (User) context.getBean("userAAAA");

5.2、bean配置

<!--
    声明bean:告诉spring要创建的对象。
    id:spring通过id找到要创建的对象
    class:类的全限定名称,必须使用类,不能是接口,因为spring是反射机制创建对象
    name:也是别名,而且name可以设置多个别名

    spring把创建好对象放入到map中,spring框架有一个存放对象的map
    springMap.put(id,对象)
    例如:springMap.put("someService",new SomeServiceImpl());

    一个bean标签声明一个对象
-->
<bean id="hello" class="com.howie.pojo.Hello">
    <property name="message" value="Hello World"/>
</bean>

5.3、import

这个import,一般用于团队开发使用,他可以将多个配置文件,导入合并为一个。假设,现在项目中有多个人开发,这三个人负责不同的类开发,不同的类需要注册在不同的bean中,我们可以利用import将所有人的beans.xml合并为一个总的!

  • 张三(zsBean.xml)
  • 李四(lsBean.xml)
  • 王五(wwBean.xml)

使用的时候,直接使用总的配置(applicationContext.xml)就可以了

<import resource="zsBean"/>
<import resource="lsBean"/>
<import resource="wwBean"/>

6、依赖注入(DI)

6.1、概念

依赖注入(Dependency Injection,DI)。

依赖:指Bean对象的创建依赖于容器 , Bean对象的依赖资源 。

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

6.2、构造器注入

前面已经讲过了,有参无参注入等。

6.3、set注入(重点)

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

搭建测试环境,我们创建一些pojo类

Address.java

/**
 * @description: 地址类
 * @author: laizhenghua
 * @date: 2020/11/14 15:34
 */
public class Address {
    private String address;
    ...
    // setter方法一定要有,这里省略
}

Student.java

/**
 * @description: 复杂的学生类,属性包含数组、基本数据类型与引用数据类型还有集合类型等
 * @author: laizhenghua
 * @date: 2020/11/14 15:34
 */
public class Student {
    private String name;
    private Address address; // 引用类型
    private String[] books;
    private List<String> hobby;
    private Map<String,String> card;
    private Set<String> games;
    private Properties props; // 处理属性文件,k-v都是String
    private String girlFiend; // 女朋友
    ...
    // setter方法一定要有,这里省略
}

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

   <bean id="student" class="com.howie.pojo.Student">
       
   </bean>
</beans>

1、属性名注入

<bean id="student" class="com.howie.pojo.Student">
    <!--属性名注入-->
    <property name="name" value="alex"/>
</bean>

测试:

@Test
public void test(){
    String resource = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(resource);
    Student student = (Student) context.getBean("student");
    System.out.println(student.getName()); // 输出alex
}

2、bean注入

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

<bean id="address" class="com.howie.pojo.Address">
    <property name="address" value="西南林业大学新校区"/>
</bean>
<bean id="student" class="com.howie.pojo.Student">
    <!--1、属性名注入,value-->
    <property name="name" value="alex"/>
    <!--2、bean注入,ref-->
    <property name="address" ref="address"/>
</bean>

测试

@Test
public void test(){
    String resource = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(resource);
    Student student = (Student) context.getBean("student");
    System.out.println(student.getAddress()); // Address{address='西南林业大学新校区'}
}

3、数组注入

<!--3、数组注入-->
<property name="books">
    <array>
        <value>Java</value>
        <value>Python</value>
        <value>C/C++</value>
        <value>JavaScript</value>
    </array>
</property>

测试

String resource = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(resource);
Student student = (Student) context.getBean("student");

System.out.println(Arrays.toString(student.getBooks())); // [Java, Python, C/C++, JavaScript]

4、List注入

<!--List注入-->
<property name="hobby">
    <list>
        <value>写代码</value>
        <value>烫发</value>
        <value>王者荣耀</value>
    </list>
</property>

5、Map注入

<!--Map注入-->
<property name="card">
    <map key-type="java.lang.String">
        <entry value-type="java.lang.String" key="中国工商" value="122121212"/>
        <entry value-type="java.lang.String" key="建设银行" value="00021333"/>
    </map>
</property>

6、set集合注入

<!--set注入-->
<property name="games">
    <set value-type="java.lang.String">
        <value>cf</value>
        <value>lol</value>
        <value>王者荣耀</value>
    </set>
</property>

7、Null注入

<!--Null注入-->
<property name="girlFiend">
    <null/>
</property>

测试

@Test
public void test(){
    String resource = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(resource);
    Student student = (Student) context.getBean("student");
    
    System.out.println(student.getGirlFiend()); // null
}

8、Properties注入

 <!--
    Properties注入
    key=value
-->
<property name="props">
    <props>
        <prop key="学号">1001</prop>
        <prop key="班级">2017</prop>
        <prop key="专业">计算机</prop>
        <prop key="jdbc.url">jdbcxxx</prop>
    </props>
</property>

6.4、p命名和c命名注入

1、p 命名空间注入(XML Shortcut with the p-namespace)

新建一个没有有参构造器的实体类User

/**
 * @description: User实体类
 * @author: laizhenghua
 * @date: 2020/11/14 18:51
 */
public class User {
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }
    // 注意没有有参实体类
    ...
}

编写配置文件,需要在头文件中加入约束文件

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

userBean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--p名称空间注入属性-->
    <!--P(属性: properties)命名空间 , 属性依然要设置set方法-->
    <bean id="user" class="com.howie.pojo.User" p:name="alex" p:age="22"/>
</beans>

测试方法

@Test
public void userTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("userBean.xml");
    User user = (User) context.getBean("user");
    System.out.println(user); // User{name='alex', age=22}
}

2、c 命名空间注入 (XML Shortcut with the c-namespace)

编写配置文件时,需要在头文件中加入约束文件!

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

userBean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--c名称空间注入属性-->
    <!--C(构造: Constructor)命名空间 , 属性依然要设置set方法-->
    <bean id="user" class="com.howie.pojo.User" c:name="alex" c:age="21"/>
</beans>

发现问题:爆红了,刚才我们没有写有参构造!

解决:把有参构造器加上,这里也能知道,c 就是所谓的构造器注入!

测试:

@Test
public void userTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("userBean.xml");
    User user = (User) context.getBean("user");
    System.out.println(user); // User{name='alex', age=21}
}

小结:

p命名(set注入)和c命名(构造器注入)空间不能直接使用,需要导入xml约束!

xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"

6.5、Bean的作用域

在Spring中,那些组成应用程序的主体及由Spring IOC容器所管理的对象,被称之为bean。简单地讲,bean就是由IOC容器初始化、装配及管理的对象。

在这里插入图片描述
几种作用域中,request、session作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架),只能用在基于web的Spring ApplicationContext环境。

1、singleton(单例)-spring默认机制

当一个bean的作用域为Singleton,那么Spring IoC容器中只会存在一个共享的bean实例,并且所有对bean的请求,只要id与该bean定义相匹配,则只会返回bean的同一实例。

Singleton是单例类型,就是在创建起容器时就同时自动创建了一个bean的对象,不管你是否使用,他都存在了,每次获取到的对象都是同一个对象。

注意,Singleton作用域是Spring中的缺省作用域。要在XML中将bean定义成singleton,可以这样配置:

<!--c名称空间注入属性,scope作用域默认就是singleton,可以不写-->
<bean id="user" class="com.howie.pojo.User" c:name="alex" c:age="21" scope="singleton"/>

测试:

@Test
public void userTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("userBean.xml");
    User user1 = (User) context.getBean("user");
    User user2 = (User) context.getBean("user");
    System.out.println(user1 == user2); // true
}

2、Prototype(原型)

Prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。

<bean id="user" class="com.howie.pojo.User" c:name="alex" c:age="21" scope="prototype"/>
或者
<bean id="user" class="com.howie.pojo.User" c:name="alex" c:age="21" singleton="false"/>

测试:

@Test
public void userTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("userBean.xml");
    User user1 = (User) context.getBean("user");
    User user2 = (User) context.getBean("user");
    System.out.println(user1 == user2); // false
}

3、其余的request、session、application、这些个只能在web开发中使用到!

7、Bean的自动装配

7.1、自动装配说明

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

Spring中bean有三种装配机制,分别是:

1.在xml中显式配置;

2.在java中显式配置;

3.隐式的bean发现机制和自动装配。

这里我们主要讲第三种:自动化的装配bean。

Spring的自动装配需要从两个角度来实现,或者说是两个操作:

1、组件扫描(component scanning):spring会自动发现应用上下文中所创建的bean;

2、自动装配(autowiring):spring自动满足bean之间的依赖,也就是我们说的IoC/DI;

组件扫描和自动装配组合发挥巨大威力,使得显示的配置降低到最少。

7.2、测试环境搭建

1、新建一个普通maven项目,注意导入需要的包

2、编写实体类

实体Cat

/**
 * @description: Cat实体类
 * @author: laizhenghua
 * @date: 2020/11/15 13:33
 */
public class Cat {
    public void shout(){
        System.out.println("喵~");
    }
}

实体类Dog

/**
 * @description: Dog实体类
 * @author: laizhenghua
 * @date: 2020/11/15 13:35
 */
public class Dog {
    public void shout(){
        System.out.println("旺~");
    }
}

实体类Person

/**
 * @description: Person实体类
 * @author: laizhenghua
 * @date: 2020/11/15 13:36
 */
public class Person {
    private Cat cat;
    private Dog dog;
    private String message;

    public Cat getCat() {
        return cat;
    }
    ...
}

3、编写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">

    <!--配置cat和dog类-->
    <bean id="cat" class="com.howie.pojo.Cat"/>
    <bean id="dog" class="com.howie.pojo.Dog"/>

    <bean id="person" class="com.howie.pojo.Person">
        <property name="cat" ref="cat"/>
        <property name="dog" ref="dog"/>
        <property name="message" value="hello spring"/>
    </bean>
</beans>

4、测试

@Test
public void test(){
    ApplicationContext context = new ClassPathXmlApplicationContext("personBean.xml");
    Person person = context.getBean("person", Person.class);
    System.out.println(person);
    
	person.getCat().shout();
	person.getDog().shout();
}

一个人有两个宠物!结果正常输出,环境OK

7.3、byName自动装配

autowire byName (按名称自动装配)

由于在手动配置xml过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。

采用自动装配将避免这些错误,并且使配置简单化。

测试:

1、修改bean配置,增加一个属性 autowire=“byName”

<!--
    byName: 会自动在容器上下文中查找,和自己对象set方法后面的值对应的beanId
-->
<bean id="person" class="com.howie.pojo.Person" autowire="byName">
    <property name="dog" ref="dog"/>
    <property name="message" value="hello spring"/>
</bean>

2、再次测试,结果依旧成功输出!

3、我们将 cat 的bean id修改为 catXXX

4、再次测试, 执行时报空指针java.lang.NullPointerException。因为按byName规则找不对应set方法,真正的setCat就没执行,对象就没有初始化,所以调用时就会报空指针错误。

小结:

当一个bean节点带有 autowire byName的属性时。

① 将查找其类中所有的set方法名,例如setCat,获得将set去掉并且首字母小写的字符串,即cat。

② 去spring容器中寻找是否有此字符串名称id的对象。

③ 如果有,就取出注入;如果没有,就报空指针异常。

7.4、byType自动装配

使用autowire byType首先需要保证:同一类型的对象,在spring容器中唯一。如果不唯一,会报不唯一的异常。

NoUniqueBeanDefinitionException
<!--
    byType: 会自动在容器上下文中查找,和自己对象对象属性类型相同的bean
-->
<bean id="person" class="com.howie.pojo.Person" autowire="byType">
    <property name="dog" ref="dog"/>
    <property name="message" value="hello spring"/>
</bean>

1、测试,可以正常输出!ok

2、我们再来注册一个类型为 cat 的 bean。此时cat类型的bean在容器中已不唯一

3、这时,我们发现,xml 配置文件已爆红!所以说使用byType自动装配时,对象属性的要注入的bean必须唯一才能注入成功!

7.5、使用注解实现自动装配

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

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

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

2、开启属性注解支持!

<context:annotation-config/>

3、完整xml配置文件实例!

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

1、关于@Autowired的说明 ?

  • @Autowired可以对成员变量、方法和构造函数进行标注,来完成自动装配的工作,这里必须明确:@Autowired是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Qualifier使用,并且不支持id匹配。

2、如何使用@Autowired ?

  • 需要导入 spring-aop的maven依赖!
  • 直接在属性上使用即可!也可以在set方式上使用!
    使用Autowired我们可以不用编写Set方法了,前提是你这个自动装配的属性在IOC (Spring))容器中存在,且符合名字byname!

3、xml配置文件

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

<!--配置cat和dog类-->
<bean id="cat" class="com.howie.pojo.Cat"/>
<bean id="dog" class="com.howie.pojo.Dog"/>

<bean id="person" class="com.howie.pojo.Person"/>
<!--开启注解支持-->
<context:annotation-config/>
</beans>

4、@Autowired的使用

/**
 * @description: Person实体类
 * @author: laizhenghua
 * @date: 2020/11/15 13:36
 */
public class Person {
    @Autowired
    private Cat cat;
    @Autowired
    private Dog dog;
    @Value("Java天下第一!")
    private String message;
    ...
    // 可以没有set方法
}

5、测试

 @Test
 public void test(){
     ApplicationContext context = new ClassPathXmlApplicationContext("personBean.xml");
     Person person = context.getBean("person", Person.class);
     // System.out.println(person);
     person.getCat().shout(); // 喵~
     person.getDog().shout(); // 旺~
     System.out.println(person.getMessage()); // Java天下第一!
 }

扩展:@Autowired(required=false) 说明:false,对象可以为null;true,对象必须存对象,不能为null。

//如果允许对象为null,设置required = false,默认为true
@Autowired(required = false)
private Cat cat;
@Qualifier
  • @Autowired是根据类型自动装配的,加上@Qualifier则可以根据byName的方式自动装配
  • @Qualifier不能单独使用。

测试实验步骤:

1、配置文件修改内容,保证类型存在对象。且名字不为类的默认名字!

<!--配置cat和dog类-->
<bean id="cats" class="com.howie.pojo.Cat"/>
<bean id="cat123" class="com.howie.pojo.Cat"/>
<bean id="dogs" class="com.howie.pojo.Dog"/>
<bean id="dog123" class="com.howie.pojo.Dog"/>

2、没有加Qualifier测试,直接报错

3、在属性上添加Qualifier注解

/**
 * @description: Person实体类
 * @author: laizhenghua
 * @date: 2020/11/15 13:36
 */
public class Person {
    @Autowired
    @Qualifier(value = "cat123") // 指明此属性注入的bean是cat123
    private Cat cat;
    @Autowired
    @Qualifier(value = "dog123") // 指明此属性注入的bean是dog123
    private Dog dog;
    ...
}

测试,成功输出!

@Resource

说明:此注解并不是Spring的,而是Java的(import javax.annotation.Resource)

  • @Resource如有指定的name属性,先按该属性进行byName方式查找装配;
  • 其次再进行默认的byName方式进行装配;
  • 如果以上都不成功,则按byType的方式自动装配。
  • 都不成功,则报异常。

实体类:

/**
 * @description: Person实体类
 * @author: laizhenghua
 * @date: 2020/11/15 13:36
 */
public class Person {
    //如果允许对象为null,设置required = false,默认为true
    @Resource
    private Cat cat;
    @Resource(name = "dog123")
    private Dog dog;
    private String message;
    ...
}

xml配置文件

<!--配置cat和dog类-->
<bean id="cat" class="com.howie.pojo.Cat"/>
<bean id="cat123" class="com.howie.pojo.Cat"/>
<bean id="dog123" class="com.howie.pojo.Dog"/>
<bean id="dogs" class="com.howie.pojo.Dog"/>

测试:结果OK

@Autowired与@Resource的异同

同:

  • 都可以实现自动装配bean,都可以放在属性字段上或setter上

异:

  • @Autowired是spring定义,而@Resource是Java自己的,来源不同
  • @Autowired默认按类型装配(属于spring规范),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用
  • @Resource默认通过byname的方式实现,如果找不到名字,则通过byType实现!如果两个都找不到的情况下,就报错!
  • 它们的作用相同都是用注解方式注入对象,但执行顺序不同。@Autowired先byType,@Resource先byName。

8、使用注解开发

8.1、bean的实现

说明:前面我们已经知道,spring4之后,想要使用注解形式,必须得要引入aop的包。
在这里插入图片描述
在配置文件当中,还得要引入一个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
        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>

我们之前都是使用 bean 的标签进行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: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/>
    <!--指定要扫描的包,这个包下的注解就会生效-->
    <context:component-scan base-package="com.howie.pojo"/>
</beans>

2、在指定包下编写类,增加注解

/**
 * @description: User实体类
 * @author: laizhenghua
 * @date: 2020/11/14 18:51
 */
@Component // 此注解等价于<bean id="user" class="com.howie.pojo.User"/>
public class User {
    @Value("alex")
    private String name;
    private Integer age;
    ...
}

3、测试

@Test
public void test(){
    String path = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(path);
    User user = context.getBean(User.class);
    System.out.println(user.getName()); // alex
}

8.2、属性注入

使用注解注入属性

1、可以不用提供set方法,直接在属性名上添加@value(“值”)

/**
 * @description: User实体类
 * @author: laizhenghua
 * @date: 2020/11/14 18:51
 */
@Component // 此注解等价于<bean id="user" class="com.howie.pojo.User"/>
public class User {
    @Value("alex")
    private String name;
    @Value(value = "21")
    private Integer age;
    ...
}

2、如果提供了set方法,在set方法上添加@value(“值”)即可;

3、测试

@Test
public void test(){
    String path = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(path);
    User user = context.getBean(User.class);
    System.out.println(user); // User{name='alex', age=21}
}

8.3、衍生注解

我们这些注解,就是替代了在配置文件当中配置步骤而已!更加的方便快捷!

1、@Component三个衍生注解

为了更好的进行分层,Spring可以使用其它三个注解,功能一样,目前使用哪一个功能都一样。

@Controller:web层

@Service:service层

@Repository:dao层

2、写上这些注解,就相当于将这个类交给Spring管理装配了!

3、使用这些注解时,我们还需要在配置文件中,修改扫描包的范围!

<!--指定要扫描的包,这个包下的注解就会生效-->
<context:component-scan base-package="com.howie"/>

8.4、自动装配注解

@Autowired、@Qualifire等前面已经讲过了

8.5、作用域

@scope(value = “singleton”)

singleton:默认的,Spring会采用单例模式创建这个对象。关闭工厂 ,所有的对象都会销毁。

prototype:多例模式。关闭工厂 ,所有的对象不会销毁。内部的垃圾回收机制会回收。

/**
 * @description: User实体类
 * @author: laizhenghua
 * @date: 2020/11/14 18:51
 */
@Component // 此注解等价于<bean id="user" class="com.howie.pojo.User"/>
@Scope(value = "singleton") // 设置作用域类别,这里设置为单例
public class User {
    @Value("alex")
    private String name;
    ...
}

8.6、小结

XML与注解比较

  • XML可以适用任何场景 ,结构清晰,维护方便

  • 注解不是自己提供的类使用不了,开发简单方便

xml与注解整合开发 :推荐最佳实践

  • xml管理Bean

  • 注解完成属性注入

  • 使用过程中, 可以不用扫描,扫描是为了类上的注解

<context:annotation-config/>  

作用:

  • 进行注解驱动注册,从而使注解生效

  • 用于激活那些已经在spring容器里注册过的bean上面的注解,也就是显示的向Spring注册

  • 如果不扫描包,就需要手动配置bean

  • 如果不加注解驱动,则注入的值为null!

9、基于Java类进行配置(完全注解)

现在我们要舍弃Spring的xml配置,全权交给Java进行配置,实现xml零配置。

9.1、JavaConfig

JavaConfig 原来是 Spring 的一个子项目,它通过 Java 类的方式提供 Bean 的定义信息,在 Spring4 的版本, JavaConfig 已正式成为 Spring4 的核心功能 。

9.2、实现完全注解

@Configuration和@Bean是实现完全注解的关键!

1、新建实体类User

/**
 * @description: User实体类
 * @author: laizhenghua
 * @date: 2020/11/15 19:29
 */
@Component // 将这个类标注为Spring的一个组件,放到容器中!
public class User {
    public String name = "alex"; // 显示赋值
}

2、新建一个config配置包,编写MyConfig配置类,替代 xml 配置文件

/**
 * @description: 配置文件类
 * @author: laizhenghua
 * @date: 2020/11/15 19:35
 */
/*@Configuration这个也会Spring容器托管,注册到容器中,因为他本来就是一个@Component*/
@Configuration // 代表这是一个配置类
@ComponentScan(value = "com.howie.pojo") // 配置扫描包,可写可不写
public class MyConfig {
    // @Bean只写在方法上,返回的是一个对象,但是一般不获取已经在容器中存在的对象。
    // 通过方法注册一个bean,这里的返回值就Bean的类型,方法名就是bean的id!
    @Bean
    public User user(){
        return new User();
    }
}

3、测试

@Test
public void test(){
    // 加载配置类,通过AnnotationConfig上下文来获取容器,通过配置类的cLass对象加载
    ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
    User user = context.getBean("user", User.class);
    // 打印user的name属性
    System.out.println(user.name); // alex
}

小结:我们发现上面的例子,我们完全没有使用xml配置文件,而是通过@Configuration和@Bean注解创建配置类,实现了bean的注入与获取,这就是完全注解实现!

9.3、导入其他配置类

还有一个问题,我们想导入其他配置类该如何做呢?

1、我们再编写一个配置类!

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

2、在之前的配置类中我们来选择导入这个配置类

@Configuration
@Import(MyConfig2.class)  //导入合并其他配置类,类似于配置文件中的 inculde 标签
public class MyConfig {

   @Bean
   public User user(){
       return new User();
  }
}

关于这种Java类的配置方式,我们在之后的SpringBoot 和 SpringCloud中还会大量看到,我们需要知道这些注解的作用即可!

10、静态代理与动态代理

10.1、代理模式

设计模式:是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。

设计模免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。”套路”

在之前的学习中,不知道小伙伴们有没有学习过代理模式呢?代理模式在Java开发中扮演很重要的角色!

代理设计就是为其他对象提供一种代理赖控制对这个对象的访问!

AOP的底层机制就是动态代理!在认识AOP之前我们先要了解代理模式!

代理模式又分为:

  • 静态代理
  • 动态代理

在这里插入图片描述

10.2、静态代理

静态代理角色分析:

  • 抽象角色:一般使用接口或者抽象类来实现
  • 真实角色:被代理的角色
  • 代理角色:代理真实角色 ,代理真实角色后 , 一般会做一些附属的操作
  • 客户 : 使用代理角色来进行一些操作

代码实现:

Rent.java 即抽象角色。

/**
 * @description: // 抽象角色:租房接口
 * @author: laizhenghua
 * @date: 2020/11/15 22:11
 */
public interface Rent {
    void rent(); // 出租房屋
}

Host.java 即真实角色(被代理的角色)

/**
 * @description: 房东
 * @author: laizhenghua
 * @date: 2020/11/15 22:12
 */
public class Host implements Rent{
    public void rent() {
        System.out.println("房东要出租房子!");
    }
}

Proxy.java 即代理角色,可以理解为中间商(增加附属操作,如打扫房间签合同)

/**
 * @description: 代理类,中间商
 * @author: laizhenghua
 * @date: 2020/11/15 22:17
 */
public class Proxy implements Rent{
    private Host host;
    public Proxy(Host host){
        this.host = host;
    }

    public void rent(){
        clear();
        host.rent();
        signContract();
    }
    /*
     * @description: 增加的附属操作,打扫房间
     * @author: laizhenghua
     * @date: 2020/11/16 12:30
     * @param:
     * @return: void
     */
    public void clear(){
        System.out.println("我是中间商,我已经帮你打扫好房子了,你要不要出租房子!");
    }
    /*
     * @description: 签合同
     * @author: laizhenghua
     * @date: 2020/11/16 9:25
     * @param:
     * @return: void
     */
    public void signContract(){
        System.out.println("签合同!");
    }
}

Client.java 即客户

/**
 * @description: 客户类,一般客户都会去找代理!
 * @author: laizhenghua
 * @date: 2020/11/15 22:15
 */
public class Client {
    public static void main(String[] args) {
        // 房东要租房子
        Host host = new Host();
        // 中介(代理),帮房东出租房子,但是代理会有一些附属操作,比如打扫房间(clear)
        Proxy proxy = new Proxy(host);
        // 你不用面对房东,直接找中介
        proxy.rent();
    }
}

1、分析

在这个过程中,你直接接触的就是中介,就如同现实生活中的样子,你看不到房东,但是你依旧租到了房东的房子通过代理,这就是所谓的代理模式,程序源自于生活,所以学编程的人,一般能够更加抽象的看待生活中发生的事情。

2、静态代理的好处:

  • 可以使得我们的真实角色更加纯粹,不再去关注一些公共的事情
  • 公共的业务由代理来完成,实现了业务的分工
  • 公共业务发生扩展时变得更加集中和方便

3、缺点:

  • 类多了,多了代理类,工作量变大了,开发效率降低

OK,到了现在代理模式大家应该都没有什么问题了,重点大家需要理解其中的思想:

我们在不改变原来的代码的情况下,实现了对原有功能的增强,这是AOP中最核心的思想

4、聊聊AOP:纵向开发,横向开发

在这里插入图片描述

10.3、动态代理

我们已知道静态代理的好处与缺点,那么我们现在想要静态代理的好处,又不想要静态代理的缺点,有没有更好的解决方案呢?答案就是动态代理!

  • 首先动态代理的角色和静态代理的角色划分一样!
  • 动态代理的代理类是动态生成的,静态代理的代理类是我们提前写好的!
  • 动态代理分为两大类:①基于接口的动态代理(JDK动态代理)。②基于类的动态代理(CGLIB)。还有一种就是使用Java字节码实现(Javassist)

这里我们主要掌握JDK的动态代理!,JDK动态代理有两个核心类分别是 InvocationHandler(调用处理程序)Proxy(代理类)

关于动态代理的学习不是很友好!这里建议反复观看视频自己学习。

视频讲解地址:https://www.bilibili.com/video/BV1WE411d7Dv?p=19

动态代理的好处:

  • 静态代理有的它都有,静态代理没有的,它也有!
  • 可以使得我们的真实角色更加纯粹,不再去关注一些公共的事情
  • 公共的业务由代理来完成 ,实现了业务的分工
  • 公共业务发生扩展时变得更加集中和方便
  • 一个动态代理 , 一般代理某一类业务
  • 一个动态代理可以代理多个类,代理的是接口!

11、AOP编程

上面我们了解l代理模式,这是AOP的基础,一定要先搞懂它!接下来我们就要进入正题,进行AOP的学习!

11.1、什么是AOP

AOPAspect 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织入,需要导入一个依赖包!

<!--aspectjWeaver-->
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
    <scope>runtime</scope>
</dependency>
① 第一种方式:使用Spring API

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

/**
 * @description: userService接口
 * @author: laizhenghua
 * @date: 2020/11/16 18:25
 */
public interface UserService {
    int add();
    int delete();
    int update();
    void query();
}

实现类

/**
 * @description: UserService接口实现类
 * @author: laizhenghua
 * @date: 2020/11/16 18:28
 */
public class UserServiceImpl implements UserService {

    public int add() {
        System.out.println("增加了一个用户");
        return 0;
    }

    public int delete() {
        System.out.println("删除了一个用户");
        return 0;
    }

    public int update() {
        System.out.println("更新了一个用户");
        return 0;
    }

    public void query() {
        System.out.println("查询了一个用户");
    }
}

然后去写我们的增强类 , 我们编写两个 , 一个前置增强 一个后置增强。

1、前置增强类

/**
 * @description: 增强类,前置通知
 * @author: laizhenghua
 * @date: 2020/11/16 18:32
 */
public class BeforeLog implements MethodBeforeAdvice {
    /*
    method: 要执行目标对象的方法
    objects: 参数
    o: 目标对象
     */
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println(o.getClass().getName() + "的" + method.getName() + "被执行了!");
    }
}

2、后置增强类

/**
 * @description: 增强类,后置通知
 * @author: laizhenghua
 * @date: 2020/11/16 18:52
 */
public class AfterLog implements AfterReturningAdvice {
    /*
    returnValue: 返回值
    method被调用的方法
    args 被调用的方法的对象的参数
	target 被调用的目标对象
     */
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName() + "类,执行了方法" + method.getName() + "返回了" + returnValue);
    }
}

3、最后去spring的文件中注册,并实现aop切入实现 ,注意导入约束

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

<!--配置UserServiceImpl-->
<bean id="userService" class="com.howie.service.impl.UserServiceImpl"/>
<!--配置beforeLog的bean-->
<bean id="beforeLog" class="com.howie.log.BeforeLog"/>
<!--配置afterLog的bean-->
<bean id="afterLog" class="com.howie.log.AfterLog"/>
<!--实现方式1:使用Spring原生API接口,配置AOP,需要导入AOP的约束-->
<aop:config>
    <!--
        设置切入点。
        expression:表达式。
        execution(要执行的位置 * * * * *)
        -->
    <aop:pointcut id="pointcut" expression="execution(* com.howie.service.impl.UserServiceImpl.*(..))"/>
    <!--执行环绕通知-->
    <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
    <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>

4、测试方法

@Test
public void test2(){
   String path = "applicationContext.xml";
   ApplicationContext context = new ClassPathXmlApplicationContext(path);
   // 注意点:动态代理代理的是接口
    UserService userService = (UserService) context.getBean("userService");
    userService.add();
}

5、结果

在这里插入图片描述
AOP的重要性:很重要一定要理解其中的思路,主要是思想的理解这一块。

Spring的AOP就是将公共的业务 (日志,安全等) 和领域业务结合起来,当执行领域业务时,将会把公共业务加进来实现公共业务的重复利用,领域业务更纯粹,技术员专注领域业务,其本质还是动态代理。

6、切入点表达式(expression)总结

(1)切入点表达式作用:知道对哪个类里面的哪个方法进行增强
(2)语法结构:execution([权限修饰符] [返回类型] [类全路径] [方法名称](参数列表))
  • 举例 1:对 com.atguigu.dao.BookDao 类里面的 add 进行增强
execution(* com.atguigu.dao.BookDao.add(..))
  • 举例 2:对 com.atguigu.dao.BookDao 类里面的所有的方法进行增强
execution(* com.atguigu.dao.BookDao.* (..))
  • 举例 3:对 com.atguigu.dao 包里面所有类,类里面所有方法进行增强
execution(* com.atguigu.dao.*.* (..))
② 第二种方式:自定义类来实现AOP

目标业务类不变依旧是userServiceImpl

1、第一步 : 写我们自己的一个切入类

/**
 * @description:
 * @author: laizhenghua
 * @date: 2020/11/16 20:24
 */
public class MyPointcut {
    public void before(){
        System.out.println("===方法执行前===");
    }
    public void after(){
        System.out.println("===方法执行后===");
    }
}

2、修改配置文件

<!--配置AOP,需要导入AOP的约束-->
<!--方式二:自定义类实现AOP-->
<bean id="myPointcut" class="com.howie.mydi.MyPointcut"/><!--注册bean-->
<aop:config>
    <!--自定义切面。ref: 要引入的类-->
    <aop:aspect ref="myPointcut">
        <!--切入点-->
        <aop:pointcut id="point" expression="execution(* com.howie.service.impl.UserServiceImpl.*(..))"/>
        <!--通知-->
        <aop:before method="before" pointcut-ref="point"/>
        <aop:after method="after" pointcut-ref="point"/>
    </aop:aspect>
</aop:config>

3、测试

@Test
public void test2(){
   String path = "applicationContext.xml";
   ApplicationContext context = new ClassPathXmlApplicationContext(path);
   // 注意点:动态代理代理的是接口
    UserService userService = (UserService) context.getBean("userService");
    userService.add();
}

结果

在这里插入图片描述

③ 第三种方式:使用注解

相关注解介绍:


@Aspect:作用是把当前类标识为一个切面供容器读取
 
@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 publicvoid型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
@Around:环绕增强,相当于MethodInterceptor
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice

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

@Aspect
public class AnnotationPointcut {
   @Before("execution(* com.kuang.service.UserServiceImpl.*(..))")
   public void before(){
       System.out.println("---------方法执行前---------");
  }

   @After("execution(* com.kuang.service.UserServiceImpl.*(..))")
   public void after(){
       System.out.println("---------方法执行后---------");
  }

   @Around("execution(* com.kuang.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.kuang.config.AnnotationPointcut"/>
<aop:aspectj-autoproxy/>

aop:aspectj-autoproxy:说明

通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配
置@aspectJ切面的bean创建代理,织入切面。当然,spring 在内部依旧采用
AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体
实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了

<aop:aspectj-autoproxy />有一个proxy-target-class属性,默认为false,
表示使用jdk动态代理织入增强,当配为<aop:aspectj-autoproxy  poxy-
target-class="true"/>时,表示使用CGLib动态代理技术织入增强。不过即使
proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动
使用CGLib动态代理。

12、整合MyBatis

12.1、导入相关maven依赖

1、junit

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

2、MyBatis

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

3、mysql数据库

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>

4、spring相关

<!--Spring相关-->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
<!--Spring连接数据库,还需要Spring-jdbc的包-->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-jdbc</artifactId>
   <version>5.2.10.RELEASE</version>
</dependency>

5、aspectJ AOP 织入器

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

6、mybatis-spring整合包(重点)

<dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis-spring</artifactId>
   <version>2.0.2</version>
</dependency>

7、配置Maven静态资源过滤问题!

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

12.2、整合实现方式1

整合之前我们还需要了解一些MyBatis-Spring的知识!

MyBatis-Spring官方地址:http://mybatis.org/spring/zh/index.html

1、编写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"
       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">

2、配置数据源替换mybaits的数据源

<!--引入外部属性文件-->
<context:property-placeholder location="classpath:db.properties"/>
<!--
    DataSource: 使用Spring的数据源替换MyBatis的。比如c3p0,druid,dbcp
    这里使用Spring提供的JDBC (org.springframework.jdbc.datasource.DriverManagerDataSource)
-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${prop.driver}"/>
    <property name="url" value="${prop.url}"/>
    <property name="username" value="${prop.username}"/>
    <property name="password" value="${prop.password}"/>
</bean>

3、配置SqlSessionFactory,关联MyBatis

<!--SqlSessionFactory:-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <!--绑定MyBatis配置文件,此时MyBatis配置里只有 别名(typeAliases) 和 设置(settings) 的配置了-->
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <!--配置Mapper映射文件-->
    <property name="mapperLocations" value="classpath:com/howie/dao/*.xml"/>
</bean>

4、注册sqlSessionTemplate,关联sqlSessionFactory

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

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

/**
 * @description: UserDao接口实现类
 * @author: laizhenghua
 * @date: 2020/11/17 13:03
 */
public class UserDaoImpl implements UserDao {
    // 我们所有的操作都要使用sqlSession来完成,sqlSession不用我们自己创建了,Spring来管理
    private SqlSessionTemplate sqlSession;

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

    public List<User> getUserList() {
        UserDao dao = sqlSession.getMapper(UserDao.class);
        return dao.getUserList();
    }
}

6、注册bean实现

<!--UserDaoImpl-->
<bean id="userDao" class="com.howie.dao.impl.UserDaoImpl">
    <property name="sqlSession" ref="sqlSession"/>
</bean>

7、测试

@Test
public void test2(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserDao userDao = context.getBean("userDao", UserDaoImpl.class);
    List<User> userList = userDao.getUserList();
    for (User user : userList){
        System.out.println(user);
    }
}

输出结果

在这里插入图片描述
结果成功输出!现在我们的Mybatis配置文件的状态!发现都可以被Spring整合!

<?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">
<!--Mybatis核心配置文件-->
<configuration>
    <!--配置别名-->
    <typeAliases>
        <package name="com.howie.pojo"/>
    </typeAliases>
    <!--设置-->
</configuration>

12.3、整合实现方式2

mybatis-spring1.2.3版以上的才有这个

dao继承Support类 ,直接利用 getSqlSession() 获得 , 然后直接注入SqlSessionFactory 比起方式1 , 不需要管理SqlSessionTemplate , 而且对事务的支持更加友好 ,可跟踪源码查看。

官方地址:http://mybatis.org/spring/zh/sqlsession.html#SqlSessionDaoSupport

1、将我们上面写的UserDaoImpl修改一下

public class UserDaoImpl extends SqlSessionDaoSupport implements UserMapper {
   public List<User> getUserList() {
       UserDao userDao = getSqlSession().getMapper(UserDao.class);
       return userDao.getUserList();
  }
}

2、修改bean的配置

<bean id="userDao" class="com.howie.dao.impl.UserDaoImpl">
   <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

3、测试

public void test2(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserDao userDao = context.getBean("userDao", UserDaoImpl.class);
    List<User> userList = userDao.getUserList();
    for (User user : userList){
        System.out.println(user);
    }
}

总结 : 整合到spring以后可以完全不要mybatis的配置文件,除了这些方式可以实现整合之外,我们还可以使用注解来实现,这个等我们后面学习SpringBoot的时候还会测试整合!

13、Spring中的事务

13.1、回顾事务

1、事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。

2、事务处理(事务操作):保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态。-- 事务处理原则

3、为确保数据库中数据的一致性,数据的操纵应当是离散的成组的逻辑单元:当它全部完成时,数据的一致性可以保持,而当这个单元中的一部分操作失败,整个事务应全部视为错误,所有从起始点以后的操作应全部回退到开始状态。

4、事务的ACID(acid)属性

  • 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么 都发生,要么都不发生。
  • 一致性(Consistency) 事务必须使数据库从一个一致性状态变换到另外一个一致性状态 。
  • 隔离性(Isolation) 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个 事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是 永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。防止数据损坏!

总结下来就是,事务就是要么都成功要么都失败!事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!

13.2、搭建测试环境

在之前的案例(spring整合MyBatis)中,我们给userDao接口新增两个方法,删除和增加用户。

// 添加一个用户
int addUser(User user);
// 删除一个用户
int deleteUser(Integer id);
// 测试方法,分别要执行添加与删除
void test();

mapper映射文件中,我们故意把 deletes 写错。

<!--添加-->
<insert id="addUser" parameterType="User">
    insert into test.user(id,name,password,perms,email,birthday)
    value(#{id},#{name},#{password},#{perms},#{email},#{birthday})
</insert>
<!--删除,故意写错delete-->
<delete id="deleteUser" parameterType="int">
    deletes from test.user where id = #{id}
</delete>

编写接口的实现类

/**
 * @description: UserDao接口实现类
 * @author: laizhenghua
 * @date: 2020/11/17 13:03
 */
public class UserDaoImpl implements UserDao {
    // 我们所有的操作都要使用sqlSession来完成,sqlSession不用我们自己创建了,Spring来管理
    private SqlSessionTemplate sqlSession;

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

    public List<User> getUserList() {
        UserDao dao = sqlSession.getMapper(UserDao.class);
        return dao.getUserList();
    }
	// 添加用户
    public int addUser(User user) {
        UserDao dao = sqlSession.getMapper(UserDao.class);
        return dao.addUser(user);
    }
	// 删除用户
    public int deleteUser(Integer id) {
        UserDao dao = sqlSession.getMapper(UserDao.class);
        return dao.deleteUser(id);
    }
    
    // 测试方法
    public void test() {
        UserDao dao = sqlSession.getMapper(UserDao.class);

        // 新建用户
        User user = new User();
        user.setId(4);
        user.setName("Java");
        user.setPassword("123");

        dao.addUser(user); // 执行添加操作
        dao.deleteUser(4); // 执行删除操作
    }
}

编写测试方法

@Test
public void test3(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserDao userDao = context.getBean("userDao", UserDaoImpl.class);
    userDao.test(); // 执行测试方法
}

报错:sql异常,delete写错了

结果 :新用户添加成功!

没有进行事务的管理;我们想让他们都成功才成功,有一个失败,就都失败,我们就应该需要事务

以前我们都需要自己手动管理事务,十分麻烦!

但是Spring给我们提供了事务管理,我们只需要配置即可;

13.3、Spring中的事务管理

官方文档地址:http://mybatis.org/spring/zh/transactions.html

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

1、编程式事务管理(这里不讲,可查看官方文档自行学习)

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

2、声明式事务管理(AOP风格)

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

3、在Spring中配置事务

使用Spring管理事务,注意头文件的约束导入 : tx

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

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

要开启 Spring 的事务处理功能,在 Spring 的配置文件中创建一个 DataSourceTransactionManager 对象。

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

事务管理器(transactionManager)

  • 无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。
  • 就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。

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

<!--结合AOP实现事务的织入-->
<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!--给那些方法配置事务-->
    <!--propagation:配置事务的传播属性,新特性-->
    <tx:attributes>
        <!--
        <tx:method name="test" propagation="REQUIRED"/>
        <tx:method name="deleteUser" propagation="REQUIRED"/>
        <tx:method name="addUser" propagation="REQUIRED"/>
        -->
        <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

注意:导入aop的头文件!

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

测试

删掉刚才插入的数据,再次测试!

@Test
public void test3(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserDao userDao = context.getBean("userDao", UserDaoImpl.class);
    userDao.test();
}

我们发现,程序虽然报错了,但是数据并没有像刚才那样插入数据库!这就是我们希望看到的,当程序发生异常或产生错误,我们应该让事务回滚!回到最初的状态!

完整applicationContext.xml配置文件包括整合MyBatis和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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--引入外部属性文件-->
    <context:property-placeholder location="classpath:db.properties"/>
    <!--
        DataSource: 使用Spring的数据源替换MyBatis的。比如c3p0,druid,dbcp
        这里使用Spring提供的JDBC (org.springframework.jdbc.datasource.DriverManagerDataSource)
    -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${prop.driver}"/>
        <property name="url" value="${prop.url}"/>
        <property name="username" value="${prop.username}"/>
        <property name="password" value="${prop.password}"/>
    </bean>
    <!--SqlSessionFactory:-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!--绑定MyBatis配置文件,此时MyBatis配置里只有 别名(typeAliases) 和 设置(settings) 的配置了-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!--配置Mapper映射文件-->
        <property name="mapperLocations" value="classpath:com/howie/dao/*.xml"/>
    </bean>
    <!--
    sqlSession
    SqlSessionTemplate: 就是我们使用的sqlSession
    -->
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <!--只能使用构造器注入,因为没有set方法-->
        <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>
    <!--UserDaoImpl-->
    <bean id="userDao" class="com.howie.dao.impl.UserDaoImpl">
        <property name="sqlSession" ref="sqlSession"/>
    </bean>

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

    <!--结合AOP实现事务的织入-->
    <!--配置事务通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--给那些方法配置事务-->
        <!--propagation:配置事务的传播属性,新特性-->
        <tx:attributes>
            <!--
            <tx:method name="test" propagation="REQUIRED"/>
            <tx:method name="deleteUser" propagation="REQUIRED"/>
            <tx:method name="addUser" propagation="REQUIRED"/>
            -->
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!--配置事务切入-->
    <aop:config>
        <!--pointcut: 切入点-->
        <aop:pointcut id="txPointcut" expression="execution(* com.howie.dao.*.*(..))"/>
        <!--advisor: 通知-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>
</beans>

为什么需要事务?

  • 如果不配置事务,可能存在数据提交不一致的情况下;
  • 如果我们不在SPRING中去配置声明式事务,我们就需要在代码中手动配置事务!
  • 事务在项目的开发中十分重要,设计到数据的一致性和完整性问题,不容马虎!

end

Thank you for watching!

end

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lambda.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值