Spring 入门学习

1,038 阅读15分钟

一、核心概念

Ioc (Inversion of Control)控制反转

  • 使用对象时,由主动 new 产生对象转换为由 外部 提供对象,此过程中对象创建控制权由程序转移到外部,这种思想称为控制反转。

Spring 技术对 IoC 思想进行了实现

  • Spring 提供了一个容器,称为 IoC 容器,用来充当 IoC 思想中的 “外部”
  • IoC 容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在 IoC 容器中统称为 Bean

DI(Dependency Injection)依赖注入

  • 在容器中建立 bean 与 bean 之间的依赖关系的整个过程,称为 依赖注入。

二、IoC 入门案例

思路分析:

  1. 管理什么?(Service 与 Dao)
  2. 如何将被管理的对象告知 IoC容器?(配置)
  3. 被管理的对象交给 IoC 容器,如何获取到 IoC 容器?(接口)
  4. IoC 容器得到后,如何从容器中获取 bean?(接口方法)
  5. 使用 Spring 导入哪些坐标?(pom.xml)

目录结构:

DAO 层:

  • BookDao
package dao;

public interface BookDao {
    public void save();
}
  • BookDaoImpl
package dao;

import service.BookService;

public class BookDaoImpl implements BookDao {

    public void save() {
        System.out.println("book dao save...");
    }
}

步骤:

  1. 在 pom.xml 中导入 Spring 坐标
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.3.RELEASE</version>
</dependency>
  1. 定义 Spring 管理的类(接口)
package service;

public interface BookService {
    public void save();
}
package service;

import dao.BookDao;
import dao.BookDaoImpl;

public class BookServiceImpl implements BookService{

    private BookDao bookDao = new BookDaoImpl();

    public void save() {
        System.out.println("book service save");
        bookDao.save();
    }
}
  1. 创建 Spring 配置文件,配置对应类作为 Spring 管理的 bean

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

    <bean id="bookService" class="service.BookServiceImpl"></bean>

</beans>
  • bean 标签表示配置 bean
  • id 属性表示给 bean 起名字
  • class 属性表示给 bean 定义类型

注意:bean 定义时,id 属性在同一个上下文中不能重复。

  1. 初始化 IoC 容器(Spring核心容器),通过容器获取 bean
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import service.BookService;

public class App {
    public static void main(String[] args) {
        // 加载配置文件得到上下文对象,也就是容器对象
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        // 获取资源
        BookService bookService = (BookService) ctx.getBean("bookService");
        bookService.save();
    }
}

三、DI 入门案例

思路分析:

  1. 基于 IoC 管理 bean
  2. Service 中使用 new 形式创建的 Dao 对象是否保留?(否)
  3. Service 中需要的 Dao 对象如何进入到 Service 中?(提供方法)
  4. Service 与 Dao 间的关系如何描述?(配置)

步骤:

  1. 删除使用 new 形式创建对象的代码

  1. 提供依赖对象对应的 setter 方法
public class BookServiceImpl implements BookService{

    private BookDao bookDao;

    public void save() {
        System.out.println("book service save");
        bookDao.save();
    }

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }
}
  1. 配置 service 与 dao 之间的关系
<?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="bookService" class="service.BookServiceImpl">
        <property name="bookDao" ref="bookDao"></property>
    </bean>

    <bean id="bookDao" class="dao.BookDaoImpl"></bean>

</beans>
  • property 标签表示配置当前 bean 的属性
  • name 属性表示哪一个具体的属性
  • ref 属性表示参照哪一个 bean

四、bean 配置

4.1 bean 基础配置

4.2 bean 别名配置

name 属性里,可以通过 逗号 或 空格 分割。

4.3 bean 作用范围配置

五、bean 实例化

5.1 实例化 bean 的三种方式

  1. 提供可访问的构造方法(常用)

  • 配置:在spring配置文件中使用bean标签,配以 id 和 class 属性之后,且没用其他属性和标签时。

  • 注意:无参构造方法如果不存在,将抛出异常 BeanCreationException
  1. 静态工厂(了解)使用某个类的静态方法创建对象,并存入spring容器。

  • 配置

配置工厂实现类 OrderDaoFactory,factory-method配置工厂实现类里面的方法。

  1. 实例工厂(了解)使用某个类中的方法创建对象,并存入spring容器。

  • 配置

  1. 方法3实例工厂的变种FactoryBean(掌握,后面经常用到)

  • 配置

5.2 bean 的生命周期

  • bean 生命周期:bean 从创建到销毁的整体过程。
  • 初始化容器
  1. 创建对象(内存分配)

  2. 执行构造方法

  3. 执行属性注入(set 操作)

  4. 执行 bean 初始化方法

  • 使用 bean
  1. 执行业务操作

  • 关闭/销毁容器
  1. 执行 bean 销毁方法
  • bean 生命周期控制:在 bean 创建后到销毁前做一些事情。

bean 生命周期控制:

有两种方法:

  1. 提供生命周期控制方法

  • 配置生命周期控制方法

执行顺序:(先将bean销毁,再关闭容器后的执行顺序)

  1. 实现 InitializingBean,DisposableBean 接口(了解)

bean 销毁时机

  • 容器关闭前触发 bean 的销毁
  • 关闭容器方式:
    • 手工关闭容器:ConfigurableApplicationContext 接口 close() 操作
    • 注册关闭钩子,在虚拟机退出前先关闭容器再退出虚拟机:ConfigurableApplicationContext 接口 registerShutdownHook() 操作

5.3 bean 对象的作用范围

bean 标签的 scope 属性,作用是用于指定 bean 的作用范围,取值可以为:

  • singleton:单例的(默认值)
  • prototype 多例的。
  • request 作用于 web 应用类的请求范围。
  • session 作用于 web 应用类的会话范围。
  • global-session 作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是 session。

解释下 global-session 的意思:

当我们第一次请求网址的时候,服务端会将用户的session信息存入空闲的服务器中

但是服务器的状态是瞬息万变的,可能存有你session信息的服务器变成满负荷了,当你进行第二次登录验证的时候,需要转到另外一台空闲的服务器进行验证,但是另一台服务器没用存你的session,这就会导致验证失败。

这时候就需要 global-session了,它将全部的服务器看成是一台服务器进行存取,这时候所有的服务器都可以共享这个 session 了。

六、依赖注入

思考:向一个类中传递数据的方式有几种?

  • 普通方法(set 方法)
  • 构造方法

依赖注入的方式:

  • set 注入
    • 简单类型
    • 引用类型
  • 构造器注入
    • 简单类型
    • 引用类型

6.1 set 注入

setter 注入需要使用的标签 property,出现的位置:bean 标签内部。

标签是属性为:

  • name:用于指定注入时所调用的 set 方法名称。
  • value:用于提供基本类型和String类型的数据。
  • ref:用于指定其他的 bean 类型数据,它指的是在 spring 的 IoC 核心容器中出现过的 bean 对象。

优势:

创建对象时没有明确的限制,可以直接使用默认构造函数。

弊端:

如果有某个成员必须有值,则获取对象是有可能 set 方法没有执行。

注入 -- 引用类型

  • 在 bean 中定义引用类型属性,并提供可访问的 set 方法

  • 配置中使用 property 标签 ref 属性注入引用类型对象

注入 -- 简单类型

  • 在 bean 中定义引用类型属性,并提供可访问的 set 方法

  • 配置中使用 property 标签 value 属性注入简单类型数据

6.2 构造器注入

构造器注入使用的标签 constructor-arg,标签出现的位置:bean 标签内部。

标签中的属性:

  • type:用于指定要注入数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型。
  • index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引位置从 0 开始。
  • name:用于指定给构造函数中指定名称的参数赋值。

以上三个用于指定给构造函数中哪个参数赋值。

  • value:用于提供基本类型和String类型的数据。
  • ref:用于指定其他的 bean 类型数据,它指的是在 spring 的 IoC 核心容器中出现过的 bean 对象。

优势:

在获取 bean 对象时,注入数据是必须的操作,否则对象无法创建成功。

弊端:

改变了 bean 对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供。

注入 -- 引用类型(了解)

  • 在 bean 中定义引用类型属性,并提供可访问的 构造 方法

  • 配置中使用 constructor-arg 标签 ref 属性注入引用类型对象

注入 -- 简单类型(了解)

  • 在 bean 中定义引用类型属性,并提供可访问的 set 方法

  • 配置中使用 constructor-arg 标签 value 属性注入简单类型数据

注入 -- 参数适配(了解)

  • 配置中使用 constructor-arg 标签 type 属性设置按形参类型注入

  • 配置中使用 constructor-arg 标签 index 属性设置按形参位置注入

6.6 依赖注入方式选择

七、自动装配

IoC 容器根据 bean 所依赖的资源在容器中自动查找,并注入到 bean 中的过程称为 自动装配。

自动装配的方式:

  • 按类型(常用)
  • 按名称

7.1 依赖自动装配

  • 配置中使用 bean 标签 autowire 属性设置自动装配的类型

依赖自动装配特征:

八、集合注入

  • 注入 数组 对象

  • 注入 List 对象(重点)

  • 注入 Set 对象

  • 注入 Map 对象(重点)

  • 注入 Properties 对象

九、Spring 事务

9.1 简介

PlatformTransactionManager平台事务管理器。

DataSourceTransactionManager数据源事务管理器(JDBC 技术)。

步骤:

  1. 在业务层接口上添加 Spring 事务管理

  1. 设置事务管理器

  1. 开启注解式事务驱动

9.2 spring 事务角色

outMoney 和 inMoney 分别开启了事务T1和事务T2,但是当事务T1发生异常的时候,只有T1会发生回滚,事务T2毫无影响。

于是就加了Spring 事务@Transactional,让 transfer 类开启事务 T,让事务T1和事务T2加入事务T。

  • 事务角色
    • 事务管理员:发起事务方,在 Spring 中通常指代业务层开启事务的方法。
    • 事务协调员:加入事务方,在 Spring 中通常指代数据层方法,也可以时业务层方法。

9.3 事务属性

为什么需要 rollbackFor属性,因为有限异常是不回滚的,比如 IOExpection

9.4 事务传播行为

  • 事务传播行为:事务协调员对事务管理员所携带事务的处理态度。

案例:转账业务追加日志

开始 Spring 事务后,LogService 的实现类会加入到 Spring 日志,如果中间有任意的异常,都会被回滚,导致无法对其转账失败进行日志的记录。

所以我们要使 LogService 的实现类单独开启一个事务。

步骤:

  1. 在业务层接口上添加 Spring 事务,设置事务传播行为 REQUIRES_NEW(需要新事物)

以下为所有的事务传播行为:

十、Spring 相关注解

我们前面在学 IOC 的时候知道如果想让 Spring 创建对象,必须要在配置文件中写 bean 标签。

<bean id="calculateService"  class="com.xxl.service.impl.CalculateServiceImpl" />
<bean id="proxyBeanPostProcessor" class="com.xxl.aop.ProxyBeanPostProcessor"/>
<bean id="testMyAspect"  class="com.xxl.aop.TestAspect" />
......

可是如果想让 Spring 管理一堆对象,我们就要写一堆 bean 标签。所以 Spring 为了简化代码,提供了一些与创建对象相关的注解。

10.1 创建对象相关注解

10.1.1 @Component

作用:将当前类对象存入 spring 容器中。

属性:

  • value:用于指定 bean 的 id。当我们不写时,它的默认值为当前类名,且首字母小写。

配置注解扫描

但是光加注解是不行的,还需要在配置文件中配置注解扫描,演示如下:

@Component
public class AccountServiceImpl implements IAccountService {

    private IAccountDao accountDao = new AccountDaoImpl();

    public AccountServiceImpl() {
        System.out.println("对象创建了");
    }

    @Override
    public void saveAccount() {
        accountDao.saveAccount();
    }
}
public class client {

    /**
     * 获取 spring 的 IoC核心容器,并根据id获取对象
     * @param args
     */
    public static void main(String[] args) {
        // 1, 获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        // 2. 根据id获取Bean对象
        IAccountService as = (IAccountService) ac.getBean("accountServiceImpl");

        System.out.println(as);
    }
}

如果直接这样运行就会报错,报错信息如下:

此时我们需要在 xml 配置文件中进行配置:配置所需要的标签不是在 bean 的约束中,而是一个名称为 context 名称空间和约束中。

我们可以进入 spring 的官方文档中搜索 xmlns: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">


</beans>

然后在此配置文件里添加如下内容:

<context:component-scan base-package="com.example"> </context:component-scan>

这个内容是在告知 spring 在创建容器时要扫描的包。base-package: 添加注解的类所在的包位置。

配置了注解扫描,当程序启动的时候 Spring 会先扫描一下相关的注解,这些注解才会生效。

再次运行程序,没有报错了:

10.1.2 @Component 衍生注解

我们在开发程序的时候一般会将程序分层,例如分为控制层(controller),业务层(service),持久层(dao)。

但是 @Component 注解并不能区分这些类属于那些层,所以 Spring 提供了以下衍生注解:

  1. @Controller:表示创建控制器对象。
@Controller
public class UserController {
    
}
  1. @Service:表示创建业务层对象。
@Service
public class UserServiceImpl implements UserService {

}
  1. @Repository:表示创建持久层对象。
@Repository
public class UserDaoImpl implements UserDao {

}

这三个注解的作用和 @Component 的作用一样,都是用来创建对象。

10.2 注入相关注解

作用就和在 xml 配置文件中的 bean 标签中写一个 property标签的作用时一样的。

@Autowired

  1. 作用: 自动按照类型注入,只要容器中有唯一的一个 bean 对象类型和要注入的变量类型匹配,就可以注入成功。

下面来演示下:

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Override
    public void saveAccount() {
        System.out.println("save Account...");
    }
}
@Component("accountService")
public class AccountServiceImpl implements IAccountService {

    @Autowired
    private IAccountDao accountDao;

    public AccountServiceImpl() {
        System.out.println("对象创建了");
    }

    @Override
    public void saveAccount() {
        accountDao.saveAccount();
    }
}
public class client {

    /**
     * 获取 spring 的 IoC核心容器,并根据id获取对象
     * @param args
     */
    public static void main(String[] args) {
        // 1, 获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        // 2. 根据id获取Bean对象
        IAccountService as = (IAccountService) ac.getBean("accountService");

        System.out.println(as);
    }
}

运行程序:

首先我们要理解下 Spring 的 IoC 容器的结构:Map 结构

AccountServiceImpl类 和 AccountDaoImpl类上发现了注解,于是将他们放入 Spring 的 IoC 容器中,注解的 id 作为 key,其类名和继承的类或者实现的接口作为 value。

接着发现了我们要注入的变量 accountDao,发现唯一的一个匹配类,则注入成功!

  1. 如果 IoC 容器中没有任何 bean 的类型和要注入的变量类型匹配,则报错。
  1. 如果 IoC 容器中有多个类型匹配时,步骤如下:
    1. 首先按照类型,圈定出来匹配的对象。
    2. 使用变量名称作为 bean 的 id,在圈定出来对象的里面继续查找。如果有一个一样的就注入成功,如果都不一样则会报错。

下面来演示下:

创建两个类分别如下:

此时 Spring 的 IoC 容器里的内容变为如下:

使用接口

main 函数如下:

运行代码,结果报错:

当我们将注入的变量名改为accountDao1

运行程序,结果如下:

当我们将注入的变量名改为accountDao2

运行程序,结果如下:

可以发现打印的结果不一样。为什么会这样呢?

解释:

  1. 首先按照类型,圈定出来匹配的对象。
  2. 使用变量名称作为 bean 的 id,在圈定出来对象的里面继续查找。

@Qualifier

@Autowired 是基于类型进行注入,所注入对象的类型必须和目标 变量类型相同或者是他的子类、实现类。

如果想基于名字注入,可以和 @Qualifier 注解连用:

属性:

  • value:用于指定注入 bean 的 id。
@Autowired
@Qualifier("orderDAOImpl")
private OrderDAO orderDAO;

@Qualifier 注解的另一种用法:将注解加在方法的参数上

public QueryRunner createQueryRunner(@Qualifier("ds2) DataSource dataSource) {
	return new QueryRunner(dataSource);
}

@Bean(name = "ds1")
public DataSource createDataSource() {
	/**
     * 操作数据库1
     */
    return DataSource;
}

@Bean(name = "ds2")
public DataSource createDataSource() {
	/**
     * 操作数据库2
     */
    return DataSource;
}

@Resource

作用:直接按照 bean 的 id 注入,它可以独立使用。

属性:

  • name:用于指定 bean 的 id。
@Resource("orderDAOImpl")
private OrderDAO orderDAO;

注意:以上三个注入都只能注入其他 bean 类型的数据,而基本类型和String类型无法使用上述注解实现。

另外,集合类型的注入只能通过 XML 来实现。


@Value

作用:用于注入基本类型和 String 类型的数据。

属性:

  • value:用于指定数据的值,它可以使用 spring 中的 SpEL(也就是spring 的el表达式)

SpEL的写法:${表达式}

10.3 改变作用范围的注解

他们的作用就和在 bean 标签中使用 scope属性实现的功能是一样的。

@Scope

作用:用于指定 bean 的作用范围。

属性:

  • value:指定范围的取值。常用取值:singleton、prototype。

不写 @Scope 注解,默认就是 singleton,所以可以省略。

@Component
// 可以省略不写
@Scope("singleton")
public class User {
    
}

修改为多例:

@Component
@Scope("prototype")
public class User {
    
}

10.4 生命周期相关注解

他们的作用就和在 bean 标签中使用 init-methoddestory-method的作用是一样的。

  1. @PostConstruct

用于指定初始化方法。

  1. @PreDestroy

用于指定销毁方法。

演示如下:

@Component("accountService")
public class AccountServiceImpl implements IAccountService {

    @Autowired
    private IAccountDao accountDao2 = null;

    @PostConstruct
    public void init() {
        System.out.println("初始化方法执行了");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("销毁方法执行了");
    }

    public AccountServiceImpl() {
        System.out.println("对象创建了");
    }

    @Override
    public void saveAccount() {
        accountDao2.saveAccount();
    }
}
public class client {

    /**
     * 获取 spring 的 IoC核心容器,并根据id获取对象
     * @param args
     */
    public static void main(String[] args) {
        // 1, 获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        // 2. 根据id获取Bean对象
        IAccountService as = (IAccountService) ac.getBean("accountService");

        as.saveAccount();
    }
}

执行程序,发现只要初始化方法执行了,销毁方法没有执行。

原因是,在main程序结束时,也就是spring容器销毁时,还没有来得及执行类中的销毁方法,整个spring容器就已经被销毁了。因此我们需要手动进行关闭。

但是 main 程序中的 ApplicationContext没有关闭方法,因此我们需要将变量的类型变为它的子类ClassPathXmlApplicationContext,然后进行close操作:

执行程序,显示结果:

10.4 Spring 配置文件相关注解

@Configuration

@Configuration 注解用于替换 xml 配置文件。

@Configuration
public class SpringConfig {
    
}

意思就是说你在一个类上面加一个 @Configuration 注解,这个类就可以看成 Spring 的配置类,你就不用再写 xml 文件了。

我们之前是根据 xml 文件创建 Spring 的工厂,那怎样根据配置类创建工厂呢?

有两种方式:

方式一:根据类.class

ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

方式二:根据配置类所在的路径

ApplicationContext ctx = new AnnotationConfigApplicationContext("com.xxl");

还有个细节:当配置类作为 AnnotationConfigApplicationContext 对象创建参数时,该注解可以不写

结果还是可以出来

@ComponentScan

@ComponentScan 注解相当于 xml 配置文件中的注解扫描标签:

<context:component-scan base-package="com.xxl"/>

作用:用于通过注解指定的 spring 在创建容器时要扫描的包。

属性:

  • value:注解所在的包路径。

例如:

@Configuration
@ComponentScan(value = "com.xxl")
public class SpringConfig {
}

当要扫描多个包路径的时候,可以使用{}括起来

@ComponentScan({"com.xxl","com.xlx"})

@Bean

作用:用于把当前方法的返回值作为 bean 对象存入 spring 的 IoC 容器中。

属性:

  • name:用于指定 bean 的 id。当不写时,默认值是当前方法的名称。
@Configuration
public class SpringConfig {

    @Bean
    public Product getProduct(){
        return new Product();
    }
}

细节:

当我们使用注解配置方法时,如果方法有参数,spring 框架会去容器中查找有没有可用的 bean 对象,查找的方式和 @Autowired 注解的作用是一样的。

10.5 其他注解

@Import

作用:用于导入其他的配置类。

属性:

  • value:用于指定其他配置类的字节码。当我们使用 Import 注解之后,有 Import 注解的类就是父配置类,而导入的都是子配置类。

父配置类

@CompinentScan("com.example")
@Import(JdbcConfig.class)
public class SpringConfig {

}

子配置类

public class JdbcConfig {
	@Bean
    public ....
}

十一、代理设计模式

11.1 为什么要使用代理设计模式?

先来看一个案例:加入你是一个厂家,要卖产品给消费者

接口:

/**
 * 对生产厂家要求的接口
 */
public interface IProducer {

    /**
     * 销售
     * @param money
     */
    public void saleProduct(float money);

    /**
     * 售后
     * @param money
     */
    public void afterService(float money);
}

实现类:

/**
 * 一个生产者
 */
public class Producer implements IProducer{
    @Override
    public void saleProduct(float money) {
        System.out.println("产品卖出的价格:" + money);
    }

    @Override
    public void afterService(float money) {
        System.out.println("售后服务的价格:" + money);
    }
}

然后我们要卖出产品给消费者,编写 Client 类

/**
 * 模拟一个消费者
 */
public class Client {
    public static void main(String[] args) {
        Producer producer = new Producer();
        producer.saleProduct(10000);
        producer.afterService(2000);
    }
}

这对于厂商是很麻烦的,因为每有一个消费者购买产品,厂商都要去对接。所以我们需要 代理 ****来为我们解决这个麻烦。

使用代理模式的作用:

  1. 功能增强:在你原有的功能上,增加了额外的功能,新增加的功能,叫做功能增强。
  2. 控制访问:代理类不让你访问目标,例如商家不让用户访问厂家。

实现代理的方式:

  1. 静态代理
  2. 动态代理

11.2 静态代理

  • 代理类是自己手工实现的,自己创建一个 java 类,表示代理类。
  • 同时你所要代理的目标类是确定的。

特点:实现简单、容易理解。

当项目中,目标类和代理类很多的时候,有以下缺点:

  1. 当目标类增加了,代理类可能也需要成倍的增加,代理类的数量过多。
  2. 当你的接口中功能增加了或者修改了,会影响众多的实现类、厂家类、代理类都需要修改。影响比较多。

模拟一个用户购买 u 盘的行为。

用户是客户端类。商家是代理,代理某个品牌的u盘。厂家是目标类。

三者的关系:用户(客户端)--- 商家(代理)--- 厂家(目标)

实现步骤:

  1. 创建一个接口,实现卖U盘的方法,表示你的厂家和商家做的事情。
  2. 创建厂家类,实现步骤1中的接口。
  3. 创建商家,就是代理,也需要实现步骤1中的接口。
  4. 创建客户端类,调用商家的方法买一个U盘。

演示:

创建厂家接口:

package com.example.static_proxy.service;

// 表示功能的,厂家和商家都要完成的功能
public interface UsbSell {

    // 定义方法 参数 amount 表示一次购买的数量,暂时不用
    // 返回值白哦是一个U盘的价格
    float sell(int amount);
}

厂家类实现该接口:

package com.example.static_proxy.factory;

import com.example.static_proxy.service.UsbSell;

// 目标类:金士顿厂家,不接受用户的单独购买
public class UsbKingFactory implements UsbSell {
    @Override
    public float sell(int amount) {
        // 一个U盘85元
        // 后期根据amount,可以实现不同的价格,如买10000个,单价为80
        return 85.0f;
    }
}

创建商家类:

package com.example.static_proxy.shangjia;

import com.example.static_proxy.factory.UsbKingFactory;
import com.example.static_proxy.service.UsbSell;

// taobao是一个商家,代理金士顿U盘的销售
public class TaoBao implements UsbSell {

    // 声明 商家代理的厂家具体是谁
    private UsbKingFactory factory = new UsbKingFactory();

    @Override
    public float sell(int amount) {

        // 向厂家发送订单,告诉厂家,我买了U盘,厂家发货
        float price = factory.sell(amount); // 厂家的价格
        // 商家 需要加价,也就是代理需要增加价格
        price += 25; // 增强功能,代理类在完成目标类方法调用后,增强了功能
        // 在目标类的方法调用后,你做其他的功能,都是增强的意思。

        // 增加的价格
        return price;
    }
}

创建客户端类:

package com.example.static_proxy;

import com.example.static_proxy.shangjia.TaoBao;

public class ShopMain {

    public static void main(String[] args) {
        // 创建代理的商家 taobao 对象
        TaoBao taoBao = new TaoBao();
        float price = taoBao.sell(1);
        System.out.println("通过淘宝的商家,购买U盘的单价:" + price);
    }
}

运行结果:

11.3 动态代理

在静态代理中目标类很多的时候,可以使用动态代理,避免静态代理的缺点。

动态代理中目标类即使很多,代理类数量也可以很少,当你修改了接口中的方法时,不会影响代理类。

动态代理: 在程序执行过程中,使用 jdk 反射机制,创建代理类对象(不需要定义代理类的.java源文件),并动态的指定要代理的目标类。

特点: 字节码随用随创建,随用随加载。

作用: 不修改源码的基础上对方法增强。

分类:

  • JDK 的动态代理。(理解)
  • CGLIB 动态代理。(了解)

JDK 的动态代理

回顾反射:www.yuque.com/justencount…

使用 java 反射包中的类和接口实现动态代理的功能。(接口必须有

反射包 java.lang.reflect,里面有三个类:InvocationHandlerMethodProxy

InvocationHandler

InvocationHandler接口(表示你的代理要干什么),就一个方法 invoke()

invoke() :表示代理对象要执行的功能代码。你的代理类要完成的功能就写在 invoke 方法中。

代理类完成的功能:

  • 调用目标方法,执行目标方法的功能。
  • 功能增强,在目标方法调用时,增加功能。

invoke 方法原型:

  • Object proxy:JDK 创建的代理对象,无需赋值。
  • Method method:目标类中的方法,JDK 通过 method 对象的。
  • Object[] args:目标类中方法的参数,JDK 提供的。

如何使用该接口:

  1. 创建类实现接口 InvocationHandler。
  2. 重写 invoke 方法,把原来静态代理中代理类要完成的功能写到这里。

Method

Method类:表示方法的,确切的说时目标类中的方法。

作用:通过 Method 可以执行某个目标类的方法,Method.invoke(目标对象,方法参数)

说明:method.invoke 就是用来执行目标方法的。

Proxy

Proxy类:核心对象,创建代理对象。之前创建对象都是 new 类的构造方法,现在使用的是 Proxy 类的方法,代替 new 的使用。

方法:静态方法 newProxyInstance()

作用:创建代理对象。

参数:

  1. ClassLoader loader:类加载器,负责向内存中加载对象的。如,使用反射获取对象的ClassLoader类 a,a.getClass().getClassLoader(),目标对象的类加载器。
  2. Class<?>[] interfaces:接口,目标对象实现的接口,也是反射获取的。
  3. InvocationHandler h:我们自己写的,代理类要完成的功能。

返回值:代理对象。

实现动态代理的步骤

  1. 创建接口,定义目标类要完成的功能。
  2. 创建目标类实现接口。
  3. 创建 InvocationHandler 接口的实现类,在 invoke 方法中完成代理类的功能
  • 调用目标方法。
  • 增强功能
  1. 使用 Proxy 类的静态方法,创建代理对象,并把返回值转为接口类型。

代码演示:

创建接口,定义目标类要完成的功能:

// 表示功能的,厂家和商家都要完成的功能
public interface UsbSell {

    // 定义方法 参数 amount 表示一次购买的数量,暂时不用
    // 返回值白哦是一个U盘的价格
    float sell(int amount);
}

创建目标类实现接口:

// 目标类:金士顿厂家,不接受用户的单独购买
public class UsbKingFactory implements UsbSell {
    @Override
    public float sell(int amount) {
        System.out.println("目标类中,执行sell目标方法");
        // 一个U盘85元
        // 后期根据amount,可以实现不同的价格,如买10000个,单价为80
        return 85.0f;
    }
}

创建 InvocationHandler 接口的实现类,在 invoke 方法中完成代理类的功能:

// 必须实现 InvocationHandler 的接口,完成代理类要完成的功能(1)调用目标方法 (2)功能增强
public class MySellHandler implements InvocationHandler {

    private Object target = null;

    // 动态代理:目标对象是活动的,不是固定的,需要传入进来。
    // 传入是谁,就给谁创建代理
    public MySellHandler(Object target) {
        // 给目标对象赋值
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object res = null;
        res = method.invoke(target,args);//执行目标方法

        // 方法增强:增加价格
        if (res != null) {
            Float price = (Float) res;
            price += 25;
            res = price;
        }

        // 在目标类的方法调用后,可以做其他的功能,都是增强的意思
        System.out.println("淘宝商家,给你返一个优惠卷");

        // 增加的价格
        return res;
    }
}

使用 Proxy 类的静态方法,创建代理对象,并把返回值转为接口类型:

public class MainShop {
    public static void main(String[] args) {

        // 创建代理对象,使用 Proxy
        // 1. 创建目标对象
        UsbKingFactory factory = new UsbKingFactory();
        // 2. 创建 InvocationHandler 对象
        InvocationHandler handler = new MySellHandler(factory);
        // 3. 创建代理对象
        UsbSell proxy = (UsbSell) Proxy.newProxyInstance(factory.getClass().getClassLoader(),
                factory.getClass().getInterfaces(),
                handler);
        // 4. 通过代理执行方法
        float price = proxy.sell(1);
        System.out.println("通过动态代理,调用方法:" + price);
    }
}

运行程序,结果:

其中,代理对象 proxy 的类型为 com.sun.proxy.$Proxy0

简易的执行流程:

CGLIB 的动态代理

CGLIB(Code Generation Library) 是第三方的工具库,创建代理对象。

对于无接口的类,要为其创建动态代理,就要使用 CGLIB 来实现。

CGLIB 的原理是继承,CGLIB 通过继承目标类,创建它的子类,在子类中重写父亲中同名的方法,实现功能的修改。

因为 CGLIB 是继承,重写方法,所以要求目标类不能是 final 的,方法也不能是 final 的。

涉及的类:Enhancer

提供者:第三方 CGLIB 库

如何创建代理对象:使用 Enhancer类中的 create方法。

创建代理对象的要求:被代理类不能是最终类。

ceate 方法的参数:

  • Class:字节码,用于指定被代理对象的字节码。
  • Callback:用于提供增强代码。它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。我们一般写的都是该接口的子接口实现类:MethodInterceptor

演示:

在 pox.xml 中引入 cglib 依赖

编写接口和其实现类

public interface IProducer {
    public void Product(float amount);
}
public class Producer implements IProducer{
    @Override
    public void Product(float amount) {
        System.out.println("生产所需费用为:" + amount*80);
    }
}

编写main函数,创建代理对象

package com.example;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class cglibClient {
    public static void main(String[] args) {
        final Producer producer = new Producer();

        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             *
             * @param o
             * @param method
             * @param objects
             * 以上三个参数和基于 JDK 代理中的 invoke 方法的参数是一样的
             * @param methodProxy:当前执行方法的代理对象
             * @return
             * @throws Throwable
             */
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object returnValue = null;

                //1. 获取方法执行的参数
                Float amount = (Float) objects[0];
                // 2. 判断当前方法是不是生产产品
                if ("Product".equals(method.getName())) {
                    returnValue = method.invoke(producer,amount*0.8f);
                }

                return returnValue;
            }
        });

        cglibProducer.Product(5);
    }
}

代码运行结果: