程序设计

Spring AOP 处理流程概述

Spring AOP 处理流程概述

D瓜哥
AOP 是 Spring 框架的最核心的两个功能之一,在前面的 Spring 启动流程概述 和 Spring Bean 生命周期概述 两篇文章中,分别介绍了 Spring 启动过程和 Spring Bean 的生命周期,对 IoC 有了一个细致介绍。这里来细致分析一下 Spring AOP 的实现原理和处理流程。 基本概念 先来了解几个基本概念,D瓜哥私以为这些概念是 AOP 中最核心的内容,了解了基本概念,可以说基本上掌握了一半的 AOP 内容。 学习概念最权威的地方,当然就是官方文档。所以,这些概念可以在 Spring Framework Documentation: AOP Concepts 中看到最权威的介绍。 Join point(连接点): 所谓的连接点是指那些被拦截到的点。在 Spring 中,连接点指的是方法,因为 Spring 只支持方法类型的连接点。在 Spring 中,使用 Pointcut(切入点): 所谓的切入点,是指要对哪些 Join point(连接点) 进行拦截的定义。如果 Join point(连接点) 是全集,那么 Pointcut(切入点) 就是被选中的子集。写 AOP 代码的时候,一般是用 Pointcut(切入点) 表达式进行对 Join point(连接点) 进行选择。 Advice(通知/增强): 所谓的通知就是指拦截到 Join point(连接点) 之后所要做的事情。通知根据作用位置不同,又细分为: Before advice(前置通知): 在 Join point(连接点) 之前运行的通知。这种通知,不能阻止执行流程继续到 Join point(连接点)。
Spring Bean 生命周期概述

Spring Bean 生命周期概述

D瓜哥
在 Spring 启动流程概述 中,分析了 Spring 的启动流程。本文就来说明一下 Spring Bean 整个生命周期。如果有不清楚的地方,可以参考上文的“附录:启动日志”。 直接上图:Spring Bean 生命周期流程图。内容较多,图片文字偏小,请放大看(矢量图,可以任意放大): Figure 1. Spring Bean 生命周期流程图 下面是文字说明。 Bean 生命周期简述 调用 InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation,主要是判断 AnnotationAwareAspectJAutoProxyCreator 是否可以生成代理。 调用构造函数 调用 MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition,主要是通过 CommonAnnotationBeanPostProcessor、 AutowiredAnnotationBeanPostProcessor 收集依赖信息。 InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation,这步什么也没做。 调用 InstantiationAwareBeanPostProcessor#postProcessProperties,主要是完成依赖注入。 调用 AutowiredAnnotationBeanPostProcessor#setBeanFactory,注入 BeanFactory 等相关信息。 调用 BeanPostProcessor#postProcessBeforeInitialization,主要是注入 ApplicationContext 等相关信息。 调用 InitializingBean#afterPropertiesSet、 init-method 方法 调用 BeanPostProcessor#postProcessAfterInitialization,主要是生成 AOP 代理类。 Bean 生命周期详解 从 getBean() 方法获取 Bean 时,如果缓存中没有对应的 Bean,则会创建 Bean,整个流程如下: InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation — 目前有如下四个: ImportAwareBeanPostProcessor — 继承父类实现,无所事事。 AnnotationAwareAspectJAutoProxyCreator — 继承父类实现,判断是否属于基础切面类,如果有指定的 Target 则生成代理。
Spring 启动流程概述

Spring 启动流程概述

D瓜哥
对于 Spring 启动流程和 Bean 的生命周期,总有一些小地方搞的不是很清楚,干脆直接通过修改代码增加日志输出,使用断点单步调试,把整个流程捋顺了一点点的。 除了加载配置文件或者基础配置类外,Spring 的启动过程几乎都被封装在 AbstractApplicationContext#refresh 方法中,可以说弄清楚了这个方法的执行过程,就摸清楚了 Spring 启动全流程,下面的流程分析也是以这个方法为骨架来展开的。 流程概要 下面完整流程有些太复杂,所以,提炼一个简要的过程,方便糊弄面试官,哈哈哈😆 创建容器,读取 applicationContext.register(Config.class) 指定的配置。 准备 BeanFactory,注册容器本身和 BeanFactory 实例,以及注册环境配置信息等。 执行 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 注册 BeanDefinition。有三点需要注意: 目前只有一个 ConfigurationClassPostProcessor 实现类,Spring 中大量的 Bean 都是在这一步被该类注册到容器中的。 执行顺序是 ① PriorityOrdered ② Ordered ③ 普通的顺序来执行 在执行上一步时,如果发现注册了 BeanDefinitionRegistryPostProcessor 类型的 Bean,就会在循环里继续调用 postProcessBeanDefinitionRegistry 方法。MyBATIS 和 Spring 整合的 MapperScannerConfigurer 类就是在这一步执行的。 执行 BeanFactoryPostProcessor#postProcessBeanFactory 方法。目前只有一个 ConfigurationClassPostProcessor 实现类。 注册 CommonAnnotationBeanPostProcessor 和 AutowiredAnnotationBeanPostProcessor 为 BeanPostProcessor。 注册 ApplicationEventMulticaster,用于广播事件的。 注册 ApplicationListener 预加载以及注册所有非懒加载的 Bean 启动时序图 Spring 启动流程的时序图如下: Figure 1.
Spring 扩展点实践:整合 MyBATIS

Spring 扩展点实践:整合 MyBATIS

D瓜哥
在上一篇文章 Spring 扩展点概览及实践 中介绍了 Spring 内部存在的扩展点。学以致用,现在来分析一下 Spring 与 MyBATIS 的整合流程。 示例程序 为了方便分析源码,先根据官方文档 mybatis-spring – MyBatis-Spring | Getting Started 搭建起一个简单实例。 数据库方面,直接使用功能了 MySQL 示例数据库: MySQL : Employees Sample Database,需要的话,自行下载。 package com.diguage.truman.mybatis; import com.mysql.cj.jdbc.Driver; import com.zaxxer.hikari.HikariDataSource; import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.Test; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; /** * @author D瓜哥, https://www.diguage.com/ * @since 2020-05-29 17:11 */ public class MybatisTest { @Test public void test() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.
Spring 扩展点概览及实践

Spring 扩展点概览及实践

D瓜哥
学习 Spring 代码,最重要的是掌握 Spring 有哪些扩展点,可以利用这些扩展点对 Spring 做什么扩展操作。说得更具体一点,如果自己开发一个框架,如何与 Spring 进行整合,如果对 Spring 的扩展点有一个比较清晰的认识,势必会事半功倍。 @Import 先来看一下 @Import 注解的定义: @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Import { /** * {@link Configuration @Configuration}, {@link ImportSelector}, * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import. */ Class<?>[] value(); } 从声明可以看出,使用时,只需要指定 Class 实例即可;从方法的文档中可以看出,Class 实例可以分为三种:ImportSelector、ImportBeanDefinitionRegistrar 和常规组件类。示例如下: @Configuration @Import(LogImportSelector.class) public static class Config { } 在 org.springframework.context.annotation.ConfigurationClassParser#processImports 方法中,集中了对 @Import 注解的处理。从代码可以非常清晰地看出,分了三种情况进行处理: ImportSelector ImportBeanDefinitionRegistrar 常规组件 Class 下面分别对其进行介绍。 ImportSelector 先来看一下 ImportSelector 接口的定义:
HikariCP 源码分析 --  ConcurrentBag

HikariCP 源码分析 -- ConcurrentBag

D瓜哥
以前无意间搜资料了解到 HikariCP,一下子就被它的简洁代码和卓越性能吸引住了。以前也有翻过它的代码,但是不是很系统,最近再次翻阅,正好做些笔记,方便以后学习。 D瓜哥最近在学习 Java 并发知识。那就从 HikariCP 自定义的并发集合 ConcurrentBag 开始学习。 在 HikariCP 的 Wiki 中,有 Down the Rabbit Hole · ConcurrentBag 的章节来专门介绍 ConcurrentBag: ConcurrentBag 的灵感借鉴自 C# .NET 的 ConcurrentBag 类。但是实现却是完全不同的。这里的 ConcurrentBag 有如下特性: A lock-free design ThreadLocal caching Queue-stealing Direct hand-off optimizations 下面,通过代码来对此做个说明。 在 ConcurrentBag 类的定义中,声明了集合元素必须是 IConcurrentBagEntry 的子类。先来看看这个接口的定义: public interface IConcurrentBagEntry { int STATE_NOT_IN_USE = 0; int STATE_IN_USE = 1; int STATE_REMOVED = -1; int STATE_RESERVED = -2; boolean compareAndSet(int expectState, int newState); void setState(int newState); int getState(); } 接下来,看一下成员变量:

推荐几本 Java 并发编程的书

D瓜哥
最近,D瓜哥的一个小伙伴向我抱怨,Java 并发是个大坑,问我怎么看?我回答,当然是用眼睛看啊… D瓜哥觉得,想学好 Java 并发,最重要的还是啃书。幸运的是,Java 中还是有不少关于并发的优秀书籍可以看。正好利用这个机会,把看过的、个人认为还不错的书推荐一波。没有看过的就不多言了。 Java并发编程实战 如果只选一本书来深入研究并发,那肯定是这本书。 Java并发编程实战 (豆瓣) — 这本书是必看的。JDK 中 JUC 就是这本书的作者们写的。虽然书名含有 Java 一次,但是,里面更多是原理性的东西,各种语言都适用。只是例子少了一些。这本书需要多读几遍。(据说翻译不行,推荐看英文版) 放个英文版图片镇楼: Java并发编程的艺术 Java并发编程的艺术 (豆瓣) — 这本书也不错,讲了很多源码方面的内容,非常棒。另外,在讲解 Double Lock 方面的知识时,涉及了很多 Java Memory Model 方面的知识,可以先看看 深入理解Java虚拟机(第3版)(豆瓣) 最后两章的内容,来提前补充一下这么方面的知识。 实战Java高并发程序设计 实战Java高并发程序设计(第2版) (豆瓣) — 这本书也不错,针对 Java 8 写的,Java 8 中的很多新知识都有涉猎,例子也很全面。广度和深度,得到了兼顾,非常棒。 Java编程思想 Java编程思想(第4版)(豆瓣) — 虽然这本书已经出来十余年了,但是依然经典。第 21 章 并发,用大量的例子和陈述来介绍并发。非常棒。美中不足,是针对 Java 5 编写的,现在已经 Java 8 了。不过,作者又出了一本书,可以理解成升级版。 On Java 8 On Java 8 (豆瓣) — 这是《Java编程思想》的姊妹版和升级版。Bruce Eckel 的写书功底和对语言的理解毋庸置疑。目前中文版还没有正式版,网上已经有热心网友做起来搬运工,感兴趣自行 Google。 Java 9 并发编程实战 Java 9 并发编程实战 (豆瓣) — 入门的话,这本书是不错的选择。每个特性一个例子,整本书大概 80% 的篇幅都是代码。所以,一定也不用担心有读书压力。

动态规划入门

D瓜哥
本篇文章是 D瓜哥 读《算法导论》的读书笔记。记录下来是为了方便整理思路,以便啃下“动态规划”这块骨头。 目前侧重记录书中关于“动态规划原理”的介绍。接下来会把书中的例子结合 Java 代码演绎一遍。后续会根据D瓜哥的学习和理解,逐步完善。最终希望达到通过这一篇文章,就能学会、理解动态规划。 山高水远,道阻且长,愿一起努力! — 2020年01月23日 动态规划(dynamic programming)与分治方法相似,都是通过组合子问题的解来求解原问题(在这里,“programming”指的是一种表格法,并非编写计算机程序)。 分治方法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。 动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。 在这种情况下,分治算法会做许多不必要的工作,它会反复地求解那些公共子问题。 动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。 动态规划方法通常用来求解最优化问题(optimization problem)。 设计一个动态规划算法的步骤: 刻画一个最优解的结构特征。 递归地定义最优解的值。 计算最优解的值,通常采用自底向上的方法。 利用计算出的信息构造一个最优解。 算法原理 适合应用动态规划方法求解的最优化问题应该具备的两个要素:最优子结构和子问题重叠。 最优子结构 用动态规划方法求解最优化问题的第一步就是刻画最优解的结构。如果一个问题的最优解包含其子问题的最优解,则称此问题具有最优子结构性质。因此,某个问题是否适合应用动态规划算法,它是否具有最优子结构性质是一个好线索。 发掘最优子结构性质的通过模式 证明问题最优解的第一个组成部分是做出一个选择。做出这次选择会产生一个或多个待解的子问题。 对于一个给定问题,在其可能的第一步选择中,你假定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择。 给定可获得最优解的选择后,你确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间。 利用“剪切-粘贴”(cut-and-paste)技术证明:作为构造原问题最优解的组成部分,每个子问题的解就是它本身的最优解。证明这一点是利用反证法:假定子问题的解不是其自身的最优解,那么我们就可以从原问题的解中“剪切”掉这些非最优解,将最优解“粘贴”进去,从而得到原问题一个更优的解,这与最初的解是原问题最优解的前提假设锚段。 一个刻画子问题空间的好经验是:保持子问题空间尽可能简单,只在必要时才扩展它。 对于不同问题领域,最优子结构的不同体现在两个方面: 原问题的最优解中涉及多少个子问题,以及 在确定最优解使用哪些子问题时,我们需要考察多少种选择。 可以用子问题的总数和每个子问题需要考察多少种选择这两个因素的乘积来粗略分析动态规划算法的运行时间。 在动态规划方法中,通常自底向上地使用最优子结构。也就是说,首先求得子问题的最优解,然后求原问题的最优解。在求解原问题过程中,我们需要在涉及的子问题中做出选择,选出能得到原问题最优解的子问题。原问题最优解的代价通常就是子问题最优解的代价加上由此次选择直接产生的代价。 能够使用贪心算法的问题也必须具有最优子结构性质。贪心算法和动态规划最大的不同在于,它并不是首先寻找子问题的最优解,然后在其中进行选择,而是首先做出一次“贪心”选择—​在当时(局部)看来最优的选择—​然后求解选出的子问题,从而不必费心求解所有可能相关的子问题。 问题:使用贪心算法和动态规划的界线是什么?什么时候使用贪心?什么时候使用动态规划? 重叠子问题 适合用动态规划方法求解的最优化问题应该具备的第二个性质是子问题空间必须足够“小”,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。 如果递归算法反复求解相关的子问题,则就称为最优化问题具有重叠子问题(overlapping subproblems)性质。 与之相对的,适合用分治算法求解的问题通常在递归的每一步都生成全新的子问题。 动态规划算法通常这样利用重叠子问题性质:对每个子问题求解一次,将解存入一个表中,当再次需要这个子问题时直接查表,每次查表的代价为常量时间。 一个问题是否适合用动态规划求解同事依赖于子问题的无关性和重叠性。两个子问题如果不共享资源,它们就是独立的。而重叠是指两个子问题实际上是同一个子问题,只是作为不同问题的子问题出现而已。 将自顶向下的递归算法(无备忘录)与自底向上的动态规划算法进行比较,后者要高效得多,因为它利用了重叠子问题性质。 重构最优解 从实际考虑,通常将每个子问题所做的选择存在一个表中,这样就不必根据代价来重构这些信息。 备忘 可以保持自顶向下策略,同时达到与自底向上动态规划方法相似的效率。思路就是对自然但低效的递归算法加入备忘机制。维护一个表记录子问题的解,但仍然保持递归算法的控制流程。 带备忘的递归算法为每个子问题维护一个表项来保存它的解。每个表项的初值设为一个特殊值,表示尚未填入子问题的解。当递归调用过程中第一次额遇到子问题时,计算其解,并存入对应表项。随后每次遇到同一个问题,只是简单地查表,返回其解。 一个更通用的备忘方法是使用散列技术,以子问题参数为关键字。 通常情况下,如果每个子问题都必须至少求解一次,自底向上动态规划算法会比自顶向下备忘算法快,因为自底向上算法没有递归调用的开销,表的维护开销也更小。而且,对于某些问题,可以利用表的访问模式来进一步降低时空代价。相反,如果子问题空间中的某些子问题完全不必求解,备忘方法就会体现出优势了,因为它只会求解那些绝对必要的子问题。 参考资料 算法导论(原书第3版) Dynamic Programming vs Divide-and-Conquer - ITNEXT How to Solve Any Dynamic Programming Problem - Pramp Blog | Coding Interview & Job Search Resources for Developers