一句话概括:Spring是一个轻量级、非入侵式的控制反转(loC)和面向切面(AOP)的框架。
2003年,一个音乐家Rod Johnson决定发展一个轻量级的Java开发框架,Spring作为Java战场的龙骑兵渐渐崛起,并淘汰了EJB这个传统的重装骑兵。
到了现在,企业级开发的标配基本就是Spring5 + Spring Boot 2 + JDK 8
Spring 有哪些特性呢?
Spring 有很多优点:
IOC:Inversion Of Control 控制反转
DI:Dependency Injection 依赖注入
AOP:Aspect Oriented Programming 面向切片编程
Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring 工厂用于生成 Bean,并且管理Bean的生命周期,实现高内聚低耦合的设计理念。
Spring提供了面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等切面功能。
支持通过配置就来完成对事务的管理,而不需要通过硬编码的方式,以前重复的一些事务提交、回滚的JDBC代码,都可以不用自己写了。
Spring 对 Junit 提供支持,可以通过注解快捷地测试 Spring 程序。
方便集成各种优秀框架,Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz等)的直接支持。
Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等)都提供了模板化的封装,这些封装API的提供使得应用难度大大降低。
Spring框架是分模块存在,除了最核心的spring Core Container
是必要模块之外,其他模块都是可选
,大约有20 多个模块。
最主要的七大模块:
Spring有很多模块,甚至广义的SpringBoot、SpringCloud也算是Spring的一部分,我们来分模块,按功能来看一下一些常用的注解:
Web:
容器:
@Component:表示一个带注释的类是一个"组件",成为Spring管理的Bean。当使用基于注解的配置和类路径扫描时,这些类被视为自动检测的候选对象。同时@Component还是一个元注解。
@Service:组合注解(组合了@Component注解),应用在service层(业务逻辑层)。
@Repository:组合注解(组合了@Component注解),应用在dao层(数据访问层)。
@Autowired:Spring提供的工具(由Spring的依赖注入工具(BeanPostProcessor、BeanFactoryPostProcessor)自动注入)。
@Qualifier:该注解通常跟@Autowired一起使用,当想对注入的过程做更多的控制,@Qualifier可帮助配置,比如两个以上相同类型的 Bean 时 Spring 无法抉择,用到此注解
@Configuration:声明当前类是一个配置类(相当于一个Spring配置的xml文件)
@Value:可用在字段,构造器参数跟方法参数,指定一个默认值,支持#{}跟${}两个方式。一般将SpringbBoot 中的 application.properties 配置的属性值赋值给变量。
@Bean:注解在方法上,声明当前方法的返回值为一个Bean。返回的Bean对应的类中可以定义init()方法和destroy()方法,然后在
@Bean (initMethod="init", destroyMethod="destroy")
定义,在构造之后执行 init,在销毁之前执行 destroy。
@Scope:定义我们采用什么模式去创建Bean(方法上,得有@Bean)其设置类型包括:Singleton、Prototype、Request、Session、GlobalSession。
AOP:
事务:
Spring 框架中广泛使用了不同类型的设计模式,下面我们来看看到底有哪些设计模式?
Java是面向对象的编程语言,一个个实例对象相互合作组成了业务逻辑,原来,我们都是在代码里创建对象和对象的依赖。
所谓的IOC(控制反转):就是由容器来负责控制对象的生命周期和对象间的关系。以前是我们想要什么,就自己创建什么,现在是我们需要什么,容器就给我们送来什么。
也就是说,控制对象生命周期的不再是引用它的对象,而是容器。对具体对象,以前是它控制其它对象,现在所有对象都被容器控制,所以这就叫控制反转。
DI(依赖注入):指的是容器在实例化对象的时候把它依赖的类注入给它。有的说法IOC和DI 是一回事,有的说法是 IOC 是思想,DI 是 IOC 的实现。
为什么要使用 IOC 呢?
最主要的是两个字解耦,硬编码会造成对象间的过度耦合,使用IOC之后,我们可以不用关心对象间的依赖,专心开发应用就行。
PS:或者问法是"你有自己实现过简单的Spring吗?"
Spring 的IOC 本质就是一个大工厂,我们想想一个工厂是怎么运行的呢?
生产产品: 一个工厂最核心的功能就是生产产品。在Spring里,不用Bean自己来实例化,而是交给Spring,应该怎么实现呢?——答案毫无疑问,反射。
那么这个厂子的生产管理是怎么做的?你应该也知道——工厂模式。
库存产品:工厂一般都是有库房的,用来库存产品,毕竟生产的产品不能立马就拉走。Spring我们都知道是一个容器,这个容器里存的就是对象,不能每次来取对象,都得现场来反射创建对象,得把创建出的对象存起来。
订单处理:还有最重要的一点,工厂根据什么来提供产品呢?订单。这些订单可能五花八门,有线上签签的、有到工厂签的、还有工厂销售上门签的……最后经过处理,指导工厂的出货。
在Spring里,也有这样的订单,它就是我们 bean的定义和依赖关系,可以是xml形式,也可以是我们最熟悉的注解形式。
我们简单地实现一个mini版的Spring IOC:
Bean 定义:
Bean通过一个配置文件定义,把它解析成一个类型。
userDao:cn.fighter3.bean.UserDao
BeanDefinition.java
bean定义类,配置文件中bean定义对应的实体
public class BeanDefinition {
private String beanName;
private Class beanClass;
//省略略getter、setter
}
ResourceLoader.java
资源加载器,用来完成配置文件中配置的加载
public class ResourceLoader {
public static Map<String, BeanDefinition> getResource() {
Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>(16);
Properties properties = new Properties();
try {
InputStream inputStream =
ResourceLoader.class.getResourceAsStream("/beans.properties");
properties.load(inputStream);
Iterator<String> it = properties.stringPropertyNames().iterator();
while (it.hasNext()) {
String key = it.next();
String className = properties.getProperty(key);
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setBeanName(key);
Class clazz = Class.forName(className);
beanDefinition.setBeanClass(clazz);
beanDefinitionMap.put(key, beanDefinition);
}
inputStream.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return beanDefinitionMap;
}
}
BeanRegister.java
对象注册器,这里用于单例bean的缓存,我们大幅简化,默认所有bean都是单例的。可以看到所谓单例注册,也很简单,不过是往HashMap里存对象。
public class BeanRegister {
//单例Bean缓存
private Map<String, Object> singletonMap = new HashMap<>(32);
/**
* 获取单例Bean
*
* @param beanName bean名称
* @return
*/
public Object getSingletonBean(String beanName) {
return singletonMap.get(beanName);
}
/**
* 注册单例bean
*
* @param beanName
* @param bean
*/
public void registerSingletonBean(String beanName, Object bean) {
if (singletonMap.containsKey(beanName)) {
return;
}
singletonMap.put(beanName, bean);
}
}
对象工厂,我们最核心的一个类,在它初始化的时候,创建了 bean 注册器,完成了资源的加载。
获取 bean 的时候,先从单例缓存中取,如果没有取到,就创建并注册一个 bean
public class BeanFactory {
private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
private BeanRegister beanRegister;
public BeanFactory() {
//创建bean注册器
beanRegister = new BeanRegister();
//加载资源
this.beanDefinitionMap = new ResourceLoader().getResource();
}
/**
* 获取bean
*
* @param beanName bean名称
* @return
*/
public Object getBean(String beanName) {
//从bean缓存中取
Object bean = beanRegister.getSingletonBean(beanName);
if (bean != null) {
return bean;
}
//根据bean定义,创建bean
return createBean(beanDefinitionMap.get(beanName));
}
/**
* 创建Bean
* @param beanDefinition bean定义
* @return
*/
private Object createBean(BeanDefinition beanDefinition) {
try {
Object bean = beanDefinition.getBeanClass().newInstance();
//缓存bean
beanRegister.registerSingletonBean(beanDefinition.getBeanName(), bean);
return bean;
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
public class UserDao {
public void queryUserInfo(){
System.out.println("A good man.");
}
}
public class ApiTest {
@Test
public void test_BeanFactory() {
//1.创建bean工厂(同时完成了加载资源、创建注册单例bean注册器的操作)
BeanFactory beanFactory = new BeanFactory();
//2.第一次获取bean(通过反射创建bean,缓存bean)
UserDao userDao1 = (UserDao) beanFactory.getBean("userDao");
userDao1.queryUserInfo();
//3.第二次获取bean(从缓存中获取bean)
UserDao userDao2 = (UserDao) beanFactory.getBean("userDao");
userDao2.queryUserInfo();
}
}
A good man.
A good man.
至此,我们一个乞丐+破船版的Spring就完成了,代码也比较完整,有条件的可以跑一下。
可以这么形容,BeanFactory 是 Spring 的“心脏”,ApplicantContext 是完整的“身躯”。
BeanFactory接口
BeanFactory是类的通用工厂,可以创建并管理各种类的对象。
Spring为BeanFactory 提供了很多种实现,最常用的是 XmlBeanFactory,但在Spring 3.2 中已被废弃,建议使用XmlBeanDefinitionReader、DefaultListableBeanFactory。
BeanFactory接口位于类结构树的顶端,它最主要的方法就是getBean(String var1),这个方法从容器中返回特定名称的 Bean。
BeanFactory 的功能通过其它的接口得到了不断的扩展,比如AbstractAutowireCapableBeanFactory定义了将容器中的Bean按照某种规则(比如按名字匹配、按类型匹配等)进行自动装配的方法。
这里看一个XMLBeanFactory(已过期)获取 bean 的例子:
public class HelloWorldApp{
public static void main(String[] args) {
BeanFactory factory = new XmlBeanFactory (new ClassPathResource("beans.xml"));
HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");
obj.getMessage();
}
}
ApplicationContext 接口
ApplicationContext由BeanFactory派生而来,提供了更多面向实际应用的功能。可以这么说,使用BeanFactory 就是手动档,使用ApplicationContext就是自动档。
ApplicationContext 继承了 HierachicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过其他的接口扩展了BeanFactory 的功能,包括:
这是ApplicationContext的使用例子:
public class HelloWorldApp{
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
}
}
ApplicationContext包含BeanFactory的所有特性,通常推荐使用前者。
Spring的IOC容器工作的过程,其实可以划分为两个阶段:容器启动阶段和Bean实例化阶段。
其中容器启动阶段主要做的工作是加载和解析配置文件,保存到对应的 Bean 定义中。
容器启动开始,首先会通过某种途径加载Congiguration MetaData,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的Congiguration MetaData进行解析和分析,并将分析后的信息组为相应的BeanDefinition
最后把这些保存了Bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry,这样容器启动就完成了。
在Spring 中,基本容器 BeanFactory 和扩展容器 ApplicationContext 的实例化时机不太一样,BeanFactory 采用的是延迟初始化的方式,也就是只有在第一次getBean()的时候,才会实例化Bean;ApplicationContext启动之后会实例化所有的 Bean 定义。
Spring IOC中Bean的生命周期大致分为四个阶段:实例化(Instantiation)、属性赋值(Populate)、初始化、(Initialization)、销毁(Destruction)。
我们再来看一个稍微详细一些的过程:
简单总结一下,Bean生命周期里初始化的过程相对步骤会多一些,比如前置、后置的处理。
最后通过一个实例来看一下具体的细节:
详细代码实现PersonBean类实例可以参考这篇文章:Bean容器生命周期,好像人的一生。
有三种方式:直接编码方式、配置文件方式、注解方式。
Spring支持构造方法注入、属性注入、工厂方法注入,其中工厂方法注入,又可以分为静态工厂方法注入和非静态工厂方法注入。
public CatDaoImpl(String message){
this. message = message;
}
<bean id="CatDaoImpl" class="com.CatDaoImpl">
<constructor-arg value=" message ">constructor-arg>
bean>
public class Id {
private int id;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
}
<bean id="id" class="com.id ">
<property name="id" value="123">property>
bean>
工厂方法注入
静态工ㄏ注入
静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让Spring管理所有对象,我们不能直接通过"工程类.静态方法()
"来获取对象,而是依然通过 Spring 注入的形式获取:
public class DaoFactory { //静态工厂
public static final FactoryDao getStaticFactoryDaoImpl(){
return new StaticFacotryDaoImpl();
}
}
public class SpringAction {
//注入对象
private FactoryDao staticFactoryDao;
//注入对象的 set 方法
public void setStaticFactoryDao(FactoryDao staticFactoryDao) {
this.staticFactoryDao = staticFactoryDao;
}
}
<property name="staticFactoryDao" ref="staticFactoryDao">property>
bean>
<bean name="staticFactoryDao" class="DaoFactory" factory-method="getStaticFactoryDaoImpl">bean>
非静态工厂注入
非静态工厂,也叫实例工厂,意思是工厂方法不是静态的,所以我们需要首先 new 一个工厂实例,再调用普通的实例方法。
//非静态工厂
public class DaoFactory {
public FactoryDao getFactoryDaoImpl(){
return new FactoryDaoImpl();
}
}
public class SpringAction {
//注入对象
private FactoryDao factoryDao;
public void setFactoryDao(FactoryDao factoryDao) {
this.factoryDao = factoryDao;
}
}
<bean name="springAction" class="SpringAction">
<property name="factoryDao" ref="factoryDao">property>
bean>
<bean name="daoFactory" class="com.DaoFactory">bean>
<bean name="factoryDao" factory-bean="daoFactory" factory-method="getFactoryDaoImpl">bean>
什么是自动装配?
Spring IOC容器知道所有Bean的配置信息,此外,通过Java反射机制还可以获知实现类的结构信息,如构造方法的结构、属性等信息。掌握所有Bean的这些信息后,Spring IOC容器就可以按照某种规则对容器中的Bean进行自动装配,而无须通过显式的方式进行依赖配置。
Spring提供的这种方式,可以按照某些规则进行Bean的自动装配,
Spring 提供了哪几种自动装配类型?
Spring 提供了 4 种自动装配类型:
Spring 的 Bean 主要支持五种作用域:
以下三个作用域于只在Web应用中适用:
首先结论在这:Spring 中的单例 Bean不是线程安全的。
因为单例Bean,是全局只有一个Bean,所有线程共享。
如果说单例Bean,是一个无状态的,也就是线程中的操作不会对Bean中的成员变量执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc的Controller、Service、Dao 等,这些 Bean 大多是无状态的,只关注于方法本身。
假如这个Bean是有状态的,也就是会对Bean中的成员变量进行写操作,那么可能就存在线程安全的问题。
单例 Bean 线程安全问题怎么解决呢?
常见的有这么些解决办法:
什么是循环依赖?
Spring 循环依赖:简单说就是自己依赖自己,或者和别的 Bean 相互依赖。
只有单例的Bean才存在循环依赖的情况,原型(Prototype)情况下,Spring会直接抛出异常。原因很简单,AB循环依赖,A实例化的时候,发现依赖B,创建B实例,创建B 的时候发现需要A,创建A1实例……无限套娃,直接把系统干垮。
Spring 可以解决哪些情况的循环依赖?
Spring不支持基于构造器注入的循环依赖,但是假如AB循环依赖,如果一个是构造器注入,一个是setter注入呢?
看看几种情形:
第四种可以而第五种不可以的原因是Spring在创建Bean时默认会根据自然排序进行创建,所以A会先于B进行创建。
所以简单总结,当循环依赖的实例都采用setter方法注入的时候,Spring可以支持,都采用构造器注入的时候,不支持,构造器注入和 setter 注入同时存在的时候,看天。
PS:其实正确答案是开发人员做好设计,别让 Bean 循环依赖,但是没办法,面试官不想听这个。
我们都知道,单例Bean初始化完成,要经历三步:
注入就发生在第二步,属性赋值,结合这个过程,Spring 通过三级缓存解决了循环依赖:
我们来看一下三级缓存解决循环依赖的过程:
当A、B 两个类发生循环依赖时:
A 实例的初始化过程:
所以,我们就知道为什么Spring能解决setter注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。
不行,主要是为了生成代理对象。如果是没有代理的情况下,使用二级缓存解决循环依赖也是OK 的。但是如果存在代理,三级没有问题,二级就不行了。
因为三级缓存中放的是生成具体对象的匿名内部类,获取Object的时候,它可以生成代理对象,也可以返回普通对象。使用三级缓存主要是为了保证不管什么时候使用的都是一个对象。
假设只有二级缓存的情况,往二级缓存中放的显示一个普通的 Bean 对象,Bean 初始化过程中,通过BeanPostProcessor去生成代理对象之后,覆盖掉二级缓存中的普通Bean对象,那么可能就导致取到的Bean对象不一致了。
实现@Autowired 的关键是:AutowiredAnnotationBeanPostProcessor
在Bean的初始化阶段,会通过Bean 前后置处理器来进行一些前置和后置的处理。
实现@Autowired 的功能,是通过后置处理器来完成的。这个后置处理器就是AutowiredAnnotationBeanPostProcessor。
Spring在创建bean的过程中,最终会调用到 doCreateBean()
方法,在doCreateBean()
方法中会调用 populateBean()
方法,来为 bean 进行属性填充,完成自动装配等工作。
在 populateBean()
方法中一共调用了两次后置处理器,第一次是为了判断是否需要属性填充,如果不需要进行属性填充,那么就会直接进行 return,如果需要进行属性填充,那么方法就会继续向下执行,后面会进行第二次后置处理器的调用,这个时候,就会调用到AutowiredAnnotationBeanPostProcessor的 postProcessPropertyValues()
方法,在该方法中就会进行@Autowired 注解的解析,然后实现自动装配。
postProcessorPropertyValues()
方法中,会先调用 findAutowiringMetadata()
方法解析出bean中带有@Autowired注解、@Inject和@Value注解的属性和方法。然后调用 metadata.inject()
方法,进行属性填充。
AOP:面向切面编程。简单说,就是把一些业务逻辑中的相同的代码抽取到一个独立的模块中,让业务逻辑更加清爽。
具体来说,假如我现在要crud写一堆业务,可是如何在业务代码前后进行打印日志和参数的校验呢?
我们可以把日志记录
和数据校验
可重用的功能模块分离出来,然后在程序的执行的合适的地方动态地植入这些代码并执行。这样就简化了代码的书写。
业务逻辑代码中没有参和通用逻辑的代码,业务模块更简洁,只包含核心业务代码。实现了业务逻辑和通用逻辑的代码分离,便于维护和升级,降低了业务逻辑和通用逻辑的耦合性。
AOP可以将遍布应用各处的功能分离出来形成可重用的组件。在编译期间、装载期间或运行期间实现在不修改源代码的情况下给程序动态添加功能。从而实现对业务逻辑的隔离,提高代码的模块化能力。
AOP的核心其实就是动态代理,如果是实现了接口的话就会使用JDK动态代理,否则使用CGLIB代理,主要应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。
AOP有哪些核心概念?
切面(Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象
连接点(Joinpoint):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
切点(Pointcut):对连接点进行拦截的定位
通知(Advice):所谓通知指的就是指拦截到连接点之后要执行的代码,也可以称作增强
目标对象(Target):代理的目标对象
织入(Weabing):织入是将增强添加到目标类的具体连接点上的过程。
编译期织入:切面在目标类编译时被织入
类加载期织入:切面在目标类加载到 JVM 时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。
运行期织入:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。SpringAOP 就是以这种方式织入切面。
Spring 采用运行期织入,而 AspectJ采用编译期织入和类加载器织入。
引介(introduction):引介是一种特殊的增强,可以动态地为类添加一些属性和方法
AOP 有哪些环绕方式?
AOP 一般有 5 种环绕方式:
多个切面的情况下,可以通过@Order 指定先后顺序,数字越小,优先级越高。
这里给出一个小例子,SpringBoot项目中,利用AOP打印接口的入参和出参日志,以及执行时间,还是比较快捷的。
引入依赖:引入 AOP 依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
自定义注解:自定义一个注解作为切点
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface WebLog {
}
配置AOP 切面:
@Aspect:标识切面
@Pointcut:设置切点,这里以自定义注解为切点,定义切点有很多其它种方式,自定义注解是比较常用的一种。
@Before:在切点之前织入,打印了一些入参信息
@Around:环绕切点,打印返回参数和接口执行时间
@Aspect
@Component
public class WebLogAspect {
private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
/**
* 以自定义 @WebLog 注解为切点
**/
@Pointcut("@annotation(cn.fighter3.spring.aop_demo.WebLog)")
public void webLog() {}
/**
* 在切点之前织入
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 打印请求相关参数
logger.info("========================================== Start ==========================================");
// 打印请求 url
logger.info("URL:{}", request.getRequestURL().toString());
// 打印 Http method
logger.info("HTTP Method: {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
logger.info("Class Method: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
logger.info("IP: {}", request.getRemoteAddr());
// 打印请求入参
logger.info("Request Args: {}",new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
}
/**
* 在切点之后织入
* @throws Throwable
*/
@After("webLog()")
public void doAfter() throws Throwable {
// 结束后打个分隔线,方便查看
logger.info("=========================================== End ===========================================");
}
/**
* 环绕
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始时间
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出参
logger.info("Response Args: {}", new ObjectMapper().writeValueAsString(result));
// 执行耗时
logger.info("Time-Consuming: {} ms", System.currentTimeMillis() - startTime);
return result;
}
}
使用:只需要在接口上加上自定义注解
@GetMapping("/hello")
@WebLog(desc = "这是一个欢迎接口")
public String hello(String name){
return "Hello "+name;
}
执行结果:可以看到日志打印了入参、出参和执行时间
Spring的AOP是通过动态代理来实现的,动态代理主要有两种方式JDK动态代理和Cglib动态代理,这两种动态代理的使用和原理有些不同。
JDK动态代理
Interface:对于JDK动态代理,目标类需要实现一个Interface。
InvocationHandler:InvocationHandler是一个接口,可以通过实现这个接口,定义横切逻辑,再通过反射机制(invoke)调用目标类的代码,在次过程,可能包装逻辑,对目标方法进行前置后置处理。
Proxy:Proxy 利用InvocationHandler动态创建一个符合目标类实现的接口的实例,生成目标类的代理对
我们来看一个常见的小场景,客服中转,解决用户问题:
JDK动态代理实现:
接口
public interface ISolver {
void solve();
}
目标类:需要实现对应接口
public class Solver implements ISolver {
@Override
public void solve() {
System.out.println("疯狂掉头发解决问题……");
}
}
动态代理工厂:ProxyFactory,直接用反射方式生成一个目标对象的代理对象,这里用了一个匿名内部类方式重写InvocationHandler方法,实现接口重写也差不多
public class ProxyFactory {
// 维护一个目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("请问有什么可以帮到您?");
// 调用目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("问题已经解决啦!");
return null;
}
});
}
}
客户端:Client,生成一个代理对象实例,通过代理对象调用目标对象方法
public class Client {
public static void main(String[] args) {
//目标对象:程序员
ISolver developer = new Solver();
//代理:客服小姐姐
ISolver csProxy = (ISolver) new ProxyFactory(developer).getProxyInstance();
//目标方法:解决问题
csProxy.solve();
}
}
Cglib动态代理实现:
目标类:Solver,这里目标类不用再实现接口。
public class Solver {
public void solve() {
System.out.println("疯狂掉头发解决问题……");
}
}
动态代理工厂:
public class ProxyFactory implements MethodInterceptor {
//维护一个目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//为目标对象生成代理对象
public Object getProxyInstance() {
//工具类
Enhancer en = new Enhancer();
//设置父类
en.setSuperclass(target.getClass());
//设置回调函数
en.setCallback(this);
//创建子类对象代理
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("请问有什么可以帮到您?");
// 执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("问题已经解决啦!");
return null;
}
}
客户端:Client
public class Client {
public static void main(String[] args) {
//目标对象:程序员
Solver developer = new Solver();
//代理:客服小姐姐
Solver csProxy = (Solver) new ProxyFactory(developer).getProxyInstance();
//目标方法:解决问题
csProxy.solve();
}
}
Spring AOP
Spring AOP属于运行时增强,主要具有如下特点:
AspectJ
AspectJ是一个易用的功能强大的AOP框架,属于编译时增强,可以单独使用,也可以整合到其它框架中,是AOP编程的完全解决方案。Aspecty需要用到单独的编译器ajc。
Aspecty属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:
整体对比如下:
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的。
Spring 只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过数据库自己的事务机制实现。
Spring 支持编程式事务
管理和声明式
事务管理两种方式:
编程式事务
声明式事务
声明式事务管理建立在 AOP 之上的。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
优点是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过 @Transactional 注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
Spring的接口TransactionDefinition中定义了表示隔离级别的常量,当然其实主要还是对应数据库的事务隔离级别:
Spring事务的传播机制说的是,当多个事务同时存在的时候——一般指的是多个事务方法相互调用时,Spring如何处理这些事务的行为。
事务传播机制是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。
Spring默认的事务传播行为是PROPAFATION_REQUIRED,它适合绝大多数情况,如果多个ServiceX#methodX()
都工作在事务环境下(均被Spring事务增强),且程序中存在调用链Service1#method1()->Service2#method2()->Service3#method3()
,那么这3个服务类的三个方法通过Spring的事务传播机制都工作在同一个事务中。
就是通过AOP/动态代理。
MethodInterceptor
,事务增强对该接口的实现为TransactionInterceptor
,类图如下:事务拦截器TransactionInterceptor
在invoke
方法中,通过调用父类TransactionAspectSupport
的invokeWithinTransaction
方法进行事务处理,包括开启事务、事务提交、异常回滚。
1、@Transactional 应用在非 public 修饰的方法上
如果Transactional注解应用在非 public 修饰的方法上,Transactional将会失效。
是因为在Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的intercept方法 或 JdkDynamicAopProxy的invoke方法会间接调用AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute方法,获取Transactional 注解的事务配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
2、@Transactional 注解属性 propagation 设置错误
3、@Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException的异常)或者 Error才回滚事务,其他异常不会触发回滚事务。
// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。
4、同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
//@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
/**
* B 插入字段为 3的数据
*/
this.insertB();
/**
* A 插入字段为 2的数据
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
这种情况是最常见的一种@Transactional注解失效场景
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段为 3的数据
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务就不能正常回滚了,会抛出异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
Spring MVC 虽然整体流程复杂,但是实际开发中很简单,大部分的组件不需要开发人员创建和管理,只需要通过配置文件的方式完成配置即可,真正需要开发人员进行处理的只有 Handler(Controller) 、View 、Model。
当然我们现在大部分的开发都是前后端分离,Restful风格接口,后端只需要返回Json数据就行了。
PS:这是一道全新的八股,毕竟ModelAndView这种方式应该没人用了吧?现在都是前后端分离接口,八股也该更新换代了。
我们都知道Restful接口,响应格式是json,这就用到了一个常用注解:@ResponseBody
@GetMapping("/user")
@ResponseBody
public User user(){
return new User(1,"张三");
}
加入了这个注解后,整体的流程上和使用ModelAndView大体上相同,但是细节上有一些不同:
客户端向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet
DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller来处理
DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller
Controller被封装成了ServletInvocableHandlerMethod,HandlerAdapter处理器适配器去执行invokeAndHandle方法,完成对Controller的请求处理
HandlerAdapter执行完对Controller的请求,会调用HandlerMethodReturnValueHandler去处理返回值,主要的过程:
5.1. 调用RequestResponseBodyMethodProcessor,创建ServletServerHttpResponse(Spring对原生ServerHttpResponse的封装)实例
5.2. 使用HttpMessageConverter的write方法,将返回值写入ServletServerHttpResponse的OutputStream输出流中
5.3. 在写入的过程中,会使用JsonGenerator(默认使用Jackson框架)对返回值进行Json序列化
执行完请求后,返回的ModealAndView为null,ServletServerHttpResponse里也已经写入了响应,所以不用关心View的处理
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。
Spring Boot 以 约定大于配置
核心思想开展工作,相比Spring具有如下优势:
SpringBoot开启自动配置的注解是@EnableAutoConfiguration
,启动类上的注解@SpringBootApplication
是一个复合注解,包含了@EnableAutoConfiguration:
EnableAutoConfiguration
只是一个简单的注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector
类
@AutoConfigurationPackage //将main同级的包下的所有组件注册到容器中
@Import({AutoConfigurationImportSelector.class}) //加载自动装配类 xxxAutoconfiguration
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
AutoConfigurationImportSelector
实现了ImportSelector
接口,这个接口的作用就是收集需要导入的配置类,配合@Import()
就可以将相应的类导入到Spring容器中
获取注入类的方法是selectImports(),它实际调用的是getAutoConfigurationEntry
,这个方法是获取自动装配类的关键,主要流程可以分为这么几步:
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
//1.获取到注解的属性
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//2.获取需要自动装配的所有配置类,读取META-INF/spring.factories,获取自动配置类路径
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//3.1.移除重复的配置
configurations = this.removeDuplicates(configurations);
//3.2.处理需要排除的配置
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
知道了自动配置原理,创建一个自定义SpringBoot Starter也很简单。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
编写配置文件
这里定义了属性配置的前缀
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
private String name;
//省略getter、setter
}
自动装配
创建自动配置类HelloPropertiesConfigure
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloPropertiesConfigure {
}
配置自动类
在/resources/META-INF/spring.factories
文件中添加自动配置类路径
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.fighter3.demo.starter.configure.HelloPropertiesConfigure
测试
至此,随手写的一个自定义SpringBoot-Starter就完成了,虽然比较简单,但是完成了主要的自动装配的能力。
创建一个工程,引入自定义starter依赖
<dependency>
<groupId>cn.fighter3groupId>
<artifactId>demo-spring-boot-starterartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
在配置文件里添加配置
hello.name=张三
测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloTest {
@Autowired
HelloProperties helloProperties;
@Test
public void hello(){
System.out.println("你好,"+helloProperties.getName());
}
}
运行结果
SpringApplication 这个类主要做了以下四件事情:
SpringBoot 启动大致流程如下 :
SpringCloud是Spring官方推出的微服务治理框架。
Spring Cloud Alibaba微服务框架:
什么是微服务?
微服务架构主要要解决哪些问题?
有哪些主流微服务框架?
SpringCloud有哪些核心组件?
PS:微服务后面有机会再扩展,其实面试一般都是结合项目去问。
资料来源:面渣逆袭:Spring三十五问,四万字+五十图详解!建议收藏!