Spring详解

Spring 详解

1. 简介

  • Spring:春天 --------->> 为软件行业带来了春天
  • 2002年,首次推出 Spring 的雏形:interface21 框架
  • 2004年3月24日,Spring 正式发布 1.0 版本
  • Rod Johnson 是 Spring 框架的创始人
  • Spring 理念:使现有的技术更容易使用,整合了现有的技术框架

1.1 准备工作

  • 官网:Spring - 官网

  • GitHub:Spring - GitHub

  • 导包

    <!-- spring-webmvc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.7</version>
    </dependency>
    <!-- spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.7</version>
    </dependency>
    

1.2 优点

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

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

1.3 组成

  1. spring core:提供了spring 的核心功能,BeanFactory 是 spring 核心容器的主要组件,它通过 Ioc 把程序的配置和依赖性与实际的代码分开,是整个 spring 的基础。

  2. spring context:通过配置文件向 spring 提供上下文信息,它构建在 BeanFactory 之上,另外增加了国际化和资源访问等功能。

  3. spring dao:提供了一个简单有效的 JDBC 应用。

  4. spring aop:提供了面向方面编程的功能。

  5. spring orm:spring除了有自己的 JDBC 以外还提供了对其他 ORM 框架的支持,如 Hibernate,都可以和 spring 进行良好的结合。

  6. spring web:提供了简化的处理多部分请求以及把请求参数绑定到域的任务。

  7. spring MVC:提供了 MVC2 模式的实现,也可以和 struts 良好的集成在一起。

1.4 拓展

  • Spring Boot:
    • 一个快速开发的脚手架
    • 基于 Spring Boot 可以快速开发单个微服务
    • 约定大于配置
  • Spring Cloud:
    • 基于 Spring Boot 实现

1.5 弊端

  • 发展了太久之后,违背了当初的设计理念
  • 人称 “配置地狱”

2. IOC 的理论推导

此前,当我们想要实现一个业务时,需要以下几个部分:

  1. Dao 接口
  2. Dao 实现类
  3. Service 业务接口
  4. Service 业务实现类

示例:

UserDao.java:

package com.wmwx.dao;

public interface UserDao {
    public void getUser();
}

UserDaoImpl.java:

package com.wmwx.dao;

public class UserDaoImpl implements UserDao {
    @Override
    public void getUser() {
        System.out.println("默认获取用户数据");
    }
}

UserService.java:

package com.wmwx.service;

public interface UserService {
    public void getUser();
}

UserServiceImpl.java:

package com.wmwx.service;

import com.wmwx.dao.UserDao;
import com.wmwx.dao.UserDaoImpl;
import com.wmwx.dao.UserDaoMysqlImpl;
import com.wmwx.dao.UserDaoOracleImpl;

public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();

    @Override
    public void getUser() {
        userDao.getUser();
    }
}

测试类:

public class UserDaoTest {
    @Test
    public void getUser(){
        UserService userService = new UserServiceImpl();
        userService.getUser();
    }
}

输出结果:

默认获取用户数据

此时,输出结果没有问题,整个程序看起来也没有问题。

但是,当需求改变,需要我们新增一个实现类调用此实现类时,就要做一些相应的改动。

比如,我们想要新增一个通过 MySQL 读取数据的实现类,并调用它,则需要经过以下几步:

  1. 新增 UserDaoMysqlImpl.java:

    package com.wmwx.dao;
    
    public class UserDaoMysqlImpl implements UserDao{
        @Override
        public void getUser() {
            System.out.println("Mysql获取用户数据");
        }
    }
    
  2. 修改 UserServiceImpl.java

    package com.wmwx.service;
    
    import com.wmwx.dao.UserDao;
    import com.wmwx.dao.UserDaoImpl;
    import com.wmwx.dao.UserDaoMysqlImpl;
    import com.wmwx.dao.UserDaoOracleImpl;
    
    public class UserServiceImpl implements UserService {
        //修改此处的 new
        private UserDao userDao = new UserDaoMysqlImpl();
    
        @Override
        public void getUser() {
            userDao.getUser();
        }
    }
    
  3. 测试类的输出结果:

    Mysql获取用户数据
    

整个过程看起来也没什么问题,只是修改了 Service 实现类而已。

但是,如果我们需求再次改变,需要使用Oracle、SqlServer…来获取用户数据,就不得不面临以下的问题:

频繁修改源代码!

//每次更改需求都要更改new
//private UserDao userDao = new UserDaoImpl();
//private UserDao userDao = new UserDaoMysqlImpl();
//private UserDao userDao = new UserDaoOracleImpl();
private UserDao userDao = new UserDaoSqlserverImpl();

这是我们非常不愿意看到的事情。因此,就有了以下的解决方案:

UserSerciceImpl.java:

package com.wmwx.service;

import com.wmwx.dao.UserDao;

public class UserServiceImpl implements UserService {
    //利用set方法实现动态注入需求
    private UserDao userDao;
    public void setUserDao(UserDao userDao){
        this.userDao = userDao;
    }

    @Override
    public void getUser() {
        userDao.getUser();
    }
}

测试类:

@Test
public void getUser(){
    UserService userService = new UserServiceImpl();
    //在此处调用set方法
    ((UserServiceImpl) userService).setUserDao(new UserDaoSqlserverImpl());
    userService.getUser();
}

输出结果:

Sqlserver获取用户数据

输出结果与之前完全相同,但与之前相比,却是发生了革命性的改变

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

这种思想,从本质上解决了问题,程序员无需再去管理程序的创建。系统的耦合性大大降低,可以更加专注在业务上的实现。

这就是**控制反转(IOC)**的原型!

3. IOC 的本质

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

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

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

3.1 Hello Spring

示例:

Hello.java:

package com.wmwx.pojo;

public class Hello {
    private String str;

    public String getStr() {
        return str;
    }

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

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

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

    <!-- 直接使用Spring创建对象,在Spring中,这些都称为Bean -->
    <bean id="hello" class="com.wmwx.pojo.Hello">
        <property name="str" value="Spring" />
    </bean>

</beans>

测试类:

@Test
public void HelloTest(){
    //获取Spring的上下文对象
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    //对象已经全部放在Spring中管理了,需要时直接取出即可
    Hello hello = (Hello) context.getBean("hello");
    System.out.println(hello.toString());
}

输出结果:

Hello{str='Spring'}

过去创建对象时使用的方法:

  • 变量类型 变量名 = new 类名();
  • 例如:Hello hello = new Hello();

使用 bean 标签后的方法如下:

  • <bean id="变量名" class="类名"></bean>
  • 例如:<bean id="hello" class="Hello"></bean>
  • 使用 property 标签即可为属性赋值

在这一部分中,我们已经将控制权完全交给了 Spring 容器。无论是在实现类中,还是在测试类中,程序已经彻底不用修改了。

因此,所谓的 IOC 就是指:对象全部由 Spring 来创建、管理、装配。

3.2 优化代码

现在让我们来优化一下 2. 中的代码:

步骤如下:

  1. 添加 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 http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="mysqlImpl" class="com.wmwx.dao.UserDaoMysqlImpl" />
        <bean id="oracleImpl" class="com.wmwx.dao.UserDaoOracleImpl" />
        <bean id="sqlserverImpl" class="com.wmwx.dao.UserDaoSqlserverImpl" />
    
        <bean id="userServiceImpl" class="com.wmwx.service.UserServiceImpl">
            <!-- ref:引用Spring中容器创建好的对象 -->
            <!-- value:具体的值,基本数据类型 -->
            <property name="userDao" ref="sqlserverImpl" />
        </bean>
    
    </beans>
    
  2. 修改测试类:

    @Test
    public void getUser(){
        //获取Spring的上下文对象
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        //对象已经全部放在Spring中管理了,需要时直接取出即可
        UserServiceImpl userServiceImpl = (UserServiceImpl) context.getBean("userServiceImpl");
        userServiceImpl.getUser();
    }
    
  3. 输出结果:

    Sqlserver获取用户数据
    

成功!输出结果完全相同!

4. IOC 创建对象的方式

  1. 默认使用无参构造方法创建对象

  2. 当我们使用有参构造时,可以采用以下方法:

    • 按下标赋值:

      <!-- 按下标赋值的有参构造 -->
      <bean id="user" class="com.wmwx.pojo.User">
          <constructor-arg index="0" value="刘季恒" />
      </bean>
      
    • 按类型赋值(不建议使用)

      <!-- 按类型赋值的有参构造(不建议使用) -->
      <bean id="user" class="com.wmwx.pojo.User">
          <constructor-arg type="java.lang.String" value="刘季恒" />
      </bean>
      
    • 按参数名赋值(推荐使用)

      <!-- 按参数名赋值(推荐使用) -->
      <bean id="user" class="com.wmwx.pojo.User">
          <constructor-arg name="name" value="刘季恒" />
      </bean>
      
  3. 在配置文件加载时,容器中管理的对象就已经初始化了

    示例:

    User.java:

    public class User {
        private String name;
    
        public User(String name) {
            this.name = name;
            System.out.println(this.name+"的有参构造被创建了!");
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    

    beans.xml:

    <bean id="user" class="com.wmwx.pojo.User">
        <constructor-arg name="name" value="刘季恒" />
    </bean>
    

    测试类:

    @Test
    public void testUser(){
        //获取上下文时就已经创建对象了
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        //这时输出 "刘季恒的有参构造被创建了!"
    
        //getBean只是获取了已经创建完成的对象
        User user = (User) context.getBean("user");
    
        System.out.println(user.toString());
        //这时输出 "User{name='刘季恒'}"
    }
    

5. Spring 配置

5.1 alias 别名

示例:

beans.xml:

<!-- 如果添加了别名,也可以使用别名来获取对象 -->
<alias name="user" alias="userNew" />

测试类:

@Test
public void testUser(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user1 = (User) context.getBean("user");
    //使用别名也可以获取原对象
    User user2 = (User) context.getBean("userNew");
    System.out.println(user1.toString());
    System.out.println(user2.toString());
}

5.2 bean

示例:

beans.xml:

<!--
        id:bean的唯一标识符,也就是变量名
        class:bean对象所对应的全限定名(即:包名 + 类名)
        name:同样是别名,但可以同时设置多个别名,通过空格、逗号、分号来分割
    -->
<bean id="user2" class="com.wmwx.pojo.User" name="userTwo u2,userT;ut">
    <constructor-arg name="name" value="刘季恒" />
</bean>

测试类:

@Test
public void testUser(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user2_1 = (User) context.getBean("user2");
    System.out.println(user2_1.toString());		//输出 "User{name='刘季恒'}"
    User user2_2 = (User) context.getBean("userT");
    System.out.println(user2_2.toString());		//输出 "User{name='刘季恒'}"
    User user2_3 = (User) context.getBean("ut");
    System.out.println(user2_3.toString());		//输出 "User{name='刘季恒'}"
    User user2_4 = (User) context.getBean("u2");
    System.out.println(user2_4.toString());		//输出 "User{name='刘季恒'}"
}

5.3 import 导入

一般在团队合作开发时使用,可将多个配置文件导入合并至一个总的配置文件中(比如 applicationContext.xml)。

示例:

applicationContext.xml:

<!-- 若多个配置文件中出现同id的bean,则下面的配置文件会覆盖掉上面的 -->
<import resource="beans.xml" />
<import resource="beans2.xml" />
<import resource="beans3.xml" />

6. DI(依赖注入)

6.1 构造器注入

即:通过有参构造方法进行注入,前文已有,此处不再赘述。

6.2 Set 注入

  • Set注入:通过 setter 方法进行注入
    • 依赖:bean 对象的创建依赖于容器
    • 注入:bean 对象中的所有属性由容器来注入

示例:

  1. 复杂类型

    Address.java:

    package com.wmwx.pojo;
    
    public class Address {
        private String address;
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    
        @Override
        public String toString() {
            return "Address{" +
                    "address='" + address + '\'' +
                    '}';
        }
    }
    
  2. 真实测试对象

    Student.java:

    package com.wmwx.pojo;
    
    import java.util.*;
    
    public class Student {
        private String name;
        private Address address;
        private String[] books;
        private List<String> hobbies;
        private Map<String, String> cards;
        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> getHobbies() {
            return hobbies;
        }
    
        public void setHobbies(List<String> hobbies) {
            this.hobbies = hobbies;
        }
    
        public Map<String, String> getCards() {
            return cards;
        }
    
        public void setCards(Map<String, String> cards) {
            this.cards = cards;
        }
    
        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.toString() +
                    ", books=" + Arrays.toString(books) +
                    ", hobbies=" + hobbies +
                    ", cards=" + cards +
                    ", games=" + games +
                    ", wife='" + wife + '\'' +
                    ", info=" + info +
                    '}';
        }
    }
    
  3. 配置文件
    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">
    
        <bean id="address" class="com.wmwx.pojo.Address">
            <property name="address" value="珏国" />
        </bean>
        <bean id="student" class="com.wmwx.pojo.Student">
            <!-- 第一种:普通值注入,直接使用value -->
            <property name="name" value="吕沁" />
            <!-- 第二种:bean注入,直接使用ref -->
            <property name="address" ref="address" />
            <!-- 第三种:数组注入,使用array和value -->
            <property name="books">
                <array>
                    <value>赤红之热血</value>
                    <value>黑寂之长夜</value>
                    <value>蔚蓝之花海</value>
                    <value>纯洁之冰雪</value>
                </array>
            </property>
            <!-- 第四种:列表注入,使用list和value -->
            <property name="hobbies">
                <list>
                    <value>做体操</value>
                    <value>吃美食</value>
                    <value>打游戏</value>
                </list>
            </property>
            <!-- 第五种:Map注入,使用map和entry -->
            <property name="cards">
                <map>
                    <entry key="身份证" value="000000" />
                    <entry key="角色编号" value="005" />
                </map>
            </property>
            <!-- 第六种:Set注入,使用set和value -->
            <property name="games">
                <set>
                    <value>斗皇</value>
                    <value>我的世界</value>
                </set>
            </property>
            <!-- 第七种:null值注入,使用null -->
            <property name="wife">
                <null />
            </property>
            <!-- 第八种:property注入,使用props和prop -->
            <property name="info">
                <props>
                    <prop key="性别"></prop>
                    <prop key="年龄">23</prop>
                    <prop key="游戏ID">雪仙</prop>
                </props>
            </property>
        </bean>
    
    </beans>
    
  4. 测试类
    StudentTest.java:

    @Test
    public void studentTest(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Student student = (Student) context.getBean("student");
        System.out.println(student.toString());
    }
    
  5. 输出结果:

    Student{name='吕沁', address=Address{address='珏国'}, books=[赤红之热血, 黑寂之长夜, 蔚蓝之花海, 纯洁之冰雪], hobbies=[做体操, 吃美食, 打游戏], cards={身份证=000000, 角色编号=005}, games=[斗皇, 我的世界], wife='null', info={性别=女, 游戏ID=雪仙, 年龄=23}}
    

6.3 拓展方式

6.3.1 P 命名空间

需在配置文件中添加 xmlns:p="http://www.springframework.org/schema/p"

然后便可以在 bean 标签中调用 p:属性名 来进行 Set 注入

示例:

User.java:

package com.wmwx.pojo;

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

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

    <!-- p命名空间,可以直接注入属性的值:property -->
    <bean id="user" class="com.wmwx.pojo.User" p:name="刘季恒" p:age="18" />

</beans>

测试类:

@Test
public void studentTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("UserBeans.xml");
    User user = (User) context.getBean("user");
    System.out.println(user.toString());
}

输出结果:

User{name='刘季恒', age=18}
6.3.2 C 命名空间

需在配置文件中添加 xmlns:c="http://www.springframework.org/schema/c"

然后便可以在 bean 标签中调用 c:属性名 来进行 构造器注入

示例:

User.java:

package com.wmwx.pojo;

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

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

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

    <!-- c命名空间,可以通过构造器注入属性的值:constructor -->
    <bean id="user2" class="com.wmwx.pojo.User" c:name="刘季恒" c:age="18" />

</beans>

测试类:

@Test
public void studentTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("UserBeans.xml");
    User user2 = (User) context.getBean("user2");
    System.out.println(user2.toString());
}

输出结果:

User{name='刘季恒', age=18}

6.4 Bean 的作用域

6.4.1 单例模式(默认模式)
<bean id="" class="" scope="singleton" />

这单例模式下,无论从容器中读取多少次,本质都是同一个对象

示例:

applicationContext.xml:

<bean id="user" class="com.wmwx.pojo.User" p:name="刘季恒" p:age="18"/>
//或
<bean id="user" class="com.wmwx.pojo.User" p:name="刘季恒" p:age="18" scope="singleton"/>

测试类:

@Test
public void studentTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    User userF = (User) context.getBean("user");
    User userS = (User) context.getBean("user");
    System.out.println(userF==userS);
}

输出结果:

true
6.4.2 原型模式
<bean id="" class="" scope="prototype" />

在原型模式下,每次读取,都会创建一个新对象

示例:

applicationContext.xml:

<bean id="user2" class="com.wmwx.pojo.User" c:name="刘季恒" c:age="18" scope="prototype"/>

测试类:

@Test
public void studentTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    User user2F = (User) context.getBean("user2");
    User user2S = (User) context.getBean("user2");
    System.out.println(user2F==user2S);
}

输出结果:

false

7. Bean 的自动装配

自动装配:

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

在 Spring 中,共有以下三种装配方式:

  1. 显式地装配在 xml 中
  2. 显式地装配在 java 中
  3. 隐式地自动装配(最为重要)

7.1 测试环境搭建

需求:一个人有两个宠物

示例:

Dog.java:

package com.wmwx.pojo;

public class Dog {
    public void shout(){
        System.out.println("汪汪汪!");
    }
}

Cat.java:

package com.wmwx.pojo;

public class Cat {
    public void shout(){
        System.out.println("喵~");
    }
}

Person.java:

package com.wmwx.pojo;

public class Person {
    private String name;
    private Dog dog;
    private Cat cat;

    public String getName() {
        return name;
    }

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

    public Dog getDog() {
        return dog;
    }

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

    public Cat getCat() {
        return cat;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", dog=" + dog +
                ", cat=" + cat +
                '}';
    }
}

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

    <bean id="dog" class="com.wmwx.pojo.Dog" />
    <bean id="cat" class="com.wmwx.pojo.Cat" />

    <bean id="person" class="com.wmwx.pojo.Person">
        <property name="name" value="周松雅" />
        <property name="dog" ref="dog" />
        <property name="cat" ref="cat" />
    </bean>

</beans>

测试类:

@Test
public void personTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Person person = (Person) context.getBean("person");
    person.getDog().shout();
    person.getCat().shout();
}

输出结果:

汪汪汪!
喵~

7.2 byName 自动装配

byName:

  • 会自动在容器上下文中寻找与自己对象set方法后面的值对应的bean的id
  • 需要保证所有 bean 的 id 唯一
  • bean 需要和自动注入的属性的 set 方法名一致
<bean id="dog" class="com.wmwx.pojo.Dog" />
<bean id="cat" class="com.wmwx.pojo.Cat" />

<!-- byName:会自动在容器上下文中寻找与自己对象set方法后面的值对应的bean的id -->
<bean id="person" class="com.wmwx.pojo.Person" autowire="byName">
    <property name="name" value="周松雅" />
</bean>

7.3 byType 自动装配

byType:

  • 会自动在容器上下文中寻找与自己属性类型相同的bean的id
  • 需要保证所有 bean 的 class 唯一
  • bean 需要和自动注入的属性的类型一致
<!-- 使用byType时无需提供id -->
<bean class="com.wmwx.pojo.Dog" />
<bean class="com.wmwx.pojo.Cat" />

<!-- byType:会自动在容器上下文中寻找与自己属性类型相同的bean的id -->
<bean id="person" class="com.wmwx.pojo.Person" autowire="byType">
    <property name="name" value="周松雅" />
</bean>

7.4 使用注解实现自动装配

使用步骤:

  1. 导入约束

    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 />
    
  • 完整配置文件示例:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
        <context:annotation-config />
    </beans>
    
    
7.4.1 @Autowired 和 @Qualifier
  • 对属性添加 @Autowired 注解,可以让属性自动采用 byType 进行自动装配
  • 如果相同类型的 bean 不止一个,则可以添加 @Qualifier(value="") 注解,改用 byName 自动装配

示例:

Person.java:

package com.wmwx.pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

public class Person {
    private String name;
    @Autowired
    @Qualifier(value = "dog1")
    private Dog dog;
    @Autowired
    @Qualifier(value = "cat1")
    private Cat cat;

    public String getName() {
        return name;
    }

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

    public Dog getDog() {
        return dog;
    }

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

    public Cat getCat() {
        return cat;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", dog=" + dog +
                ", cat=" + cat +
                '}';
    }
}

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

    <context:annotation-config />

    <bean id="dog1" class="com.wmwx.pojo.Dog" />
    <bean id="dog2" class="com.wmwx.pojo.Dog" />
    <bean id="cat1" class="com.wmwx.pojo.Cat" />
    <bean id="cat2" class="com.wmwx.pojo.Cat" />
    <bean id="person" class="com.wmwx.pojo.Person" />

</beans>

测试类:

@Test
public void personTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Person person = (Person) context.getBean("person");
    person.getDog().shout();
    person.getCat().shout();
}

输出结果:

汪汪汪!
喵~
7.4.2 @Resource
  • 对属性添加 @Resource 注解,可以让属性自动采用 byType 进行自动装配
  • 如果相同类型的 bean 不止一个,会自动改用 byName 进行自动装配
  • 如果没有 id 与属性名相同的 bean,可以添加 @Resource(name="") 的注解,使用 byName 自动装配

示例:

Person.java:

package com.wmwx.pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

import javax.annotation.Resource;

public class Person {
    private String name;
    @Resource(name = "dog1")
    private Dog dog;
    @Resource
    private Cat cat;

    public String getName() {
        return name;
    }

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

    public Dog getDog() {
        return dog;
    }

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

    public Cat getCat() {
        return cat;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", dog=" + dog +
                ", cat=" + cat +
                '}';
    }
}

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

    <context:annotation-config />

    <bean id="dog1" class="com.wmwx.pojo.Dog" />
    <bean id="dog2" class="com.wmwx.pojo.Dog" />
    <bean id="cat1" class="com.wmwx.pojo.Cat" />
    <bean id="cat2" class="com.wmwx.pojo.Cat" />
    <bean id="person" class="com.wmwx.pojo.Person" />

</beans>

测试类:

@Test
public void personTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Person person = (Person) context.getBean("person");
    person.getDog().shout();
    person.getCat().shout();
}

输出结果:

汪汪汪!
喵~
7.4.3 @Autowired 和 @Resource 的异同

相同点:

  • 都是用来自动装配的
  • 都可以放在属性字段上

不同点:

  • @Autowired 默认通过 byType 的方式实现
  • @Resource 默认通过 byName 的方式实现
  • 如果相同类型的 bean 不止一个,则可以在 @Autowired 后添加 @Qualifier(value="")
  • 如果相同类型的 bean 不止一个,@Resource 会自动改用 byName 进行自动装配
  • 如果没有 id 与属性名相同的 bean,可以添加 @Resource(name="") 的注解,使用 byName 自动装配

8. 使用注解开发

使用须知:

  • Spring 4 之后,若需使用注解开发,则必须先导入 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
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
        <!-- 指定要扫描的包,这个包下的注解就会生效 -->
        <context:component-scan base-package="包名" />
    </beans>
    

注解包含以下几类:

  1. bean 的注解:@Component

    package com.wmwx.pojo;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    //@Component 等价于 <bean id="user" class="com.wmwx.pojo.User" />
    @Component
    public class User {
    	
    }
    
  2. property 的注解:@Value("")

    package com.wmwx.pojo;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class User {
    
        private String name;
    
        public String getName() {
            return name;
        }
    
        //@Value("") 等价于 <property name="name" value="" />
        @Value("刘季恒")
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
  3. 衍生注解

    在 WEB 开发中,我们常进行 MVC 分层。在三个分层中,@Component 衍生出了以下三种注解:

    • DAO 层:@Repository
    • Service 层:@Service
    • Controller 层:@Controller

    这四个注解的功能实质上是相同的。

  4. 自动装配

    7.4

  5. 作用域:@Scope("")

    package com.wmwx.pojo;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    @Component
    //@Scope("") 等价于 <bean id="" class="" scope="prototype" />
    @Scope("prototype")
    public class User {
    
        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 + '\'' +
                    '}';
        }
    }
    
  6. XML注解

    • XML 更加万能,适用于任何场合
    • XML 维护简单方便
    • 注解必须在自己的类中使用
    • 注解维护相对复杂
    • 最佳实践:XML 用来管理 Bean,注解只负责完成属性的注入

9. 使用 Java 配置 config

示例:

User.java:

package com.wmwx.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class User {
    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 + '\'' +
                '}';
    }
}

MyConfig.java:

package com.wmwx.config;

import com.wmwx.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

//@Configuraion代表这是一个配置类,就与之前的applicationContext.xml作用一致
@Configuration
//等价于在 applicationContext.xml 中扫描包
@ComponentScan("com.wmwx")
//导入另一个config
@Import(MyConfig2.class)
public class MyConfig {
    //注册一个Bean,就相当于一个bean标签
    //id取决于方法名
    //class取决于返回值类型
    @Bean
    public User getUser(){
        return new User();
    }
}

MyConfig2.java:

package com.wmwx.config;

import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig2 {
}

测试类:

import com.wmwx.config.MyConfig;
import com.wmwx.pojo.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class UserTest {
    @Test
    public void userTest(){
        /*
            如果完全使用了配置类方式去做,我们就只能通过AnnotationConfig上下文来获取容器
            并传递配置类的class对象作为参数来加载
        */
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
        //通过方法名来获取Bean
        User user = (User) context.getBean("getUser");
        System.out.println(user.toString());
    }
}

10. 代理模式

【注】:代理模式是 SpringAOP 的底层。

代理模式的分类:

  • 静态代理
  • 动态代理

10.1 静态代理

角色分析:

  • 抽象角色:一般会使用接口或者抽象类来解决
  • 真实角色:被代理的角色
  • 代理角色:代理真实角色的角色
  • 客户:访问代理对象的角色

步骤:

  1. 接口

    Rent.java:

    package com.wmwx.demo01;
    
    //出租房子
    public interface Rent {
        public void rent();
    }
    
  2. 真实角色

    Host.java:

    package com.wmwx.demo01;
    
    //房东
    public class Host implements Rent{
        @Override
        public void rent() {
            System.out.println("房东要出租房子!");
        }
    }
    
  3. 代理角色

    Proxy.java:

    package com.wmwx.demo01;
    
    //房屋中介
    public class Proxy implements Rent{
        private Host host;
    
        public Proxy() {
        }
    
        public Proxy(Host host) {
            this.host = host;
        }
    
        //出租
        @Override
        public void rent() {
            //代理角色存在附加行为
            this.seeHouse();
            host.rent();
            this.fee();
        }
    
        //看房子
        public void seeHouse(){
            System.out.println("中介带客户看房子!");
        }
    
        //收中介费
        public void fee(){
            System.out.println("中介要收中介费!");
        }
    }
    
  4. 客户访问

    Client.java:

    package com.wmwx.demo01;
    
    //客户
    public class Client {
        public static void main(String[] args) {
            Host host = new Host();
            //代理
            Proxy proxy = new Proxy(host);
            proxy.rent();
        }
    }
    
  5. 输出结果

    中介带客户看房子!
    房东要出租房子!
    中介要收中介费!
    

静态代理的好处:

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

静态代理的缺点:

  • 每一个真实角色都会产生一个代理角色,导致代码量翻倍,开发效率降低

10.2 静态代理的加深理解

示例:

需求:一个 UserService 可以增删改查,先需对其增加业务——操作前打印一个日志

  1. 接口:

    UserService.java:

    package com.wmwx.demo02;
    
    public interface UserService {
        public void addUser();
        public void removeUser();
        public void updateUser();
        public void queryUser();
    }
    
  2. 真实对象

    UserServiceImpl.java:

    package com.wmwx.demo02;
    
    //真实对象
    public class UserServiceImpl implements UserService{
        @Override
        public void addUser() {
            System.out.println("增加了一个用户!");
        }
    
        @Override
        public void removeUser() {
            System.out.println("删除了一个用户!");
        }
    
        @Override
        public void updateUser() {
            System.out.println("修改了一个用户!");
        }
    
        @Override
        public void queryUser() {
            System.out.println("查找了一个用户!");
        }
    }
    
  3. 代理角色

    UserServiceProxy.java:

    package com.wmwx.demo02;
    
    public class UserServiceProxy implements UserService{
    
        private UserServiceImpl userService;
    
        public void setUserService(UserServiceImpl userService) {
            this.userService = userService;
        }
    
        @Override
        public void addUser() {
            log("add");
            userService.addUser();
        }
    
        @Override
        public void removeUser() {
            log("remove");
            userService.addUser();
        }
    
        @Override
        public void updateUser() {
            log("update");
            userService.addUser();
        }
    
        @Override
        public void queryUser() {
            log("query");
            userService.addUser();
        }
    
        //日志功能
        public void log(String msg){
            System.out.println("[DEBUG] 调用了"+msg+"方法");
        }
    }
    
  4. 客户

    Client.java:

    package com.wmwx.demo02;
    
    public class Client {
        public static void main(String[] args) {
            UserServiceImpl userService = new UserServiceImpl();
            UserServiceProxy proxy = new UserServiceProxy();
            proxy.setUserService(userService);
            proxy.addUser();
        }
    }
    
  5. 输出结果

    [DEBUG] 调用了add方法
    增加了一个用户!
    

10.3 动态代理

  • 动态代理和静态代理角色相同
  • 动态代理的代理类是动态生成的,而不是预先写好的
  • 动态代理分为两大类:
    1. 基于接口的动态代理(典型:JDK 动态代理)
    2. 基于的动态代理(典型:cglib)
  • 动态代理的好处:
    • 静态代理的所有好处
    • 一个动态代理类代理的是一个接口,一般对应一类业务
    • 一个动态代理类可以代理多个实现同一接口的类

编写动态代理工具类:

ProxyInvocationHandler.java:

package com.wmwx.demo04;

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

//这个类用来自动生成代理类
public class ProxyInvocationHandler implements InvocationHandler {
    //被代理的接口
    private Object target;

    public void setTarget(Object target) {
        this.target = target;
    }

    //生成得到代理类
    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    //处理代理实例并返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //动态代理的本质,就是使用反射来实现
        log(method.getName());
        Object result = method.invoke(target, args);
        return result;
    }

    public void log(String msg){
        System.out.println("[DEBUG] 调用了"+msg+"方法。");
    }
}

示例:

UserService.java:

package com.wmwx.demo04;

public interface UserService {
    public void addUser();
    public void removeUser();
    public void updateUser();
    public void queryUser();
}

UserServiceImpl.java:

package com.wmwx.demo04;

//真实对象
public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("增加了一个用户!");
    }

    @Override
    public void removeUser() {
        System.out.println("删除了一个用户!");
    }

    @Override
    public void updateUser() {
        System.out.println("修改了一个用户!");
    }

    @Override
    public void queryUser() {
        System.out.println("查找了一个用户!");
    }
}

测试类:

@Test
public void userTest(){
    //真实角色
    UserServiceImpl userService = new UserServiceImpl();
    //代理角色,目前未代理任何对象
    ProxyInvocationHandler pih = new ProxyInvocationHandler();
    //设置要代理的对象
    pih.setTarget(userService);
    //动态生成代理类
    UserService proxy = (UserService) pih.getProxy();
    proxy.addUser();
}

输出结果:

[DEBUG] 调用了addUser方法。
增加了一个用户!

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种类型的通知:

  1. 前置通知:连接点位于方法执行前
  2. 后置通知:连接点位于方法执行后
  3. 环绕通知:连接点位于方法执行前后
  4. 异常抛出通知:连接点为方法抛出异常时
  5. 引介通知:连接点为类中增加新的方法属性时

11.3 使用 Spring 实现 AOP

欲使用 AOP 织入,需要导入一个依赖:

<!-- aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>
11.3.1 使用 Spring 的 API 接口实现 AOP

示例:

UserService.java:

package com.wmwx.service;

public interface UserService {
    public void addUser();
    public void removeUser();
    public void updateUser();
    public void queryUser();
}

UserServiceImpl.java:

package com.wmwx.service;

//真实对象
public class UserServiceImpl implements UserService{
    @Override
    public void addUser() {
        System.out.println("增加了一个用户!");
    }
    @Override
    public void removeUser() {
        System.out.println("删除了一个用户!");
    }
    @Override
    public void updateUser() {
        System.out.println("修改了一个用户!");
    }
    @Override
    public void queryUser() {
        System.out.println("查找了一个用户!");
    }
}

BeforeLog.java:

package com.wmwx.log;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class BeforeLog implements MethodBeforeAdvice {
    //method:要执行的目标对象的方法
    //args:参数
    //target:目标对象
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName() + "的方法" + method.getName() + "被执行了!");
    }
}

AfterLog.java:

package com.wmwx.log;

import org.springframework.aop.AfterReturningAdvice;

import java.lang.reflect.Method;

public class AfterLog implements AfterReturningAdvice {
    //returnValue:返回值
    //method:要执行的目标对象的方法
    //args:参数
    //target:目标对象
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("执行了"+method.getName()+"方法,返回值为"+returnValue);
    }
}

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: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">
       <!-- 配置AOP,需要导入AOP的约束 -->

    <!-- 注册bean -->
    <bean id="userService" class="com.wmwx.service.UserServiceImpl" />

    <!-- 方式一:使用原生的 Spring API 接口 -->
    <bean id="beforeLog" class="com.wmwx.log.BeforeLog" />
    <bean id="afterLog" class="com.wmwx.log.AfterLog" />
    <aop:config>
        <!-- 切入点 -->
        <!-- execution(方法名) -->
        <aop:pointcut id="pointcut" expression="execution(* com.wmwx.service.UserServiceImpl.*(..))"/>
        <!-- 执行环绕增加 -->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut" />
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut" />
    </aop:config>

</beans>

测试类:

@Test
public void userTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //此处注意:动态代理的对象是接口而不是实现类
    UserService userService = (UserService) context.getBean("userService");
    userService.addUser();
}

输出结果:

增加了一个用户!
执行了addUser方法,返回值为null
11.3.2 使用自定义类实现 AOP

示例:

DiyPointCut.java:

package com.wmwx.diy;

public class DiyPointCut {
    public void before(){
        System.out.println("现在是方法执行之前。");
    }

    public void after(){
        System.out.println("现在是方法执行之后。");
    }
}

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: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.wmwx.service.UserServiceImpl" />

    <!-- 方式二:使用自定义类 -->
    <bean id="diyPointCut" class="com.wmwx.diy.DiyPointCut" />
    <aop:config>
        <!-- 自定义切面,ref为要引用的类 -->
        <aop:aspect ref="diyPointCut">
            <!-- 切入点 -->
            <aop:pointcut id="point" expression="execution(* com.wmwx.service.UserServiceImpl.*(..))"/>
            <!-- 通知 -->
            <aop:before method="before" pointcut-ref="point" />
            <aop:after method="after" pointcut-ref="point" />
        </aop:aspect>
    </aop:config>

</beans>

测试类:

@Test
public void userTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //此处注意:动态代理的对象是接口而不是实现类
    UserService userService = (UserService) context.getBean("userService");
    userService.addUser();
}

输出结果:

现在是方法执行之前。
增加了一个用户!
现在是方法执行之后。
11.3.3 使用注解实现 AOP

示例:

AnnotationPointCut.java:

package com.wmwx.diy;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

//方式三:使用注解方式解决AOP
//@Aspect 标注这个类是一个切面
@Aspect
public class AnnotationPointCut {
    //@Before 标注这是前置通知
    @Before("execution(* com.wmwx.service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("现在是方法执行之前。");
    }

    //@After 标注这是后置通知
    @After("execution(* com.wmwx.service.UserServiceImpl.*(..))")
    public void after(){
        System.out.println("现在是方法执行之后。");
    }

    //@Around 标注这是环绕通知
    //可以给定一个ProceedingJoinPoint类型的参数,代表要获取的处理切入的点
    @Around("execution(* com.wmwx.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("现在是环绕前。");
        //执行方法
        Object proceed = jp.proceed();
        System.out.println("现在是环绕后。");
        Signature signature = jp.getSignature();
        System.out.println("签名:"+signature);
        System.out.println(proceed);
    }
}

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: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.wmwx.service.UserServiceImpl" />

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

</beans>

测试类:

@Test
public void userTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //此处注意:动态代理的对象是接口而不是实现类
    UserService userService = (UserService) context.getBean("userService");
    userService.addUser();
}

输出结果:

现在是环绕前。
现在是方法执行之前。
增加了一个用户!
现在是方法执行之后。
现在是环绕后。
签名:void com.wmwx.service.UserService.addUser()
null

12. 整合 MyBatis

步骤:

  1. 导入相关依赖

    • Junit

      <!-- Junit -->
      <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.11</version>
          <scope>test</scope>
      </dependency>
      
    • mybatis

      <!-- MyBatis -->
      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.6</version>
      </dependency>
      
    • mysql

      <!-- MySQL -->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.23</version>
      </dependency>
      
    • spring-jdbc

      <!-- spring-jdbc -->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>5.3.7</version>
      </dependency>
      
    • spring

      <!-- spring-webmvc -->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.3.7</version>
      </dependency>
      
    • aop

      <!-- aspectjweaver -->
      <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.4</version>
      </dependency>
      
    • mybatis-spring

      <!-- mybatis-spring -->
      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>2.0.6</version>
      </dependency>
      
  2. 编写配置文件

  3. 测试

12.1 回忆 MyBatis

  1. 编写核心配置文件

    mybatis-config.xml:

    <?xml version="1.0" encoding="UTF8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    
    <configuration>
    
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
        </settings>
    
        <typeAliases>
            <package name="com.wmwx.pojo" />
        </typeAliases>
    
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT"/>
                    <property name="username" value="root"/>
                    <property name="password" value="123456"/>
                </dataSource>
            </environment>
        </environments>
    
        <mappers>
            <mapper class="com.wmwx.mapper.UserMapper" />
        </mappers>
    
    </configuration>
    
  2. 编写实体类

    User.java:

    package com.wmwx.pojo;
    
    import lombok.Data;
    
    @Data
    public class User {
        private int id;
        private String name;
        private String pwd;
    }
    
  3. 编写 Mapper 接口

    UserMapper.java:

    package com.wmwx.mapper;
    
    import com.wmwx.pojo.User;
    
    import java.util.List;
    
    public interface UserMapper {
        public List<User> selectUser();
    }
    
  4. 编写 Mapper 配置文件

    UserMapper.xml:

    <?xml version="1.0" encoding="UTF8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.wmwx.mapper.UserMapper">
    
        <select id="selectUser" resultType="User">
            select * from user
        </select>
    
    </mapper>
    
  5. 测试

    UserTest.java:

    import com.wmwx.mapper.UserMapper;
    import com.wmwx.pojo.User;
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    import org.junit.Test;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.List;
    
    public class UserTest {
        @Test
        public void selectUser() throws IOException {
            String resources = "mybatis-config.xml";
            InputStream in = Resources.getResourceAsStream(resources);
            SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(in);
            SqlSession sqlSession = sessionFactory.openSession(true);
    
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> userList = userMapper.selectUser();
            for (User user : userList) {
                System.out.println(user);
            }
    
            sqlSession.close();
        }
    }
    
  6. 输出结果

    User(id=1, name=刘季恒, pwd=197699)
    User(id=2, name=周松雅, pwd=654321)
    User(id=3, name=沈琉灵, pwd=101010)
    User(id=4, name=邓凌光, pwd=197812)
    User(id=5, name=周龙, pwd=000000)
    User(id=6, name=李章泯, pwd=789456)
    

12.2 MyBatis-Spring

12.1 的基础上改用 mybatis-spring,步骤如下:

  1. 编写数据源配置

    spring-dao.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">
    
        <!-- DataSource:使用Spring的数据源替换Mybatis的配置 -->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT" />
            <property name="username" value="root" />
            <property name="password" value="123456" />
        </bean>
    
    </beans>
    
  2. 编写 sqlSessionFactory 配置

    spring-dao.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">
    
        <!-- DataSource:使用Spring的数据源替换Mybatis的配置 -->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT" />
            <property name="username" value="root" />
            <property name="password" value="123456" />
        </bean>
        
        <!-- 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/wmwx/mapper/*.xml" />
        </bean>
    
    </beans>
    
  3. 编写 sqlSessionTemplate 配置

    spring-dao.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">
    
        <!-- DataSource:使用Spring的数据源替换Mybatis的配置 -->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT" />
            <property name="username" value="root" />
            <property name="password" value="123456" />
        </bean>
        <!-- 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/wmwx/mapper/*.xml" />
        </bean>
    
        <!-- SqlSessionTemplate:就是我们使用的sqlSession -->
        <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
            <!-- 只能使用构造器注入,因为SqlSessionTemplate没有set方法 -->
            <constructor-arg index="0" ref="sqlSessionFactory" />
        </bean>
    
    </beans>
    
  4. 为接口添加实现类

    UserMapperImpl.java:

    package com.wmwx.mapper;
    
    import com.wmwx.pojo.User;
    import org.mybatis.spring.SqlSessionTemplate;
    
    import java.util.List;
    
    public class UserMapperImpl implements UserMapper {
        //曾经我们所有的操作都由sqlSession完成,现在改用sqlSessionTemplate
        private SqlSessionTemplate sqlSessionTemplate;
    	//为后面的spring注入留下set方法
        public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
            this.sqlSessionTemplate = sqlSessionTemplate;
        }
    
        @Override
        public List<User> selectUser() {
            UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
            List<User> userList = mapper.selectUser();
            return userList;
        }
    }
    
  5. 将实现类注入到 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">
    
        <!-- 导入前面编写的spring-dao配置文件 -->
        <import resource="spring-dao.xml" />
    
        <!-- 实现类 -->
        <bean id="userMapper" class="com.wmwx.mapper.UserMapperImpl">
            <property name="sqlSessionTemplate" ref="sqlSession" />
        </bean>
    
    </beans>
    
  6. 测试

    UserTest.java:

    import com.wmwx.mapper.UserMapper;
    import com.wmwx.pojo.User;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class UserTest {
        @Test
        public void selectUser() {
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
            for (User user : userMapper.selectUser()) {
                System.out.println(user);
            }
        }
    }
    
  7. 输出结果

    User(id=1, name=刘季恒, pwd=197699)
    User(id=2, name=周松雅, pwd=654321)
    User(id=3, name=沈琉灵, pwd=101010)
    User(id=4, name=邓凌光, pwd=197812)
    User(id=5, name=周龙, pwd=000000)
    User(id=6, name=李章泯, pwd=789456)
    

12.3 SqlSessionDaoSupport

12.1 的基础上改用 SqlSessionDaoSupport,步骤如下:

  1. 编写数据源配置

    spring-dao.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">
    
        <!-- DataSource:使用Spring的数据源替换Mybatis的配置 -->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT" />
            <property name="username" value="root" />
            <property name="password" value="123456" />
        </bean>
    
    </beans>
    
  2. 编写 sqlSessionFactory 配置

    spring-dao.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">
    
        <!-- DataSource:使用Spring的数据源替换Mybatis的配置 -->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT" />
            <property name="username" value="root" />
            <property name="password" value="123456" />
        </bean>
        
        <!-- 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/wmwx/mapper/*.xml" />
        </bean>
    
    </beans>
    
  3. 为接口添加实现类

    UserServiceImpl2.java:

    package com.wmwx.mapper;
    
    import com.wmwx.pojo.User;
    import org.mybatis.spring.support.SqlSessionDaoSupport;
    
    import java.util.List;
    
    public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper{
        @Override
        public List<User> selectUser() {
            return getSqlSession().getMapper(UserMapper.class).selectUser();
        }
    }
    
  4. 将实现类注入到 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">
    
        <import resource="spring-dao.xml" />
    
        <!-- 实现类 -->
        <bean id="userMapper2" class="com.wmwx.mapper.UserMapperImpl2">
            <!-- SqlSessionDaoSupport需要注入sqlSessionFactory -->
            <property name="sqlSessionFactory" ref="sqlSessionFactory" />
        </bean>
    
    </beans>
    
  5. 测试

    UserTest.java:

    import com.wmwx.mapper.UserMapper;
    import com.wmwx.pojo.User;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class UserTest {
        @Test
        public void selectUser() {
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserMapper userMapper = context.getBean("userMapper2", UserMapper.class);
            for (User user : userMapper.selectUser()) {
                System.out.println(user);
            }
        }
    }
    
  6. 输出结果

    User(id=1, name=刘季恒, pwd=197699)
    User(id=2, name=周松雅, pwd=654321)
    User(id=3, name=沈琉灵, pwd=101010)
    User(id=4, name=邓凌光, pwd=197812)
    User(id=5, name=周龙, pwd=000000)
    User(id=6, name=李章泯, pwd=789456)
    

13. 声明式事务

13.1 事务

事务中的语句要么全部执行要么全部不执行

事务的 ACID 原则:

  • 原子性(A):事务必须是一个自动工作的单元,要么全部执行,要么全部不执行。
  • 一致性(C):事务把数据库从一个一致状态带入到另一个一致状态,事务结束的时候,所有的内部数据都是正确的。
  • 隔离性(I):并发多个事务时,一个事务的执行不受其他事务的影响。
  • 持久性(D):事务提交之后,数据是永久性的,不可再回滚,不受关机等事件的影响

13.2 Spring 中的事务管理

  • 声明式事务:采用 AOP
  • 编程式事务:需要在代码中进行事务的管理

示例:声明式事务

无需修改业务逻辑,只需在 spring-dao.xml 中添加配置即可。

spring-dao.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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- DataSource:使用Spring的数据源替换Mybatis的配置 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT" />
        <property name="username" value="root" />
        <property name="password" value="123456" />
    </bean>
    
    <!-- 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/wmwx/mapper/*.xml" />
    </bean>

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

    <!-- 配置声明式事务 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
    </bean>
    <!-- 结合AOP 实现事务的织入 -->
    <!-- 配置事务通知 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 给哪些方法配置事务 -->
        <tx:attributes>
            <!-- 给所有方法配置事务的传播特性propagation -->
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!-- 配置事务切入 -->
    <aop:config>
        <aop:pointcut id="pointCut" expression="execution(* com.wmwx.mapper.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut" />
    </aop:config>

</beans>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值