# Spring框架入门
**Repository Path**: mxgitstorage/SpringStudy
## Basic Information
- **Project Name**: Spring框架入门
- **Description**: 此仓库用于存放在学习Spring框架中的一些代码,以及Spring框架的理解
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 0
- **Created**: 2021-06-21
- **Last Updated**: 2023-02-06
## Categories & Tags
**Categories**: Uncategorized
**Tags**: Spring, Java
## README
# Spring
## 什么是Spring
Spring是当下最主流的开源企业应用开发框架(现在你只需要知道这是个开发框架,还没到深究它是个什么框架的时候),其最初由一位名为Rod Johnson的程序员在2002年提出并建立, 初指是为了解决EJB(企业JavaBeans)开发的复杂性和软件开发中代码的耦合性,想方设法让JEE开发变得更加简单,令任何人都可以轻松的使用Spring框架进行JEE开发。
Spring是一个从实际开发中剥离出来的框架,因此Spring已经完成了开发中大量的相似步骤,留给开发者的工作仅仅是完成特定应用的特定部分,极大的提高了开发效率,从简单易用的角度出发,Spring不仅可以用于服务器开发,还可以用于任何Java程序的开发。
上头这段解释挺晦涩难懂的,但你在网上能看到百分之90的人都这么写,搞得刚入门的小程序员研究了半天也没研究明白Spring到底是个啥。其实对于Spring最通俗的解释就是,当你的程序需要使用数据库,而你又不想写那啰嗦的JDBC工具类时。欸嘿,这个时候我们就可以使用Spring了,Spring里头集成了JDBC操作,你可以使用简单的几条语句调用Spring集成的JDBC操作,Spring一行代码换十行,这波极限一换多它不亏。再或者,~~你想创建个对象,欸嘿,你没对象~~,啊不对,是你要new一个对象时,你需要写:
```java
Object ob=new一个Object();
```
但是你在Spring中可以这么写:
```xml
```
来进行对象的创建,顺带说一句,Spring里头有一个东西叫bean,这个bean到处都是,这个bean它就是,~~你没有的~~对象~~哈哈哈哈哈嗝~~。其实这么说也不严谨,严谨的说,**bean是一个被Spring加工处理过的对象,被Spring加工处理过的对象叫做bean**。
然后还可以这么写:
```java
@Component
public class NiDuiXiang{
private Integer age;
private String name;
private String SanWei;
}
```
这里使用了一个@Component注解创建了一个名为NiDuiXiang的bean(对象),莫研究什么是注解,莫关心这个@Component是什么东西,后面再说。好了,这时你的脑子里对Spring有了个大概的了解,如果还是没有大概那就接着往下看。
## Spring的优缺点
Spring在普通人眼中就是个神的存在,但是在不普通人眼里,依赖Spring会让你变得萎靡不振,会让你意志消沉巴拉巴拉巴拉等。Spring看似完美,但是♂深入研究后还是能扒出不少东西的。
> Spring的优点
- 非侵入式设计,使用Spring不会影响你自己的原代码,框架与代码耦合度低。
- 对象工厂&容器,将所有对象的创建和维护都交给Spring管理,提高程序复用。
- 支持面向切面开发,在不破坏原代码的前提下对程序的功能进行集中处理。
- 支持声明事务,只需要通过简单的配置就可以管理事务,不需要写繁琐的代码。
- 集成各类测试框架,可以通过注解快速测试。
- 优秀的集成性,可以集成其他框架,Spring内部为其他主流开源框架提供了直接支持。
- Spring对常用API进行了封装,方便调用,如常用的JDBC,Mail等等...
> Spring的缺点(粗略)
>
> - Spring非常的*庞大* **庞大**和***庞大***,Spring明明是一个轻量级框架,却什么都有。
>
>~~PS:Spring的源码已经达到了庞大的100w+行,在多来几年可能就赶上个Windows了?~~
>
> - 谨慎阅读Spring源码,如此庞大的源码,结合其他巨佬的说法,就是即使Spring底层实现一个很简单的功能,也写的很复杂,一般人读完后可能会有副作用。如果遇到面试非要问,那也只能硬着头皮去看了。
## Spring的结构
> Spring体系结构图

> Spring采用分层架构,Spring的各种功能被划分到多个模块当中,其大致可以理解成五个模块,不做细致分析,仅用于理解
>
> - Data Access/Integration:数据访问/集成模块。
> - Web:Web应用模块,提供Web上下文操作,Servlet支持等等
> - AOP:面向切面模块。
> - Core Container:核心容器,Spring核心组成。
> - Test:单元测试模块。
至此,什么是Spring框架就基本介绍完毕。
# 第一个Spring程序
粗略介绍完Spring之后我们开始编写第一个Spring程序,这边使用Idea+Maven+JDK1.8作为环境使用,环境不绝对,按自己习惯。
你能看到Spring的教程,基本上能熟练使用编译器和基本项目构建工具了。
> 第一个Spring程序准备工作
- Spring所需jar包,一个Spring程序需要使用如下五个基本包
spring-core-x.x.x.RELEASE.jar:Spring核心工具类包,所有组件都要用到。
spring-beans-x.x.x.RELEASE.jar:Bean处理包,主要包括Bean的创建定义初始化,配置文件读取等等等。
spring-context-x.x.x.RELEASE.jar:Spring基础功能扩展包(IOC基础上),提供了许多企业级服务的支持。
spring-expression-x.x.x.RELEASE.jar:Spring表达式(SpEL)包,例如#{}界定符。
和一个第三方包
commons-logging-x.x.jar:一个日志框架,可以跟其他日志框架配合使用也可以单独使用来记录日志。
其中,x.x.x表示的是当前Spring的版本,如Spring5的jar版本就是spring-core-5.0.1.RELEASE.jar。
至此,准备并导入完Spring的五个基本包之后,我们可以开始编写第一个Spring程序了。
> 第一个Spring程序
第一步,我们创建一个普普通通的类:
```java
package com.mx.entity;
public class FirstSpring{
public void hello(){
System.in.println("Hello spring");
}
}
```
第二步,我们创建一个Spring配置文件,Spring程序当中都需要有一个配置文件,配置Spring的各种操作,我们将其命名为applicationContext.xml,名字随意,是xml配置文件即可:
```xml
```
第三步,我们再创建一个普普通通的类,并在类中加载Spring配置文件:
```java
package com.mx.entity;
public class SpringMain{
public static void main(String[] args){
String path="applicationContext.xml";
//Spring applicationContext.xml配置文件路径,当你放在与项目同一路径下时,可以使用相对路径。
ApplicationContext application=new ClassPathXmlApplicationContext(path);
//初始化Spring容器并加载配置文件,此时的application是一个初始化了的Spring容器,
//什么是容器,容器就是装东西的东西,Spring容器装的就是一堆bean。
FirstSpring fs=(FirstSpring)application.getBean("firstSpring");
//通过application获取实例化后的bean,getBean方法根据bean id去xml配置文件当中找到对应id的bean。
fs.hello();
//调用hello方法后控制台就可以正常输出hello方法中的System.in.println("Hello spring")了。
}
}
```
至此,一个传统的Spring程序完成了。可以看到Spring程序与传统Java程序最本质的区别(我是这么理解的),就是不用写new了。程序将创建对象的过程交给Spring去管理,由Spring容器帮你去new。同时这也是Spring最核心的概念之一,控制反转(IOC)。莫急着探究什么是IOC,后面会和你深入探讨♂。
> 使用配置类编写Spring程序
在上面我们使用了标准的Spring配置文件进行Spring程序编写,但xml总会有些奇奇怪怪的小毛病,所以Spring3之后提供了一个配置类来替代繁琐的xml配置文件。
> 配置类演示
我们直接改写上方的FirstSpring,进行代码重用(偷懒)
新建一个配置类:
```java
package com.mx.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig{
@Bean//注解声明这是一个bean,提交给容器进行处理,bean id默认以方法名/类名首字母小写。
//这个方法的bean id就是getFirstSpring。
public FirstSpring getFirstSpring(){
return new FirstSpring();
}
}
```
我们又创建一个配置类测试类:
```java
package com.mx.test;
public class SpringConfigTest{
public static void main(String[] args) {
ApplicationContext application=new AnnotationConfigApplicationContext(SpringConfig.class);
//配置类使用的AnnotationConfigApplicationContext类加载配置类与传统xml配置文件的CPXAC不同。
//参数填配置类的.class对象。
FirstSpring fs=(FirstSpring)application.getBean("getFirstSpring");
//获取bean
fs.hello();
//控制台成功输出hello方法。
}
}
```
可以看到,Spring使用@Configuration注解来声明一个配置类。这个注解就相当于xml文件中的beans标签,可以在被修饰的类中进行传统xml文件中的操作。配置类里头有一个@Bean注解来声明一个bean,就相当于xml文件中的bean标签,这么写可是一个xml文件也没有哦。
到此,我们的第一个Spring程序的两种创建方式结束了,你已经对Spring有了一个基本的认识,我们要开始深入♂探讨了。
# 什么是IOC
在OOP面对对象软件设计中,万物基于基于对象,对象与对象构成一个整体,对象与对象之间的关系组成一个系统,对象与对象之间的相互配合,能更好的实现一个完整的功能(搁着搁着呢)。一个程序的组成其实可以看做一组齿轮,齿轮与齿轮相互结合,才能带动系统运作。

如图所示你会发现,传统软件的对象组成就像齿轮一样,每个齿轮(对象)紧密结合,一个齿轮转动带动其他齿轮一起转动,一个齿轮不转,其他齿轮也不转,齿轮之间的关系很亲密。这时我们可以说这些齿轮的耦合程度很高,齿轮之间的结合相当于对象之间的耦合。当对象之间的耦合度过高时,一个对象出现问题,就会出现极限一换多的情况,整个系统因为一个对象(齿轮)出现问题,整个系统就陷入了停滞。软件中的对象与否,模块与否,都会存在着耦合,耦合过高,软件就会出现奇奇怪怪的问题。为了解决软件里头的耦合度,一个外国(Michael Mattsson 迈克尔·马特森)软件工程专家提出了一种叫IOC的思想,注意,**IOC是一种思想**,不是一种花里胡哨的工具,也不是一种花里胡哨的技术方法,IOC它就是一种思想。
## IOC的理解
IOC(Inversion of Control 控制反转)的提出皆在解决代码之间的耦合度。通常我们创建一个对象需要自己手动new一个对象出来,并对对象进行属性赋值等各种操作与管理,这时创建对象的操作权限在我们的手中,我们new时他就有对象,不new就没有。IOC的核心想法是指将对象创建的过程从我们手中交给一个IOC容器手中,由IOC容器替我们进行对象的创建和管理。

如图,中间的大齿轮就是一个IOC容器,它负责将Object1,2,3,4分割开来,使得其不再相互关联,而是通过IOC容器进行关联,就好比你自己找对象和通过媒婆帮你找对象是一个道理,需要一个中间商帮你牵线搭桥,至于中间商如何给你拉来一个相亲对象的,你完全不需要了解。于是乎你就可以理解,实现了IOC思想的地方,叫做IOC容器,而Spring,本质上也是一个IOC容器,里面包含了对对象创建的控制,对象属性的操作等等等。看到这,你应该明白了什么是IOC和什么是IOC容器。但是问题又来了,你听说过控制反转IOC,就肯定听说过依赖注入DI,那问题又来了,DI是什么东西,和IOC又有什么关系,那你就给看下边了。
## DI的理解
DI(Dependency Injection)依赖注入,好迷惑的词,它又跟IOC有什么关系?什么叫依赖叫注入?莫慌,让我们先写一个demo进行研究:
```java
public class A{
public void say(){
System.out.println("A类中的方法");
}
}
public class B{
private A a=new A();
public void depend(){
a.say();
}
}
```
根据这个小demo可以看出,我们有一A一B两个类,B当中有一个成员A的对象,当我们需要在B中调用A的方法时,就必须通过A才能调用,这时我们可以说B依赖A,B依赖了A才能调用A中的方法,这就是依赖。哪什么是注入呢,我们在B中创建了一个A类型的成员a,要想使用成员a,就给初始化它:A a=new A()完成了一个初始化的动作,这个初始化动作就是注入,将想要的值赋值给a,a就完成了初始化,可以说a完成了注入。那问题又来了,依赖知道了,注入知道了,哪依赖注入又是什么?demo当中,B依赖A实现了B的depend方法,但是,你怎么知道成员a一定是B所依赖的A呢,这时肯定是要将a赋值成你所依赖的A啦,我们再划分下结构:
> A a=new A();
>
> a.say();
>
> > a是B的成员变量
> >
> > > A是B的依赖,A被B依赖,A成为了B的被依赖对象,B是A的调用者,将被依赖的对象A赋值给成员a,就是a=new A(),A是成员a需要注入的外部属性,注入后就可以通过a调用被依赖对象A中的方法,这就是依赖注入。
那么问题又来了,叭叭了半天,依赖注入DI和控制反转IOC又是什么关系?**其实DI和IOC是一个东西,DI的过程实现了IOC的思想,有了DI就实现了IOC,这个过程由你自己注入变成了Spring帮你注入,实现了一个反复横跳,而这个反复横跳,就是控制反转。**不知道为什么这帮搞计算机的老头老太要弄出这么多名词来扰乱心智。 你看我给你废话解析一下,Spring是不是一个大媒婆,是不是帮你找了符合条件的对象,你报了个三围,这个三围是不是你想要的对象的属性?Spring帮你把对应属性的对象搞出来再送到你面前,你就不用自己去找这个对象了。于是乎我们就可以这么理解,Spring帮你找对象的过程是一个**控制反转**的过程,你给Spring提供了一个三围,Spring根据三围帮你找到了这个对象,三围不就是需要**注入**到对象的值吗。总结流程下来就是,Spring帮你完成了对象的创建和初始化,整个处理过程你也不关心,Spring靠DI实现了IOC,DI就是IOC的实现方法,废话了半天,只有**DI是IOC的实现方法**这一句是最重要的。看了半天你发现,这就是一个你在搁着搁着呢的过程。~~感谢你又浪费了人生十分钟~~
# Spring的DI方式
既然知道了DI,就肯定给明白Spring对于对象是怎么注入的,对于传统的对象注入方式我们有两种,一种是类自身构造方法的注入:
```java
public class A{
private int id;
private String name;
public A(int id,String name){
this.id=id;
this.name=name;
}
}
//构造方法注入
A a=new A(1,"MX");
```
一种是通过类自身的Set方法进行属性注入:
```java
public class A{
private int id;
private String name;
public void setId(int id){
this.id=id;
}
public void setName(String name){
this.name=name;
}
}
//Set方法注入
A a=new A();
a.setId(1);
a.setName("MX")
```
Spring本质也是通过这两种方法进行DI的,只不过这个过程通过配置文件或注解实现。
## 构造方法注入
构造方法注入的意思就是,Spring通过类的构造方法进行注入,注意,是有参构造方法,不是有参你注哪去。
### 基本构造方法注入
```xml
```
此时Spring会根据constructor标签的顺序去匹配构造方法参数,第一个constructor标签的索引就是0,第二个就是1,以此类推。这是最简单的构造方法注入,同时Spring官方文档中提供了多种构造方法注入的玩法。
### 根据索引注入
假设我们有一个User类,类中有int id,String name两个属性和其对应的构造方法User(int id,String name),然后在配置文件中进行装配:
```xml
```
index属性表示的是构造方法中的参数的索引,第一个index是第一个索引0,第二个是索引1,以此类推。index="0" 对应的是参数id,value="20"表示将id赋值为20。
### 根据参数名进行注入
```xml
```
Spring通过name来绑定构造方法中的参数名进行注入。
### 根据构造函数的参数类型进行注入
```xml
```
type属性表示的是构造函数的参数类型,Spring会根据参数列表的索引和类型进行匹配,当索引为0且对应的参数类型是int时,id=20。需要注意的是,当你使用封装类作为数据类型时,type不可简写,如当id的类型是Integer时,type就必须为java.lang.Integer,否则会报类型不匹配异常。
写着写着突然突发奇想,没有index索引时,Spring是不是根据constructor-arg标签的排列来确定构造函数索引的呢?让我们进米奇妙妙屋探讨探讨。
```java
public User(String name,Integer age,String phone,int sex)
```
有那么个构造函数,0-3索引分别是name,age,phone,sex,在配置文件中打乱顺序装配一下并运行:

按着此装配方式来说,正常对应的构造函数参数索引应该是age->name->phone->sex,而我们的是name->age->phone->sex,但是spring居然注入成功了,本着走近科学的态度,我们打乱顺序再试一次:

当打乱phone和name的位置时,它没了它没了,它注乱了。仔细研究过后发现,Spring可以根据constructor-arg标签进行顺序注入,也可以根据constructor-arg标签内的name,index属性打乱顺序注入。type属性也可以进行乱序注入,但前提是类中的属性类型单一,如果不单一,就会出现上面的情况,随便找个类型相同的属性就注入了。
### 属性是类时的构造方法注入
创建一个类,类中有属性依赖另一个类:
```java
public class A{
private B b;
public A(B b){
this.b=b;
}
}
```
在配置文件中可以这样做:
```xml
```
通过ref引入b的引用,将其赋值给a。**注意**:要用ref而不是value。关于ref或者value的使用区别,可以去下面看[关于value和ref的区别](#关于value和ref的区别)。引用完成后,类A中的b就赋上了值B。
## Set方法注入
Spring提供了关于Set方法的注入方式,其与使用构造方法还是有那么一点区别的。
老规矩,上来就是一个User类,并提供俩Set方法:
```java
public class User{
private int id;
private String name;
private UserMapper userMapper;
public void setId(int id){
this.id=id;
}
public void setName(String name){
this.name=name;
}
public void setUserMapper(UserMapper userMapper){
this.userMapper=userMapper;
}
}
```
然后在配置文件中配置bean并注入属性:
```xml
```
值得注意的是,使用Set方法注入时bean标签中的子标签变成了property,property标签选择的是Set方法的方法名,默认是根据set后面所跟的属性名的小写,如setId,其在property中的值就是id,最后再使用value属性对其进行赋值。当需要初始化的bean中有属性是依赖类时,就需要传入其的bean引用了(ref)。
更值得注意,当你使用Set方法注入时,Spring是根据类的Set方法和默认无参构造进行初始化的,当你有需要时重写了有参构造方法,Java就会覆盖掉默认的无参构造,配置文件中会报**没有匹配的构造方法**的,这时还要写上一个无参构造才行,新人常犯错误之一。

## 奇奇怪怪的注入
除了传统的构造方法和set方法注入,Spring还提供了几个奇奇怪怪的注入方法,老规矩,上来就是一个User类,里头有-有参无参构造和Set方法。
```java
public class User{
private int id;
private String name;
public User(){}
public User(int id,String name){
this.id=id;
this.name=name;
}
public void setId(int id){
this.id=id;
}
public void setName(String name){
this.name=name;
}
}
```
### p名称空间注入
p名称空间注入的本质还是Set方法注入,只是简化了一些步骤,可以在bean的自闭合标签中使用。使用p名称空间时,需要在配置文件的xml约束中添加p标签的约束:
```xml
```
p标签简化了的注入方法,使其可以在bean自闭合标签中使用,还是需要依赖Set方法和无参构造的。
```xml
```
这就完成了一个p名称空间注入,本质就是Set方法注入,重要的话要说两遍。
### c名称空间注入
既然有property标签的简化版,根据爱因斯坦相对论,就肯定会有constructor-arg标签的简化版,这就是c名称空间。其依赖的是有参构造方法进行注入,语法和p名称空间区别不大,仅仅把xml约束改为c就没了。
```xml
```
这就完成了一个简单的c名称空间注入,根据使用情况来说,p和c两个名称空间用的比较少,了解即可。
## 复杂类型注入
一个类当中的成员属性类型是多种多样的。可能会有List,也可能会有Map等等等复杂的类型。这时就需要变着花来注入了。
老规矩,一个User类,对应有参无参构造,Set方法:
```java
public class User {
private String name;
private Integer age;
private List address;
private Mapmap;
public User() {}
public User(String name, Integer age, List address, Map map) {
this.name = name;
this.age = age;
this.address = address;
this.map = map;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setAddress(List address) {
this.address = address;
}
public void setMap(Map map) {
this.map = map;
}
```
随后在配置文件中配置bean:
```xml
M78星云
银河系
```
这就完成了一个复杂属性的注入,其中List类型的注入需要在property标签中选定List属性然后再使用子标签list设置List中的value值。Map类型同理,只不过需要使用entry键值对的方式进行赋值,一定要确认好有无无参构造方法,重要的事情要说一遍。
本章Spring常用的注入方法总结完毕,剩下的不常用的花里胡哨的方法有需要再说。
## 关于value和ref的区别
在注入时,通常会有两个属性,一是value,一是ref,很容易让新手迷惑。其实通过他们两个的中文含义就可以知道个大概。
value翻译过来是值,ref是reference的缩写,翻译过来是引用,从别的地方拿过来用,叫引用。
当你需要赋值的属性是一串字符时,我们使用value,当你需要赋值的属性是一个类对象时,就需要使用ref对其他对象进行引入然后赋值了。这就是value和ref的区别,**value使用的是字符串,ref引用的是其他类对象**。
## 使用构造方法注入还是Set方法注入
了解到最后,你会迷惑为什么Spring会同时提供两种方法注入呢,直接使用一种不更好?存在即合理,仔细想想构造方法一般是干什么用的?是给对象中的属性赋初始值的。**当你想要一个对象创建时就带有初值,就需要使用构造方法注入**,就像一辆车开出工厂,就必须要有发动机和轮子。**当你只想在对象完成创建后再对对象进行赋值,就需要使用Set方法注入。**这是关于使用哪一个方法注入的笼统解释。当然也有复杂一点的解释,就是通过构造方法注入的对象,其对属性的依赖程度很高,因为其必须在对象初始化时就注入属性。Set方法注入却是在对象创建完成后在进行属性注入,其对属性的依赖程度较低,当你需要对象带初始值时就需要使用构造方法注入。
到这,Spring基本的IOC原理就结案了。
# 什么是AOP
前面提到(好像提到了吧),Spring有两个重要组成部分,一个是IOC另一个就是AOP。AOP的全称是Aspect-oriented Programming ,即面向切面编程,是对OOP的一种补充。AOP和IOC一样,都是一种概念or思想。
那么什么是面向切面编程呢?在传统软件当中,面对CRUD(增删改查)等操作,通常都会有对应的事务和日志之类的方法。当你想给每个操作方法都加入事务or日志方法时,你需要给每个方法写入相同的代码去实现事务和日志操作。这就使得整体代码变得很复杂和冗余。如果想对加入的方法进行改造or删除,需要进入到代码当中去修改,无形之中减少了大量的摸鱼时间。加上代码中或多或少的依赖关系,有时候一小点改变都会牵一发而动全身,这也违背了IOC原则。

为了解决上述的这些麻烦事,AOP概念出现了,AOP的出现让程序员为复杂的程序添加功能时不再手忙脚乱,也使修改后的代码之间的耦合度降低。AOP就像用一块芝士插入到肯德基疯狂星期四瀑布芝士汁汁厚牛堡当中,不改动整体而又能添加功能。要想深入理解AOP,那就需要了解一个叫**代理模式**的东西,**代理模式**是AOP实现的**关键**。
## 什么是代理模式
代理模式,通俗的说,就是一个中介,它会帮你在使用类和被代理类之间进行一系列操作。就好比卖车的销售,你告诉销售你的需求,他就去领导那帮你申请价格申请优惠。这时的你就是一个使用类,销售是一个代理类,领导是一个被代理类,销售加载两者之间进行交流。**看到这,你是不是想起了前面讲IOC概念时把IOC比喻为一个媒婆,但是IOC和AOP两者使用的模式不同,IOC是工厂模式,AOP是代理模式,这点不要搞混了。**这个教程是给简单入门的,就不过多深入了解代理模式,想深入的就前往[知识的海洋](www.baidu.com)探索,这东西没个几十年功力,水太深你把握不住的。下面我们写一个小Demo来演示下代理模式是什么样的。
老规矩,上来就是一个User类,写好构造方法和Set/Get方法:
```Java
public class User {
private String name;
private String password;
}
```
再来一个UserService接口,内有两个方法:
```Java
public interface UserService {
void login(String name,String password);
void register(User user);
}
```
再来一个供代理类使用的接口,里面方法和UserService接口方法的相同:
```java
public interface UserProxyService {
void login(String name,String password);
void register(User user);
}
```
随后再来一个UserServiceImpl作为被代理类并实现UserService接口的方法:
```Java
public class UserServiceImpl implements UserService {
//这是一个需要代理的类
@Override
public void login(String name, String password) {
System.out.println("登陆成功:用户名是:"+name+"密码是:"+password);
}
@Override
public void register(User user) {
System.out.println("注册成功:用户名是:"+user.getName());
}
}
```
然后再来个UserServiceProxyImpl代理类为被代理类的方法添加功能:
```Java
public class UserServiceProxyImpl implements UserProxyService {
//这是一个代理类
User user=new User("MX","1234");
//模拟数据库中的数据
UserServiceImpl userService=new UserServiceImpl();
//被代理类依赖
@Override
public void login(String name, String password) {
System.out.println("进行登录账户密码检查!");
//这是进行代理后额外添加的功能
if (name.equals(user.getName())&&password.equals(user.getPassword())){
userService.login(name,password);
//这是原功能
}else {
System.out.println("登陆失败,账户密码错误!");
}
}
@Override
public void register(User user) {
System.out.println("进行用户名是否存在检查!");
//这是进行代理后额外添加的功能
if (user.getName().equals(this.user.getName())){
System.out.println("注册失败,用户名存在!");
}else {
userService.register(user);
//这是原功能
}
}
}
```
最后来个测试方法实现代理功能:
```Java
▶ @org.junit.Test
public void User(){
User user1=new User("MX","1234");
//模拟传入的数据
UserServiceProxyImpl userServiceProxy=new UserServiceProxyImpl();
//当代理了被代理类时,调用的就给是代理类中的修改过的方法了。
userServiceProxy.register(user1);
userServiceProxy.login(user1.getName(), user1.getPassword());
}
```
可以看到,代理类UserServiceProxyImpl对UserServiceImpl被代理类中的两个方法进行了功能添加,添加了用户名密码校验的功能,并且没有改动UserServiceImpl被代理类中的方法。这时的UserServiceProxyImpl就是一个中间人,四处忙活,忙活完后可以在控制台看到成功对被代理类的两个方法添加新功能成功的对user和user1对象中的值进行校验和输出:

这就完成了一个简单的代理模式的例子,仔细发现,这个代理模式是使用接口和实现类对原始类进行方法修改的,代理接口和原方法接口里头的方法是对应的,当我需要进行新的方法添加时,还是需要修改两个接口中的方法和实现,这无形之中增加了代码开销。对上面这种只能对一个类进行特定方法改变的代理模式,我们成为**静态代理模式**。你可以看到,采用这个模式的代理类是需要依赖被代理类的,而且一旦想增加其他方法,就需要改动代理类中的代码,当程序庞大时,就需要写很多个代理类。这显然是违背IOC原则的,耦合度过高了,这样一来,静态代理的弊端还是很多的。为了解决静态代理的弊端,在实际开发中我们都会采用动态代理来进行代理模式的实现,那么问题来了,什么是动态代理。
## 什么是动态代理
静态代理之所以叫静态代理,是因为在程序编译时,程序员手动写出来的静态代理的代理类会加入到编译的过程中,同时也会产生class文件。而动态代理是不需要创建文件,不需要参与程序的编译,其在程序运行的过程中进行代理操作,也不会产生class文件。动态代理的原理是通过程序运行期间[反射](#什么是反射)(别管,后面会大概说说什么是反射,不要深究,你把握不住的)来生成任意代理类。即使动态代理中的代理目标有很多个,代理类往往只需要有一个或多个,一个代理类可以代理多个目标,较为灵活,所以被称为动态代理。动态代理有两种方式,一种是JDK自带的动态代理,一种是使用第三方代码工具库CGLIB的动态代理。
### JDK动态代理
JDK内部提供了三个用于实现动态代理的类和接口,位于java.lang.reflect包下,因为都是基于反射的,所以一并丢在了reflect包下。
要实现自带的动态代理,需要具备两个类和一个接口:
- Proxy:代理类,创建代理对象
- InvocationHandler:接口,实现接口方法来增强被代理类方法
- Method:表示被代理类中的方法
实现了这三个东西后,才能创建一个完整的动态代理。动态代理比静态代理难理解多了,非常的抽象,功能非常的多也非常的复杂,我们只能先通过几个小demo对动态代理的两个类一个接口进行了解后在研究其内心深处的秘密。
新规矩,上来就是一个TargetDao作为目标接口,内有一个方法,提供给JDK动态代理,因为实现JDK动态代理是需要通过接口的,没有接口就无法实现:
```java
public interface TargetDao{
public void print(String msg);
}
```
然后再来一个普通类TargetImpl作为接口的实现类,也就是被代理类:
```java
public class TargetImpl implements TargetDao{
@Override
public void print(String msg){
System.out.println("msg:"+msg);
}
}
```
然后再来一个测试方法,注意,我们不用传统的new方法对TargetImpl进行测试,而是使用Method类进行测试:
```java
▶ @org.junit.Test
public void test() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException{
TargetDao target=new TargetImpl();
Method method=TargetDao.calss.getMethod("print",String.class);
Object res=method.invoke(target,"你好Method")
}
```
运行后可以看到控制台正常输出方法:

神奇的是,我们并没有按照传统的new方法去创建一个对象并调用其方法,而是通过Method类的invoke方法去调用,Mehtod类通过反射机制获取到类方法的所有信息,然后通过invoke方法来调用目标内被getMethod(方法名,方法参数类型) 获取到的方法名和参数,同时将参数传入。method.invoke(target,"msg")就等同于:target.print("msg");只不过这个过程由JVM来进行而不是用户手动进行。同时Method获取方法是实现动态代理的第一要求,你知道了JVM是怎么获取方法名称和参数的,剩下的就好办很多了。
我们直接开始上手一个简单的动态代理例子,创建一个含有接口,接口实现类(被代理类),和实现了InvocationHandler接口的增强类的项目:
这个接口可以称其为代理接口:
```java
public interface TargetDao {
public void print(String msg);
}
```
接口实现类(被代理类):
```java
public class Target implements TargetDao {
@Override
public void print(String msg) {
System.out.println("msg:"+msg);
}
}
```
实现了InvocationHandler的增强类:
```java
public class TargetProxy implements InvocationHandler {
private Object object;
public TargetProxy(Object object) {
this.object = object;
}
public Object getProxy(){
return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(),this);
//这个this就是TargetProxy本身,因为它实现了InvocationHandler接口。
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置增强方法");
Object res=method.invoke(object,args);
System.out.println("后置增强方法");
return res;
//return null;
//如果被代理方法有返回从参数,这个res就是需要返回的参数,没有参数可以用null代替
}
}
```
最后再来个main方法整合项目:
```java
public class Test {
public static void main(String[] args) throws IOException {
Target target=new Target();
//被代理类
TargetDao targetDao= (TargetDao) new TargetProxy(target).getProxy();
//开始代理操作
targetDao.print("原始方法");
}
}
```
最后运行项目,可以看到print方法被成功代理并增强了:

这就是一个简单的动态代理例子,可以看到传入一个被代理类,随后通过newProxyInstance()创建代理类,随后InvocationHandler中的invoke()方法给被代理的方法进行功能增强,同时method.invoke()执行了被代理类Target中的print()方法,位于method.invoke()前的就是前置增强方法,位于method.invoke()后的就是后置增强方法,可以判断出,位于InvocationHandler中的invoke()方法中的增强方法都是环绕增强的,在写一个就是前置,在后面写一个就是后置,全写就是环绕。那么问题又来了,为什么会有两个invoke()方法?其实这两个invoke方法无多大血缘关系,唯一关系就是,它俩都在反射包内吧,InvocationHandler中的invoke()方法是用来执行method.invoke()并增强的。Method的invoke方法是用来执行被代理类的原方法的,就这么简单。
前面有提到,JDK动态代理是需要依靠接口才能实现的,为什么要依赖接口呢,这就要去看Proxy类里头的东西了。**Proxy类是JAVA提供的一个用于创建代理对象的类**,最后再通过这个代理类进行花里胡哨的操作,同时这个过程是动态创建的,不会在target文件夹里产生class文件,这就是为什么要叫动态代理的原因。Proxy提供了创建动态代理类的一堆方法,大都是静态的,可以直接调用。

可以看到,Proxy内部有很多方法,而我们只需要关注其中最重要的newProxyInstance方法,这是创建代理类的关键,newProxyInstance方法的参数列表分别是:
- ClassLoader:类加载器,用来加载被代理类的。
- Class>[] interfaces:被代理类所实现的接口列表,一个数组里面放的是接口中的方法名。
- 实现了InvocationHandler接口的增强类。
通过参数列表我们可以看到,有一个参数是需要获取被代理类的实现接口的,这也间接的说明了Proxy是需要依赖接口的,但是这样子不太严谨,我们给去看JDK动态代理给我们生成的代理类class文件。这里我们可以利用Java自带的HSDB工具进行监控--[HSBD如何使用](www.baidu.com):

动态代理生成的代理类是由$Proxy开头的类名,打开类说明后可以看到,Proxy给我们生成了一个位于com.sun.proxy包内的$Proxy0代理类,这些操作都是在程序运行期间创建的,用后即销毁,你只能通过监控工具来看。可以看到$Proxy0代理类中继承了Proxy类和实现了TargetDao接口,那么重点来了:
**由于Proxy是用于创建代理类的,代理类又必须继承Proxy来实现其中的方法,所以Proxy是所有代理类的父类,又因为Java特点之一就是单继承,所以Proxy创建出来的代理类没办法再对被代理类进行继承来增强方法,所以,所以,只能通过实现接口来创建被代理类的!!!方法!!!**这就是为什么JDK动态代理需要使用接口的原因了。
Method和Proxy一个获取方法,一个创建代理类,两个类就这么总结完了,没什么复杂的,这时JDK动态代理就剩下最后一个InvocationHandler了,InvocationHandler就是一个接口,里面就只有一个返回类型为Object的invoke(Object proxy, Method method, Object[] args)方法,其参数列表是:
- Object proxy:Proxy创建的代理类。
- Method method:Method对象。
- Object[] args:这个Object数组里头放得是被代理类的被代理的方法的参数,比如方法有两个参数,那么args[0]就是第一个参数。
这些参数都不需要你手动去写,JVM全帮你写好了,你只需要在invoke里调用并增强方法即可。
InvocationHandler的作用就是给被代理类的方法进行增强的,没了,就这么简单。其工作流程可以这么认为:

1.创建代理类
2.获取被代理方法
3.Proxy调用InvocationHanlder进行方法增强
4.返回被增强后的方法。
JDK动态代理的三个要素就这么废话完了,,其实掌握动态代理与否不会影响你使用AOP,只不过提前掌握能少走点弯路,好了下面要开始讲CGLIB了。
### CGLIB动态代理
CGLIB是啥呢,CGLIB是一个代码生成库(CGLIB很强大,直接在操作字节码,所以什么都能干,并不是只能干动态代理这一件事,不要记忆的太死!!),它可以为没有接口的类创建代理,是对JDK动态代理只能通过接口来操作的缺点的补充。CGLIB的作者就像我一样,十分的懒,自己写的东西到现在都没一个使用文档之类的介绍,会用就用,不会用就拉倒的那种,还没到看能看源码的功力,所以CGLIB了解了解怎么用就行了,没有这个能力去深挖。
先来一个小案例看看通过CGLIB代理是怎么代理的。老规矩,上来先来个被代理类User:
```java
public class Target {
public void say(){
System.out.println("User");
}
}
```
再来一个代理类写增强方法:
```java
public class TargetProxy implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("前置增强");
Object insult=methodProxy.invokeSuper(o,objects);
System.out.println("后置增强");
return insult;
}
}
```
最后测试一下:
```java
@org.junit.Test
public void ProxyTest(){
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback(new TargetProxy());
Target target= (Target) enhancer.create();
target.say();
}
```
成功在控制台输出:

可以看到,与传统的JDK动态代理不一样,CGLIB使用了MethodInterceptor来替代InvocationHandler,使用Enhancer替代Proxy,而且也不需要使用接口了。不过你不能这么想,JDK动态代理只能通过接口,哪CGLIB是不是只能通过继承?JDK动态代理使用接口的原因是Java只能单继承,但是接口可以实现多个,CGLIB也可以通过接口,就是几乎不大使用罢了。
CGLIB被很多AOP框架使用,例如Spring自带的Spring-AOP就是使用的JDK动态代理+CGLIB。Spring-AOP默认使用的是JDK动态代理,当被代理类没有接口时,就会转向使用CGLIB,切换较为灵活。
既然知道了CGLIB是通过继承来创建代理类的,你就肯定知道被继承的类不能是final类的,final类不能被继承。
我们不会在CGLIB当中做太多赘述,只能说点到为之。到此,AOP前期的代理模式就了解个大概了,下面要开始学习Spring-AOP的基本操作了。
### 关于何时使用JDK动态代理和CGLIB动态代理
其实这个不是你需要考虑的问题,各种AOP框架都帮你实现好了,好比Spring-AOP默认使用JDK动态代理,当被代理类没有接口时,就会转向使用CGLIB,切换较为灵活。同时扩展一下,JDK动态代理是比CGLIB慢的,所以当你需要频繁创建代理时,就需要使用CGLIB了,当你只需要创建单例代理或较少代理时,就可以使用JDK动态代理。
## Spring-AOP准备工作
了解完了AOP与代理模式的关系,接下来就可以去研究AOP了。前面提到,AOP是一个概念,也是一种操作,是一个不改变原代码的同时进行代码功能修改的方法,自然就会有一些奇怪的概念专属小名词。要想充分了解AOP,就需要了解AOP的四个关键词:
- 连接点(JoinPoint):指一个类中有多少个可以被增强的方法,例如类中有五个方法,其中只有两个方法需要增强,那么这两个方法就是切入点。
- 切入点(PointCut):指实际被增强的方法,例如,一个类中有四个方法,而我只增强了其中一个方法,被增强的那个方法就是切入点。
- 通知(Advice):指给方法加了什么东西,给方法增加的功能叫通知,通知可以称为增强,增强可以称为通知。AOP的通知有五种类型,在前面的JDK动态代理时已经讲过了三个,分别是:
- 前置通知(Before):位于被增强方法前的功能。
- 后置通知(After):位于被增强方法后的功能。
- 环绕通知(Around):位于被增强方法前后的功能。
- 异常通知(Throwing):当被增强方法出现异常时的功能。
- 最终通知(Return):当被增强方法结束时的功能。
- 切面(Aspect):给业务方法增强功能时的操作,这么说不太好理解,白话来说就是,将一个增强方法(切入点)放在被增强方法中(通知)的过程。
拓展
- 织入(Weaving):是指将切面应用到目标对象后创建新的代理对象的过程,例如动态代理在程序运行中将增强方法动态的切入到被代理类中的过程,这个就叫织入,织入是一个动作,是使用何种方式将切面切入到被代理类当中的动作。这也很好的解释了为什么AOP被称为面向切面编程而不是面向板面编程。
要开始研究AOP,先准备好AOP必备jar包:
> spring-aop-x.x.x.jar:该包是进行SpringAOP开发的必备组件包。
>
> aspectjweaver-x.x.x.jar:该包是Aspectj的编译包,支持切入点表达式,同时Spring会重用里面的一些方法。
>
> aspectjrt-x.x.x.jar:该包是Aspectj对AOP注解支持的包,当你不使用注解的时候可以省略。
>
> 当然也可以偷懒,直接使用spring-boot-starter-aop-x.x.x.jar,这个包一次性的倒入了所有AOP相关,就是冗杂了点,谁叫他方便呢。
### 切入点表达式
切入点表达式是一个让程序明白需要对哪一个类中的哪一个方法进行增强的表达式,其组成结构为:
> execution(权限修饰符 返回类型 方法全路径名 (参数列表))
举个🌰,有一个类中的方法需要被增强,这个类叫Entity,内有一个方法void say(String say),我们要对该方法进行增强,需要编写切入点表达式:
> execution(* com.mx.Entity.say(..))
>
> 解析:
>
> execution():限定词,是表达式的开始。
>
> *:方法的权限通配修饰符,表示public or private or更多,也可以自己手动写。
>
> 空格:被增强的方法的返回类型,当在表达式中使用了*通配符时,可以省略方法的返回类型。
>
> com.mx.Entity.say():表示被增强的方法的全路径位于com.mx包下的Entity类中。
>
> (..):方法的参数列表,..可以表示方法的一个或多个参数,也属于通配符。
特例:
> execution(public void com.mx.Entity.say(void))
>
> 当你手动加入权限修饰符,就不可省略方法返回值,否则会报错,所以一般都建议直接使用通配符。
增强该类中所有方法:
> execution(* com.mx.Entity.*(..))
>
> 还是使用通配符*,通配符真是个万能的好东西呢。
>
> 切入点表达式的写法不唯一,遵循规范就好。你要是愿意,都是**也是可以的,毕竟官方文档都有七八种写法。
我们直接上手实例,老规矩上来就是一个Entity类:
```java
package com.mx20.entity;
public class Entity {
public void entityBaseFun(){
System.out.println("我是原方法");
}
public String entityBaseParamFun(String say){
return say;
}
}
```
在来个增强类和增强方法:
```java
package com.mx20.Proxy;
public class EntityProxy {
public void enhanceEntity(){
System.out.println("我是增强方法");
}
}
```
随后在配置文件中配配置AOP:
```xml
```
配置文件解析:
>aop:config 标签:表示这一个AOP配置。
>
>aop:pointcut 标签:表示这是一个切入点表达式。
>
>aop:aspect 标签:表示这是一个切面动作,将增强方法以什么方式切入到表达式所指向的被增强方法,内有一个属性表示将使用哪一个增强类,该例子中我们使用的是EntityProxy这个增强类。
>
>>该标签中有一个aop:before子标签,表示前置增强,标签内有属性method表示需要使用增强类中的哪个增强方法,属性pointcut-ref表示切入到哪,切入到id为allenhance的表达式所指向的方法。
我们直接在测试方法中获取bean:
```java
@org.junit.Test
public void proxyTest(){
ApplicationContext applicationContext=new ClassPathXmlApplicationContext("application.xml");
Entity entity=applicationContext.getBean("entity",Entity.class);
entity.entityBaseFun();
String result=entity.entityBaseParamFun("我是带参数的原方法");
System.out.println(result);
}
```
运行结果:

可以看到,增强方法成功在被增强方法前输出,表示AOP操作成功。对此这就是一个完整的基于配置文件的AOP演示了,不复杂也不难理解,凑合着用。反正以后都是用注解了,谁还写那些麻烦的配置文件阿巴阿巴阿巴了。
## 怎么在程序中实现AOP
在标准开发中,AOP方式被集成在专门的AOP框架中,目前最常见的AOP框架是Aspectj。Spring自身也有原生的SpringAOP,也集成了Aspectj,这两个的区别在于,Aspectj是完全的AOP框架,而SpringAOP只能实现AOP的部分功能,不完全也几乎不用,就不赘述过多了。
在程序中实现AOP,一般使用基于注解的AOP,二般使用如上所示的基于配置文件的AOP(繁琐)。
# 小结
写到这一万多字,不完全介绍了Spring框架中两个最重要也最基础的概念,虽然都是废话(反正也不是给一般人看的),备战2022的秋招,不再会有那么多的时间去维护仓库了,或许记起码云的密码时?
# Spring中的注解
# 施工中。。。
2023.12.19,听我的,下辈子别选Java了,最近在重构这篇文章和代码,发现太多不严谨和不合理了。