遇到的问题:项目中需要用到策略模式,把策略实现以集合的形式注入到Service 中。因为要强制校验策略的顺序,所以采用的是构造器注入,简单明了。结果Spring 启动失败,
Is there an unresolvable circular reference?
。
首先,查看Spring启动异常的日志,throw BeanCurrentlyInCreationException
/**
* Exception thrown in case of 引用了当前正在创建中的bean.
* Typically happens when constructor autowiring matches the currently constructed bean.
* 直接指明出现在构造器注入时发生
*/
public class BeanCurrentlyInCreationException extends BeanCreationException {
}
然后,根据异常日志提示的信息,可以梳理出依赖的关系链,aService 构造器注入的策略bService,bService 实现业务注入cService,而cService注入了aService。
需要特别强调的是:循环依赖和Spring 创建Bean 的顺序有严格的关系。例如:AService、CService 类的顺序随着ClassPath扫描后,自然的就是先创建AService Bean。如果CService 先扫描到,那么就不会出现 BeanCurrentlyInCreationException。所以深入的分析,循环依赖问题和操作系统的文件系统或者xml 配置的顺序都有关系
。
2个解决方案:
策略集合采用属性Setter注入,单独写@PostConstruct 初始化方法,完成策略顺序的校验和打印。替换掉构造器注入的写法。这个方案比较直接。
指定Spring容器创建Bean的顺序,保证cService在aService 之前创建,这样就解耦了循环依赖。详细解释如下分析可知。
// 强制指定Spring 容器先创建CService。这样可以通过earlySingletonExposure 方案解决循环 @DependsOn(value = "cService") @Service public class AService { private CService cService; @Autowired public AService(CService cService) { this.cService = cService; } } @Service public class CService { @Autowired private AService aService; }
文章主要记录查看源码的思路,和总结,便于后续方便查阅。
具体的循环依赖源码实现分析,参考:彻底理解SpringIOC、DI-这篇文章就够了、图解Spring解决循环依赖♻️
Spring对于循环依赖的处理,如果从设计的原因去看,阅读源码可能会清晰一下。如果直接入手看源码,可能会比较绕、困惑很多。一是循环依赖是个循环调用、递归的过程,静态阅读或者动态调试都会比较复杂;另外一个是由于Spring丰富的功能设计,不能非常直接的看到循环依赖核心的实现方案。
一种Insight 方法是:直接BeanCurrentlyInCreationException
打断点,查看全部的调用链,从中分析出现循环依赖的原因。属于逆向的看源码。
一种Insight方法是:先分析为什么出现循环依赖,如果要解决循环依赖需要做哪些设计。再结合博客的讲解(Spring 文档没有找到对循环依赖设计思路的描述),提纲挈领的去阅读源码。属于正向设计思路的看源码。
resolving a circular reference
),只限定在singleton 模式下,prototype不支持。默认 bean初始化是可以通过拆解初始化步骤解耦循环,如果是构造器注入,则把循环依赖拉低到JDK层级,不可控无法实现。
想要得到原始对象,肯定是要构造器执行完成后
。Spring 托管构造器后,需要的构造器参数由Spring find,如果Spring检测到循环依赖后,自然就终止了初始化过程。对应到源码就是createBeanInstance,该方法没有执行完,没有earlySingletonExposure原始对象暴露给Spring容器
。因为是singleton 模式,全局唯一的引用,支持提前赋值(注入)到其他bean 对象中
。原始对象的setter注入由populateBean完成。通过拆分bean 的instantiate、set/inject 步骤,并且把singleton 引用提前缓存到全局变量中,这样即使是循环依赖也可以提前得到初始化中的原始对象。即使概念意义的Bean没有初始化完成
,也不会出现循环依赖无法解决的情况。/**
* Instantiate the given bean using its default constructor.
* 默认的初始化bean 的实现,
* 简单描述为:return new BeanWrapper(Foo.class.getConstructor().newInstance())
* @From AbstractAutowireCapableBeanFactory
*/
protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
try {
Object beanInstance;
final BeanFactory parent = this;
// 对象instantiate。根据BeanDefinition 获取bean Class的信息,解析默认的构造函数,通过反射得到对象
// Spring在此处也应用了策略模式,增加扩展性的同时,也降低了阅读的流畅性。
beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
// 将原始的对象包装为Bean,暴露给Spring容器。包装的目的是:方便对原始对象做各种转换和操作。
BeanWrapper bw = new BeanWrapperImpl(beanInstance);
initBeanWrapper(bw);
return bw;
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
}
}
本文关注 Spring 注入泛集合的调用链,以及注入集合的排序问题和应用。
泛集合是指:Array、Collection、Map。
构造注入
方式的调用链(Spring Version >= 4.x):
org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor
提供各种构造器的识别、构造参数实例化的实现
org.springframework.beans.factory.support.ConstructorResolver#createArgumentArray
org.springframework.beans.factory.support.ConstructorResolver#resolveAutowiredArgument
org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency
Spring 依赖注入的核心实现方法
。
org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveMultipleBeans
泛集合注入实现,把需要的 beans 封装为 Array、Collection、Map。
其中,Array和Collection 类型的注入是经过排序后
的对象,Map 排序是以 LinkedHashMap 体现。
org.springframework.core.Ordered,声明顺序的接口,用于类似拦截器、组件的执行优先级声明。
// 根据升序顺序的约定,order value 越小,排序越靠前, 优先级越高。
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
org.springframework.core.OrderComparator, 按照升序排序。支持实现 Ordered 接口的对象排序。
org.springframework.core.annotation.AnnotationAwareOrderComparator,支持使用顺序注解的对象排序,如:@Order
和@Priority 。兼容Ordered接口 。
public class OrderComparator implements Comparator<Object> {
/**
* Shared default instance of OrderComparator.
*/
public static final OrderComparator INSTANCE = new OrderComparator();
// 升序排序
public int compare(Object o1, Object o2) {
boolean p1 = (o1 instanceof PriorityOrdered);
boolean p2 = (o2 instanceof PriorityOrdered);
// 先区分是否实现Ordered 接口。实现 Ordered 接口的对象,排序靠前。
if (p1 && !p2) {
return -1;
}
else if (p2 && !p1) {
return 1;
}
// 获取 order value,再基于value 对比排序。
int i1 = getOrder(o1);
int i2 = getOrder(o2);
return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
}
/**
* 对外提供排序方法,方便调用。
*/
public static void sort(List<?> list) {
if (list.size() > 1) {
Collections.sort(list, INSTANCE);
}
}
}
part1 主要分析动态sql 参数相关的解析,对于xml-> sql 的过程没有详细分析,此文补上。part1 GO.
问题:工作中遇到的一个bug,mybatis 查询有个参数为0,导致拼接的sql异常。
<!--问题sql片段,入参source=0,导致拼接完的sql没有这个条件 -->
<if test="source != null and source != ''">
and foo.source = #{source,jdbcType=INTEGER}
</if>
关键类:ExpressionEvaluator
、IfSqlNode
IfSqlNode 负责解析if 标签的sql片段
ExpressionEvaluator 实现表达式的解析,底层是通过OGNL实现。
上述问题的测试
ExpressionEvaluator evaluator = new ExpressionEvaluator();
// test = true
boolean test = evaluator.evaluateBoolean("source == ''", ImmutableMap.of("source", 0));
源码实现
public boolean evaluateBoolean(String expression, Object parameterObject) {
try {
Object value = Ognl.getValue(expression, parameterObject);
if (value instanceof Boolean) return (Boolean) value;
// 需要特别注意的是,表达式解析也支持返回为数字,如果为0, 则返回false
if (value instanceof Number) return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
return value != null;
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
最近接手的项目,配置项实在是多。使用 Spring @Value 注入配置。大部分配置项是固定的,但也不能彻底写死,以备临时调整。
简化配置的思路是:根据改动的频率,把固化的配置以默认值表达式形式配置到代码中。从而减少散落在maven、properties、动态配置平台的配置项。
约定大于配置 配置表达式类似于@Value("${foo.skill.switch:false}")
但是这个默认值是语法还是自定义实现,需要看看原理。
多人开发的工程,经常会遇到配置缺失导致应用启动失败。
在Spring框架中,如果你想要忽略无法解析的占位符,以避免抛出异常,你可以在配置属性解析时设置 ignoreUnresolvablePlaceholders 属性为true。
// 当你设置了ignoreUnresolvablePlaceholders为true后,如果Spring遇到无法解析的占位符,它将不会抛出异常,而是会保留原始的占位符字符串。
@Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
configurer.setIgnoreUnresolvablePlaceholders(true);
return configurer;
}
/**
* 对该方法进行递归调用,解析配置值
* 直至解析到值(解析到默认值),或者抛出异常(IllegalArgumentException("Could not resolve placeholder XXX'))
* new PropertyPlaceholderHelper("${", "}", ":", false);
* @see org.springframework.util.PropertyPlaceholderHelper#parseStringValue
*/
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
// Recursive invocation, parsing placeholders contained in the placeholder key.
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
// 默认值placeholder 解析实现
if (propVal == null && this.valueSeparator != null) {
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0, separatorIndex);
// 默认值截取
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
// 递归解析,spring 想的太周全了
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
} else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
// 如果 Spring 遇到无法解析的占位符,它将不会抛出异常,而是会保留原始的占位符字符串。
startIndex = buf.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}else {
// 既没有默认配置,也没有启用忽略找不到的配置项,抛异常,启动失败。❌
throw new IllegalArgumentException("Could not resolve placeholder '" + placeholder + "'" + " in value \"" + value + "\"");
}
}
org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency
项目中HandlerInterceptor preHandle方法会把登录的用户信息存储在ThreadLocal,方便请求逻辑中获取,
afterCompletion方法中会remove。当发生异常时,想通过@ExceptionHandler打印当前的用户名,还能拿到用户信息吗?
// mvc请求处理入口
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
Exception dispatchException = null;
try {
mappedHandler = getHandler(processedRequest);
// 登录校验和用户信息的缓存就是在此执行
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 注意,业务发生异常,postHandle 是不会执行的
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
// 返回结果处理,返回ModelAndView,包括Exception 处理,参考processHandlerException
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 用户信息ThreadLocal 是在此remove 的
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
}
可以在请求发生异常时,在ExceptionHandler 中可以拿到用户信息
。
这样就很方便根据打印的日志定位和排查问题。