熟练掌握spring框架第三篇

接上篇【熟练掌握spring框架第二篇】

bean的生命周期

参考:http://javainsimpleway.com/spring-bean-life-cycle/

这是一个比较基础但是又比较高频的面试题。如果面试官问你spring bean的生命周期都有哪些?那应该怎样回答呢?在回答之前可以先分析一下这个题目。首先想想面试官问这个问题的目的是什么?换位思考,如果我是面试官,我希望通过这个题目了解求职者对spring框架的了解程度,它是如何管理bean的。在整个bean对生命周期中都有哪些是我们可以参与的。常用的场景是什么?不同类型的bean的生命周期有什么不同吗?如果求职者这几个问题都能清楚的表示出来,那我认为这道面试题他pass了。学习bean的生命周期目的还是为了在实际工作中可以进行自由扩展。以满足业务需要。那下面就从这几个方面分析下bean的生命周期。

先看下下面这张图,来源:http://javainsimpleway.com/spring-bean-life-cycle/

img
  1. 首先实例化bean
  2. populateBean
  3. 调用初始化方法之前首先调用所有bean有感知的方法,包括BeanNameAwareBeanClassLoaderAwareBeanFactoryAware
  4. 然后执行BeanPostProcessorpostProcessBeforeInitialization
  5. 执行初始化方法,如果bean实现了InitializingBean会调用他的afterPropertiesSet方法。比如之前提到的RepositoryFactoryBeanSupport就通过afterPropertiesSet进行repository的创建。
  6. 反射调用自定义init-method方法。
  7. 然后执行BeanPostProcessorpostProcessAfterInitialization

其中当执行到ApplicationContextAwareProcessorpostProcessBeforeInitialization时,调用bean的应用级的有感知的方法。比如ApplicationContextAwareEnvironmentAware这些。

我们熟悉的BeanPostProcessor还有AutowiredAnnotationBeanPostProcessor,用来进行属性自动装配。

RequiredAnnotationBeanPostProcessor,它可以确保声明"必需"属性的bean实际上已配置了值,否则就会爆出类似下面这样的错误

image-20210506213820813

CommonAnnotationBeanPostProcessor处理@PostConstruct@PreDestroy,执行@PostConstruct的逻辑是在它的父类InitDestroyAnnotationBeanPostProcessorpostProcessBeforeInitialization里进行的。执行@PreDestroy的逻辑是在InitDestroyAnnotationBeanPostProcessorpostProcessBeforeDestruction里进行的。

所以@PostConstruct执行的时候,bean的属性已经装填完成了。并且只会被执行一次,可以执行一些需要依赖项的初始化工作。

@PreDestroy的原理是利用了jdk的shutdown hook,可以实现应用程序的优雅关闭。注意shutdown hook不应该执行耗时的操作,这样会导致程序不能正常退出。一般运维写脚本的时候都会设置一个超时时间,一旦超过,就使用kill -9强制退出。

Spring管理的Bean默认是单例的。bean的所有scope有如下这些

image-20210507120640098

来源:spring官方文档

request session application 只存在于web应用上下文中。websocket存在websocket环境中。这些本文不做详细描述,singleton详细读者已经很熟悉了,那么我们着重关注下prototype这个类型。

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class A {  
}

定义一个简单的类,声明为scope为prototype。spring启动后调用applicationContext.getBean("a"),代码流程大致如下。

  1. 调用AbstractBeanFactorydoGetBean方法
  2. 判断如果原型bean正在创建则直接抛出异常。
  3. 拿到相应的BeanDefinition,判断如果是Prototype类型
  4. 调用beforePrototypeCreation标记正在创建
  5. createBean创建bean,和创建单例bean是同一个方法。
  6. 调用afterPrototypeCreation清除标记

所以prototype类型的bean是不支持循环依赖的。另外由于和创建singletonbean是同一个方法,所以bean的所有有感知的方法也都是差不多的。一个很重要的不同就是原型bean@PreDestroy是不会执行的。原因很简单destroy方法是通过shutdownhook调用beanFactorydestroySingletons方法实现的。spring没有定义prototypebean的销毁动作。

更多详细的解释可以参考:https://bluebreeze0812.github.io/learn/2019/10/17/Spring-Destroy-Prototype-Beans/

spring 动态代理与AOP

代理模式

image-20210507160352635

代理模式是GoF 23种Java常用设计模式之一,隶属于结构型模式。一个随处可见的应用场景就是rpc框架比如dubbo里面的service调用。本地调用的service实际上是远程对象的代理对象。调用代理对象的方法实际是调用了远程对象的方法。又比如 JAVA RMI ,当然了对远程代理这里不做过多描述。今天我们要讲的是spring的动态代理。众所周知,Spring代理实际上是对JDK代理CGLIB代理做了一层封装。那么我们先来看下jdk和cglib代理。这也是烹饪spring aop这道大菜比不可少的佐料。

JDK动态代理

public class JdkProxyDemo {
    public interface Calculator {
        int add(int a, int b);
        int subtract(int a, int b);
    }
    public static class CalculatorImpl implements Calculator {
        @Override
        public int add(int a, int b) {
            return a + b;
        }
        @Override
        public int subtract(int a, int b) {
            return a - b;
        }
    }
    public static class ProxyFactory implements InvocationHandler {
        private final Calculator real;
        public ProxyFactory(Calculator real) {
            this.real = real;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("before");
            Object result = method.invoke(real, args);
            System.out.println("after");
            return result;
        }
    }
    public static void main(String[] args) {
        Calculator real = new CalculatorImpl();
        ProxyFactory proxyFactory = new ProxyFactory(real);
        Calculator proxy = (Calculator) Proxy.newProxyInstance(real.getClass().getClassLoader(), new Class[]{Calculator.class}, proxyFactory);
        System.out.println(proxy.add(1, 2));
        System.out.println(proxy.subtract(2, 1));
    }
}

由上面这个简单的例子可以总结出jdk动态代理有如下特点。

  1. 创建代理对象需要三要素:类加载器,代理对象需要实现的接口列表。InvocationHandler实例。
image-20210507183624908
  1. 代理对象的class是com.sun.proxy.$Proxy0实现了Calculator接口
  2. 代理对象持有InvocationHandler实例的引用,而InvocationHandler持有被代理对象的引用。
  3. InvocationHandler的invoke方法代理了接口的所有方法。你可以在被代理对象执行前后添加逻辑,你甚至不调用代理对象的方法都可以。
  4. 代理对象需要实现的接口列表是必须的。这也是jdk动态代理最大的特点。代理对象和被代理对象都实现了共同的接口。否则是无法代理的。

cglib动态代理

字节码生成类库,它封装了ASM,它是一个字节码操作框架,类似的框架还有javaassit,大概原理就是解析.class文件然后动态修改它。

public class CglibProxyDemo {
    public static class Calculator {
        public int add(int a, int b) {
            return a + b;
        }
    }
    public static class CalculatorInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("before add");
            Object o1 = methodProxy.invokeSuper(o, objects);
            System.out.println("after add");
            return o1;
        }
    }
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Calculator.class);
        enhancer.setCallback(new CalculatorInterceptor());
        Calculator calculator = (Calculator) enhancer.create();
        System.out.println(calculator.add(1, 2));
    }
}

由上面这个简单的例子我们可以总结出cglib动态代理有如下特点:

  1. 生成的代理对象的class是com.family.spring.core.CglibProxyDemo$Calculator$$EnhancerByCGLIB$$b4da3734
  2. 它是Calculator类的子类。遵循继承规则,子类不能覆盖父类的私有方法。也就是说私有方法是不能被代理的。
  3. MethodInterceptor定义了一个方法拦截器。这个拦截器会拦截代理类的所有可以代理的方法。你也可以决定是否调用父类真实的方法。
  4. cglib代理和jdk代理有两个很重要的区别,第一就是不需要共同的接口,第二不需要准备一个被代理的对象。

如果读者对于代理的class结构到底是什么样感兴趣的话。也可以使用java代理技术读取jvm里面相应的class文件,进行分析。

spring动态代理

为什么需要AOP

软件开发是一个演变的过程,从最初的POP(面向过程程序设计)到OOP(面向对象程序设计)再到AOP(面向切面编程),未来可能还有一堆的OP,每种编程思想都是软件开发进化的产物。都是为了解决特定的问题应运而生的。那么AOP产生的背景是什么呢。我认为随着软件系统的复杂化,一些与核心业务逻辑无关的内容越来越多。比如:记录日志,权限验证,事务控制,错误信息检测。而这些逻辑又散落在程序的每一个地方。这样不仅会增加写代码的复杂性和工作量,还会大大增加代码的维护成本。比如权限验证,如果每个接口都手写代码去判断当前用户是否有该接口的访问权限的话,那真的很蛋疼。所以聪明的程序员们就想把这些代码放到同一个地方,然后采取动态植入的方式添加到业务代码执行前后,这样代码统一起来了,而且业务逻辑里面几乎看不到添加的代码,程序员就可以专心致志的进行CRUD了,这种设计思想有个高大上的名字就是AOP,英文全称是Aspect Oriented Programming,维基百科管这个叫编程范式。为了让这个设计理念更加专业化,还特地引入一堆的专业术语。下面就简单阐述下每个术语的含义。

术语 含义
通知 Advice 类似于前面说的权限验证,springaop支持的通知有:前置通知,后置通知,异常通知,最终通知,环绕通知五种
连接点 JoinPoint 就是允许使用通知的地方,比如说方法连接点(方法执行前后),异常连接点(抛出异常时)等
切点 Pointcut 织入通知的连接点就叫做切点。
切面 Aspect 切面就是通知和切点的结合,两者组合一起定义了切面三要素:要做什么何时做何地做
织入 weaving 把切面应用到目标对象来创建新的代理对象的过程

有了上面的概念理解,我们对spring aop仍然是理论层面的。那么他的实现是怎样的呢。下面就以一个简单的例子一探究竟。
核心代码:

@Aspect
@Component
public class MonitorAspect {
    @Pointcut("execution(* com.family.spring.core..*.*(..))  ")
    public void pointCut() {
    }
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object result = pjp.proceed();
        stopWatch.stop();
        System.out.println("执行" + pjp.getSignature().getName() + "共花费了" + stopWatch.getTotalTimeMillis() + "毫秒");
        return result;
    }
}
@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringAopDemoApplication implements ApplicationRunner {
    @Autowired
    private ApplicationContext applicationContext;
    public static void main(String[] args) {
        SpringApplication.run(SpringAopDemoApplication.class, args);
    }
    @Override
    public void run(ApplicationArguments args) throws Exception {
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.login();
        userService.register();
    }
}
//userService很简单,就定义了两个方法: login register

程序输出是这样的:

执行login共花费了1000毫秒
执行register共花费了2000毫秒
执行run共花费了3009毫秒

分析:getBean拿到的userService肯定是代理之后的对象。那它是什么时候被代理的呢。debug发现在执行bean的初始化时,会调用所有的BeanPostProcessor逐个处理。其中有一个特别的Processor是:AnnotationAwareAspectJAutoProxyCreator,而这个processor就是@EnableAspectJAutoProxy引入的。打开注解 @EnableAspectJAutoProxy的源码发现,它的核心是导入了一个AspectJAutoProxyRegistrar(AspectJ自动代理登记员)的类。而这个类的作用就是往注册中心注册AnnotationAwareAspectJAutoProxyCreator这个BeanPostProcessor。是不是和之前说的@EnableJpaRepositories 如出一辙。线索找到了,接下来就是解刨它的postProcessAfterInitialization方法了。

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
}
//wrapIfNecessary就是用来生成代理对象的。

继续跟进,终于找到了进行对象代理的罪魁祸首了。就是我们的ProxyFactory

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
  if (shouldProxyTargetClass(beanClass, beanName)) {
    proxyFactory.setProxyTargetClass(true);
  }
  else {
    evaluateProxyInterfaces(beanClass, proxyFactory);
  }
}

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
  proxyFactory.setPreFiltered(true);
}

return proxyFactory.getProxy(getProxyClassLoader());

这是spring对jdk和cglib动态代理的一个封装类。它的getProxy里的createAopProxy方法是这样的。

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (!IN_NATIVE_IMAGE &&
                (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                        "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
}

翻译成自然语言就是optimizeproxyTargetClass,被代理的类没有接口这三个条件其中任何一个成立,就有机会走cglib动态代理,否则都是走jdk动态代理。另外就算判断有机会走cglib的话,如果目标类是接口还是会走jdk动态代理。下面看下sping aop中关于切面的抽象

image-20210508145708694

使用ProxyFactory代理对象,是必须要添加通知的。如果没有通知就好比代理对象收了钱,但是啥事也没干。一种简单的添加方式是,传入一个MethodInterceptor,实现拦截。

proxyFactory.addAdvice((MethodInterceptor) invocation -> {
      System.out.println("before");
      Object result = invocation.proceed();
      System.out.println("after");
      return result;
});

但是更高级的方式就是添加Advisor,可以翻译为顾问,让顾问告诉我通知是什么?spring内置了一个强大的顾问,名为InstantiationModelAwarePointcutAdvisorImpl,它的getAdvice方法,可以动态的返回不同类型的通知。详见:ReflectiveAspectJAdvisorFactorygetAdvice方法。前面说的那个BeanPostProcessor正是添加了这个顾问实现了环绕通知。

未完待续,更多内容请关注【熟练掌握spring框架】第四篇

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容