Spring资料教程
家庭压力、生存压力、竞争内卷 -> 框架实现机制原理 -> 海量底层源码
Java 给了你喝咖啡的时间,但培训机构和职业讲师花着专业的时间推出死磕源码的课程,让众多业务开发人员还要花费大量的时间、精力和金钱被迫去啃源码!源码重要吗?作用到底有多大?打个比方,手机、电脑、汽车都是比较重要的生活设备,绝大多数人都只需要知道怎样使用就足够了,或者了解一些技术构造,防止被维修人员坑钱!真正需要掌握技术实现原理的,技术发明者、技术维护者。而在技术之上做开发的人员按需求了解是理想状态,疯狂内卷的残酷职场,技术能力仅仅是一个环节,学历、经验、年龄、相貌、性格、健康等因素都可能影响职业生涯。说直白一点,你花费了大量的时间、精力、金钱学习钻研高级岗位教程,最后因为将近中年、学历不够,高不成低不就,像菜市场的蔬菜一样,被人挑来挑去,甚至迫不得已,改行换工作!那前面好几年的技术学习积累,岂不是白白浪费掉了吗?多么宝贵的年轻时光啊!
疯狂内卷最终得获益者是谁?国家社会?公司?培训机构?
**内卷风暴中受损害最大,一定是普通院校的人(双非、三非),越普通,边缘化影响越严重。**好的环境和资源,都被名校关环的人占据了,普通人连门槛都够不到!迫不得已花费大量时间精力金钱学习培训机构的课程,最后还是因为学历、经验、年龄被拒之门外!
啃 spring 源码,很像跑马拉松,42公里,或许你跑过10公里状态还可以,持续4个10公里呢,就感到不太容易了吧。
日常训练,持续半年到一年,结合每月实战,跑完马拉松就只是多个实战结果中的一个。真正的困难在于长时间的训练过程和每次的实战演练。
spring 的源码还是较为庞大的,不是一天、一周、一个月就能彻底吃透的,三个月、半年、一年的持续研究梳理,熟练理解实现原理还是能做到的,但记住各个模块核心功能方法名称,方法要对,流程图、重复记忆。
源码教程是把利刃,把握的好,能一路披荆斩棘,学不好、不会用,切到手的概率很大,搞不好还成了伤害自己的凶器(花了大量时间学习,得到的回报甚微,学了用不到,很快就忘了,最坏的情况是还没学完,就已经到了中年危机)。
人才,人,才。人合才次,不合才庸。
大厂确实会因为你的技术深度细节不够而刷掉你,小公司以同样的深度不够不要你,多半是第一印象就没想要你,或者是面试过程交流很一般,没什么特殊印象,或者特殊优势。
海量的框架源码,就像一座座迷宫,并且迷宫的中心藏有财富的密码。
Java 给了你一杯咖啡,你的上司看见了说,拿来吧你,还有时间喝咖啡啊,要抓紧时间看源码啊!
大家都一个劲的往里钻啊!拼命地卷啊!多少年轻人的宝贵青年时光都耗在了这场财富内卷的竞争当中?!
没家境背景,没学历光环,成了这场内卷风暴的受损害最严重的人!被资本商人疯狂压榨收割!自己还没有任何能力条件反抗!
996、8106、007,资本家称之为福报,要不是迫于生计,谁要你这吸血的福报!三十五岁中年危机就像是社会大学【研究生】的一条分数线,前面十几年的能力经验和人脉关系积累够你超出这条分数线,你就能在社会职场继续深造。达不到分数线,并不能说明你能力不行,该被社会淘汰。研究生、博士生,都有一条资格筛选分数线,你没达到分数线,但你已经是本科生了啊!并且经历了十几年的社会考验,这些还不足以证明你的能力吗?
被分数线筛选掉了又怎样?整个国家那么多人,难道过了 35 岁就没路可走了?真要这样,那国家和社会都乱套了!少年、青年时期很美好,很宝贵,但中壮年也是人生的黄金时期啊!三十而立,正是大展身手的时候!别被那些别有用心的营销视频贩卖焦虑,博取关注,卖资料视频收割!真的,阳光正当时,青壮年的,找到一条事业道路,干就是了!找不到出路?没资源、没人脉、没对象,条件很差,怎么办?自我认识、自我定位再清楚一点,导致这一切的根本原因是什么?以自身现有的条件,能做哪些改善?停止消极抱怨,停止自卑退缩,一切的突破口最终还是在自己身上!!整天 35 岁危机焦虑,反而造成大量的精神内耗,每天的时间除去生理活动,能用于工作的也就十几个小时。时间和健康就是你最宝贵的财富!有多大本事变现,就看你自己了。
扯太远了。。回到走迷宫。蛮干硬看肯定走不远,绕来绕去还可能把自己困死在里面。
第一个走迷宫的人,拿到了财富密码,出了畅销的资料书籍,告诉你怎么走迷宫,然而财富密码已经被拿走了。
后面的人为什么还要走迷宫?而且是看着人家的资料书手把手的扶着走?是要准备自己造迷宫?还是走知名大迷宫可以增加自身的竞争力?可以彰显自身的能力,有吹牛皮谈资的底气?
Spring 概述
官网:
https://spring.io/projects/spring-framework
官方下载地址:
https://repo.spring.io/release/org/springframework/spring/
GitHub:
https://github.com/spring-projects/spring-framework
Spring 是什么
Spring是一个开源框架。
Spring为简化企业级应用开发而生.使用Spring可以使简单的JavaBean实现以前只有EJB才能实现的功能。
Spring是一个IOC(DI)和AOP容器框架。
具体描述Spring:
轻量级:Spring是非侵入性的-基于Spring开发的应用中的对象可以不依赖于Spring的API
**依赖注入(**DI --- dependency injection、IOC)
面向切面编程(AOP --- aspect oriented programming)
**容器:**Spring是一个容器,因为它包含并且管理应用对象的生命周期
**框架:**Spring实现了使用简单的组件配置组合成一个复杂的应用,在Spring中可以使用XML和Java注解组合这些对象
一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上Spring自身也提供了展现层的SpringMVC和持久层的Spring JDBC)
Spring 优点
开源免费的框架。
轻量级非入侵。引入不影响原来代码运行,占用资源少,执行效率高。
针对接口编程,解耦合。Spring提供了IOC控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的对象创建方式,现在由容器完成。对象之间的依赖解耦合。
AOP编程的支持。通过Spring提供的AOP功能,方便进行面向切面的编程。可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
方便集成各种优秀框架。
总结:Spring是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架。
Spring 模块
主干重点
别抱怨!Spring 目前确实是 Java 最重要的框架,没有之一,如果有,那就还有 Spring Boot。高级开发职位,多少都会问,大厂必问,跟着博客、视频教程梳理,尽量多理解掌握几个核心原理,总比什么都不知道要强!
第一次看源码肯定头疼!看不下去、想放弃的时候,就缩小范围,比如就只看 refresh 这一个方法的业务逻辑,IOC 初始化相关的业务大概梳理一下,其他的就先不管!
最好是结合面试题,有目的的看一下源码,面试的时候能回答一些是一些!如果是大厂,肯定得是真的懂,大厂有专门负责面试的面试官,他们能把主流技术的知识体系全部记在脑子里,投机取巧没有用的!除非你的学历很亮眼,或者工作经验很知名。
无论是跟着源码书籍,还是视频教程,源码学习都是非常枯燥乏味的,90%的人学源码看源码就是为了面试。每次都是看一点点就犯困、想睡觉、头痛、掉头发。竞争内卷真的是非常激烈,要么硬着脑袋坚持,要么尽早准备找退路吧!在中年危机面前,大多数没有优势的求职者(常年 CRUD 的“底层”码农,被内卷最惨,技术面很广!同时又要挖得很深!还要面试算法!时间精力全搭进去也不可能全面吃透!还不如选安卓、算法等专职岗位,不至于把时间精力分散到各种各样的框架组件底层源码),他们的坚持,可能等不到收获,就被优化淘汰了!
源码的作用是用来学习核心功能实现原理,不是用来记面试题、背答案应付面试的!盲目地看源码,各大开源框架,海量源码细节,要看到猴年马月?看不完的,也记不住,深陷其中只会被源码细节拖垮!最终后果是好几个月,甚至更长时间都没有工作,没有钱,在一线城市能撑多久??
真正专业的面试官从一些细节就能知道你是不是真正钻研学习过源码,问几个不常见的面试题看你的回答就知道了,一问三不知,支支吾吾讲不出自己的理解,说明就只是背了重点面试题。
DI 原理
IOC 原理 -> IOC初始化
Bean 生命周期与管理
AOP 原理 -> AOP实例
事务管理 -> 默认REQUIRED
设计模式 -> 单例、工厂方法(简单工厂)、代理、装饰
框架核心实现(原理)要点
常见面试题
IOC 原理
- IOC 和 DI 是什么?
- Spring IOC 的理解,其初始化过程?
Bean 生命周期与管理
- BeanFactory 和 FactoryBean 的区别?
- BeanFactory 和 ApplicationContext 的区别?
- ApplicationContext 上下文的生命周期?
- Spring Bean 的生命周期?
- Spring 如何解决循环依赖?三级缓存
AOP 原理
- Spring AOP 的实现原理?
事务管理
- Spring 是如何管理事务的,事务管理机制?
- Spring 的不同事务传播行为有哪些,干什么用的?
框架核心实现原理
- Spring 中用到了那些设计模式?单例、代理、工厂模式、模板方法、适配器、装饰器、观察者
- Spring MVC 的工作原理?
- Spring MVC 如何保证 Controller 并发的安全?ThreadLocal,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量
看源码建议
- 注意思路和方法,名师出高徒,好的资料教程成就高级工程师
- 源码中的注释很关键
- 先梳理主干脉络,再扣细节
- 大胆猜测,小心验证
- 见名知意,善用翻译
- 坚持看、坚持看、坚持看,马拉松不是一两天就能跑得下来的!几个月、半年、一年、常年坚持
Spring IOC 控制反转
控制反转(IoC Inversion of Control),是一个概念,是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对象控制权的转移,从程序代码本身反转到了外部容器。通过容器实现对象的创建,属性赋值,依赖的管理。
Spring 框架使用依赖注入(DI)实现 IoC。
Spring容器是一个超级大工厂,负责创建、管理所有的Java对象,这些Java对象被称为Bean。Spring容器管理着容器中Bean之间的依赖关系,Spring使用“依赖注入”的方式来管理Bean之间的依赖关系。使用IoC实现对象之间的解耦合。
IOC 和 DI 是什么?
IOC 即控制反转,将对象的创建、赋值、依赖交由 spring 容器进行管理。
DI 依赖注入,在程序运行期间,由 spring 容器动态地将依赖对象注入到组件中。一般通过构造函数注入或者setter注入。
第一个 Spring 程序
创建 Maven 项目
File – New – Project,选择Maven项目,不建议勾选“create from archetype”,直接下一步,创建最原始的Maven项目即可。“create from archetype”(从典型案例中创建),会到中央仓库下载一些pom文件已经默认配置的依赖,部分依赖可能用不到。
删除 project 下的 src 目录
删除src目录,使当前project称为一个工作空间,只用来存放接下创建的Module项目即可。这是个开发习惯,工作空间通常不写代码,代码都在具体的项目里面。
pom.xml 中添加依赖
添加Spring5的依赖,spring-webmvc引入的jar包比较全,不用单个添加依赖。
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.1</version>
</dependency>
自定义加入Spring5的依赖。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1.RELEASE</version>
</dependency>
定义接口与实体类
创建 Spring 配置文件
<?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:自定义对象的名称,全局唯一
class:类的全限定名称,不能是接口
-->
<bean id="userService" class="com.vegetable.service.impl.UserServiceImpl" autowire="byName">
<!-- 引用类型赋值 -->
<property name="userDao" ref="userDaoOracleImpl"/>
</bean>
<bean id="userDaoMySqlImpl" class="com.vegetable.dao.impl.UserDaoMySqlImpl"/>
<bean id="userDaoOracleImpl" class="com.vegetable.dao.impl.UserDaoOracleImpl"/>
</beans>
定义测试类
@Test
public void springDemo1() {
// 创建 spring 容器上下文对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 从 spring 容器中获取对象,使用对象id
UserService userService = context.getBean("userService", UserServiceImpl.class);
userService.getUser();
}
使用 Spring 创建非自定义类对象
Spring配置文件加入java.util.Date定义
<bean id="myDate" class="java.util.Date" />
MyTest测试类中:
调用getBean(“myDate”);获取日期类对象。
基于 XML 的 DI / 自动装配
bean实例在调用无参构造器创建对象后,就要对bean对象的属性进行初始化。初始化是由容器自动完成的,称为注入。
根据注入方式的不同,常用的有两类:set注入、构造注入。
★Setter 方法注入
Setter 注入也叫设值注入,是指通过setter方法传入被调用者的实例。这种注入方式简单、直观,因而在Spring的依赖注入中大量使用。
简单类型
public abstract class User {
protected String name;
protected String password;
// constructors
// getter、setter
// toString
}
public class BilibiliUser extends User {
private String mobile;
private String nickname;
private String[] visitors;
private List<User> followers;
private List<User> following;
private Set<String> histories;
private Map<String, Integer> videoCount;
private Properties props;
// constructors
// getter、setter
// toString
}
<bean id="bilibiliUser" class="com.vegetable.model.BilibiliUser">
<!-- 简单类型的属性赋值 -->
<property name="name" value="b2020"/><!-- setName("b2020") -->
<property name="password" value="123456"/><!-- setPassword("123456") -->
</bean>
@Test
public void test01() {
// 创建 spring 容器上下文对象
ApplicationContext context = new ClassPathXmlApplicationContext("user.xml");
// 从 spring 容器中获取对象,使用对象id
User user = context.getBean("bilibiliUser", User.class);
System.out.println(user);
}
<bean id="myDate" class="java.util.Date">
<property name="time" value="1607442658000"/>
</bean>
@Test
public void test02() {
// 创建 spring 容器上下文对象
ApplicationContext context = new ClassPathXmlApplicationContext("user.xml");
// 从 spring 容器中获取对象,使用对象id
Date date = context.getBean("myDate", Date.class);
System.out.println(date);
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
}
引用类型
当指定bean的某属性值为另一bean的实例时,通过ref指定它们间的引用关系。ref的值必须为某bean的id值。
<!-- 注册 bean 对象
id:自定义对象的名称,全局唯一
class:类的全限定名称,不能是接口
-->
<bean id="userService" class="com.vegetable.service.impl.UserServiceImpl" autowire="byName">
<!-- 引用类型赋值 -->
<property name="userDao" ref="userDaoOracleImpl"/>
</bean>
<bean id="userDaoMySqlImpl" class="com.vegetable.dao.impl.UserDaoMySqlImpl"/>
<bean id="userDaoOracleImpl" class="com.vegetable.dao.impl.UserDaoOracleImpl"/>
Collection 集合
bean | ref | idref | array | list | set | map | props | value | null
<!-- bean | ref | idref | array | list | set | map | props | value | null -->
<bean id="bilibiliUser3" class="com.vegetable.model.BilibiliUser">
<!-- 数组 -->
<property name="visitors">
<array>
<value>"不瘦十斤不改名"</value>
<value>"今天你吃瓜了吗"</value>
<value>"我站的够高啊"</value>
<value>"b站雨林"</value>
<value>"疯狂厨房"</value>
<value>"不笑你打我P股"</value>
</array>
</property>
<!-- list -->
<property name="followers">
<list>
<!--<value>"我站的够高啊"</value>-->
<ref bean="bilibiliUser"/>
<ref bean="bilibiliUser2"/>
</list>
</property>
<!-- map -->
<property name="videoCount">
<map>
<entry key="life" value="100"/>
<entry key="knowledge" value="50"/>
<entry key="game" value="10"/>
</map>
</property>
<!-- set -->
<property name="histories">
<set>
<value>"牛顿今天cue半佛了吗"</value>
<value>"蜡笔小小小勋又双叒叕[yòu shuāng ruò zhuó]在秀恩爱"</value>
<value>"猜猜老湿今天修了几台电脑"</value>
<value>"所长今天眨眼了吗"</value>
</set>
</property>
<!-- null -->
<property name="password">
<null/>
</property>
<!-- properties -->
<property name="props">
<props>
<prop key="driver">"test011111"</prop>
<prop key="url">"test022222"</prop>
<prop key="username">"test033333"</prop>
<prop key="password">"test044444"</prop>
</props>
</property>
</bean>
构造器注入
Spring通过类的构造方法,赋予设置值,并初始化对象实例。
<bean id="bilibiliUser" class="com.vegetable.model.BilibiliUser">
<!-- 构造器 name 注入 -->
<constructor-arg name="name" value="b2020"/>
<constructor-arg name="password" value="123456"/>
</bean>
<bean id="bilibiliUser2" class="com.vegetable.model.BilibiliUser">
<!-- 构造器 index 注入 -->
<constructor-arg value="b2020"/>
<constructor-arg value="123456"/>
</bean>
<constructor-arg/>标签中用于指定参数的属性有:
name:指定参数名称。
index:指明该参数对应着构造器的第几个参数,从0开始。不过,该属性可以不指定,但要注意,若参数类型相同,或之间有包含关系,则需要保证赋值顺序要与构造器中的参数顺序一致。
静态工厂的方法注入
通过调用静态工厂的方法来获取自己需要的对象,为了让spring管理所有对象,不能直接通过”工程类.静态方法()”来获取对象,而是依然通过spring注入的形式获取。
package com.bless.springdemo.factory;
import com.bless.springdemo.dao.FactoryDao;
import com.bless.springdemo.dao.impl.FactoryDaoImpl;
import com.bless.springdemo.dao.impl.StaticFacotryDaoImpl;
public class DaoFactory {
//静态工厂
public static final FactoryDao getStaticFactoryDaoImpl(){
return new StaticFacotryDaoImpl();
}
}
同样看关键类,这里需要注入一个FactoryDao对象,这里看起来跟第一种注入一模一样,但是看随后的xml会发现有很大差别:
public class SpringAction {
//注入对象
private FactoryDao staticFactoryDao;
public void staticFactoryOk(){
staticFactoryDao.saveFactory();
}
//注入对象的set方法
public void setStaticFactoryDao(FactoryDao staticFactoryDao) {
this.staticFactoryDao = staticFactoryDao;
}
}
Spring的IOC配置文件,注意看指向的class并不是FactoryDao的实现类,而是指向静态工厂DaoFactory,并且配置 factory-method=”getStaticFactoryDaoImpl”指定调用哪个工厂方法:
<!--配置bean,配置后该类由spring管理-->
<bean name="springAction" class="com.bless.springdemo.action.SpringAction" >
<!--(3)使用静态工厂的方法注入对象,对应下面的配置文件(3)-->
<property name="staticFactoryDao" ref="staticFactoryDao"></property>
</bean>
<!--(3)此处获取对象的方式是从工厂类中获取静态方法-->
<bean name="staticFactoryDao" class="com.bless.springdemo.factory.DaoFactory" factory-method="getStaticFactoryDaoImpl"></bean>
实例工厂的方法注入
实例工厂的意思是获取对象实例的方法不是静态的,所以需要首先new工厂类,再调用普通的实例方法:
public class DaoFactory {
//实例工厂
public FactoryDao getFactoryDaoImpl(){
return new FactoryDaoImpl();
}
}
那么下面这个类没什么说的,跟前面也很相似,但是需要通过实例工厂类创建FactoryDao对象:
public class SpringAction {
//注入对象
private FactoryDao factoryDao;
public void factoryOk(){
factoryDao.saveFactory();
}
public void setFactoryDao(FactoryDao factoryDao) {
this.factoryDao = factoryDao;
}
}
最后看spring配置文件:
<!--配置bean,配置后该类由spring管理-->
<bean name="springAction" class="com.bless.springdemo.action.SpringAction">
<!--(4)使用实例工厂的方法注入对象,对应下面的配置文件(4)-->
<property name="factoryDao" ref="factoryDao"></property>
</bean>
<!--(4)此处获取对象的方式是从工厂类中获取实例方法-->
<bean name="daoFactory" class="com.bless.springdemo.factory.DaoFactory"></bean>
<bean name="factoryDao" factory-bean="daoFactory" factory-method="getFactoryDaoImpl"></bean>
p 命名空间注入
可以直接注入属性的值,相当于<property/>的简写。
需要引入约束:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>
<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>
<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>
c 命名空间注入
可以通过构造器注入,相当于<constructor-arg/>的简写。
需要引入约束:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
<!-- traditional declaration with optional argument names -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>
<!-- c-namespace declaration with argument names -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>
</beans>
显式使用依赖
depends-on可以在初始化使用此元素地bean之前,显示强制初始化一个或多个bean。
依赖多个bean,可以用逗号、空格或分号分隔。
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
补充:
depends-on既可以指定初始化顺序,也可以在singleton单例模式下指定销毁顺序。先销毁被依赖的bean,然后再销毁bean本身。
懒加载(Lazy-initialized Beans)
Spring加载ApplicationContext.xml配置文件初始化容器时,会先创建和配置所有的单例bean。如果希望有些单例bean在首次请求时而不是启动时创建,可以在<bean/>中加入lazy-init="true"显示指定懒加载bean。
懒加载对于depends-on依赖的bean不生效。一个非懒加载的bean依赖了一个懒加载的bean,懒加载的bean依然会先加载。
可以在<beans/>中通过default-lazy-init="true"显示指定所有bean都是懒加载。
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
自动装配规则
- no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
- byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
- byType:通过参数的数据类型进行自动装配。
- constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
- autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。
byName 方式自动注入
byName需要保证所有bean的id唯一,并且这个bean id需要和自动注入属性(字段)的名称一致。
byType 方式自动注入
byType需要保证所有bean的class唯一,并且这个bean需要和自动注入属性的类型一致。
指定多个 Spring 配置文件
在实际应用里,随着应用规模的增加,系统中Bean数量也大量增加,导致配置文件变得非常庞大、臃肿。为了避免这种情况的产生,提高配置文件的可读性与可维护性,可以将Spring配置文件分解成多个配置文件。
包含关系的配置文件:
多个配置文件中有一个总文件,总配置文件将各其它子文件通过<import/>引入。在Java代码中只需要使用总配置文件对容器进行初始化即可。
也可使用通配符。但此时要求父配置文件名不能满足所能匹配的格式,否则将出现循环递归包含。就本例而言,父配置文件不能匹配spring-*.xml的格式,即不能起名为spring-total.xml。
基于注解的 DI / 自动装配
添加注解约束并开启注解支持
<?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">
<context:annotation-config/>
</beans>
在 Spring 配置文件中配置组件扫描器
<?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">
<context:annotation-config/>
<context:component-scan base-package="org.example"/>
</beans>
多个不同包路径,可以使用逗号、分号、空格分隔,但不建议用空格?
定义 Bean 的注解 @Component
需要在类上使用注解@Component,该注解的value属性用于指定该bean的id值。
另外,Spring还提供了3个创建对象的注解:
@Repository用于对Dao实现类进行注解
@Service用于对Service实现类进行注解
@Controller用于对Controller实现类进行注解
这三个注解与@Component都可以创建对象。只是名称不同,注解作用都是将对象交由spring容器管理。底层逻辑一样?
@Repository、@Service、@Controller是对@Component注解的细化,标注不同层的对象。即持久层对象,业务层对象,控制层对象。
@Component不指定value属性,bean的id是类名的首字母小写。
Bean 的作用域注解:
@scope(“singleton”)
@scope(“prototype”)
简单类型属性注入 @Value
需要在属性上使用注解@Value,该注解的value属性用于指定要注入的值。
使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。
byType 自动注入 @Autowired
需要在引用属性上使用注解@Autowired,该注解默认按类型自动装配Bean的方式。
使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。
byName 自动注入 @Autowired 与 @Qualifier
Qualifier 预选
**当多个对象类型相同时,可以再指定对象id。**在引用属性上联合使用注解@Autowired与@Qualifier。@Qualifier的value属性用于指定要匹配的Bean的id值。类中无需set方法,也可加到set方法上。
@Autowired还有一个属性required,默认值为true,表示当匹配失败后,会终止程序运行。若将其值设置为false,则匹配失败,将被忽略,未匹配的属性值为null。
JDK 注解 @Resource 自动注入
Spring提供了对jdk中@Resource注解的支持。@Resource注解既可以按名称匹配 Bean也可以按类型匹配Bean。默认按名称注入。使用该注解,要求JDK必须是6及以上版本。@Resource可在属性上,也可在set方法上。
@Resource注解若不带任何参数,默认按名称的方式注入,按名称不能注入bean则会按照类型进行Bean的匹配注入。
@Resource注解指定其name属性,则name的值即为按照名称进行匹配的Bean的id。
使用 JavaConfig 实现配置
使用全Java代码配置的形式替代xml配置文件。
XML 与注解的对比
XML
优点:
配置和代码分离。
在xml中做修改,无需编译代码,只需重启服务器即可将新的配置加载。
缺点:编写麻烦,效率低,大型项目过于复杂。
注解
优点:
方便
直观
高效(代码少,没有配置文件的书写那么复杂)。
缺点:以硬编码的方式写入到Java代码中,修改是需要重新编译代码的。
循环依赖
具体查看《Spring IOC 容器初始化、Bean 的生命周期、循环依赖问题.drawio》。
IOC 容器初始化过程
Spring IOC 容器初始化过程
(长图不用怕,标识分段很清晰,每一段对应具体步骤。至于其中的方法名称,眼熟一下就好,记不住很正常。记住了也是应付面试,过不了多久全都忘光!)
按照自己的理解梳理记忆:(第一眼看着很吓人,其实就是一点一点从粗体要点展开补充了内容,先主干后细节)
- **Resource 资源定位。**通过 xml 或注解对应的 ApplicationContext 类进行资源定位。(ClassPathXmlApplicationContext、AnnotationConfigApplicationContext)
- **BeanDefinition 载入。**在 refresh() 入口方法,创建 BeanFactory 入口(DefaultListableBeanFactory),加载 BeanDefinition 入口,通过 Resource 资源类型对应的 BeanDefinitionReader 解析资源,把用户定义好的 Bean 转换成 IOC 容器内部的bean类型 BeanDefition。(XmlBeanDefinitionReader#loadBeanDefinitions、AnnotatedBeanDefinitionReader#doRegisterBean)
- **BeanDefinition 注册。**通过 BeanDefinitionRegistry 接口的实现类 DefaultListableBeanFactory#registerBeanDefinition,将前面载入的 BeanDefition 保存到 beanDefinitionMap(ConcurrentHashMap) 中。
Resource 定位
一般使用外部资源来描述Bean对象,所以IOC容器第一步就是需要定位Resource外部资源。Resource的定位其实就是BeanDefinition的资源定位,它是由ResourceLoader通过统一的Resource接口来完成的,这个Resource对各种形式的BeanDefinition的使用都提供了统一接口。
BeanDefinition 载入
第二个过程就是BeanDefinition的载入,BeanDefinitionReader读取,解析Resource定位的资源,也就是将用户定义好的Bean表示成IOC容器的内部数据结构也就是BeanDefinition,在IOC容器内部维护着一个BeanDefinition Map的数据结构,通过这样的数据结构,IOC容器能够对Bean进行更好的管理。
在配置文件中每一个bean都对应着一个BeanDefinition对象。
BeanDefinition 注册
第三个过程则是注册,即向IOC容器注册这些BeanDefinition,这个过程是通过BeanDefinitionRegistery接口来实现的。
在IOC容器内部其实是将第二个过程解析得到的BeanDefinition注入到一个HashMap容器中,IOC容器就是通过这个HashMap来维护这些BeanDefinition的。
IOC 初始化过程通常不包括 Bean 的依赖注入,Bean 的载入和依赖注入是两个独立的过程,依赖注入在第一次调用 getBean。
可以通过设置 lazyinit 为 false,关闭懒加载,Bean 的依赖注入就会在容器初始化的时候完成。
Spring 容器的启动流程
(1)初始化 Spring 容器,注册内置的 BeanPostProcessor 的 BeanDefinition 到容器中
- 实例化 BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象
- 实例化 BeanDefinitionReader 注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取转化成 BeanDefinition 对象,(BeanDefinition 是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等)
- 实例化 ClassPathBeanDefinitionScanner 路径扫描器,用于对指定的包目录进行扫描查找 bean 对象
(2)将配置类的 BeanDefinition 注册到容器中
(3)调用 refresh() 方法刷新容器
public void refresh() throws BeansException, IllegalStateException {
// 添加一个synchronized 防止出现refresh还没有完成出现其他的操作(启动,或者销毁)
synchronized (this.startupShutdownMonitor) {
// 1.准备工作
// 记录下容器的启动时间、
// 标记“已启动”状态,关闭状态为false、
// 加载当前系统属性到环境对象中
// 准备一系列监听器以及事件集合对象
prepareRefresh();
// 2. 创建容器对象:DefaultListableBeanFactory,加载XML配置文件的属性到当前的工厂中(默认用命名空间来解析),就是上面说的BeanDefinition(bean的定义信息)这里还没有初始化,只是配置信息都提取出来了,(包含里面的value值其实都只是占位符)
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3. BeanFactory的准备工作,设置BeanFactory的类加载器,添加几个BeanPostProcessor,手动注册几个特殊的bean等
prepareBeanFactory(beanFactory);
try {
// 4.子类的覆盖方法做额外的处理,就是刚开始说的 BeanFactoryPostProcessor ,具体的子类可以在这步的时候添加一些特殊的BeanFactoryPostProcessor完成对beanFactory修改或者扩展。
// 到这里的时候,所有的Bean都加载、注册完成了,但是都还没有初始化
postProcessBeanFactory(beanFactory);
// 5.调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法
invokeBeanFactoryPostProcessors(beanFactory);
// 6.注册 BeanPostProcessor 处理器 这里只是注册功能,真正的调用的是getBean方法
registerBeanPostProcessors(beanFactory);
// 7.初始化当前 ApplicationContext 的 MessageSource,即国际化处理
initMessageSource();
// 8.初始化当前 ApplicationContext 的事件广播器,
initApplicationEventMulticaster();
// 9.从方法名就可以知道,典型的模板方法(钩子方法),感兴趣的同学还可以再去复习一下之前写的设计模式中的-模版方法模式
// 具体的子类可以在这里初始化一些特殊的Bean(在初始化 singleton beans 之前)
onRefresh();
// 10.注册事件监听器,监听器需要实现 ApplicationListener 接口。这也不是的重点,过
registerListeners();
// 11.初始化所有的 singleton beans(lazy-init 的除外),重点关注
finishBeanFactoryInitialization(beanFactory);
// 12.广播事件,ApplicationContext 初始化完成
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// 13.销毁已经初始化的 singleton 的 Beans,以免有些 bean 会一直占用资源
destroyBeans();
cancelRefresh(ex);
// 把异常往外抛
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
Spring IOC 底层实现
这个问题翻译一下就是,说一下你对 Spring IOC 的理解
底层实现,是一个比较专业的术语,真要回答正确,面试官自己可能都要花很长时间
可以先简单概述一下自己的理解,面试官觉得不满意,会让你在继续深入谈谈。别一上来就扯各种代码流程方法,一听就是背的!多累啊,面试官都记不住具体的方法名,这回答纯属卖力不讨好。
BeanFactory 与 FactoryBean 的区别
- **BeanFactory **是个Factory,也就是IOC容器或对象工厂,在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的,提供了实例化对象和取对象的功能。
- FactoryBean 是个Bean,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。
BeanFactory 与 ApplicationContext 的区别
简化记忆:
BeanFactory 是 Spring 最低层的接口,它只提供了IOC容器最基本的功能,bean 定义、bean 配置、bean 依赖、bean 生命周期。
ApplicationContext 是 BeanFactory 的子接口,对 IOC 容器操作方法进行了扩展,国际化、资源访问、加载多配置等。
①、提供的功能不同:
BeanFactory:是Spring里面最底层的接口,它只提供了IOC容器最基本的功能,给具体的IOC容器的实现提供了规范。包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系等。
ApplicationContext:它作为BeanFactory的子接口,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能。
ApplicationContext 类结构
public interface ApplicationContext extends
EnvironmentCapable,
ListableBeanFactory,
HierarchicalBeanFactory,
MessageSource,
ApplicationEventPublisher,
ResourcePatternResolver {
}
ApplicationContext 额外提供的功能有:
- 默认初始化所有的Singleton,也可以通过配置取消预初始化。
- 继承MessageSource,因此支持国际化。
- 资源访问,比如访问URL和文件(ResourceLoader);
- 事件机制,(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层;
- 同时加载多个配置文件。
- 消息发送、响应机制(ApplicationEventPublisher);
- 以声明式方式启动并创建Spring容器。
②、 启动时的状态不同:
BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
③、BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
④、BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
两者装载 bean 的区别
BeanFactory 在启动的时候不会去实例化Bean,只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。
ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。可以为Bean配置lazy-init=true来让Bean延迟实例化。
用 BeanFactory 还是 ApplicationContent?
BeanFactory 延迟实例化的优点:
应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;
缺点:速度会相对来说慢一些。而且有可能会出现空指针异常的错误,而且通过bean工厂创建的bean生命周期会简单一些
ApplicationContext 不延迟实例化的优点:
- 所有的Bean在启动的时候都加载,系统运行的速度快;
- 在启动的时候所有的Bean都加载了,就能在系统启动的时候,尽早的发现系统中的配置问题
- 建议web应用,在启动的时候就把所有的Bean都加载了。
缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,缺点就是消耗服务器的内存
ApplicationContext 上下文的生命周期
可以借鉴Servlet的生命周期,实例化、初始init、接收请求service、销毁destroy;
Spring上下文中的Bean也类似,【Spring上下文的生命周期】
- 实例化一个Bean,也就是通常说的new;
- 按照Spring上下文对实例化的Bean进行配置,也就是IOC注入
- 如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的是Spring配置文件中Bean的ID;
- 如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(),传递的是Spring工厂本身(可以用这个方法获取到其他Bean);
- 如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文,该方式同样可以实现步骤4,但比4更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法;
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用After方法,也可用于内存或缓存技术;
- 如果这个Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法;
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postAfterInitialization(Object obj, String s)方法;
注意:以上工作完成以后就可以用这个Bean了,那这个Bean是一个single的,所以一般情况下调用同一个ID的Bean会是在内容地址相同的实例
- 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean接口,会调用其实现的destroy方法
- 最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
以上10步骤可以作为面试或者笔试的模板,另外这里描述的是应用Spring上下文Bean的生命周期,如果应用Spring的工厂也就是BeanFactory的话去掉第5步就Ok了。
长段文字描述对第一次接触这个问题的人很不友好啊,看得迷迷糊糊的,看完还是不懂。并且长篇大论的理论概念很容易让人犯困!借助形象的流程图,有助于理解和记忆。 |
---|
Bean 的生命周期与管理
生命周期
Spring框架中,一旦把一个Bean纳入Spring IOC容器之中,这个Bean的生命周期就会交由容器进行管理,一般担当管理角色的是BeanFactory或者ApplicationContext。
下面以BeanFactory为例,说明一个Bean的生命周期活动。
- Bean的建立, 由BeanFactory读取Bean定义文件,并生成各个实例;
- Setter注入,执行Bean的属性依赖注入;
- BeanNameAware的setBeanName(), 如果实现该接口,则执行其setBeanName方法;
- BeanFactoryAware的setBeanFactory(),如果实现该接口,则执行其setBeanFactory方法;
- BeanPostProcessor的processBeforeInitialization(),如果有关联的processor,则在Bean初始化之前都会执行这个实例的processBeforeInitialization()方法;
- InitializingBean的afterPropertiesSet(),如果实现了该接口,则执行其afterPropertiesSet()方法;
- Bean定义文件中定义init-method;
- BeanPostProcessors的processAfterInitialization(),如果有关联的processor,则在Bean初始化之前都会执行这个实例的processAfterInitialization()方法;
- DisposableBean的destroy(),在容器关闭时,如果Bean类实现了该接口,则执行它的destroy()方法;
- Bean定义文件中定义destroy-method,在容器关闭时,可以在Bean定义文件中使用“destory-method”定义的方法;
如果使用ApplicationContext来维护一个Bean的生命周期,则基本上与上边的流程相同,只不过在执行BeanNameAware的setBeanName()后,若有Bean类实现了org.springframework.context.ApplicationContextAware接口,则执行其 setApplicationContext()方法,然后再进行BeanPostProcessors的processBeforeInitialization() 实际上,ApplicationContext除了向BeanFactory那样维护容器外,还提供了更加丰富的框架功能,如Bean的消息,事件处理机制等。
作用域
singleton
Spring IoC容器中只会存在一个共享的Bean实例,无论有多少个Bean引用它,始终指向同一对象。
singleton是Bean的默认作用域。
默认情况下是容器初始化的时候创建,但也可设定运行时再初始化bean。
DefaultSingletonBeanRegistry类里的singletonObjects哈希表保存了单例对象。
Spring容器可以管理singleton作用域下bean的生命周期,在此作用域下,Spring能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁。
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
缺陷:
多线程环境,没有同步的情况,出现同时修改对象数据问题。
prototype
每次通过Spring容器获取prototype定义的bean时,容器都将创建一个新的Bean实例,每个Bean实例都有自己的属性和状态。
当容器创建了bean的实例后,bean的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的bean的生命周期。
对有状态的bean使用prototype作用域,而对无状态的bean使用singleton作用域。
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
缺陷:
创建多个对象,空间占用较大,加重GC回收任务量。
request
- 在一次Http请求中,容器会返回该Bean的同一实例。而对不同的Http请求则会产生新的Bean,而且该bean仅在当前Http Request内有效。
session
- 在一次Http Session中,容器会返回该Bean的同一实例。而对不同的Session请求则会创建新的实例,该bean实例仅在当前Session内有效。
application
- ServletContext 整个应用周期。
websocket
- WebSocket 整个应用周期。
Spring AOP 面向切面编程
OOP 面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
① JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;
InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最终生成的代理对象; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
② 如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。
AOP面向切面编程术语
切面(Aspect)
切面泛指交叉业务逻辑。事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。
连接点(JoinPoint)
连接点指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。
切入点(PointCut)
切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。
被标记为final的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
目标对象(Target)
目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。
通知(Advice)
通知类型 | 连接点 | 实现接口 |
---|---|---|
前置通知(@Before) | 方法执行前 | |
返回通知(@AfterReturning) | 方法正常返回值后 | |
后置通知(@After) | 方法执行后 | |
环绕通知(@Around) | 方法执行前后 | |
异常通知(@AfterThrowing) | 方法抛出异常 | |
引介通知 | 类中增加新的方法属性 |
通知表示切面的执行时间,Advice也叫增强。
换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方
法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
切入点定义切入的位置,通知定义切入的时间。
Advice 的执行顺序
(1)没有异常情况下的执行顺序:
- around before advice
- before advice
- target method 执行
- after advice
- around after advice
- afterReturning advice
(2)出现异常情况下的执行顺序:
- around before advice
- before advice
- target method 执行
- after advice
- around after advice
- afterThrowing advice
- java.lang.RuntimeException:异常信息
代理模式
Spring AOP 底层实现使用的是动态代理(JDK、CGLIB)。
静态代理
抽象角色:一般是接口或者是抽象类。
真实角色:被代理的角色。
代理角色:代理真实角色,代理真实角色之后,一般会做一些附属(增强的操作)。
/**
* 抽象角色:以租房为例,这是一个租房子的接口
*/
public interface Rent {
void rent();
}
/**
* 功能和描述:真实角色,实现抽象角色对应的接口(Rent)
**/
public class Host implements Rent{
public void rent(){
System.out.println("房屋出租");
}
}
/**
* 代理角色:同真实角色实现同一个接口
*/
public class Proxy implements Rent {
private Host host;
public Proxy() {
}
public Proxy(Host host) {
this.host = host;
}
public void setHost(Host host) {
this.host = host;
}
public void rent() {
seeHouse();
host.rent();
fee();
}
//**************代理角色附带(增强)的一些功能**************//
private void seeHouse(){
System.out.println("带租客看房子");
}
private void fee(){
System.out.println("收取中介费");
}
}
/**
* 功能和描述:测试静态代理
**/
public class Client {
public static void main(String[] args) {
//定义一个真实角色
Host host = new Host();
//定义代理角色
Proxy proxy = new Proxy(host);
//使用代理角色的实例去实现具体操作
proxy.rent();
}
}
静态代理总结:
优点:
使真实角色处理的业务更加的纯粹,不再关注一些公共的事。
公共的业务由代理来完成,实现了业务的分工。
公共业务的扩展变得更加集中和方便。
缺点:
- 类变多了,多了代理类,工作量变大了,且不易扩展。
解决此问题的方案就是使用动态代理。
动态代理
动态代理的实现方式:
基于接口:jdk 动态代理
基于类:cglib 的动态代理
基于字节码:JAVAssist
基于接口的 jdk 动态代理
基于jdk的动态代理的特点是必须要有接口,记住一个类Proxy(java.lang.reflect.Proxy,别导错包)和一个接口InvocationHandler(调用处理程序)。
- 创建InvocationHandler代理调用处理程序实现类。
- 聚合通用代理目标。
- 创建proxy代理对象。
/**
* JDK 动态代理
*/
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 {
return method.invoke(target, args);
}
}
public class MyTest {
public static void main(String[] args) {
// 真实角色
Rent rent = new Host();
//Proxy proxy = new Proxy(rent);
//proxy.rent();
// 创建代理调用处理程序对象
ProxyInvocationHandler pih = new ProxyInvocationHandler();
pih.setTarget(rent);
// 动态生成代理类角色
Rent proxy = (Rent) pih.getProxy();
// 代理角色执行
proxy.rent();
}
}
基于类的 cglib 动态代理
CGLib动态代理可以作用在类上,不要求必须是接口。
- 加入依赖
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
- 创建一个拦截器类实现MethodInterceptor接口
/**
* 基于类的 cglib 动态代理
*/
public class RentMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("执行 cglib 动态代理");
return methodProxy.invokeSuper(o, objects);
}
}
与 JDK 代理对比:
JDK代理要求被代理的类必须是接口,有很强的局限性。
而CGLIB动态代理则没有此类强制性要求。简单的说,CGLIB 会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。
但是如果被代理类被final修饰,那么它不可被继承,即不可被代理;同样,如果被代理类中存在final修饰的方法,那么该方法也不可被代理。
Spring 对 AOP 的支持
Spring 创建代理的规则:
- 默认使用jdk动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了。
- 当需要代理的类不是接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB。
Spring AOP 的底层实现原理
AspectJ 对 AOP 的实现
对于AOP这种编程思想,很多框架都进行了实现。Spring就是其中之一,可以完成面向切面编程。然而,AspectJ也实现了AOP的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring又将AspectJ的对于AOP的实现也引入到了自己的框架中。
在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。
AspectJ是一个优秀面向切面的框架,它扩展了Java语言,提供了强大的切面实现。
AspectJ 的通知类型
AspectJ中常用的通知有五种类型:
前置通知
后置通知
环绕通知
异常通知
最终通知
AspectJ 的切入点表达式
AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
解释:
modifiers-pattern 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
?表示可选的部分
以上表达式共4个部分。
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
execution(* com.xyz.service.*.*(..))
指定切入点为:定义在service包里的任意类的任意方法。
execution(* com.xyz.service..*.*(..))
指定切入点为:定义在service包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
加入 AOP 约束和 jar 包依赖
Spring配置文件加入约束:
<?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/beans/spring-aop.xsd">
</beans>
pom.xml文件添加依赖:
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
<scope>runtime</scope>
</dependency>
AOP 实现方式一
实现spring AOP对应的通知接口。
public interface UserService {
void add();
void delete();
void update();
void query();
}
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("新增了一个用户");
}
@Override
public void delete() {
System.out.println("删除了一个用户");
}
@Override
public void update() {
System.out.println("更新了一个用户");
}
@Override
public void query() {
System.out.println("查询了一个用户");
}
}
/**
* spring aop 前置通知
*/
@Component
public class BeforeLog implements MethodBeforeAdvice {
/**
* 前置通知
* @param method 执行的方法
* @param args 方法参数
* @param target 执行对象
* @throws Throwable
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName() + "-" + method.getName() + "-running...");
}
}
/**
* spring aop 后置通知
*/
@Component
public class AfterLog implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName() + " return value = " + returnValue);
}
}
/**
* spring aop 环绕通知
*/
@Component
public class AroundLog implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
// 怎么环绕
System.out.println("====" + methodInvocation.getMethod().getName() + "====");
return null;
}
}
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
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
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 赋值的时候记得修改schema后面的项 -->
<!-- 开启注解支持 -->
<context:annotation-config/>
<!-- 配置组件扫描器 -->
<context:component-scan base-package="com.vegetable.dynamic"/>
<!-- 配置AOP,需要导入AOP依赖 -->
<!-- AOP方式一:使用AOP通知接口 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="pointcut01" expression="execution(* com.vegetable.dynamic.UserServiceImpl.*(..))"/>
<!-- 配置通知 -->
<aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut01"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut01"/>
<aop:advisor advice-ref="aroundLog" pointcut-ref="pointcut01"/>
</aop:config>
</beans>
@Test
public void testAop01() {
// 创建 spring 容器上下文对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 动态代理的是接口,写实现类会报错
UserService userService = context.getBean("userService", UserService.class);
userService.add();
}
缺陷:
需要写多个类实现对应的通知接口,要实现的接口名也不好记。
AOP 实现方式二
自定义一个切入业务类。
/**
* 自定义切入业务类
*/
@Component
public class MyPointCut {
/**
* 前置通知
* 在目标方法执行之前执行执行的通知。
* 前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息。
* 注意,如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。
*/
public void beforeLog(JoinPoint jp) { // 可以选择额外的传入一个JoinPoint连接点对象,必须用方法的第一个参数接收。
Class clz = jp.getTarget().getClass();
Signature signature = jp.getSignature(); // 通过JoinPoint对象获取更多信息
String name = signature.getName();
System.out.println("MyPointCut - before - [" + clz + "] - [" + name + "]");
}
public void afterLog(JoinPoint jp, Object msg) {
Class clz = jp.getTarget().getClass();
Signature signature = jp.getSignature();
String name = signature.getName();
System.out.println("MyPointCut - afterReturn - [" + clz + "] - [" + name + "] - [" + msg + "]");
}
public void aroundLog(ProceedingJoinPoint jp) throws Throwable {
System.out.println("MyPointCut - around before...");
Object obj = jp.proceed(); // 显式的调用目标方法
System.out.println("MyPointCut - around after...");
}
public void exceptionLog(JoinPoint jp, Throwable e) {
Class clz = jp.getTarget().getClass();
String name = jp.getSignature().getName();
System.out.println("MyPointCut - afterThrow - [" + clz + "] - [" + name + "] - " + e.getMessage());
}
public void finalLog(JoinPoint jp) {
Class clz = jp.getTarget().getClass();
String name = jp.getSignature().getName();
System.out.println("MyPointCut - after - [" + clz + "] - [" + name + "]");
}
}
<!-- AOP实现方式二:自定义业务类 -->
<!-- proxy-target-class:false-基于接口的JDK代理;true-基于类的cglib代理 -->
<aop:config proxy-target-class="true">
<!-- 配置切入点 -->
<aop:pointcut id="pointcut02" expression="execution(* com.vegetable.dynamic.UserServiceImpl.*(..))"/>
<!-- 配置切面业务类 -->
<aop:aspect ref="myPointCut">
<!-- 前置通知 -->
<aop:before method="beforeLog" pointcut-ref="pointcut02"/>
<!-- 后置通知 -->
<!-- <aop:after-returning method="afterLog" pointcut-ref="pointcut02"/> -->
<aop:after-returning method="afterLog" pointcut-ref="pointcut02" returning="msg"/>
<!-- 环绕通知 -->
<aop:around method="aroundLog" pointcut-ref="pointcut02"/>
<!-- 异常通知 -->
<aop:after-throwing method="exceptionLog" pointcut-ref="pointcut02" throwing="e"/>
<!-- 最终通知 -->
<aop:after method="finalLog" pointcut-ref="pointcut02"/>
</aop:aspect>
</aop:config>
@Test
public void testAop01() {
// 创建 spring 容器上下文对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 动态代理的是接口,写实现类会报错
UserService userService = context.getBean("userService", UserService.class);
userService.add();
}
实际开发如果不要求用注解,可以采用这种xml配置方式,切入业务类写好之后,给中配置放在xml文件中,方便集中管理,也不用频繁修改Java代码。
AOP 注解实现
直接在Java代码里面写注解实现。
@Aspect
@Component
public class AnnotationPointcut {
@Before("execution(* com.vegetable.dynamic.UserServiceImpl.*(..))")
public void before(){
System.out.println("---------方法执行前---------");
}
@After("execution(* com.vegetable.dynamic.UserServiceImpl.*(..))")
public void after(){
System.out.println("---------方法执行后---------");
}
//在环绕增强中,可以给定一个参数,代表要获取处理切入的点。
@Around("execution(* com.vegetable.dynamic.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕前");
System.out.println("签名:"+jp.getSignature());
//执行目标方法proceed
Object proceed = jp.proceed();
System.out.println("环绕后");
System.out.println(proceed);
}
}
<!-- AOP实现方式三:注解 -->
<aop:aspectj-autoproxy/><!-- 自动设置代理 -->
@Test
public void testAop01() {
// 创建 spring 容器上下文对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 动态代理的是接口,写实现类会报错
UserService userService = context.getBean("userService", UserService.class);
userService.add();
}
环绕前
签名:void com.vegetable.dynamic.UserService.add()
---------方法执行前---------
新增了一个用户
---------方法执行后---------
环绕后
null
五种通知的常见使用场景
前置通知:请求日志信息记录、校验参数。
后置通知:记录日志(方法已经成功调用)。
环绕通知:控制事务、权限控制。
异常通知:异常处理、控制事务。
最终通知:记录日志(方法已经调用,但不一定成功)。
Spring 主要使用的设计模式
简单工厂模式
实现方式:BeanFactory。 Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
实现原理:
bean容器的启动阶段:
- 读取bean的xml配置文件,将bean元素分别转换成一个BeanDefinition对象。
- 然后通过BeanDefinitionRegistry将这些bean注册到beanFactory中,保存在它的一个ConcurrentHashMap中。
- 将BeanDefinition注册到了beanFactory之后,在这里Spring为提供了一个扩展的切口,允许通过实现接口BeanFactoryPostProcessor 在此处来插入定义的代码。典型的例子就是:PropertyPlaceholderConfigurer,一般在配置数据库的dataSource时使用到的占位符的值,就是它注入进去的。
容器中bean的实例化阶段:
实例化阶段主要是通过反射或者CGLIB对bean进行实例化,在这个阶段Spring又给暴露了很多的扩展点:
- 各种的Aware接口,比如 BeanFactoryAware,对于实现了这些Aware接口的bean,在实例化bean时Spring会帮注入对应的BeanFactory的实例。
- BeanPostProcessor接口,实现了BeanPostProcessor接口的bean,在实例化bean时Spring会帮调用接口中的方法。
- InitializingBean接口,实现了InitializingBean接口的bean,在实例化bean时Spring会帮调用接口中的方法。
- DisposableBean接口,实现了BeanPostProcessor接口的bean,在该bean死亡时Spring会帮调用接口中的方法。
设计意义:
- 松耦合。可以将原来硬编码的依赖,通过Spring这个beanFactory这个工长来注入依赖,也就是说原来只有依赖方和被依赖方,现在引入了第三方——spring这个beanFactory,由它来解决bean之间的依赖问题,达到了松耦合的效果。
- bean的额外处理。通过Spring接口的暴露,在实例化bean的阶段可以进行一些额外的处理,这些额外的处理只需要让bean实现对应的接口即可,那么spring就会在bean的生命周期调用实现的接口来处理该bean。
工厂方法
- 实现方式:FactoryBean接口。
- 实现原理: 实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean.getOjbect()方法的返回值。
- 例子:
- 典型的例子有spring与mybatis的结合。
- 代码示例
- 说明: 看上面该bean,因为实现了FactoryBean接口,所以返回的不是 SqlSessionFactoryBean 的实例,而是她的 SqlSessionFactoryBean.getObject() 的返回值。
单例模式
Spring依赖注入Bean实例默认是单例的。
Spring的依赖注入(包括lazy-init方式)都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。
分析getSingleton()方法
getSingleton()过程图 ps:spring依赖注入时,使用了 双重判断加锁 的单例模式
总结
单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
spring对单例的实现:spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
适配器模式
- 实现方式:SpringMVC中的适配器HandlerAdatper。
- 实现原理:HandlerAdatper根据Handler规则执行不同的Handler。
- 实现过程: DispatcherServlet根据HandlerMapping返回的handler,向HandlerAdatper发起请求,处理Handler。HandlerAdapter根据规则找到对应的Handler并让其执行,执行完毕后Handler会向HandlerAdapter返回一个ModelAndView,最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。
- 实现意义: HandlerAdatper使得Handler的扩展变得容易,只需要增加一个新的Handler和一个对应的HandlerAdapter即可。因此Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法。这样在扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展了。
装饰器模式
- 实现方式: Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。
- 实质:
- 动态地给一个对象添加一些额外的职责。
- 就增加功能来说,Decorator模式相比生成子类更为灵活。
代理模式
- 实现方式: AOP底层,就是动态代理模式的实现。
- 动态代理:在内存中构建的,不需要手动编写代理类
- 静态代理:需要手工编写代理类,代理类引用被代理对象。
- 实现原理: 切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。
织入:把切面应用到目标对象并创建新的代理对象的过程。
观察者模式
实现方式:spring的事件驱动模型使用的是 观察者模式 ,Spring中Observer模式常用的地方是listener的实现。
具体实现:
事件机制的实现需要三个部分,事件源,事件,事件监听器
ApplicationEvent抽象类**[事件]**
- 继承自jdk的EventObject,所有的事件都需要继承ApplicationEvent,并且通过构造器参数source得到事件源。
- 该类的实现类ApplicationContextEvent表示ApplicaitonContext的容器事件。
ApplicationListener接口**[事件监听器]**
- 继承自jdk的EventListener,所有的监听器都要实现这个接口。
- 这个接口只有一个onApplicationEvent()方法,该方法接受一个ApplicationEvent或其子类对象作为参数,在方法体中,可以通过不同对Event类的判断来进行相应的处理。
- 当事件触发时所有的监听器都会收到消息。
ApplicationContext接口**[事件源]**
- ApplicationContext是spring中的全局容器,翻译过来是”应用上下文”。
- 实现了ApplicationEventPublisher接口。
- 职责:负责读取bean的配置文档,管理bean的加载,维护bean之间的依赖关系,可以说是负责bean的整个生命周期,再通俗一点就是平时所说的IOC容器。
ApplicationEventMulticaster抽象类**[事件源中publishEvent方法需要调用其方法getApplicationEventMulticaster]**
- 属于事件广播器,它的作用是把Applicationcontext发布的Event广播给所有的监听器。
策略模式
- 实现方式:Spring框架的资源访问Resource接口 。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。
- Resource 接口介绍
- source 接口是具体资源访问策略的抽象,也是所有资源访问类所实现的接口。
- Resource 接口主要提供了如下几个方法:
- getInputStream():定位并打开资源,返回资源对应的输入流。每次调用都返回新的输入流。调用者必须负责关闭输入流。
- exists():返回 Resource 所指向的资源是否存在。
- isOpen():返回资源文件是否打开,如果资源文件不能多次读取,每次读取结束应该显式关闭,以防止资源泄漏。
- getDescription():返回资源的描述信息,通常用于资源处理出错时输出该信息,通常是全限定文件名或实际 URL。
- getFile:返回资源对应的 File 对象。
- getURL:返回资源对应的 URL 对象。
最后两个方法通常无须使用,仅在通过简单方式访问无法实现时,Resource 提供传统的资源访问的功能。
- Resource 接口本身没有提供访问任何底层资源的实现逻辑,针对不同的底层资源,Spring 将会提供不同的 Resource 实现类,不同的实现类负责不同的资源访问逻辑。
- Spring 为 Resource 接口提供了如下实现类:
- UrlResource:访问网络资源的实现类。
- ClassPathResource:访问类加载路径里资源的实现类。
- FileSystemResource:访问文件系统里资源的实现类。
- ServletContextResource:访问相对于 ServletContext 路径里的资源的实现类.
- InputStreamResource:访问输入流资源的实现类。
- ByteArrayResource:访问字节数组资源的实现类。
这些 Resource 实现类,针对不同的的底层资源,提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的资源访问。
模板方法模式
- 经典模板方法定义:
- 父类定义了骨架(调用哪些方法及顺序),某些特定方法由子类实现
- 最大的好处:代码复用,减少重复代码。除了子类要实现的特定方法,其他方法及方法调用顺序都在父类中预先写好了。
- 所以父类模板方法中有两类方法:
- 共同的方法:所有子类都会用到的代码
- 不同的方法:子类要覆盖的方法,分为两种:
- 抽象方法:父类中的是抽象方法,子类必须覆盖
- 钩子方法:父类中是一个空方法,子类继承了默认也是空的
注:为什么叫钩子,子类可以通过这个钩子(方法),控制父类,因为这个钩子实际是父类的方法(空方法)!
- Spring模板方法模式实质:
是模板方法模式和回调模式的结合
,是Template Method不需要继承的另一种实现方式。Spring几乎所有的外接扩展都采用这种模式。 - 具体实现: JDBC的抽象和对Hibernate的集成,都采用了一种理念或者处理方式,那就是模板方法模式与相应的Callback接口相结合。
- 采用模板方法模式是为了以一种统一而集中的方式来处理资源的获取和释放,比如,JdbcTempalte。
- 引入回调原因:
- JdbcTemplate是抽象类,不能够独立使用,每次进行数据访问的时候都要给出一个相应的子类实现,这样肯定不方便,所以就引入了回调 。
Spring 事务
事务传播行为
事务传播行为类型 | 说明 |
---|---|
REQUIRED | 如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。(默认传播行为) |
REQUIRES_NEW | 无论当前存不存在事务,都创建新事务进行执行。会挂起当前存在的事务。 |
SUPPORTS | 如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。 |
NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
NESTED | 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。 |
MANDATORY | 如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。 |
NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
注意:这7种传播行为有个前提,他们的事务管理器是同一个的时候,才会有上面描述中的表现行为。
没必要死记硬背全记住,就记住 3 个重点常用的就行了。
事务传播特性举例
内外都用 REQUIRED
:
- 在外围方法未开启事务的情况下Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- 在外围方法开启事务的情况下Propagation.REQUIRED修饰的内部方法会加入到外围方法的事务中,所有Propagation.REQUIRED修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。(内部方法try-catch了异常,也还是会一同回滚,因为内外事务为同一个)
内外都用 REQUIRES_NEW
:
- 在外围方法未开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- 在外围方法开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
外默认, 内用 NESTED
:
- 在外围方法未开启事务的情况下Propagation.NESTED和Propagation.REQUIRED作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
- 在外围方法开启事务的情况下Propagation.NESTED修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务。
REQUIRED,REQUIRES_NEW,NESTED 异同
NESTED和REQUIRED修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是REQUIRED是加入外围方法事务,所以和外围事务同属于一个事务,一旦REQUIRED事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED是外围方法的子事务,有单独的保存点,所以NESTED方法抛出异常被回滚,不会影响到外围方法的事务。
NESTED和REQUIRES_NEW都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。
实际案例:
假设有一个注册的方法,方法中调用添加积分的方法,如果希望添加积分不会影响注册流程(即添加积分执行失败回滚不能使注册方法也回滚),会这样写:
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void register(User user){
try {
membershipPointService.addPoint(Point point);
} catch (Exception e) {
//省略...
}
//省略...
}
//省略...
}
还规定注册失败要影响addPoint()
方法(注册方法回滚添加积分方法也需要回滚),那么addPoint()
方法就需要这样实现:
@Service
public class MembershipPointServiceImpl implements MembershipPointService{
@Transactional(propagation = Propagation.NESTED)
public void addPoint(Point point){
try {
recordService.addRecord(Record record);
} catch (Exception e) {
//省略...
}
//省略...
}
//省略...
}
注意到在addPoint()
中还调用了addRecord()
方法,这个方法用来记录日志。他的实现如下:
@Service
public class RecordServiceImpl implements RecordService{
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void addRecord(Record record){
//省略...
}
//省略...
}
注意到addRecord()
方法中propagation = Propagation.NOT_SUPPORTED
,因为对于日志无所谓精确,可以多一条也可以少一条,所以addRecord()
方法本身和外围addPoint()
方法抛出异常都不会使addRecord()
方法回滚,并且addRecord()
方法抛出异常也不会影响外围addPoint()
方法的执行。
事务隔离级别
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别(可重复读)。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。
③ ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。
④ ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。(数据库默认)
⑤ ISOLATION_SERIALIZABLE:所有事务串行依次执行。
事务失效场景
- 非 public 方法、final 方法,无效但不报错,需要自行检查数据库数据 -> spring 做了限制
- 同一个类中方法调用 -> spring AOP 代理做了判断
- 多线程调用 -> 每个线程是不同的数据库连接,事务根据数据源和连接区分
- try-catch 吞了异常 -> UnexpectedRollbackException 其实还是回滚了,只不过是非期望的回滚
- 多数据源 -> 事务管理器使用不当或范围控制不当(A事务包含B事务,事务B执行完成后,接着代码报错,A回滚,B不回滚?默认的REQUIRED,B事务不是加入了A事务吗?)
- @Transactional 注解属性配置错误 -> 传播特性配置不对;指定的回滚异常和代码异常不一致;代码出现的异常不在指定异常范围
- 数据库引擎不支持事务 myisam
Spring 注解
详细内容,可以参考专业书籍,或者技术博客
(1)什么是注解:
Java 注解就是代码中的一些特殊标记(元信息),用于在编译、类加载、运行时进行解析和使用,并执行相应的处理。它本质是继承了 Annotation 的特殊接口,其具体实现类是 JDK 动态代理生成的代理类,通过反射获取注解时,返回的也是 Java 运行时生成的动态代理对象 $Proxy1。通过代理对象调用自定义注解的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法,该方法会从 memberValues 这个Map中查询出对应的值,而 memberValues 的来源是Java常量池。
注解在实际开发中非常常见,比如 Java 原生的 @Overried、@Deprecated 等,Spring的 @Controller、@Service等,Lombok 工具类也有大量的注解,不过在原生 Java 中,还提供了元 Annotation(元注解),他主要是用来修饰注解的,比如 @Target@Retention、@Document、@Inherited 等。
- @Target:标识注解可以修饰哪些地方,比如方法、成员变量、包等,具体取值有以下几种:ElementType.TYPE/FIELD/METHOD/PARAMETER/CONSTRUCTOR/LOCAL_VARIABLE/ANNOTATION_TYPE/PACKAGE/TYPE_PARAMETER/TYPE_USE
- @Retention:什么时候使用注解:SOURCE(编译阶段就丢弃) / CLASS(类加载时丢弃) / RUNTIME(始终不会丢弃),一般来说,自定义的注解都是 RUNTIME 级别的,因为大多数情况是根据运行时环境去做一些处理,一般需要配合反射来使用,因为反射是 Java 获取运行是的信息的重要手段
- @Document:注解是否会包含在 javadoc 中;
- @Inherited:定义该注解与子类的关系,子类是否能使用。
(2)如何自定义注解?
① 创建一个自定义注解:与创建接口类似,但自定义注解需要使用 @interface
② 添加元注解信息,比如 @Target、@Retention、@Document、@Inherited 等
③ 创建注解方法,但注解方法不能带有参数
④ 注解方法返回值为基本类型、String、Enums、Annotation 或其数组
⑤ 注解可以有默认值;
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface CarName {
String value() default "";
}
Spring 事件
详细内容,可以参考专业书籍,或者技术博客
Spring 提供了以下5种标准的事件:
(1)上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
(2)上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
(3)上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
(4)上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
(5)请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。
如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。
Spring5 新特性
Spring 整合 MyBatis
具体查看《MyBatis资料教程》。
Spring 源码环境搭建
- spring源码构建-IDEA2020.1构建Spring5.2.x源码
- Spring5源码阅读环境搭建-gradle构建编译
- 手把手带你编译Spring框架源码,让你的学习事半功倍
- Spring源码环境搭建
- Spring源码分析-源码阅读环境搭建
- Spring源码分析——搭建spring源码
- Spring 源码阅读环境的搭建
下载源码
GitHub 下载 zip 压缩包
Git 克隆到本地
建议先 fork 一份到自己的仓库,方便添加注释提交。
安装配置 Gradle
安装
查看源码中使用的 gradle 版本
下载 Gradle → 官网链接
配置环境变量
新建 GRADLE_HOME
系统环境变量指向你的 Gradle 解压路径
将 %GRADLE_HOME%/bin
添加到 Path
环境变量中
新建 GRADLE_USER_HOME
环境变量,指定 Gradle 用户/仓库目录(默认为USER_HOME/.gradle),用作存放 Gradle 下载的 Jar 包等文件,目录位置、名称自己定义即可(注意不能指向和Maven相同的仓库,他们并不兼容)
仓库源配置
阿里云gradle配置: https://maven.aliyun.com/mvn/guide
仓库名称 | 阿里云仓库地址 | 阿里云仓库地址(老版) | 源地址 |
---|---|---|---|
central | https://maven.aliyun.com/repository/central | https://maven.aliyun.com/nexus/content/repositories/central | https://repo1.maven.org/maven2/ |
jcenter | https://maven.aliyun.com/repository/public | https://maven.aliyun.com/nexus/content/repositories/jcenter | http://jcenter.bintray.com/ |
public | https://maven.aliyun.com/repository/public | https://maven.aliyun.com/nexus/content/groups/public | central仓和jcenter仓的聚合仓 |
https://maven.aliyun.com/repository/google | https://maven.aliyun.com/nexus/content/repositories/google | https://maven.google.com/ | |
gradle-plugin | https://maven.aliyun.com/repository/gradle-plugin | https://maven.aliyun.com/nexus/content/repositories/gradle-plugin | https://plugins.gradle.org/m2/ |
spring | https://maven.aliyun.com/repository/spring | https://maven.aliyun.com/nexus/content/repositories/spring | http://repo.spring.io/libs-milestone/ |
spring-plugin | https://maven.aliyun.com/repository/spring-plugin | https://maven.aliyun.com/nexus/content/repositories/spring-plugin | http://repo.spring.io/plugins-release/ |
grails-core | https://maven.aliyun.com/repository/grails-core | https://maven.aliyun.com/nexus/content/repositories/grails-core | https://repo.grails.org/grails/core |
apache snapshots | https://maven.aliyun.com/repository/apache-snapshots | https://maven.aliyun.com/nexus/content/repositories/apache-snapshots | https://repository.apache.org/snapshots/ |
单个项目配置
在源码的根路径找到build.gradle文件,在repositories配置项中加入下面的代码,修改maven地址为阿里云仓库。
repositories {
// maven 本地仓库
mavenLocal()
// 新增阿里云仓库
maven { url 'https://maven.aliyun.com/repository/public'}
// 新增springsource仓库
maven { url "http://repo.springsource.org/plugins-release" }
maven { url "https://repo.spring.io/plugins-release" }
maven { url "https://repo.spring.io/libs-spring-framework-build" }
// maven 远程中央仓库
mavenCentral()
}
其他版本
repositories {
// maven 本地仓库
mavenLocal()
// 新增阿里云仓库
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/'}
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
maven { url "https://maven.aliyun.com/repository/spring" }
maven { url "https://maven.aliyun.com/repository/spring-plugin" }
// 新增springsource仓库
maven { url "http://repo.springsource.org/plugins-release" }
maven { url "https://repo.spring.io/plugins-release" }
maven { url "https://repo.spring.io/libs-spring-framework-build" }
// maven 远程中央仓库
mavenCentral()
}
具体有效配置,还需要正确的资料指引,网上的仓库配置比较杂乱。
个人认为,Spring 最新代码可以使用阿里云新仓库地址,5.2.x 及以下版本用老版仓库地址,就是版本新旧对应,会好一点吧。
再打开 Spring 源码根目录下的 settings.gradle 文件,添加阿里云仓库。
pluginManagement {
repositories {
// maven { url 'file:///E:\DevRes\gradleRepository' }
mavenLocal()
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/spring/'}
maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
maven { url 'https://repo.spring.io/plugins-release' }
mavenCentral()
gradlePluginPortal()
}
}
repositories 中写的是获取 jar 包的顺序。
先是本地的 Maven 仓库路径;接着的 mavenLocal() 是获取 Maven 本地仓库的路径,应该是和第一条一样,但是不冲突;第三条和第四条是从国内和国外的网络上仓库获取;最后的 mavenCentral() 是从Apache提供的中央仓库获取 jar 包。
全局配置
配置阿里仓库的镜像。
在gradle的安装目录下的init.d
的文件夹下添加init.gradle
。
gradle.projectsLoaded {
rootProject.allprojects {
buildscript {
repositories {
def JCENTER_URL = 'https://maven.aliyun.com/repository/public'
def GOOGLE_URL = 'https://maven.aliyun.com/repository/google'
def NEXUS_URL = 'https://maven.aliyun.com/repository/central'
all { ArtifactRepository repo ->
if (repo instanceof MavenArtifactRepository) {
def url = repo.url.toString()
if (url.startsWith('https://jcenter.bintray.com/')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $JCENTER_URL."
println("buildscript ${repo.url} replaced by $JCENTER_URL.")
remove repo
}
else if (url.startsWith('https://dl.google.com/dl/android/maven2/')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $GOOGLE_URL."
println("buildscript ${repo.url} replaced by $GOOGLE_URL.")
remove repo
}
else if (url.startsWith('https://repo1.maven.org/maven2')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $REPOSITORY_URL."
println("buildscript ${repo.url} replaced by $REPOSITORY_URL.")
remove repo
}
}
}
jcenter {
url JCENTER_URL
}
google {
url GOOGLE_URL
}
maven {
url NEXUS_URL
}
}
}
repositories {
def JCENTER_URL = 'https://maven.aliyun.com/repository/public'
def GOOGLE_URL = 'https://maven.aliyun.com/repository/google'
def NEXUS_URL = 'https://maven.aliyun.com/repository/central'
all { ArtifactRepository repo ->
if (repo instanceof MavenArtifactRepository) {
def url = repo.url.toString()
if (url.startsWith('https://jcenter.bintray.com/')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $JCENTER_URL."
println("buildscript ${repo.url} replaced by $JCENTER_URL.")
remove repo
}
else if (url.startsWith('https://dl.google.com/dl/android/maven2/')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $GOOGLE_URL."
println("buildscript ${repo.url} replaced by $GOOGLE_URL.")
remove repo
}
else if (url.startsWith('https://repo1.maven.org/maven2')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $REPOSITORY_URL."
println("buildscript ${repo.url} replaced by $REPOSITORY_URL.")
remove repo
}
}
}
jcenter {
url JCENTER_URL
}
google {
url GOOGLE_URL
}
maven {
url NEXUS_URL
}
}
}
}
简化版 init.gradle
:
allprojects {
repositories {
// 使用 maven 本地仓库
mavenLocal()
// 使用阿里云代理的中央仓库
maven { name "aliyun" ; url 'https://maven.aliyun.com/repository/public' }
// 使用 maven 中央仓库
mavenCentral()
}
buildscript {
repositories {
mavenLocal()
maven { name "aliyun" ; url 'https://maven.aliyun.com/repository/public' }
mavenCentral()
}
}
}
注意:mavenLocal() 会从以下路径查找 setting.xml 从而找到本地的 maven 仓库具体路径
- 举例第三种方式:新建环境
M2_HOME
,路径指向本地的 maven 存放路径,mavenLocal() 即可正常查找到本地已下载的 jar 包
遇到问题
初次编译很慢
gradlew :spring-oxm:compileTestJava 处理依赖很慢,2~5 个小时,还报错中断,试了几次,两天过去了,源码环境还是没搭好。
最后还是创建一个简单的 maven 项目,引入 spring 依赖,在项目内看源代码,不能修改。