SpringBoot 实战:优雅的使用枚举参数(原理篇)

简介: SpringBoot 实战:优雅的使用枚举参数 中聊了怎么优雅的使用枚举参数,本文就来扒一扒 Spring 是如何找到对应转换器 Converter 的。

image.png

该图片由minka2507在Pixabay上发布


你好,我是看山。


SpringBoot 实战:优雅的使用枚举参数 中聊了怎么优雅的使用枚举参数,本文就来扒一扒 Spring 是如何找到对应转换器 Converter 的。


找入口

对 Spring 有一定基础的同学一定知道,请求入口是DispatcherServlet,所有的请求最终都会落到doDispatch方法中的ha.handle(processedRequest, response, mappedHandler.getHandler())逻辑。我们从这里出发,一层一层向里扒。


跟着代码深入,我们会找到org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest的逻辑:


public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}

可以看出,这里面通过getMethodArgumentValues方法处理参数,然后调用doInvoke方法获取返回值。


继续深入,能够找到org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveArgument方法,这个方法就是解析参数的逻辑。


试想一下,如果是我们自己实现这段逻辑,会怎么做呢?


获取输入参数

找到目标参数

检查是否需要特殊转换逻辑

如果需要,进行转换

如果不需要,直接返回

image.png


获取输入参数的逻辑在org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName,单参数返回的是 String 类型,多参数返回 String 数组。核心代码如下:


String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
    arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}

所以说,无论我们的目标参数是什么,输入参数都是 String 类型或 String 数组,然后 Spring 把它们转换为我们期望的类型。


找到目标参数的逻辑在DispatcherServlet中,根据 uri 找到对应的 Controller 处理方法,找到方法就找到了目标参数类型。


接下来就是检查是否需要转换逻辑,也就是org.springframework.validation.DataBinder#convertIfNecessary,顾名思义,如果需要就转换,将字符串类型转换为目标类型。在我们的例子中,就是将 String 转换为枚举值。


查找转换器

继续深扒,会在org.springframework.beans.TypeConverterDelegate#convertIfNecessary方法中找到这么一段逻辑:


if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
    try {
        return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
    }
    catch (ConversionFailedException ex) {
        // fallback to default conversion logic below
        conversionAttemptEx = ex;
    }
}

这段逻辑中,调用了org.springframework.core.convert.support.GenericConversionService#canConvert方法,检查是否可转换,如果可以转换,将会执行类型转换逻辑。


检查是否可转换的本质就是检查是否能够找到对应的转换器。如果能找到,就用找到的转换器开始转换逻辑,如果找不到,那就是不能转换,走其他逻辑。


我们可以看看查找转换器的代码org.springframework.core.convert.support.GenericConversionService#getConverter,可以对我们自己写代码有一些启发:


private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<>(64);
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
    ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
    GenericConverter converter = this.converterCache.get(key);
    if (converter != null) {
        return (converter != NO_MATCH ? converter : null);
    }
    converter = this.converters.find(sourceType, targetType);
    if (converter == null) {
        converter = getDefaultConverter(sourceType, targetType);
    }
    if (converter != null) {
        this.converterCache.put(key, converter);
        return converter;
    }
    this.converterCache.put(key, NO_MATCH);
    return null;
}

转换为伪代码就是:


根据参数类型和目标类型,构造缓存 key

根据缓存 key,从缓存中查询转换器

如果能找到且不是 NO_MATCH,返回转换器;如果是 NO_MATCH,返回 null;如果未找到,继续

通过org.springframework.core.convert.support.GenericConversionService.Converters#find查询转换器

如果未找到,检查源类型和目标类型是否可以强转,也就是类型一致。如果是,返回 NoOpConverter,如果否,返回 null。

检查找到的转换器是否为 null,如果不是,将转换器加入到缓存中,返回该转换器

如果否,在缓存中添加 NO_MATCH 标识,返回 null

image.png


Spring 内部使用Map作为缓存,用来存储通用转换器接口GenericConverter,这个接口会是我们自定义转换器的包装类。我们还可以看到,转换器缓存用的是ConcurrentReferenceHashMap,这个类是线程安全的,可以保证并发情况下,不会出现异常存储。但是getConverter方法没有使用同步逻辑。换句话说,并发请求时,可能存在性能损耗。不过,对于 web 请求场景,并发损耗好过阻塞等待。


我们在看下 Spring 是如何查找转换器的,在org.springframework.core.convert.support.GenericConversionService.Converters#find中就是找到对应转换器的核心逻辑:


private final Map<ConvertiblePair, ConvertersForPair> converters = new ConcurrentHashMap<>(256);
@Nullable
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
    // Search the full type hierarchy
    List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
    List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
    for (Class<?> sourceCandidate : sourceCandidates) {
        for (Class<?> targetCandidate : targetCandidates) {
            ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
            GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
            if (converter != null) {
                return converter;
            }
        }
    }
    return null;
}
@Nullable
private GenericConverter getRegisteredConverter(TypeDescriptor sourceType,
        TypeDescriptor targetType, ConvertiblePair convertiblePair) {
    // Check specifically registered converters
    ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
    if (convertersForPair != null) {
        GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);
        if (converter != null) {
            return converter;
        }
    }
    // Check ConditionalConverters for a dynamic match
    for (GenericConverter globalConverter : this.globalConverters) {
        if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) {
            return globalConverter;
        }
    }
    return null;
}

我们可以看到,Spring 是通过源类型和目标类型组合起来,查找对应的转换器。而且,Spring 还通过getClassHierarchy方法,将源类型和目标类型的家族族谱全部列出来,用双层 for 循环遍历查找。


上面的代码中,还有一个matches方法,在这个方法里面,调用了ConverterFactory#getConverter方法,也就是用这个工厂方法,创建了指定类型的转换器。


private final ConverterFactory<Object, Object> converterFactory;
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
    boolean matches = true;
    if (this.converterFactory instanceof ConditionalConverter) {
        matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType);
    }
    if (matches) {
        Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
        if (converter instanceof ConditionalConverter) {
            matches = ((ConditionalConverter) converter).matches(sourceType, targetType);
        }
    }
    return matches;
}

类型转换

经过上面的逻辑,已经找到判断可以进行转换。其核心逻辑就是已经找到对应的转换器了,下面就是转换逻辑,在org.springframework.core.convert.support.GenericConversionService#convert中:


public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
    Assert.notNull(targetType, "Target type to convert to cannot be null");
    if (sourceType == null) {
        Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
        return handleResult(null, targetType, convertNullSource(null, targetType));
    }
    if (source != null && !sourceType.getObjectType().isInstance(source)) {
        throw new IllegalArgumentException("Source to convert from must be an instance of [" +
                sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
    }
    GenericConverter converter = getConverter(sourceType, targetType);
    if (converter != null) {
        Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
        return handleResult(sourceType, targetType, result);
    }
    return handleConverterNotFound(source, sourceType, targetType);
}

其中的GenericConverter converter = getConverter(sourceType, targetType)就是前文中getConverter方法。此处还是可以给我们编码上的一些借鉴的:getConverter方法在canConvert中调用了一次,然后在后续真正转换的时候又调用一次,这是参数转换逻辑,我们该怎么优化这种同一请求内多次调用相同逻辑或者请求相同参数呢?那就是使用缓存。为了保持一次请求中前后两次数据的一致性和请求的高效,推荐使用内存缓存。


执行到这里,直接调用ConversionUtils.invokeConverter(converter, source, sourceType, targetType)转换,其内部是使用org.springframework.core.convert.support.GenericConversionService.ConverterFactoryAdapter#convert方法,代码如下:


public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    if (source == null) {
        return convertNullSource(sourceType, targetType);
    }
    return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
}

这里就是调用ConverterFactory工厂类构建转换器(即IdCodeToEnumConverterFactory类的getConverter方法),然后调用转换器的conver方法(即IdCodeToEnumConverter类的convert方法),将输入参数转换为目标类型。具体实现可以看一下实战篇中的代码,这里不做赘述。


至此,我们把整个路程通了下来。


文末总结

在本文中,我们跟随源码找到自定义转换器工厂类和转换器类的实现逻辑。这里需要强调一下的是,由于实战篇中我们用到的例子是简单参数的方式,也就是Controller的方法参数都是直接参数,没有包装成对象。这样的话,Spring 是通过RequestParamMethodArgumentResolver处理参数。如果是包装成对象,会使用ModelAttributeMethodProcessor处理参数。这两个处理类中查找类型转换器逻辑都是相同的。


无论是GET请求,还是传参式的POST请求(即Form模式),都可以使用上面这种方式,实现枚举参数的类型转换。但是是 HTTP Body 方式却不行,为什么呢?


Spring 对于 body 参数是通过RequestResponseBodyMethodProcessor处理的,其内部使用了MappingJackson2HttpMessageConverter转换器,逻辑完全不同。所以,想要实现 body 的类型转换,还需要走另外一种方式。将在下一篇中给出。

目录
相关文章
|
4天前
|
XML Java 开发者
Spring Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
15 0
|
8天前
|
Java Spring
SpringBoot自动装配的原理
在Spring Boot项目中,启动引导类通常使用`@SpringBootApplication`注解。该注解集成了`@SpringBootConfiguration`、`@ComponentScan`和`@EnableAutoConfiguration`三个注解,分别用于标记配置类、开启组件扫描和启用自动配置。
44 17
|
25天前
|
自然语言处理 Java API
Spring Boot 接入大模型实战:通义千问赋能智能应用快速构建
【10月更文挑战第23天】在人工智能(AI)技术飞速发展的今天,大模型如通义千问(阿里云推出的生成式对话引擎)等已成为推动智能应用创新的重要力量。然而,对于许多开发者而言,如何高效、便捷地接入这些大模型并构建出功能丰富的智能应用仍是一个挑战。
98 6
|
28天前
|
Java Spring 容器
springboot @RequiredArgsConstructor @Lazy解决循环依赖的原理
【10月更文挑战第15天】在Spring Boot应用中,循环依赖是一个常见问题,当两个或多个Bean相互依赖时,会导致Spring容器陷入死循环。本文通过比较@RequiredArgsConstructor和@Lazy注解,探讨它们解决循环依赖的原理和优缺点。@RequiredArgsConstructor通过构造函数注入依赖,使代码更简洁;@Lazy则通过延迟Bean的初始化,打破创建顺序依赖。两者各有优势,需根据具体场景选择合适的方法。
51 4
|
2月前
|
Java 应用服务中间件 API
Vertx高并发理论原理以及对比SpringBoot
Vertx 是一个基于 Netty 的响应式工具包,不同于传统框架如 Spring,它的侵入性较小,甚至可在 Spring Boot 中使用。响应式编程(Reactive Programming)基于事件模式,通过事件流触发任务执行,其核心在于事件流 Stream。相比多线程异步,响应式编程能以更少线程完成更多任务,减少内存消耗与上下文切换开销,提高 CPU 利用率。Vertx 适用于高并发系统,如 IM 系统、高性能中间件及需要较少服务器支持大规模 WEB 应用的场景。随着 JDK 21 引入协程,未来 Tomcat 也将优化支持更高并发,降低响应式框架的必要性。
Vertx高并发理论原理以及对比SpringBoot
|
1月前
|
JSON NoSQL Java
springBoot:jwt&redis&文件操作&常见请求错误代码&参数注解 (九)
该文档涵盖JWT(JSON Web Token)的组成、依赖、工具类创建及拦截器配置,并介绍了Redis的依赖配置与文件操作相关功能,包括文件上传、下载、删除及批量删除的方法。同时,文档还列举了常见的HTTP请求错误代码及其含义,并详细解释了@RequestParam与@PathVariable等参数注解的区别与用法。
|
2月前
|
缓存 前端开发 Java
springboot 的单体服务 字典参数转译
本文介绍了如何在Spring Boot项目中使用缓存来管理字典参数,并确保前后端数据一致性。首先,通过`@EnableCaching`启用缓存功能,接着创建一个自定义的字典缓存类`DicCache`。然后,通过配置类将`DicCache`添加到`cacheManager`中。此外,对字典服务进行改造,使用`@CachePut`和`@CacheEvict`注解保证数据一致性。最后,实现自定义注解`@DicSerializer`和序列化处理类`DictSerializerHandel`,用于在序列化过程中自动转换字典值。通过这种方式,可最小化代码改动并提高系统性能。
springboot 的单体服务 字典参数转译
|
2月前
|
缓存 NoSQL Java
Springboot实战——黑马点评之秒杀优化
【9月更文挑战第27天】在黑马点评项目中,秒杀功能的优化对提升系统性能和用户体验至关重要。本文提出了多项Spring Boot项目的秒杀优化策略,包括数据库优化(如索引和分库分表)、缓存优化(如Redis缓存和缓存预热)、并发控制(如乐观锁、悲观锁和分布式锁)以及异步处理(如消息队列和异步任务执行)。这些策略能有效提高秒杀功能的性能和稳定性,为用户提供更佳体验。
147 6
|
2月前
|
JSON Java 数据格式
springboot 参数统一处理
springboot 参数统一处理
|
1月前
|
监控 Java Maven
springboot学习二:springboot 初创建 web 项目、修改banner、热部署插件、切换运行环境、springboot参数配置,打包项目并测试成功
这篇文章介绍了如何快速创建Spring Boot项目,包括项目的初始化、结构、打包部署、修改启动Banner、热部署、环境切换和参数配置等基础操作。
125 0