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 中可以拿到用户信息
。
这样就很方便根据打印的日志定位和排查问题。
Spring 容器默认的实现类。在遇到Spring bean 相关问题,都会涉及到 DefaultListableBeanFactory
,因此列出常用的元素,方便知识梳理和 问题定位。
ApplicationContext
Central interface to provide configuration for an application。除了提供标准的BeanFactory能力以外,还实现了ApplicationContextAware、ResourceLoaderAware这样的接口。
ApplicationContext 主要功能是实现通用的context,也是BeanFactory。不同于简单的BeanFactory,还提供了初始化BeanFactory的一些特殊的Bean。我们经常使用的功能,对外提供的方法大部分都委托BeanFactory实现。
BeanFactory
最基础的访问bean容器的接口,定义了BeanFactory 的实现标准
ListableBeanFactory
主动扫描加载bean示例的BeanFactory定义
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
获取指定Class 的bean 实例。如果不存在,就实时实例化(new Clazz 和 bean-init )
protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException
上述 getBean 逻辑的真正实现。bean 获取的核心实现都在这个方法中。
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException
Bean 实例化逻辑的真正实现。
creates a bean instance, populates the bean instance, applies post-processors, etc.
protected Object getSingleton(String beanName, boolean allowEarlyReference)
Spring 默认都是以单例模式获取bean。 循环依赖解决方案参考此处的三级 cache。
SpringMVC 从3.2 版本之后,提供了全局 Controller 增强 Advice 特性。框架初始化过程(component scanning)中,针对功能 Advice 和 异常处理分别做了不同的解析。
针对 Controller 的自定义方法
@ExceptionHandler
,@InitBinder
,@ModelAttribute
,如果想应用到全局,可以声明为 @ControllerAdvice
@ControllerAdvice
public class ExampleAdvice {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}
// @RequestMapping methods, etc.
}
复合注解,ControllerAdvice + ResponseBody = RestControllerAdvice
实际上是针对 message conversion 处理的异常进行拦截和增强。
如上,RestControllerAdvice 也属于 ControllerAdvice。因此,框架初始化过程中,只需要识别 @ControllerAdvice 注解的bean。
org.springframework.web.method.ControllerAdviceBean#findAnnotatedBeans
1️⃣ Controller 功能增强
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#initControllerAdviceCache
2️⃣ Controller 异常拦截处理
org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#initExceptionHandlerAdviceCache
1️⃣ 异常处理分发
org.springframework.web.servlet.DispatcherServlet#processHandlerException
2️⃣ 获取匹配的异常处理方法
org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#getExceptionHandlerMethod
在实际开发过程中,使用 ControllerAdvice 拦截 SomeFooException, 发现并没有达到预期效果。配置如下代码所示:
// 业务异常SomeFooException 匹配到 handle 方法, 但是异常实例和 handle 入参类型不符合,抛出 IllegalArgumentException: No suitable resolver
@ExceptionHandler({IllegalArgumentException.class, SomeFooException.class})
public ResponseEntity<String> handle(IllegalArgumentException ex) {
// ...
}
通过源代码跟踪分析,在上述 handle 方法反射调用发生异常(入参类型不匹配)。 SpringMVC 无法处理,异常传递给 Tomcat 处理。
/**
* Find an @ExceptionHandler method and invoke it to handle the raised exception.
*/
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// 根据异常类型,获取匹配的处理方法,如上述的 handle 方法
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
try {
{
// 反射调用,对 exception 进行异常包装和处理。
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
}
}
catch (Throwable invocationEx) {
// Any other than the original exception (or its cause) is unintended here,
// probably an accident (e.g. failed assertion or the like).
if (invocationEx != exception && invocationEx != exception.getCause() && logger.isWarnEnabled()) {
logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
}
// Continue with default processing of the original exception...
// SpringMVC 无法处理的异常,传递给 Web 容器(Tomcat)处理。
return null;
}
}
不同异常处理的 response
1️⃣ Tomcat 异常处理返回信息。 Http StatusCode = 500
{
"timestamp": "2023-08-03 19:55:03",
"status": 500,
"error": "Internal Server Error",
"message": "foo 必填",
"path": "/example/saveOrUpdate"
}
2️⃣ SpringMVC 自定义异常处理返回信息。 Http StatusCode = 200
{
"message": "foo 必填",
"code": 500,
"data": null,
"timestamp": 1691066025153
}
1️⃣ Advice 未生效首先 check basePackages, 是否包含对应的 Controller。
2️⃣ 全局异常拦截未生效,首先检查异常是否包含、以及匹配的优先级顺序。