一、核心概念
Ioc (Inversion of Control)控制反转
- 使用对象时,由主动 new 产生对象转换为由 外部 提供对象,此过程中对象创建控制权由程序转移到外部,这种思想称为控制反转。
Spring 技术对 IoC 思想进行了实现
- Spring 提供了一个容器,称为 IoC 容器,用来充当 IoC 思想中的 “外部” 。
- IoC 容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在 IoC 容器中统称为 Bean。
DI(Dependency Injection)依赖注入
- 在容器中建立 bean 与 bean 之间的依赖关系的整个过程,称为 依赖注入。
二、IoC 入门案例
思路分析:
- 管理什么?(Service 与 Dao)
- 如何将被管理的对象告知 IoC容器?(配置)
- 被管理的对象交给 IoC 容器,如何获取到 IoC 容器?(接口)
- IoC 容器得到后,如何从容器中获取 bean?(接口方法)
- 使用 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...");
}
}
步骤:
- 在 pom.xml 中导入 Spring 坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
- 定义 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();
}
}
- 创建 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 属性在同一个上下文中不能重复。
- 初始化 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 入门案例
思路分析:
- 基于 IoC 管理 bean
- Service 中使用 new 形式创建的 Dao 对象是否保留?(否)
- Service 中需要的 Dao 对象如何进入到 Service 中?(提供方法)
- Service 与 Dao 间的关系如何描述?(配置)
步骤:
- 删除使用 new 形式创建对象的代码
- 提供依赖对象对应的 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;
}
}
- 配置 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 的三种方式
- 提供可访问的构造方法(常用)
- 配置:在spring配置文件中使用bean标签,配以 id 和 class 属性之后,且没用其他属性和标签时。
- 注意:无参构造方法如果不存在,将抛出异常 BeanCreationException
- 静态工厂(了解)使用某个类的静态方法创建对象,并存入spring容器。
- 配置
配置工厂实现类 OrderDaoFactory,factory-method配置工厂实现类里面的方法。
- 实例工厂(了解)使用某个类中的方法创建对象,并存入spring容器。
- 配置
- 方法3实例工厂的变种:FactoryBean(掌握,后面经常用到)
- 配置
5.2 bean 的生命周期
- bean 生命周期:bean 从创建到销毁的整体过程。
- 初始化容器
-
创建对象(内存分配)
-
执行构造方法
-
执行属性注入(set 操作)
-
执行 bean 初始化方法
- 使用 bean
-
执行业务操作
- 关闭/销毁容器
- 执行 bean 销毁方法
- bean 生命周期控制:在 bean 创建后到销毁前做一些事情。
bean 生命周期控制:
有两种方法:
- 提供生命周期控制方法
- 配置生命周期控制方法
执行顺序:(先将bean销毁,再关闭容器后的执行顺序)
- 实现 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 技术)。
步骤:
- 在业务层接口上添加 Spring 事务管理
- 设置事务管理器
- 开启注解式事务驱动
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 的实现类单独开启一个事务。
步骤:
- 在业务层接口上添加 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 提供了以下衍生注解:
- @Controller:表示创建控制器对象。
@Controller
public class UserController {
}
- @Service:表示创建业务层对象。
@Service
public class UserServiceImpl implements UserService {
}
- @Repository:表示创建持久层对象。
@Repository
public class UserDaoImpl implements UserDao {
}
这三个注解的作用和 @Component 的作用一样,都是用来创建对象。
10.2 注入相关注解
作用就和在 xml 配置文件中的 bean 标签中写一个 property标签的作用时一样的。
@Autowired
- 作用: 自动按照类型注入,只要容器中有唯一的一个 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,发现唯一的一个匹配类,则注入成功!
- 如果 IoC 容器中没有任何 bean 的类型和要注入的变量类型匹配,则报错。
- 如果 IoC 容器中有多个类型匹配时,步骤如下:
-
- 首先按照类型,圈定出来匹配的对象。
- 使用变量名称作为 bean 的 id,在圈定出来对象的里面继续查找。如果有一个一样的就注入成功,如果都不一样则会报错。
下面来演示下:
创建两个类分别如下:
此时 Spring 的 IoC 容器里的内容变为如下:
使用接口
main 函数如下:
运行代码,结果报错:
当我们将注入的变量名改为accountDao1
运行程序,结果如下:
当我们将注入的变量名改为accountDao2
运行程序,结果如下:
可以发现打印的结果不一样。为什么会这样呢?
解释:
- 首先按照类型,圈定出来匹配的对象。
- 使用变量名称作为 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-method和 destory-method的作用是一样的。
- @PostConstruct
用于指定初始化方法。
- @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);
}
}
这对于厂商是很麻烦的,因为每有一个消费者购买产品,厂商都要去对接。所以我们需要 代理 ****来为我们解决这个麻烦。
使用代理模式的作用:
- 功能增强:在你原有的功能上,增加了额外的功能,新增加的功能,叫做功能增强。
- 控制访问:代理类不让你访问目标,例如商家不让用户访问厂家。
实现代理的方式:
- 静态代理
- 动态代理
11.2 静态代理
- 代理类是自己手工实现的,自己创建一个 java 类,表示代理类。
- 同时你所要代理的目标类是确定的。
特点:实现简单、容易理解。
当项目中,目标类和代理类很多的时候,有以下缺点:
- 当目标类增加了,代理类可能也需要成倍的增加,代理类的数量过多。
- 当你的接口中功能增加了或者修改了,会影响众多的实现类、厂家类、代理类都需要修改。影响比较多。
模拟一个用户购买 u 盘的行为。
用户是客户端类。商家是代理,代理某个品牌的u盘。厂家是目标类。
三者的关系:用户(客户端)--- 商家(代理)--- 厂家(目标)
实现步骤:
- 创建一个接口,实现卖U盘的方法,表示你的厂家和商家做的事情。
- 创建厂家类,实现步骤1中的接口。
- 创建商家,就是代理,也需要实现步骤1中的接口。
- 创建客户端类,调用商家的方法买一个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,里面有三个类:InvocationHandler、Method和 Proxy。
InvocationHandler
InvocationHandler接口(表示你的代理要干什么),就一个方法 invoke()
invoke() :表示代理对象要执行的功能代码。你的代理类要完成的功能就写在 invoke 方法中。
代理类完成的功能:
- 调用目标方法,执行目标方法的功能。
- 功能增强,在目标方法调用时,增加功能。
invoke 方法原型:
Object proxy:JDK 创建的代理对象,无需赋值。Method method:目标类中的方法,JDK 通过 method 对象的。Object[] args:目标类中方法的参数,JDK 提供的。
如何使用该接口:
- 创建类实现接口 InvocationHandler。
- 重写 invoke 方法,把原来静态代理中代理类要完成的功能写到这里。
Method
Method类:表示方法的,确切的说时目标类中的方法。
作用:通过 Method 可以执行某个目标类的方法,Method.invoke(目标对象,方法参数)
说明:method.invoke 就是用来执行目标方法的。
Proxy
Proxy类:核心对象,创建代理对象。之前创建对象都是 new 类的构造方法,现在使用的是 Proxy 类的方法,代替 new 的使用。
方法:静态方法 newProxyInstance()
作用:创建代理对象。
参数:
ClassLoader loader:类加载器,负责向内存中加载对象的。如,使用反射获取对象的ClassLoader类 a,a.getClass().getClassLoader(),目标对象的类加载器。Class<?>[] interfaces:接口,目标对象实现的接口,也是反射获取的。InvocationHandler h:我们自己写的,代理类要完成的功能。
返回值:代理对象。
实现动态代理的步骤
- 创建接口,定义目标类要完成的功能。
- 创建目标类实现接口。
- 创建 InvocationHandler 接口的实现类,在 invoke 方法中完成代理类的功能
- 调用目标方法。
- 增强功能
- 使用 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);
}
}
代码运行结果: