Spring学习手记

Spring学习手记

1.Spring简介

Spring是一个轻量级框架,基础版只有2m大小。

Spring的核心特性就是可以用于开发任何Java程序,但是在Java EE平台上构建web应用是是需要扩展的。Spring的目标是使Java EE开发变得更加容易,通过启用基于POJO编程模型来促进良好的编程实践。

Spring家族中有很多适用于不同场景的框架,其中基础的是Spring Framework,基本上所有其他Spring框架都是建立在Spring Framework之上的。

Spring家族


Spring Framework五大功能模块

功能模块 功能介绍
Core Container 核心容器,在Spring环境下使用任何功能都必须基于IOC容器
AOP & Aspects 面向切面编程
Testing 提供Junit/Test NG测试框架的整合
Data Access/Integeration 提供了对数据访问/集成的功能(SpringJDBC)
Spring MVC 提供了面向Web应用程序的集成功能

Spring Framework特性

  • 非侵入性:对原生技术和领域模型是零污染的
  • 控制反转 IOC:反转资源获取方向,将自己创建资源,向环境索取资源转变为环境准备好资源,我们享受使用,降低对象与对象之间的依赖。
  • 面向切面 AOP:在不修改代码的基础上,将容器内对象替换为代理类,再完成注入,增强代码功能。
  • 容器:SpringIOC是一个容器,包含且管理组件的生命周期。组件享受容器化的管理,为程序员屏蔽组件创建过程中的大量细节,降低使用门槛。
  • 声明式:之前需要编写代码才能实现的功能,现在只要声明需求即可由框架实现。
  • 组件化:也就是放在容器中的bean,并且使用注入完整组件的组装。
  • 一站式:在自身的基础之上可以整合各种企业应用的开源框架和优秀地方库,此外Spring家族系列覆盖了广泛的领域,很多方面的功能性需求都可以在Spring Framework的基础上全部使用Spring来实现。

2.IOC

2.1 IOC容器

IOC是一种反转控制的思想,而DI是IOC的一种具体实现。

以前:需要使用什么资源需要自己创建,创建的细节也需要自己了解。

现在:现在需要使用什么资源,声明即可,IOC会自动向声明位置注入资源。(容器会推送资源给需要的组件,里面也会自动提供资源的创建方式,不需要自己处理)

DI:依赖注入,是IOC的另外一种表述方式,是IOC的具体实现。

在实际上,IOC容器负责实例化,配置和组装对象。IOC容器从XML文件中获取信息并执行相应的工作。所以,IOC容器的主要任务就是:

  • 实例化应用程序类
  • 配置对象
  • 组装对象之间的依赖关系

2.2 IOC在Spring中的实现

Spring中实现IOC主要有两种方式:

  • BeanFactory,这是IOC的基本实现。而且是Spring内部使用的接口,面向Spring本身,并不提供给开发人员使用。
  • ApplicationContext,这是BeanFactory的子接口,提供了更多的高级他姓。面向的是使用者,几乎所有场景都使用ApplicationContext,而不是更加底层的BeanFactory。在这个子接口中,子类ClassPathXmlApplicationContext是使用最多的。

3. 基于XML管理Bean

虽然现在有很多的注解可以用来更加简单的管理Bean,但是使用XML方式管理Bean依旧重要,当我们使用自己注入的类库组件时,是没办法使用注解的,所以只能使用XML方式来管理。

3.1 项目搭建与注册Bean

此部分需要实际操作,可以观看原视频中对应部分进行理解学习。


3.2 获取Bean的三种方式

在IOC容器中提供的获取Bean方法一共有三种:

  • 根据id获取bean:根据配置文件中配置的bean id 属性进行获取。不过由于记忆id,所以较为少用
  • 根据类型获取bean:要注意,如果配置文件中有多个相同类型的bean时,就会抛出NoUniqueBeanDefinitionException,==也是实际开发开发中最常用的==
  • 根据id和类型获取bean:就是前两者的结合用法

如果组件实现了接口,同样可以使用接口的类型获取组件,但前提是Bean唯一。示例如下:

1
2
3
// Student implements Person
// <bean id="student" class="com.atguigu.spring.pojo.Student"></bean>
Person person = ioc.getBean(Person.class);

3.3 依赖注入

3.3.1 setter注入

顾名思义,setter注入和实体类中的set方法有关,但是和成员变量没关系。

1
2
3
4
<bean id="studentOne" class="com.atguigu.spring.pojo.Student">
<!-- setter 注入 和 set方法有关,跟成员变量没有关系-->
<property name="sname" value="张三"></property>
</bean>

3.3.2 使用构造器中注入

1
2
3
4
5
6
7
<!-- public Student(Integer sid, String sname, Integer age, String gender) -->
<bean id="studentTwo" class="com.atguigu.spring.pojo.Student">
<constructor-arg name="sid" value="1"></constructor-arg>
<constructor-arg name="sname" value="张三"></constructor-arg>
<constructor-arg name="age" value="20"></constructor-arg>
<constructor-arg name="gender" value="男"></constructor-arg>
</bean>

这里会根据标签的顺序来决定具体调用哪一个构造函数,和name属性没有关系,name属性的作用主要是用来告诉程序员对应的变量名称而已(name属性可以省略,不过因为代码可读性的要求,最好不要省略)

3.3.3 其他类型的注入方式

字面量注入

字面量包含基本数据类型以及对应的包装类型,还有String

1
2
3
4
5
6
7
8
<!-- 为String类型赋值null使用null标签 -->
<property name="sname">
<null></null>
</property>
<!-- 对于特殊字符,可以使用转义,不过最常使用的哈斯hiCDATA区 -->
<property name="sname">
<value><![CDATA[ a>b ]]</value>
</property>
为类or接口类型的属性注入

第一种方式是使用ref属性设置。这种注入方式,在IOC容器中有两个bean,分别hi是courseOne和studentOne,且都可以正常获取。

1
2
3
4
5
6
7
8
9
<bean id="courseOne" class="com.atguigu.spring.pojo.Course">
<!-- 注入省略 -->
</bean>

<bean id="studentOne" class="com.atguigu.spring.pojo.Student">
<!-- void setCourse(Course course) -->
<property name="course" ref="courseOne"></property>
</bean>

第二种方式是使用内部bean的方式注入。和第一种注入方式的区别在于:IOC容器无法获取到内部Bean。

1
2
3
4
5
6
7
8
9
<bean id="studentOne" class="com.atguigu.spring.pojo.Student">
<!-- void setCourse(Course course) -->
<property name="course">
<bean id="courseOne" class="com.atguigu.spring.pojo.Course">
<property name="cid" value="2222"></property>
<property name="cname" value="远大前程班"></property>
</bean>
</property>
</bean>
数组类型注入

使用标签实现注入。数组中的元素如果是字面量数据,就使用value标签赋值;如果是类类型,就是用ref标签赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="studentOne" class="com.atguigu.spring.pojo.Student">
<!-- String[] hobby; -->
<property name="hobby">
<array>
<value>抽烟</value>
<value>喝酒</value>
<value>烫头</value>

<!-- 类类型用 -->
<!-- <ref bean="id"></ref> -->
</array>
</property>
</bean>
List注入(两种方法)
  1. 在Property标签内部设置,字面量使用value标签,类类型使用ref标签。(和数组注入类似)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bean id="course" class="com.atguigu.spring.pojo.Course">
    <!-- List<Student> students -->
    <property name="students">
    <list>
    <ref bean="studentOne"></ref>
    <ref bean="studentTwo"></ref>
    <ref bean="studentThree"></ref>
    </list>
    </property>
    </bean>
  2. 从List类型的bean中注入,首先需要配置一个List的bean,之后通过ref属性注入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bean id="course" class="com.atguigu.spring.pojo.Course">
    <!-- List<Student> students -->
    <property name="students" ref="studentList"></property>
    </bean>

    <!-- 配置一个集合类型的bean -->
    <util:list id="studentList" >
    <ref bean="studentOne"></ref>
    <ref bean="studentTwo"></ref>
    </util:list>
Map注入
  1. 和List注入类似,也是在Property标签内部设置,如果键是字面量类型使用key属性,是类类型就使用key-ref

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <bean id="teacherOne" class="com.atguigu.spring.pojo.Teacher"></bean>
    <bean id="teacherTwo" class="com.atguigu.spring.pojo.Teacher"></bean>

    <bean id="studentOne" class="com.atguigu.spring.pojo.Student">
    <!-- Map<String, Teacher> teacherMap; -->
    <property name="teacherMap">
    <map>
    <entry key="teacherOne" value-ref="teacherOne"></entry>
    <entry key="teacherTwo" value-ref="teacherTwo"></entry>
    </map>
    </property>
    </bean>
  2. 直接从Map类型的bean注入

    1
    2
    3
    4
    5
    6
    7
    8
    <util:map id="teacherMap">
    <entry key="teacherOne" value-ref="TeacherOne"></entry>
    <entry key="teacherTwo" value-ref="teacherTwo"></entry>
    </util:map>
    <bean id="studentOne" class="com.atguigu.spring.pojo.Student">
    <!-- Map<String, Teacher> teacherMap; -->
    <property name="teacherMap" ref="teacherMap"></property>
    </bean>

3.4 Bean的作用域

scope=“single|prototype”

在bean中使用scope属性设置Bean的作用域。single表示使用单例模式,也是默认设置,获取bean所对应的对象都是同一个。prototype表示使用原型,也就是多例,获取bean所对应对象不是同一个。

另外,在WebApplicationContext环境下还有另外两个作用域:requst和session,分别表示在一次请求和一次会话中有效。


3.5 Bean的生命周期

具体的生命周期如下:

  • 实例化,使用工厂模式和反射实现
  • 依赖注入
  • 初始化,需要在bean标签中使用init-method属性指定初始化方法
  • 销毁,需要注意的时候,当IOC容器关闭之后Bean才会执行销毁方法,而且和初始化一样,需要在bean标签中使用destory-method属性指定销毁方法

注意:如果bean的作用域是单例的,生命周期的前三个步骤会在获取IOC容器的时候就执行;如果bean的作用域是多例的,那么生命周期的前三个步骤会在回去调用bean的时候才执行。

bean的后置处理器,它主要是bean初始化前后添加额外的操作。需要实现BeanPostProcessor接口且配置到IOC容器之中,要注意,bean后置处理器不是单独针对一个bean生效的,而是针对IOC容器中所有bean都会执行。


3.6 FactoryBean(了解会使用即可)

FactoryBean是Spring提供的一种整合第三方框架的机制。配置一个FactoryBean类型的bean之后,在获取bean对象获取的不是class属性中配置的这个类的对象,而是getObject方法的返回值。通过这种机制,Spring可以把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示出来。

其中有三个方法

  • getObject:通过一个对象交给IOC容器进行管理
  • getObjectType:设置提供对象的类型
  • isSingleton:所提供的对象是否是对象的

3.7 基于XML的自动装配

自动装配:根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型赋值。

说人话就是在为类类型(或者接口类型)注入的时候,在XML文件中不需要再写property标签和ref标签,而是由Spring为我们自动注入装配。

在bean标签中设置autowire属性,来设置不同的配置策略

  • no|default:默认,就是不装配。
  • byName:根据要配置的属性名,在bean中寻找到id和属性名相匹配的自动装癖。当容器当中有多个相同类型能匹配到的时候,可以使用byName进行区分,实现自动装配。
  • byType:根据类型,在bean中寻找匹配类型的bean自动装配,这也是使用最多的方式。需要注意,如果找不到合适类型的bean就不装配,如果找到多个则会抛出异常:noUniqueBeanDefinitionException

4. 基于注解管理Bean

和XML配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体功能是由框架检测到注解标记的位置,然后针对这个位置按照注解标记的内容来执行具体的操作

在Spring中,基于注解管理bean分为标记和扫描两个部分,标记就是在需要的地方添加注解,之后需要通过扫描的方式来检测,之后根据注解进行后续的操作。

使用注解管理Bean或者使用XML管理Bean,两者本质上所有的操作都是Java代码来完成的,XML和注解的作用只是告诉框架中的Java代码如何执行。

4.1 四个基础注解

  • @Component:将类标识为普通组件
  • @Controller:将类标识为控制层组件
  • @Service:将类标识为业务层组件
  • @Repository:将类标识为持久层组件

==其实对于Spring来说,后三个注解的含义和第一个注解的含义并你没有什么区别。,后三个注解只不过是在@Component的基础之上另外取的三个名字。==

对于bean的id,在注解中不写的默认情况之下是小驼峰,就比如UserController加上注解之后,它在容器中的id就是userController

当然也可以自定义id,例如@Controller("userController")

4.2 扫描组件


5. AOP

5.1 场景模拟,提出问题

在上面的场景模拟中,我们可以提出以下的问题

  • 现有代码缺陷。在针对于带有日志功能的实现类,我们发现有如下的缺陷:
    • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散精力
    • 附加功能分散在各个业务功能方法之间,不利于统一维护

解决这两个问题的思路的核心就在于:解耦。我们要把附加功能从业务功能中抽取出来、

但是解决问题也存在相对应的困难:因为要抽取的代码在方法内部,依靠之前把子类中的重复代码抽取到父类的方法是无法解决的(抽取封装只能将一段连续执行的代码抽取,无无法解决这里的问题)。因此需要使用新的技术:代理模式

5.2 代理模式

代理模式是二十三种设计模式的一种,属于结构型模式。主要作用是通过一个代理类对象,使得在调用目标方法时不是直接对目标方法进行调用,而是使用代理类对象间接调用,使不属于目标方法核心逻辑的代码从目标方法种剥离解耦,调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰、同时让附加功能能够集中在一起且方便统一维护。

说人话,我理解的代理模式就是:发现代码是不连续,没办法抽取封装到父类中。那么我就不抽取两边不连续的代码了嘛,直接抽取中间的核心连续代码,之后使用另外一个类来完成需求,在对应方法里面调用对应的方法。

在代理模式中,分为静态代理和动态代理两种

5.2.1 静态代理

特点是一对一,也就是一个代理类对应一个目标类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CalculatorStaticProxy implements calculator{

//将目标对象声明为成员变量
private CalculatorImpl target;

public CalculatorStaticProxy(CalculatorImpl target) {
this.target = target;
}
//之后在方法中调用目标对象的核心代码
@Override
public int add(int i, int j) {
System.out.println("日志,方法:add, 参数:" + i + ", " + j);
int res = target.add(i , j);
System.out.println("日志,方法:add, 结果:" + res);
return res ;
}
......
}

这样来看,静态代理确实解决了我们上面的问题。但是由于代码都写死了,完全不具备任何的灵活性。如果后续还需要附加日志的话,是不是还得声明更多的代理类?如此一来的话,也会产生大量的冗余代码,日志功能还是分散的,没办法统一管理。

由此进一步提出需求:将日志功能集中到一个代理类中,如果之后还有任何日志需求,都可以通过这一个代理类来实现。这就需要需用来进一步的代理模式;动态代理了。

5.2.2 动态代理

核心实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ProxyFactory {

private Object target;

public ProxyFactory(Object target) {
this.target = target;
}


public Object getProxy(){
/**
* 参数介绍:
* ClassLoader loader:指定加载动态生成的代理类的类加载器
* Class[] interfaces:获取目标对象实现的所有接口的class对象数组
* InvocationHandler h:设置代理类中的抽象方法如何重写
*/
ClassLoader classLoader = this.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler(){
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
System.out.println("日志,方法:" + method.getName() + "参数" + Arrays.toString(objects));
Object res = method.invoke(target, objects);
System.out.println("日志,方法;" + method.getName() + "结果:" + res);
return res;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}

之后编写测试代码如下;

1
2
3
4
5
6
7
@Test
public void testStaticProxy(){
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
//这里使用向上转型,才可以调用对应的核心方法
calculator proxy = (calculator) proxyFactory.getProxy();
proxy.add(1,2);
}

动态代理有两种;JDK动态代理,cglib动态代理。

  1. JDK动态代理:要求必须有接口,最后生成的代理类和目标类实现相同的接口在com.sun.proxy下,类名为$proxy2
  2. cglib动态代理;最终生成的代理类会继承目标类,并且和目标类在相同的包下

5.3 AOP概念和术语

AOP(Aspect Oriented Progarmming)是一种设计思想,是软件设计领域中的面向切面设计,他是面向对象编程的一种补充和完善,它以通过预编译的方式和运行期动态代理的方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。

它具体由以下几个作用:

  • 简化代码
  • 代码增强

5.3.1 术语概念

  1. 横切关注点:就是从每个方法最终抽取出来的同一类非核心业务。是根据附加功能的逻辑需要抽取的
  2. 通知:在每个横切关注点上要做的事情都需要写一个方法来实现,这就是通知,根据不同的执行时期,可以分为:
    1. 前置通知:在被代理的目标方法之前执行
    2. 返回通知:在被代理的目标方法成功结束后执行
    3. 异常通知:在被代理的目标方法异常结束后执行
    4. 后置通知:在被代理的目标方法最终结束后执行
    5. 环绕通知,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
  3. 切面:就是封装通知方法的类。
  4. 目标:也就是被代理的目标对象
  5. 代理:向目标对象应用通知之后创建的代理对象
  6. 连接点:是纯逻辑概念,可以理解为抽取横切关注点的位置或者抽取代码的地方
  7. 切入点:定位连接点的方式,是连接点的具体实现

5.4 基于注解的AOP

准备工作可以看原视频


6.jdbcTemplate

其实就是Spring中基于jdbc封装的用来操作数据库的方法而已。了解操作即可,因为后续项目的学习都是基于Spring整合MyBatis来操作数据库,对于jdbcTemplate的使用其实不多,而且其实操作数据库的方法API大多都大同小异,需要使用时候返回来查看即可。


7. 声明式事务

7.1 编程式事务

顾名思义,就是事务功能的相关操作全部都由自己编写代码完成,例如开启事务,提交事务,回滚事务和释放数据库连接。可想而知,这种粗略的方式必定存在缺陷:

  • 细节没有被屏蔽:具体操作过程中,所有的细节都需要程序员自己完成,十分的麻烦
  • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用

7.2 声明式事务

因为事务控制的代码是有规律的,其代码结构基本也是确定的,所以框架就可以将固定模式的代码抽取,进行相关的封装。之后我们只需要在配置文件中进行简单的配置即可完成操作。它的好处有很多,比如:提高了开发效率,减少了冗余代码,框架的使用会考虑到各种问题所以对应会对代码的健壮性,性能等各个方面都会得到优化


Spring学习手记
https://blake315.github.io/2022/12/31/Spring学习手记/
作者
Zeeway
发布于
2022年12月31日
许可协议