熟练掌握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框架】第四篇

wei_xin_tail

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值