注意BeanPostProcessor启动时对依赖Bean的“误伤”陷阱（is not eligible for getting processed by all...）
前言什么是BeanPostProcessor场景模拟关于BeanPostProcessor的加载顺序@Async异步注解失效得原因关于BeanPostProcessorChecker注意避免BeanPostProcessor启动时对依赖的Bean造成误伤前言本篇博文和Spring的上下文启动有较强的关联性，同时需要读者对Spring中的BeanPostProcessor有较为熟悉的了解。若之前没有接触过的同学，建议先点击一下相关阅读的文章列表，先对Spring容器有个大致的了解会效果更佳~这是曾发生在我原公司工作中的一个Spring项目的真实场景案例：简单的描述就是在使用Spring整合@Async、security的时候，出现一个诡异的现象：我把security整合进后原来的@Async就木有生效了，但是如果不把security集成进来的话，就能正常work当时还以为是spring-security的问题，甚至以为是它的bug，现在想起来确实是自己当初图样图森破，切忌不要轻易下结论啊~其实当初我也没找到根本原因，而是通过另外一种集成方式绕过了就继续撸码了。但是我心里一直记着此事，因为我认为一个问题你不知道它根本原因的时候，它就像个定时炸弹，随时可能被引爆。介于此机会，所以此处拿出来跟大家分享分享，避免采坑哈~记忆中唯一线索：BeanPostProcessorChecker这个后置处理器输出了一句 xxx is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)这样的日志~~~~今天突然想起，其实这就是个很大的突破口（因为这句日志一般情况下是不会输出的~）本文就不还原当时的场景了，而是以一个模拟的场景进行讲解、定位问题最后解决问题什么是BeanPostProcessorBeanPostProcessor是Spring的Bean工厂中一个非常重要的钩子，允许Spring框架在新创建Bean实例时对其进行定制化修改。比如我们对Bean内容进行修改、创建代理对象等等~BeanPostProcessor本身也是一个Bean，一般而言其实例化时机要早过普通的Bean，但是BeanPostProcessor有时也会依赖一些Bean，这就导致了一些普通Bean的实例化早于BeanPostProcessor的可能情况，由此如果使用不当，就会造成一些问题场景模拟现在通过我自己构造的一个场景，来模拟当时出现的问题~先看看只使用@Aysnc的现象：@EnableAsync @Configuration public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 这一步千万不能忘了，否则报错： java.lang.IllegalStateException: ThreadPoolTaskExecutor not initialized // 而且最好放在最上面 否则下面set方法对Executor都不会生效 executor.initialize(); executor.setCorePoolSize(10); //核心线程数 executor.setMaxPoolSize(20); //最大线程数 executor.setQueueCapacity(1000); //队列大小 executor.setKeepAliveSeconds(300); //线程最大空闲时间 executor.setThreadNamePrefix("fsx-Executor-"); 指定用于新创建的线程名称的前缀。 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略（一共四种，此处省略） return executor; } // 异常处理器：当然你也可以自定义的，这里我就这么简单写了~~~ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new SimpleAsyncUncaughtExceptionHandler(); } }使用@Async@Service public class HelloServiceImpl implements HelloService { @Async @Override public Object hello() { log.info("当前线程：" + Thread.currentThread().getName()); return "service hello"; } }启动、测试：16:02:58.520 [fsx-Executor-3] INFO com.fsx.service.HelloServiceImpl - 当前线程：fsx-Executor-3可以看到使用的是我们自定义的线程池里面的线程，并且HelloService是个Proxy代理对象了。@Async能够正常work，没毛病老铁接下来加入我再加入一个组件：MyBeanPostProcessor@Slf4j @Component public class MyBeanPostProcessor implements BeanPostProcessor, Ordered { @Autowired private ApplicationContext applicationContext; // 目的：在此BeanPostProcessor初始化的时候，提前把HelloServiceImpl给初始化掉~ // 通过这种方式来模拟：我们的BeanProcessor需要依赖业务的service、dao等情况~ @PostConstruct public void init() { HelloService helloService = applicationContext.getBean(HelloService.class); System.out.println(helloService.getClass()); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }或者这么写，直接使用@Autowired注入属性~~~@Slf4j @Component public class MyBeanPostProcessor implements BeanPostProcessor, Ordered { @Autowired private ApplicationContext applicationContext; @Autowired private HelloService helloService; @PostConstruct public void init() { System.out.println(helloService.getClass()); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }最终启动测试，输出为：启动输出： ... o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'helloServiceImpl' of type [com.fsx.service.HelloServiceImpl] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) class com.fsx.service.HelloServiceImpl ...发现启动的时候，输出了BeanPostProcessorChecker这个Bean检查的处理器的日志而且，而且我们的helloService这个Bean不再是Proxy代理对象了~再次请求执行目标方法看看：14:34:50.164 [http-nio-8080-exec-3] INFO com.fsx.service.HelloServiceImpl - 当前线程：http-nio-8080-exec-3应该能猜到了，它已经不是在我们的异步线程池里面执行了，很显然@Aysnc此时就没有再生效了导致这个现象的原因：就是我们在开发过程中，因为不清楚Spring容器对BeanPostProcessor、Bean的装载顺序，从而导致有时候我们需要提前用到Bean的功能，从而导致启动时的"误伤"。关于BeanPostProcessor的加载顺序可能有的人会有疑问，为什么你这里（MyBeanPostProcessor）能够直接@Autowired，但是我这里为什么得到的是Null呢？其实这里面是有文章可寻的，那就是BeanPostProcessor的加载顺序：Spring容器启动过程，从向容器注册BeanPostProcessor这一步开始说明：registerBeanPostProcessors(beanFactory); public static void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) { // 注意：此处只会拿到Bean的定义信息~~~~ // 已经被实例化的Bean最终都会调用`beanFactory.addBeanPostProcessor`而缓存在AbstractBeanFactory的字段：beanPostProcessors里，它是个CopyOnWriteArrayList // 更重要的是：最终最终所有的BeanPostProcessor的执行都会从这个List里面拿出来执行 // 所以这一步很关键：那就是按照顺序，把`BeanPostProcessor`们都实例化好，然后添加进List里 // 因此顺序是关键~~~~~如果某些Bean提前被实例化，它就很有可能不能被所有的`BeanPostProcessor`处理到了 // 这也是我们BeanPostProcessorChecker的作用，它就是检查这个然后输出日志的~ String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); // 这个beanProcessorTargetCount此处赋值了，后续就都不会变了，BeanPostProcessorChecker就是和这个进行比较的~ // beanFactory里面的Bean实例总个数+1（自己）+bean定义信息~ int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length; // 把BeanPostProcessorChecker加进去，它其实就是做了一个检查而已~~~~~~~输出一个info日志~ beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); // 1、找到所有实现PriorityOrdered的`BeanPostProcessor`，然后getBean，然后统一排序，然后beanFactory.addBeanPostProcessor() // 2、处理实现Ordered的，步骤同上 // 3、处理没实现排序接口的普通的处理器，不需要sort了，直接add进去~ // 最后注册一个特殊的处理器 beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext)); }掌握这个顺序，是我们后续解释上诉现象的根本基础。采用@Autowired注入ApplicationContext和实现接口ApplicationContextAware的区别在绝大多数情况下，这两种使用方式是等价的，都能够方便的获取到Spring容器上下文：ApplicationContext，但是在某些情况下，是有区别的，比如在如下情况下：// 和上面唯一区别是 此处实现的是`PriorityOrdered`接口，上面就是普通的Ordered接口 @Slf4j @Component public class MyBeanPostProcessor implements BeanPostProcessor, PriorityOrdered{ @Autowired private ApplicationContext applicationContext; ... }这样子注入，最终applicationContext的值为null。为何呢？其实这就和BeanPostProcessor的加载时机以及@Autowired的执行时机有关，下面通过两张截图可以清晰的看到顺序：此图表示执行到此处，容器内已经存在的BeanPostProcessor实例（注意已经是实例）：BeanPostProcessor_1此图表示BeanPostProcessor的定义信息们，他们等待被实例化：BeanPostProcessor_2下面对此几个BeanPostProcessor的Bean定义信息做分析如下：AutowiredAnnotationBeanPostProcessor它实现了PriorityOrdered接口CommonAnnotationBeanPostProcessor它也实现了PriorityOrdered接口AsyncAnnotationBeanPostProcessor没有实现任何Orderd排序接口MyBeanPostProcessor此处我们让它实现了PriorityOrdered接口===========从上面可以看到，因为实现了PriorityOrdered接口的BeanPostProcessor属于于同一级别，都是先统一调用getBean()实例化后再被统一addBeanPostProcessor。因此AutowiredAnnotationBeanPostProcessor这个后置处理器并不能作用在我们的MyBeanPostProcessor上面给我们的属性赋值（因为他俩是同一级别的），因为根本就木有生效嘛~~~~但是像我们第一个例子，MyBeanPostProcessor只是实现了普通的Ordered接口，优先级比PriorityOrdered低，所以就实现了正常的注入(因为高优先级的处理器先辈实例化了，所以可以作用于低优先级的bean了)~由上可知，我们在注册BeanPostProcessor的时候，他们的优先级的层级原则是需要注意的：高优先级的Bean能够作用于低有衔接的，反之不成立。但是同优先级的Bean不能相互作用~若是实现ApplicationContextAware接口的话，ApplicationContext不管咋样都可以被正常获取到。道理也是一样的，是因为这个接口是被ApplicationContextAwareProcessor来解析的，而它已经早早被放进了Spring容器里面，所以通过实现接口的方式任何时候都是阔仪的那平时到底使用@Autowired注入还是实现接口ApplicationContextAware呢？建议平时能够使用@Autowired注入时（我更建议使用构造器注入，这样完全不与Spring容器耦合），就尽量使用注入的方式。而不是去实现实现ApplicationContextAware接口的方式，因为这种方式属于与Spring容器强耦合的方式。（当然非常特殊情况下，只能使用ApplicationContextAware，比如上面那个情况）@Async异步注解失效得原因相信到了此处，@Async失效的原因已经不用再详细阐述一遍了。简单的说就是因为HelloService该Bean被提前初始化了，而这个时候AsyncAnnotationBeanPostProcessor根本就还没起作用（因为它仅仅是一个普通的BeanPostProcessor，加载是靠后的），所以肯定也就不能扫描到@Async这种`注解方法，从而就不能生成代理对象，那就自然而然就失效了~想说明的是，本文说明的是一类问题，而不是@Async这一个问题，请大家能够举一反三关于BeanPostProcessorChecker首先它是一个BeanPostProcessor，是PostProcessorRegistrationDelegate的一个private static内部类，它的作用就是在postProcessAfterInitialization里检查作用在此Bean上的BeanPostProcessor够不够数，如果不够数（一般是提前加载了，或者被auto-proxying了这种情况就输出一条info日志~~~） private static final class BeanPostProcessorChecker implements BeanPostProcessor { private static final Log logger = LogFactory.getLog(BeanPostProcessorChecker.class); // 从if条件可以看出哪些是不需要检测的Bean // 1、BeanPostProcessor类型不检测 // 2、ROLE_INFRASTRUCTURE这种类型的Bean不检测 （Spring自己的Bean） @Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (!(bean instanceof BeanPostProcessor) && !isInfrastructureBean(beanName) && this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount) { if (logger.isInfoEnabled()) { logger.info("Bean '" + beanName + "' of type [" + bean.getClass().getName() + "] is not eligible for getting processed by all BeanPostProcessors " + "(for example: not eligible for auto-proxying)"); } } return bean; } private boolean isInfrastructureBean(@Nullable String beanName) { if (beanName != null && this.beanFactory.containsBeanDefinition(beanName)) { BeanDefinition bd = this.beanFactory.getBeanDefinition(beanName); return (bd.getRole() == RootBeanDefinition.ROLE_INFRASTRUCTURE); } return false; } }如果你的bean被提前加载了，那就会看到这么一条日志：Bean 'XXX' of type [XXXX] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)比如我们项目中使用到了Feign面向接口进行远程调用，每次项目启动的时候，就会输出大量的类似日志，如下图：BeanPostProcessor_3Tips：一般的如果你的Config类是一个XXXConfigurer的扩展配置类，也会打印类似的消息：o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'asyncConfig' of type [com.config.AsyncConfig$$EnhancerBySpringCGLIB$$d4dbccee] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)根本原因也是上面说的。具体到代码这里简单分析一下（此处以AsyncConfigurer为例）：// 在对它（AsyncAnnotationBeanPostProcessor）进行实例化的时候（它是个BeanPostProcessor，所以会根据bean定义进行getBean()） // 所以就必须先实例化它@Bean所在的类： @Configuration @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration { @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public AsyncAnnotationBeanPostProcessor asyncAdvisor() { ... } } // 然后它的父类`AbstractAsyncConfiguration`里有个@Autowired方法： @Configuration public abstract class AbstractAsyncConfiguration implements ImportAware { @Autowired(required = false) void setConfigurers(Collection<AsyncConfigurer> configurers) { ... } } // 这个依赖注入就会去容器内找所有的`AsyncConfigurer`的实现，所以就把`AsyncConfig`给提前实例化了 // 所以打印了那么一句日志，不要感觉到惊奇。这也是为何spring这个checker里使用的日志级别是Info，而不是debug，更不是worn。 // 因为它Spring认为这个debug太轻了，但是warn又太重了，因为绝大部分情况下它都不影响程序的正常work~注意避免BeanPostProcessor启动时对依赖的Bean造成误伤BeanPostProcessor实例化时，自动依赖注入根据类型获得需要注入的Bean时，会将某些符合条件的Bean先实例化，如果此FacotryBean又依赖其他普通Bean，会导致该Bean提前启动，造成"误伤"（无法享受部分BeanPostProcessor的后置处理，例如典型的auto-proxy）。本文转载自：https://blog.51cto.com/u_3631118/3121485
Nacos服务注册中心原理总结Nacos服务注册中心原理总结客户端启动时会将当前服务的信息包含ip、端口号、服务名、分组名、集群名等信息封装为一个Instance对象，准备向Nacos服务器注册服务，在注册服务之前，会根据Instance中的信息创建一个BeatInfo对象，然后创建一个定时任务，每隔一段时间向Nacos服务器发送PUT请求并携带相关信息，作为定时心跳连接，服务器端在接收到心跳请求后，会去检查当前服务列表中有没有该实例，如果没有的话将当前服务实例重新注册，注册完成后立即开启一个异步任务，更新客户端实例的最后心跳时间，如果当前实例是非健康状态则将其改为健康状态；​2. 心跳定时任务创建完成后，通过POST请求将当前服务实例信息注册进Nacos服务器，服务器端在接收到注册实例请求后，会将请求携带的数据封装为一个Instance对象，然后为这个服务实例创建一个服务Service，一个Service下可能有多个服务实例，服务在Nacos保存到一个ConcurrentHashMap中，格式为命名空间为key，value为map，分组名和服务名为内层map的key，value为服务数据，Map(namespace,Map(group::serviceName, Service))；​3. 服务创建完成之后，开启一个定时任务（5s），检查当前服务中的各个实例是否在线，如果实例上次心跳时间大于15s就将其状态设置为不健康，如果超出30s，则直接将该实例删除；​4. 然后将当前实例添加到对应服务列表中，这里会通过synchronized锁住当前服务，然后分两种情况向集群中添加实例，如果是持久化数据，则使用基于CP模式的简单Raft协议，通过leader节点将实例数据更新到内存和磁盘文件中，并且通过CountDownLatch实现了一个简单的raft写入数据的逻辑，必须集群半数以上节点写入成功才会给客户端返回成功；​5. 如果是非持久话实例数据，使用的是基于AP模式的Distro协议，首先向任务阻塞队列添加一个本地服务实例改变任务，去更新本地服务列表，然后在遍历集群中所有节点，分别创建数据同步任务放进阻塞队列异步进行集群数据同步，不保证集群节点数据同步完成即可返回；​6. 在将服务实例更新到服务注册表中时，为了防止并发读写冲突，采用的是写时复制的思想，将原注册表数据拷贝一份，添加完成之后再替换回真正的注册表，更新完成之后，通过发布服务变化事件，将服务变动通知给客户端，采用的是UDP通信，客户端接收到UDP消息后会返回一个ACK信号，如果一定时间内服务端没有收到ACK信号，还会尝试重发，当超出重发时间后就不在重发，虽然通过UDP通信不能保证消息的可靠抵达，但是由于Nacos客户端会开启定时任务，每隔一段时间更新客户端缓存的服务列表，通过定时轮询更新服务列表做兜底，所以不用担心数据不会更新的情况，这样既保证了实时性，又保证了数据更新的可靠性；​7. 服务发现：客户端通过定时任务定时从服务端拉取服务数据保存在本地缓存，服务端在发生心跳检测、服务列表变更或者健康状态改变时会触发推送事件，在推送事件中会基于UDP通信将服务列表推送到客户端，同时开启定时任务，每隔10s定时推送数据到客户端。
@Transactional注解的运行原理@Transactional事务动态代理对象创建过程@Transactional事务动态代理对象执行过程事务失效的常见场景@Transactional注解的运行原理首先生成事务类的动态代理对象，在执行动态代理事务类的事务方法时，会执行事务拦截器TransactionInterceptor；接着事务核心组件TransactionAspectSupport开始执行事务方法主流程；首先调用Connection.setAutoCommit(false)方法开启事务，本质是基于jdbc连接开启事务方法；接着调用目标方法invocation.proceedWithInvocation()，依旧采用ReflectiveMethodInvocation方法调用组件调用目标方法；在目标方法执行完成后，判断目标方法是否发生异常；如果发生异常，就调用Connection.rollback()方法回滚事务，本质是基于jdbc连接回滚事务；如果没有发生异常，就调用Connection.commit()方法提交事务，本质是基于jdbc连接提交事务；@Transactional事务动态代理对象创建过程该创建过程和AOP的创建过程一致，只不过事务使用的是BeanFactoryTransactionAttributeSourceAdvisor事务增强advisor来处理事务代理对象；从IOC容器中获取bean；调用bean后置处理器AbstractAutoProxyCreator的postProcessBeforeInstantiation()方法；判断是否指定targetSource，如果指定了targetSource会跳过bean的实例化和初始化流程，也就是会直接创建AOP代理；如果没有指定targetSource，执行正常创建bean的流程，执行bean的初始化，执行initMethod方法；调用bean后置处理器AbstractAutoProxyCreator的postProcessAfterInitialization()方法；执行wrapIfNecessary方法包装需要被代理的bean；获取所有的增强advisors；获取xml方式配置的增强advisors；此处会获取到BeanFactoryTransactionAttributeSourceAdvisor事务增强advisor；获取@Aspect注解配置的增强advisors，调用BeanFactoryAspectJAdvisorsBuilder的buildAspectJAdvisors方法；获取IOC容器中的所有bean，判断是否添加@Aspect注解；通过@Aspect注解配置的增强advisors和事务增强的advisor合并在一起将所有的advisors返回；调用findAdvisorsThatCanApply方法，从所有增强的advisor中找到与当前bean相匹配的增强Advisors；遍历当前bean中所有方法依次与advisor的切点表达式进行匹配;判断目标类不是java自带的类并且不是Orderd接口类型，如果不满足该条件就不处理当前bean；如果满足上面的条件，就获取@Transactional注解中配置的属性（AbstractFallbackTransactionAttributeSource#getTransactionAttribute）；判断目标方法是否是public方法，如果不是，则不处理当前方法；判断目标方法上是否可以获取到@Transactional事务属性，如果可以获取到，就将BeanFactoryTransactionAttributeSourceAdvisor事务增强advisor添加到当前方法的advisors中；如果未从方法上获取事务属性，就尝试从类上获取@Transactional事务属性，如果可以获取到，就将BeanFactoryTransactionAttributeSourceAdvisor事务增强advisor添加到当前方法的advisors中；创建ProxyFactory对象，并设置相关属性；调用createAopProxy方法，获取构造方法默认创建的DefaultAopProxyFactory代理工厂对象，开始选择代理方式并创建代理对象；详细创建流程可以参考Spring AOP 动态代理的创建过程@Transactional事务动态代理对象执行过程调用事务代理对象的事务方法；调用AdvisedSupport#getInterceptorsAndDynamicInterceptionAdvice方法获取拦截器链chain；从methodCache缓存中查找是否存在目标方法对应的拦截器链；如果没有从methodCache缓存中找到对应的拦截器链，从ProxyFactory中获取目标类对应的类级别增强advisors；依次遍历所有的增强advsiors是否与目标方法匹配，该过程与@Transactional事务代理对象创建过程中的匹配类似；判断目标类不是java自带的类并且不是Orderd接口类型，如果不满足该条件就不处理当前bean；如果满足上面的条件，就获取@Transactional注解中配置的属性（AbstractFallbackTransactionAttributeSource#getTransactionAttribute）；判断目标方法是否是public方法，如果不是，则不处理当前方法；判断目标方法上是否可以获取到@Transactional事务属性，如果可以获取到，就获取BeanFactoryTransactionAttributeSourceAdvisor事务增强advisor中的拦截器；如果未从方法上获取事务属性，就尝试从类上获取@Transactional事务属性，如果可以获取到，就获取BeanFactoryTransactionAttributeSourceAdvisor事务增强advisor中的拦截器；调用AbstractBeanFactoryPointcutAdvisor#getAdvice方法获取增强Advice，该方法会最终调用beanFactory.getBean(this.adviceBeanName, Advice.class)来获取拦截器TransactionInterceptor；最终将匹配的Advisor收集到interceptorList，并放到methodCache缓存中，以便下一次从缓存获取；如果拦截器链chain为空，则通过反射调用目标方法，将结果返回；如果拦截器链chain不为空，就将拦截器链chain统一封装为MethodInvocation；调用ReflectiveMethodInvocation#proceed，执行拦截器链；执行拦截器TransactionInterceptor#invoke方法；开启事务；执行目标方法；判断执行目标方法后是否发生异常，如果发生异常，就回滚事务，并抛出该异常；如果没有发生异常，就提交事务；返回执行结果。事务失效的常见场景事务对象没有被Spring管理；事务方法不是public修饰；方法内部调用事务方法，这里直接内部调用，没有经过动态代理对象的增强处理，事务增强处理无法执行；异常在方法内部捕获，且没有抛出；rollbackFor属性配置错误；数据库引擎不支持事务。
JDK代理调用代理类$Proxy的方法时，会调用super.h.invoke方法，该方法实际调用的是JdkDynamicAopProxy#invoke方法；判断当前方法是equals方法且实现类未对equals方法进行重新，如果是，就调用JdkDynamicAopProxy#equals方法，返回执行结果；判断当前方法是hashCode方法且实现类未对hashCode方法进行重新，如果是，就调用JdkDynamicAopProxy#hashCode方法，返回执行结果；cglib代理实际调用的是DynamicAdvisedInterceptor#intercept方法；调用AdvisedSupport#getInterceptorsAndDynamicInterceptionAdvice方法获取拦截器链chain；从methodCache缓存中查找是否存在目标方法对应的拦截器链；如果没有从methodCache缓存中找到对应的拦截器链，从ProxyFactory中获取目标类对应的类级别增强advisors；依次遍历所有的增强advsiors是否与目标方法匹配，该过程与AOP创建过程中的匹配类似；如果是advisor是IntroductionAdvisor引介增强的实现类，那么就进行类级别进行匹配（不常用）；默认的InstantiationModelAwarePointcutAdvisorImpl实现的是普通增强PointcutAdvisor接口，使用切点Pointcut进行方法级别的匹配；先使用切点表达式和目标类进行匹配，确保类级别满足切点表达式；遍历当前bean中的所有方法，使用aspectj提供的方法（SignaturePattern#matchesExactlyMethod），依次对切点表达式对方法进行精确匹配；判断方法参数是否匹配；判断方法名称是否匹配；判断方法抛出异常是否匹配；判断方法返回值是否匹配；调用DefaultAdvisorAdapterRegistry#getInterceptors方法获取advisor中的拦截器；调用InstantiationModelAwarePointcutAdvisorImpl#findAspectJAnnotationMethod方法扫描增强方法上的aspectJ注解；根据apectJ注解的类型创建不同的Advice;@Around -> AspectJAroundAdvice；@Before -> AspectJMethodBeforeAdvice；@After -> AspectJAfterAdvice；@AfterReturning -> AspectJAfterReturningAdvice；@AfterThrowing -> AspectJAfterThrowingAdvice；其中AspectJAfterAdvice、AspectJAroundAdvice、AspectJAfterThrowingAdvice三种Advice都实现了MethodInterceptor接口，因此这三种Advice本身就是拦截器；判断advice是否实现MethodInterceptor接口，如果advice接口MethodInterceptor接口，就将advice添加到interceptors中，此处会处理@Around、@After、@AfterThrowing三种注解；从默认配置的适配器AdvisorAdapter：MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter中循环判断是否支持当前的Advice，如果支持，就将advice包装为拦截器并放入interceptors中，此处会处理@Before、@AfterReturning两种注解；最终将匹配的Advisor收集到interceptorList，并放到methodCache缓存中，以便下一次从缓存获取；如果拦截器链chain为空，则通过反射调用目标方法，将结果返回；如果拦截器链chain不为空，就将拦截器链chain统一封装为MethodInvocation；调用ReflectiveMethodInvocation#proceed，执行拦截器链；执行拦截器ExposeInvocationInterceptor#invoke方法，将拦截器链暴露到ThreadLocal中，同一个线程可以通过ThreadLocal获取当前的拦截器链，继续执行下一个拦截器的invoke方法；执行拦截器AspectJAfterThrowingAdvice#invoke方法，该方法继续执行下一个拦截器的invoke方法，并对invoke方法进行try...catch代码块包裹；执行拦截器AfterReturningAdviceInterceptor#invoke方法，该方法继续执行下一个拦截器的invoke方法，并在invoke方法后执行AfterReturning增强；执行拦截器AspectJAfterAdvice#invoke方法，该方法继续执行下一个拦截器的invoke方法，并finally语句块中执行After增强；执行拦截器AspectJAroundAdvice#invoke方法，该方法会先执行Around前置增强，接着继续执行下一个拦截器的invoke方法，并在方法后执行Around后置增强；执行拦截器MethodBeforeAdviceInterceptor#invoke方法，该方法会先执行Before增强；执行目标方法；拦截器AspectJAroundAdvice执行Around后置增强；拦截器AspectJAfterAdvice在finally语句块中执行After增强;拦截器AfterReturningAdviceInterceptor，如果没有异常发生，就执行AfterReturning增强，并跳过步骤11执行步骤12；拦截器AspectJAfterThrowingAdvice的try...catch代码块如果捕捉到异常并且和@AfterThrowing注解中指定的异常类型一致则执行AfterThrowing增强；拦截器ExposeInvocationInterceptor还原ThreadLocal，停止暴露拦截器链；返回执行结果。
Spring Bean生命周期Spring Bean生命周期调用 getBean(name) 方法来加载name对应的单例对象，这里只分析Spring的单例对象生命周期。转换并解析bean的名称，处理bean别名，获取到真正的bean名称；如果beanName是以&开头的，就返回FactoryBean的工厂对象本身；如果获取的是以FactoryBean工厂自定义创建的实例对象，那么就通过FactoryBean的getObject方法来创建单例对象；从缓存中获取单例bean；先从单例缓存（一级缓存 singletonObjects）中获取；如果一级获取不到，则判断当前bean是否正在创建，如果当前bean正在创建，则从早期单例缓存（二级缓存 earlySingletonObjects）中获取；如果二级缓存获取不到，则判断当前容器是否允许早期引用，如果允许早期引用，则从对象工厂缓存（三级缓存 singletonFactories）获取bean的对象工厂ObjectFactory；调用ObjectFactory的getObject方法创建早期单例bean，并将早期单例添加到早期单例缓存（二级缓存 earlySingletonObjects）中。如果成功获取到单例bean，先尝试通过FactoryBean来实例化bean；判断name是否以&开头，如果单例bean是FactoryBean实例，返回单例bean;判断单例是否是FactoryBean的实例，调用FactoryBean的getObject方法创建单例bean，返回单例bean。未获取到单例bean，开始创建单例bean；检查当前是否存在prototy类型的bean正在创建（prototy类型的bean无法解决循环依赖的问题）；标记beanName对应的bean开始创建；获取bean的BeanDefinition并封装为RootBeanDifition（RootBeanDifition包含父类Bean的定义信息）；注册并提前初始化bean依赖的那些bean；提供一个扩展点，通过后置处理器InstantiationAwareBeanPostProcessor的postProcessBeforeInstantiation方法创建并返回一个代理对象，结束创建流程；记录beanName对应的bean开始创建；正式创建单例bean；解析BeanDifinition中bean属性class；在BeanDifinition中预先标记没有重载的方法；通过反射创建bean的实例；将bean的实例封装到BeanWrapperImpl中；通过后置处理器MergedBeanDefinitionPostProcessor的方法postProcessMergedBeanDefinition用来合并一些信息到BeanDefinition，例如将添加了@Autoired、@Resource等注解信息加入到BeanDefinition中，默认的处理器有：CommonAnnotationBeanPostProcessor、AutowiredAnnotationBeanPostProcessor、InitDestroyAnnotationBeanPostProcessor等；当前bean为单例，设置允许循环且bean正在创建；添加创建早期单例bean的对象工厂ObjectFactory到三级缓存中（对象工厂缓存 singleFactories）；调用后置处理器InstantiationAwareBeanPostProcessor的postProcessAfterInstantiation方法来去修改bean的状态，如果该方法返回false，将会跳过步骤9及步骤10（即为bean填充各种属性值）；通过后置处理器InstantiationAwareBeanPostProcessor的postProcessProperties方法处理bean属性值的扩展点，其中AutowiredAnnotationBeanPostProcessor后置处理器用于处理带有@Autowired和@Value注解的字段，利用反射API来完成属性值的注入；为bean填充各种属性值；调用感知接口的方法（即相关Aware接口），为bean注入容器中的内置对象；在bean初始化之前通过后置处理器BeanPostProcessor的postProcessBeforeInitialization方法调整bean；通过后置处理器InitDestroyAnnotationBeanPostProcessor的postProcessBeforeInitialization方法来执行@PostConstruct注解标注的初始化方法；执行bean中的初始化方法：先判断bean是否实现InitializingBean接口，如果实现则执行afterPropertiesSet方法；执行初始化方法init-method中指定的方法；在bean初始化之后通过后置处理器BeanPostProcessor的postProcessAfterInitialization方法调整bean，其中后置处理器AnnotationAwareAspectJAutoProxyCreator在其抽象父类的父类的父类AbstractAutoProxyCreator中的postProcessAfterInitialization方法中实现了AOP功能；注册DisponsableBeanAdapter在销毁bean时执行方法:先调用后置处理器DestructionAwareBeanPostProcessor的postProcessBeforeDestruction方法来执行bean的销毁处理，默认的后置处理器InitDestroyAnnotationBeanPostProcessor的postProcessBeforeDestruction方法用来执行@PreDestroy注解标注的销毁方法；先判断bean是否实现DisponsableBean接口，如果实现则执行destroy方法；执行销毁方法destory-mehtod中指定的方法；如果是首次创建实例bean，需要将创建好的实例bean添加到单例缓存（一级缓存 singletonObjects）中，删除对象工厂缓存（三级缓存 singletonFactories）和早期单例缓存（二级缓存 earlySingletonObjects）中对应的缓存。并在registeredSingletons中记录已经创建过的bean名称， 防止单例bean的重复创建；检查是否需要转换对象的类型。
ApplicationContext的初始化过程ClassPathXmlApplicationContextAnnotationConfigApplicationContextConfigurationClassPostProcessorpostProcessBeanDefinitionRegistry方法BeanFactoryPostProcessor方法ApplicationContext的初始化过程ClassPathXmlApplicationContextClassPathXmlApplication会接收一个applicationContext.xml（名称自定义）在类路径上描述spring bean的xml文件的构造方法。初始化资源管理器PathMatchingResourcePatternResolver；解析xml路径上的占位符${...}；调用refresh()方法，该方法是高级容器的启动主要方法。初始化容器上下文环境，主要内容为：在上下环境中添加属性值用于替换占位符${...}；检测指定的环境变量是否已经配置。初始化低级容器BeanFactory：创建容器 DefaultListableBeanFactory；定制容器，为容器设置参数：是否允许BeanDefinition被覆盖；是否允许循环依赖引用。将xml文件的解析交给XmlBeanDefinitionReader；解析并注册，XmlBeanFactory的主要执行流程：从classpath路径加载xml的Resource；检验并解析Resource资源得到Document；根据Document解析xml的各种属性标签；将解析到的信息封装到BeanDefinition中；注入到Spring容器beanDefinitionMap中。为BeanFacotry做一些初步的功能扩展：添加对SPEL表达式语言的支持；注册属性编辑器；添加执行器感知接口的后处理器；添加ApplicationContext额外需要忽略的感知接口；为指定接口注册对应的实现类BeanFactoryPostProcessor接口方法的执行；处理参数传进来的beanFactoryPostProcessor；从beanFactory获取所有的BeanDefiitionRegistryPostProcessor（PriorityOrdered接口和Ordered接口进行排序）；从beanFactory中获取所有BeanFactoryPostProcessor（PriorityOrdered接口和Ordered接口进行排序）。注册BeanPostProcessor：从beanFactory中获取所有BeanPostProcessor（PriorityOrdered接口和Ordered接口进行排序）注册到Spring容器中；初始化消息源MessageSource；初始化事件广播器ApplicationEventMulticaster；注册监听器ApplicationListener；初始化非延迟加载的单例；开启Spring的生命周期。AnnotationConfigApplicationContextAnnotationConfigApplicationContext会接收一个扫描的包路径（名称自定义）的构造方法。委托ClassPathBeanDefinitionScanner，将扫描的包路径转换为文件路径格式，例如：classpath*:com/test/**/*.class，扫描出路径下的所有类并转换为资源Resource数组；通过注解过滤器，过滤出标注了@Component的类并封装为注解类型的AnnotatedBeanDefinition接口的实现类AnnotatedGenericBeanDefinition；筛选出符合实例化bean条件的BeanDefinition，放入Set<BeanDefinition> candidates；循环处理每个BeanDefinition：解析@Scope注解的信息，并设置到BeanDefinition中；设置BeanDefinition默认的一些属性值；解析其它注解信息：@Lazy、@Primary、@DependsOn、@Role、@Description；将BeanDefinition封装成BeanDefinitionHolder，注入到Spring容器beanDefinitionMap中；调用refresh()方法，该方法是高级容器的启动主要方法。初始化容器上下文环境；初始化低级容器ConfigurableListableBeanFactory；为BeanFacotry做一些初步的功能扩展：添加对SPEL表达式语言的支持；注册属性编辑器；添加执行器感知接口的后处理器；添加ApplicationContext额外需要忽略的感知接口；为指定接口注册对应的实现类BeanFactoryPostProcessor接口方法的执行（使用ConfigurationClassPostProcessor后置处理器处理@Configuration和@Bean注解，流程在下一节）；处理参数传进来的beanFactoryPostProcessor；从beanFactory获取所有的BeanDefiitionRegistryPostProcessor（PriorityOrdered接口和Ordered接口进行排序）；从beanFactory中获取所有BeanFactoryPostProcessor（PriorityOrdered接口和Ordered接口进行排序）。注册BeanPostProcessor：从beanFactory中获取所有BeanPostProcessor（PriorityOrdered接口和Ordered接口进行排序）注册到Spring容器中；初始化消息源MessageSource；初始化事件广播器ApplicationEventMulticaster；注册监听器ApplicationListener；初始化非延迟加载的单例；开启Spring的生命周期。ConfigurationClassPostProcessorConfigurationClassPostProcessor实现了接口BeanDefinitionRegistryPostProcessor，而BeanDefinitionRegistryPostProcessor接口继承自BeanFactoryPostProcessor，所以有两个关键的方法:BeanFactoryPostProcessor接口的postProcessBeanFactory方法和BeanDefinitionRegistryPostProcessor接口的postProcessBeanDefinitionRegistry方法。postProcessBeanDefinitionRegistry方法获取Spring容器中注册的所有BeanDefinition进行遍历；如果BeanDefinition配置了工厂方法，该BeanDefinition不做处理，实现了工厂方法意味着在工厂方法中定义了实例化对象逻辑，不符合成为注解配置类BeanDefinition的条件；获取BeanDefinition上的注解元数据metadata，将存在注解@Configuration且属性proxyBeanMethods的值为true的BeanDefinition添加到集合configCandidates中；对集合configCandidates根据注解@Order进行排序；初始化注解配置类的解析器ConfigurationClassParser；解析注解配置类中的其它注解，例如：@PropertySources、@ComponentScans、ImportResource等；将解析后的集合对象Set<ConfigurationClass>；遍历集合Set<ConfigurationClass>，对ConfigurationClass中每个添加了@Bean的方法创建一个BeanDefition，在填充一些默认属性后再注册到Spring容器中；同时对这些@Bean对应的BeanDefinition进行重复检查（重复执行步骤3），以处理嵌套注解配置。BeanFactoryPostProcessor方法获取Spring容器中注册的所有BeanDefinition进行遍历；找到存在属性CONFIGURATION_CLASS_ATTRIBUTE且为AbstractBeanDefinition实例的BeanDefinition；初始化增强器ConfigurationClassEnhancer；遍历步骤2找到的BeanDefinition，获取BeanDefinition的类名，使用增强器ConfigurationClassEnhancer对configClass进行增强，得到增强后的enchancedClass；对enchancedClass设置回调接口BeanFactoryAwareMethodInterceptor，为字段$$beanFactory注入当前Spring容器对象；对enchancedClass设置回调接口BeanMethodInterceptor，获取@Bean对应的bean的名称beanName：使用&+beanName获取FactoryBean：未获取到FactoryBean：如果当前方法刚好是@Bean指定的工厂方法，通过@Bean对应的bean的cglib动态代理来执行；如果当前方法不是@Bean指定的工厂方法，从Spring容器获取bean的实例来执行（也就是@Bean方法中自定义创建的bean实例）；获取到FactoryBean：类型或getObject方法存在final关键字修饰，如果@Bean标注的方法返回值是接口类型的，就会通过jdk动态代理方式来生成FactoryBean的动态代理，否则，就会直接返回FactoryBean；如果FactoryBean的类或方法getObject都没有被关键字final修饰，此时会对FactoryBean使用cglib动态代理的方式未FactoryBean生成一个动态代理；不管是jdk动态代理还是cglib动态代理，如果当前bean实例调用的方法名称是getObject，此时会根据bean的名称，从Spring容器中获取bean的实例。
背景解决问题方案方式一方式二背景在项目中，往往需要执行数据库操作后，发送消息或事件来异步调用其他组件执行相应的操作，例如：用户注册后发送激活码；配置修改后发送更新事件等。但是，数据库的操作如果还未完成，此时异步调用的方法查询数据库发现没有数据，这就会出现问题。伪代码如下：void saveUser(User u) { //保存用户信息 userDao.save(u); //触发保存用户事件 applicationContext.publishEvent(new SaveUserEvent(u.getId())); } @EventListener void onSaveUserEvent(SaveUserEvent event) { //获取事件中的信息（用户id） Integer id = event.getEventData(); //查询数据库，获取用户（此时如果用户还未插入数据库，则返回空） User u = userDao.getUserById(id); //这里可能报空指针异常！ String phone = u.getPhoneNumber(); MessageUtils.sendMessage(phone); }解决问题方案解决思路是在事务提交后再做其他的处理（如异步发消息处理等），这里还是从Spring执行事务的过程中入手，Spring事务的处理过程不再分析，这里直接看Spring事务增强器TransactionInterceptor的核心处理流程，源码如下：protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // 获取事务属性 final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass); //加载配置中配置的TransactionManager final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); // 声明式事务的处理 if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; //...... retVal = invocation.proceedWithInvocation(); //...... commitTransactionAfterReturning(txInfo); return retVal; } else { // 编程式事务的处理...... } //...... }这里主要看声明式事务的处理，因为编程式事务的处理及提交都是用户在编码中进行控制。在声明式事务处理中，当方法执行完后，会执行 commitTransactionAfterReturning 方法来进行提交事务，该方法在 TransactionAspectSupport 类中，源码如下：protected void commitTransactionAfterReturning(TransactionInfo txInfo) { if (txInfo != null && txInfo.hasTransaction()) { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } }再看 commit 方法，该方法在 AbstractPlatformTransactionManager 类中，源码如下：public final void commit(TransactionStatus status) throws TransactionException { // 这里省略很多代码，如事务回滚...... processCommit(defStatus); } private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { boolean beforeCompletionInvoked = false; try { prepareForCommit(status); triggerBeforeCommit(status); triggerBeforeCompletion(status); beforeCompletionInvoked = true; boolean globalRollbackOnly = false; if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) { globalRollbackOnly = status.isGlobalRollbackOnly(); } if (status.hasSavepoint()) { status.releaseHeldSavepoint(); } else if (status.isNewTransaction()) { // 提交事务 doCommit(status); } //...... } catch (......) { // 事务异常处理...... } try { // 事务提交成功后的处理-----这里是重点 triggerAfterCommit(status); } finally { triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { cleanupAfterCompletion(status); } } private void triggerAfterCommit(DefaultTransactionStatus status) { if (status.isNewSynchronization()) { TransactionSynchronizationUtils.triggerAfterCommit(); } }最终会走到 TransactionSynchronizationUtils.triggerAfterCommit() 方法中，public static void triggerAfterCommit() { invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations()); } public static void invokeAfterCommit(List<TransactionSynchronization> synchronizations) { if (synchronizations != null) { for (TransactionSynchronization synchronization : synchronizations) { synchronization.afterCommit(); } } }上面会把缓存在 TransactionSynchronizationManager 中的 TransactionSynchronization 按顺序来执行 afterCommit 方法，其中 TransactionSynchronization 以集合形式缓存在 TransactionSynchronizationManager 的 ThreadLocal 中。方式一经过上面分析，只需要代码中重新生成个 TransactionSynchronization 并加入到 TransactionSynchronizationManager 的 TransactionSynchronization 集合中即可，所以有了解决方案，如下：private void writeStatisticsData(Long studentId) { if(TransactionSynchronizationManager.isActualTransactionActive()) { // 当前存在事务 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { executor.execute(() -> {Student student = studentService.findById(studentId); //........ }); }}); } else { // 当前不存在事务 executor.execute(() -> {Student student = studentService.findById(studentId); //........ }); } }方式二使用 @TransactionalEventListener 结合 Spring事件监听机制，该注解自从Spring4.2版本开始有的，如下：// 事件 public class StudentEvent extends ApplicationEvent { public StudentEvent(Long studentId) { super(studentId); } } // 监听器 public class StudentEventListener{ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void writeStatisticsData(StudentEvent studentEvent) { executor.execute(() -> { Student student = studentService.findById(studentEvent.getSource()); //........ }); } } @Service public class StudentService { // Spring4.2之后，ApplicationEventPublisher自动被注入到容器中，采用Autowired即可获取 @Autowired private ApplicationEventPublisher applicationEventPublisher; @Transactional public Long test() { // ...... // 插入记录 Long studentId = studentService.insert(student); // 发布事件 applicationEventPublisher.publishEvent(new StudentEvent(studentId)); // 插入学生地址记录 Long addressId = addressService.insert(address); return studentId; } }原理分析：Spring Bean在加载配置文件时，会使用 AnnotationDrivenBeanDefinitionParser 来解析 annotation-driven 标签，如下：public class TxNamespaceHandler extends NamespaceHandlerSupport { //...... @Override public void init() { registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); } }class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { @Override public BeanDefinition parse(Element element, ParserContext parserContext) { // 重点——将TransactionalEventListenerFactory加入到容器中 registerTransactionalEventListenerFactory(parserContext); String mode = element.getAttribute("mode"); if ("aspectj".equals(mode)) { // mode="aspectj" registerTransactionAspect(element, parserContext); } else { // mode="proxy" AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); } return null; } private void registerTransactionalEventListenerFactory(ParserContext parserContext) { RootBeanDefinition def = new RootBeanDefinition(); def.setBeanClass(TransactionalEventListenerFactory.class); parserContext.registerBeanComponent(new BeanComponentDefinition(def, TransactionManagementConfigUtils.TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME)); } }public class TransactionalEventListenerFactory implements EventListenerFactory, Ordered { //省略部分代码...... @Override public ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) { return new ApplicationListenerMethodTransactionalAdapter(beanName, type, method); } }class ApplicationListenerMethodTransactionalAdapter extends ApplicationListenerMethodAdapter { //省略部分代码...... @Override public void onApplicationEvent(ApplicationEvent event) { if (TransactionSynchronizationManager.isSynchronizationActive()) { // 事务存在时，生成TransactionSynchronization并加入到 TransactionSynchronizationManager的缓存集合中 TransactionSynchronization transactionSynchronization = createTransactionSynchronization(event); TransactionSynchronizationManager.registerSynchronization(transactionSynchronization); } else if (this.annotation.fallbackExecution()) { //....... } processEvent(event); } else { // 当前不存在事务什么也不做 } }上述 @TransactionalEventListener 本质上是一个 @EventListener，TransactionalEventListenerFactory类会将每一个扫描到的方法有TransactionalEventListener注解包装成ApplicationListenerMethodTransactionalAdapter对象，通过ApplicationListenerMethodTransactionalAdapter的onApplicationEvent方法可以看到若当前存在事务，就会生成TransactionSynchronization并加入到 TransactionSynchronizationManager的缓存ThreadLocal集合中，剩余流程同上述分析。(使用 @TransactionalEventListener 结合 Spring事件监听机制，并使用到异步方式感觉有点别扭，这里是为了说明问题)本文内容来自：https://blog.csdn.net/zhuqiuhui/article/details/89299360https://www.jianshu.com/p/6f9cc1384cdf
背景步骤新建索引将旧索引数据copy到新索引同步等待异步执⾏op_type 参数conflicts 配置批次大小配置根据taskId可以实时查看任务的执行状态删除旧索引使用别名通过别名访问新索引背景众所周知，Elasticsearch是⼀个实时的分布式搜索引擎，为⽤户提供搜索服务。当我们决定存储某种数据，在创建索引的时候就需要将数据结构，即Mapping确定下来，于此同时索引的设定和很多固定配置将不能改变。那如果后续业务发生变化，需要改变数据结构或者更换ES更换分词器怎么办呢？为此，Elastic团队提供了很多通过辅助⼯具来帮助开发⼈员进⾏重建索引的方案。 如果对 reindex API 不熟悉，那么在遇到重构的时候，必然事倍功半，效率低下。反之，就可以方便地进行索引重构，省时省力。步骤假设之前我们已经存在一个blog索引，因为更换分词器需要对该索引中的数据进行重建索引，以便支持业务使用新的分词规则搜索数据，并且尽可能使这个变化对外服务没有感知，大概分为以下几个步骤：​新增⼀个索引blog_lastest，Mapping数据结构与blog索引一致将blog数据同步至blog_lastest删除blog索引数据同步后给blog_lastest添加别名blog新建索引在这里推荐一个ES管理工具Kibana，主要针对数据的探索、可视化和分析。kibana_manage.jpgput /blog_lastest/ { "mappings":{ "properties":{ "title":{ "type":"text", "analyzer":"ik_max_word" }, "author":{ "type":"keyword", "fields":{ "seg":{ "type":"text", "analyzer":"ik_max_word" } } } } } }将旧索引数据copy到新索引同步等待接⼝将会在 reindex 结束后返回POST /_reindex { "source": { "index": "blog" }, "dest": { "index": "blog_lastest" } }在 kibana 中的使用如下所示kibana_manage_use.jpg当然高版本（7.1.1）中，ES都有提供对应的Java REST Client，比如ReindexRequest reindexRequest = new ReindexRequest(); reindexRequest.setSourceIndices("blog").setSource.setDestIndex("blog_lastest"); TaskSubmissionResponse taskSubmissionResponse = client.submitReindexTask(reindexRequest, RequestOptions.DEFAULT);为了防止赘述，接下来举例全部以kibana中请求介绍，如果有需要用Java REST Client，可以自行去ES官网查看。异步执⾏如果 reindex 时间过⻓，建议加上 wait_for_completion=false 的参数条件，这样 reindex 将直接返回 taskId。POST /_reindex?wait_for_completion=false { "source": { "index": "blog" }, "dest": { "index": "blog_lastest" } }返回：{ "task" : "dpBihNSMQfSlboMGlTgCBA:4728038" }op_type 参数op_type 参数控制着写入数据的冲突处理方式，如果把 op_type 设置为 create【默认值】，在 _reindex API 中，表示写入时只在 dest index中添加不存在的 doucment，如果相同的 document 已经存在，则会报 version confilct 的错误，那么索引操作就会失败。【这种方式与使用 _create API 时效果一致】POST _reindex { "source": { "index": "blog" }, "dest": { "index": "blog_lastest", "op_type": "create" } }如果这样设置了，也就不存在更新数据的场景了【冲突数据无法写入】，我们也可以把 op_type 设置为 index，表示所有的数据全部重新索引创建。conflicts 配置默认情况下，当发生 version conflict 的时候，_reindex 会被 abort，任务终止【此时数据还没有 reindex 完成】，在返回体中的 failures 指标中会包含冲突的数据【有时候数据会非常多】，除非把 conflicts 设置为 proceed。关于 abort 的说明，如果产生了 abort，已经执行的数据【例如更新写入的】仍然存在于目标索引，此时任务终止，还会有数据没有被执行，也就是漏数了。换句话说，该执行过程不会回滚，只会终止。如果设置了 proceed，任务在检测到数据冲突的情况下，不会终止，会跳过冲突数据继续执行，直到所有数据执行完成，此时不会漏掉正常的数据，只会漏掉有冲突的数据。POST _reindex { "source": { "index": "blog" }, "dest": { "index": "blog_lastest", "op_type": "create" }, "conflicts": "proceed" }我们可以故意把 op_type 设置为 create，人为制造数据冲突的场景，测试时更容易观察到冲突现象。如果把 conflicts 设置为 proceed，在返回体结果中不会再出现 failures 的信息，但是通过 version_conflicts 指标可以看到具体的数量。批次大小配置当你发现reindex的速度有些慢的时候，可以在 query 参数的同一层次【即 source 参数中】添加 size 参数，表示 scroll size 的大小【会影响批次的次数，进而影响整体的速度】，如果不显式设置，默认是一批 1000 条数据，在一开始的简单示例中也看到了。 如下，设置 scroll size 为 5000：POST /_reindex?wait_for_completion=false { "source": { "index": "blog", "size":5000 }, "dest": { "index": "blog_lastest", "op_type": "create" }, "conflicts": "proceed" }测试后，速度达到了 30 分钟 500 万左右，明显提升了很多。根据taskId可以实时查看任务的执行状态一般来说，如果我们的 source index 很大【比如几百万数据量】，则可能需要比较长的时间来完成 _reindex 的工作，可能需要几十分钟。而在此期间不可能一直等待结果返回，可以去做其它事情，如果中途需要查看进度，可以通过 _tasks API 进行查看。GET /_tasks/{taskId}返回：{ "completed" : false, "task" : { "node" : "dpBihNSMQfSlboMGlTgCBA", "id" : 4704218, "type" : "transport", "action" : "indices:data/write/reindex", …… }当执行完毕时，completed为true 查看任务进度以及取消任务，除了根据taskId查看以外，我们还可以通过查看所有的任务中筛选本次reindex的任务。GET _tasks?detailed=true&actions=*reindex返回结果：{ "nodes" : { "dpBihNSMQfSlboMGlTgCBA" : { "name" : "node-16111-9210", "transport_address" : "192.168.XXX.XXX:9310", "host" : "192.168.XXX.XXX", "ip" : "192.168.16.111:9310", "roles" : [ "ingest", "master" ], "attributes" : { "xpack.installed" : "true", "transform.node" : "false" }, "tasks" : { "dpBihNSMQfSlboMGlTgCBA:6629305" : { "node" : "dpBihNSMQfSlboMGlTgCBA", "id" : 6629305, "type" : "transport", "action" : "indices:data/write/reindex", "status" : { "total" : 8361421, "updated" : 0, "created" : 254006, "deleted" : 0, "batches" : 743, "version_conflicts" : 3455994, "noops" : 0, "retries" : { "bulk" : 0, "search" : 0 }, "throttled_millis" : 0, "requests_per_second" : -1.0, "throttled_until_millis" : 0 }, "description" : "reindex from [blog] to [blog_lastest][_doc]", "start_time_in_millis" : 1609338953464, "running_time_in_nanos" : 1276738396689, "cancellable" : true, "headers" : { } } } } } }注意观察里面的几个重要指标，例如从 description 中可以看到任务描述，从 tasks 中可以找到任务的 id【例如 dpBihNSMQfSlboMGlTgCBA:6629305】，从 cancellable 可以判断任务是否支持取消操作。 这个 API 其实就是模糊匹配，同理也可以查询其它类型的任务信息，例如使用 GET _tasks?detailed=true&actions=*byquery 查看查询请求的状态。 当集群的任务太多时我们就可以根据task_id，也就是上面提到GET /_tasks/task_id 方式更加准确地查询指定任务的状态，避免集群的任务过多，不方便查看。 如果遇到操作失误的场景，想取消任务，有没有办法呢？ 当然有啦，虽然覆水难收，通过调用 _tasks API：POST _tasks/task_id/_cancel这里的 task_id 就是通过上面的查询任务接口获取的任务id（任务要支持取消操作，即【cancellable 为 true】时方能生效）。删除旧索引当我们通过 API 查询发现任务完成后，就可以进行后续操作，我这里是要删除旧索引，然后再给新索引起别名，用于替换旧索引，这样才能保证对外服务没有任何感知。DELETE /blog使用别名POST /_aliases { "actions":[ { "add":{ "index":"blog_lastest", "alias":"blog" } } ] }通过别名访问新索引进行过以上操作后，我们可以使用一个简单的搜索验证服务。POST /blog/_search { "query": { "match": { "author": "james" } } }如果搜索结果达到我们的预期目标，至此，数据索引重建迁移完成。本文转载自：https://zhuanlan.zhihu.com/p/341337374
引言静态绑定动态绑定合适的方法属性是动态绑定的吗常见面试问题重写和重载的区别重写（override）重载（overload）静态属性和静态方法能否被继承，能否重写引言在Java方法调用的过程中，JVM是如何知道调用的是哪个类的方法源代码呢？这就涉及到程序绑定这个概念。程序绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对Java来说，绑定分为静态绑定和动态绑定；或者叫做前期绑定和后期绑定。静态绑定在程序执行前方法已经被绑定，此时由编译器或其它连接程序实现。例如：C。针对Java，可以简单地理解为程序编译期的绑定。这里特别说明一点，Java当中的方法只有final，static，private和构造方法是静态绑定。请看如下Java代码：//被调用的类 package hr.test; class Father{ public static void f1(){ System.out.println("Father— f1()"); } } //调用静态方法 import hr.test.Father; public class StaticCall{ public static void main(){ Father.f1(); //调用静态方法 } }上面的源代码中执行方法调用的语句Father.f1()被编译器编译成了一条指令：invokestatic #13。我们看看JVM是如何处理这条指令的：(1) 指令中的#13指的是StaticCall类的常量池中第13个常量表的索引项(关于常量池详见《Class文件内容及常量池 》)。这个常量表(CONSTANT_Methodref_info ) 记录的是方法f1信息的符号引用(包括f1所在的类名，方法名和返回类型)。JVM会首先根据这个符号引用找到方法f1所在的类的全限定名: hr.test.Father。(2) 紧接着JVM会加载、链接和初始化Father类。(3) 然后在Father类所在的方法区中找到f1()方法的直接地址，并将这个直接地址记录到StaticCall类的常量池索引为13的常量表中。这个过程叫常量池解析 ，以后再次调用Father.f1()时，将直接找到f1方法的字节码。(4) 完成了StaticCall类常量池索引项13的常量表的解析之后，JVM就可以调用f1()方法，并开始解释执行f1()方法中的指令了。通过上面的过程，我们发现经过常量池解析之后，JVM就能够确定要调用的f1()方法具体在内存的什么位置上了。实际上，这个信息在编译阶段就已经在StaticCall类的常量池中记录了下来。这种在编译阶段就能够确定调用哪个方法的方式，我们叫做静态绑定机制 。除了被static修饰的静态方法，所有被private修饰的私有方法、被final 修饰的禁止子类覆盖的方法都会被编译成invokestatic指令。另外所有类的初始化方法< init >和< clinit >会被编译成invokespecial指令。JVM会采用静态绑定机制来顺利的调用这些方法。动态绑定在运行时根据具体对象的类型进行绑定。若一种语言实现了后期绑定，同时必须提供一些机制，可在运行期间判断对象的类型，并分别调用适当的方法。也就是说，编译器此时依然不知道对象的类型，但方法调用机制能自己去调查，找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为：它们都要在对象中安插某些特殊类型的信息。看如下Java代码：package hr.test; //被调用的父类 class Father{ public void f1(){ System.out.println("father-f1()"); } public void f1(int i){ System.out.println("father-f1() para-int "+i); } } //被调用的子类 class Son extends Father{ public void f1(){ //覆盖父类的方法 System.out.println("Son-f1()"); } public void f1(char c){ System.out.println("Son-s1() para-char "+c); } } //调用方法 import hr.test.*; public class AutoCall{ public static void main(String[] args){ Father father=new Son(); //多态 father.f1(); //打印结果： Son-f1() } }上面的源代码中有三个重要的概念：多态(polymorphism) 、方法覆盖 、方法重载 。打印的结果大家也都比较清楚，但是JVM是如何知道f.f1()调用的是子类Sun中方法而不是Father中的方法呢？在解释这个问题之前，我们首先简单的讲下JVM管理的一个非常重要的数据结构——方法表 。在JVM加载类的同时，会在方法区中为这个类存放很多信息(详见《Java 虚拟机体系结构 》)。其中就有一个数据结构叫方法表。它以数组的形式记录了当前类及其所有超类的可见方法字节码在内存中的直接地址 。下图是上面源代码中Father和Sun类在方法区中的方法表：method_area_meta.jpg可以看出方法表有两个特点：子类方法表中继承了父类的方法，比如Father extends Object。相同的方法(相同的方法签名：方法名和参数列表)在所有类的方法表中的索引相同。比如Father方法表中的f1()和Son方法表中的- -- f1()都位于各自方法表的第11项中。对于上面的源代码，编译器首先会把main方法编译成下面的字节码指令：0 new hr.test.Son [13] //在堆中开辟一个Son对象的内存空间，并将对象引用压入操作数栈 3 dup 4 invokespecial #7 [15] // 调用初始化方法来初始化堆中的Son对象 7 astore_1 //弹出操作数栈的Son对象引用压入局部变量1中 8 aload_1 //取出局部变量1中的对象引用压入操作数栈 9 invokevirtual #15 //调用f1()方法 12 return其中invokevirtual指令的详细调用过程是这样的：(1) invokevirtual指令中的#15指的是AutoCall类的常量池中第15个常量表的索引项。这个常量表(CONSTANT_Methodref_info ) 记录的是方法f1信息的符号引用(包括f1所在的类名，方法名和返回类型)。JVM会首先根据这个符号引用找到调用方法f1的类的全限定名: hr.test.Father。这是因为调用方法f1的类的对象father声明为Father类型。(2) 在Father类型的方法表中查找方法f1，如果找到，则将方法f1在方法表中的索引项11(如上图)记录到AutoCall类的常量池中第15个常量表中(常量池解析 )。这里有一点要注意：如果Father类型方法表中没有方法f1，那么即使Son类型中方法表有，编译的时候也通过不了。因为调用方法f1的类的对象father的声明为Father类型。(3) 在调用invokevirtual指令前有一个aload_1指令，它会将开始创建在堆中的Son对象的引用压入操作数栈。然后invokevirtual指令会根据这个Son对象的引用首先找到堆中的Son对象，然后进一步找到Son对象所属类型的方法表。过程如下图所示：method_area_meta_method_pointer.jpg(4)这是通过第(2)步中解析完成的#15常量表中的方法表的索引项11，可以定位到Son类型方法表中的方法f1()，然后通过直接地址找到该方法字节码所在的内存空间。很明显，根据对象(father)的声明类型(Father)还不能够确定调用方法f1的位置，必须根据father在堆中实际创建的对象类型Son来确定f1方法所在的位置。这种在程序运行过程中，通过动态创建的对象的方法表来定位方法的方式，我们叫做动态绑定机制 。合适的方法上面的过程很清楚的反映出在方法覆盖的多态调用的情况下，JVM是如何定位到准确的方法的。但是下面的调用方法JVM是如何定位的呢？(仍然使用上面代码中的Father和Son类型)public class AutoCall{ public static void main(String[] args){ Father father=new Son(); char c='a'; father.f1(c); //打印结果：father-f1() para-int 97 } }问题是Fahter类型中并没有方法签名为f1(char)的方法呀。但打印结果显示JVM调用了Father类型中的f1(int)方法，并没有调用到Son类型中的f1(char)方法。根据上面详细阐述的调用过程，首先可以明确的是：JVM首先是根据对象father声明的类型Father来解析常量池的(也就是用Father方法表中的索引项来代替常量池中的符号引用)。如果Father中没有匹配到”合适” 的方法，就无法进行常量池解析，这在编译阶段就通过不了。那么什么叫”合适”的方法呢？当然，方法签名完全一样的方法自然是合适的。但是如果方法中的参数类型在声明的类型中并不能找到呢？比如上面的代码中调用father.f1(char)，Father类型并没有f1(char)的方法签名。实际上，JVM会找到一种“凑合”的办法，就是通过 参数的自动转型 来找 到“合适”的 方法。比如char可以通过自动转型成int，那么Father类中就可以匹配到这个方法了 (关于Java的自动转型问题可以参见《【解惑】Java类型间的转型》)。但是还有一个问题，如果通过自动转型发现可以“凑合”出两个方法的话怎么办？比如下面的代码：class Father{ public void f1(Object o){ System.out.println("Object"); } public void f1(double[] d){ System.out.println("double[]"); } } public class Demo{ public static void main(String[] args) { new Father().f1(null); //打印结果： double[] } }null可以引用于任何的引用类型，那么JVM如何确定“合适”的方法呢。一个很重要的标准就是：如果一个方法可以接受传递给另一个方法的任何参数，那么第一个方法就相对不合适。比如上面的代码: 任何传递给f1(double[])方法的参数都可以传递给f1(Object)方法，而反之却不行，那么f1(double[])方法就更合适。因此JVM就会调用这个更合适的方法。属性是动态绑定的吗首先来看代码：public class Father { protected String name="父亲属性"; public void method() { System.out.println("父类方法，对象类型：" + this.getClass()); } } public class Son extends Father { protected String name="儿子属性"; public void method() { System.out.println("子类方法，对象类型：" + this.getClass()); } public static void main(String[] args) { Father sample = new Son();//向上转型 System.out.println("调用的成员："+sample.name); } }运行结果：调用的成员为父亲的属性这个结果表明，子类的对象(由父类的引用handle)调用到的是父类的成员变量。所以必须明确，运行时（动态）绑定针对的范畴只是对象的方法。现在试图调用子类的成员变量name，该怎么做？最简单的办法是将该成员变量封装成方法getter形式：public class Father { protected String name = "父亲属性"; public String getName() { return name; } public void method() { System.out.println("父类方法，对象类型：" + this.getClass()); } } public class Son extends Father { protected String name="儿子属性"; public String getName() { return name; } public void method() { System.out.println("子类方法，对象类型：" + this.getClass()); } public static void main(String[] args) { Father sample = new Son();//向上转型 System.out.println("调用的成员："+sample.getName()); } }常见面试问题重写和重载的区别重写（override）Java的method overriding则发生在虚方法之间。调用虚方法时，Java采用的是延迟绑定 / 动态分派的语义，根据被调用对象（receiver）的实际类型来决定选择哪个版本的虚方法。一个实例方法可以重写（override）在其超类中可访问到的具有相同签名的所有实例方法，从而使能了动态分派（dynamic dispatch）；换句话说，JVM将基于实例的运行期类型来选择要调用的重写方法。重写是面向对象编程技术的基础，并且是唯一没有被普遍劝阻的名字重用形式：class Base{ public void f(){} } class Derived extends Base{ public void f(){} }重写的特点：覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配，才能达到覆盖的效果；覆盖的方法的返回值必须和被覆盖的方法的返回一致；覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致，或者是其子类；被覆盖的方法不能为private，否则在其子类中只是新定义了一个方法，并没有对其进行覆盖。重载（overload）在某个类中的方法可以重载（overload）另一个方法，只要它们具有相同的名字和不同的签名。由调用所指定的重载方法是在编译期选定的：class CircuitBreaker{ public void f (int i){} //int overloading public void f(String s){} //String overloading }有一个值得注意的地方：所谓的重载是静态绑定的也只是指在编译期f (int i)和f(String s)的方法签名不同，运行期我们知道调用的是f (int i)还是f(String s)。但是调用的是哪个类的方法还要根据实例对象去判断。例如如下代码：public class Test { public static void main(String[] args) { Animal animal = new Animal(); animal.eat(); animal.eat(1); animal.eat("1"); } } class Animal { public void eat() { } public void eat(int a) { } public void eat(String a) { } }对应的字节码：public class com.example.tsnt.Test { public com.example.tsnt.Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class com/example/tsnt/Animal 3: dup 4: invokespecial #3 // Method com/example/tsnt/Animal."<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method com/example/tsnt/Animal.eat:()V 12: aload_1 13: iconst_1 14: invokevirtual #5 // Method com/example/tsnt/Animal.eat:(I)V 17: aload_1 18: ldc #6 // String 1 20: invokevirtual #7 // Method com/example/tsnt/Animal.eat:(Ljava/lang/String;)V 23: return }可以看到其中这几行： 9: invokevirtual #4 // Method com/example/tsnt/Animal.eat:()V 14: invokevirtual #5 // Method com/example/tsnt/Animal.eat:(I)V 20: invokevirtual #7 // Method com/example/tsnt/Animal.eat:(Ljava/lang/String;)V对于调用哪个重载的方法，编译阶段已经确定下来了，但是对于调用哪个类的方法编译阶段没有确定下来，所以显示的是invokevirtual，这个要等到运行时才能确定。重载的特点：在使用重载时只能通过不同的参数样式。例如，不同的参数类型，不同的参数个数，不同的参数顺序（当然，同一方法内的几个参数类型必须不一样，例如可以是fun(int, float)， 但是不能为fun(int, int)）；不能通过访问权限、返回类型、抛出的异常进行重载；方法的异常类型和数目不会对重载造成影响；对于继承来说，如果某一方法在父类中是访问权限是priavte，那么就不能在子类对其进行重载，如果定义的话，也只是定义了一个新方法，而不会达到重载的效果。静态属性和静态方法能否被继承，能否重写静态方法和属性是属于类的，调用的时候直接通过类名.方法名完成的，不需继承机制就可以调用如果子类里面定义了和父类声明一样的静态方法和属性，那么这时候父类的静态方法或属性称之为“隐藏”，你如果想要调用父类的静态方法和属性，直接通过父类名.方法名或变量名完成，至于是否继承一说，子类是有继承静态方法和属性，但是跟实例方法和属性不太一样，存在“隐藏”的这种情况。多态之所以能够实现是依赖于继承和重写 、重载（继承和重写最为关键）。有了继承和重写就可以 实现父类的引用可以指向不同子类的对象。重写的功能是：“重写”后子类的优先级要高于父类的优先级，但是“隐藏”是没有这个优先级之分的。结论：静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能够被重写，因此不能实现多态，不能实现父类的引用可以指向不同子类的对象。非静态的方法可以被继承和重写，因此可以实现多态。附：查看字节码的方法：查看Java字节码的方法参考：java中静态属性和和静态方法的继承问题 以及多态的实质java的动态绑定与静态绑定【解惑】Java动态绑定机制的内幕Java的函数重载为什么采取静态静态绑定而非动态绑定？本文转载自：https://blog.csdn.net/sted_zxz/article/details/77980124