【小家Spring】Spring中@Value注解有多强大?从原理层面去剖析为何它有如此大的“能耐“(中)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【小家Spring】Spring中@Value注解有多强大?从原理层面去剖析为何它有如此大的“能耐“(中)

BeanExpressionResolver

策略接口,用于通过将值作为表达式进行评估来解析值(如果适用)。它持有Bean工厂~


// @since 3.0
public interface BeanExpressionResolver {
  // value此时还是复杂类型,比如本例的#{person.name}
  // BeanExpressionContext:持有beanFactory和scope的引用而已~
  @Nullable
  Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException;
}


它的唯一实现类StandardBeanExpressionResolver(当然我们是可以自己实现的,比如后面我们自定义@Value功能,通过继承StandardBeanExpressionResolver来扩展实现~毕竟Spring很暖心的给我们开了口)


StandardBeanExpressionResolver


语言解析器的标准实现,支持解析SpEL语言。

public class StandardBeanExpressionResolver implements BeanExpressionResolver {
  // 因为SpEL是支持自定义前缀、后缀的   此处保持了和SpEL默认值的统一
  // 它的属性值事public的   so你可以自定义~
  /** Default expression prefix: "#{". */
  public static final String DEFAULT_EXPRESSION_PREFIX = "#{";
  /** Default expression suffix: "}". */
  public static final String DEFAULT_EXPRESSION_SUFFIX = "}";
  private String expressionPrefix = DEFAULT_EXPRESSION_PREFIX;
  private String expressionSuffix = DEFAULT_EXPRESSION_SUFFIX;
  private ExpressionParser expressionParser; // 它的最终值是SpelExpressionParser
  // 每个表达式都对应一个Expression,这样可以不用重复解析了~~~
  private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>(256);
  // 每个BeanExpressionContex都对应着一个取值上下文~~~
  private final Map<BeanExpressionContext, StandardEvaluationContext> evaluationCache = new ConcurrentHashMap<>(8);
  // 匿名内部类   解析上下文。  和TemplateParserContext的实现一样。个人觉得直接使用它更优雅
  // 和ParserContext.TEMPLATE_EXPRESSION 这个常量也一毛一样
  private final ParserContext beanExpressionParserContext = new ParserContext() {
    @Override
    public boolean isTemplate() {
      return true;
    }
    @Override
    public String getExpressionPrefix() {
      return expressionPrefix;
    }
    @Override
    public String getExpressionSuffix() {
      return expressionSuffix;
    }
  };
  // 空构造函数:默认就是使用的SpelExpressionParser  下面你也可以自己set你自己的实现~
  public StandardBeanExpressionResolver() {
    this.expressionParser = new SpelExpressionParser();
  }
  public void setExpressionParser(ExpressionParser expressionParser) {
    Assert.notNull(expressionParser, "ExpressionParser must not be null");
    this.expressionParser = expressionParser;
  }
  // 解析代码相对来说还是比较简单的,毕竟复杂的解析逻辑都是SpEL里边~  这里只是使用一下而已~
  @Override
  @Nullable
  public Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException {
    if (!StringUtils.hasLength(value)) {
      return value;
    }
    try {
      Expression expr = this.expressionCache.get(value);
      if (expr == null) {
        // 注意:此处isTemplte=true
        expr = this.expressionParser.parseExpression(value, this.beanExpressionParserContext);
        this.expressionCache.put(value, expr);
      }
      // 构建getValue计算时的执行上下文~~~
      // 做种解析BeanName的ast为;org.springframework.expression.spel.ast.PropertyOrFieldReference
      StandardEvaluationContext sec = this.evaluationCache.get(evalContext);
      if (sec == null) {
        // 此处指定的rootObject为:evalContext   --> BeanExpressionContext 
        sec = new StandardEvaluationContext(evalContext);
        // 此处新增了4个,加上一个默认的   所以一共就有5个属性访问器了
        // 这样我们的SpEL就能访问BeanFactory、Map、Enviroment等组件了~
        // BeanExpressionContextAccessor表示调用bean的方法~~~~(比如我们此处就是使用的它)  最终执行者为;BeanExpressionContext   它持有BeanFactory的引用嘛~
        // 如果是单村的Bean注入,最终使用的也是BeanExpressionContextAccessor 目前没有找到BeanFactoryAccessor的用于之地~~~
        // addPropertyAccessor只是:addBeforeDefault 所以只是把default的放在了最后,我们手动add的还是保持着顺序的~
        // 注意:这些属性访问器是有先后顺序的,具体看下面~~~
        sec.addPropertyAccessor(new BeanExpressionContextAccessor());
        sec.addPropertyAccessor(new BeanFactoryAccessor());
        sec.addPropertyAccessor(new MapAccessor());
        sec.addPropertyAccessor(new EnvironmentAccessor());
        // setBeanResolver不是接口方法,仅仅辅助StandardEvaluationContext 去获取Bean
        sec.setBeanResolver(new BeanFactoryResolver(evalContext.getBeanFactory()));
        sec.setTypeLocator(new StandardTypeLocator(evalContext.getBeanFactory().getBeanClassLoader()));
        // 若conversionService不为null,就使用工厂的。否则就使用SpEL里默认的DefaultConverterService那个  
        // 最后包装成TypeConverter给set进去~~~
        ConversionService conversionService = evalContext.getBeanFactory().getConversionService();
        if (conversionService != null) {
          sec.setTypeConverter(new StandardTypeConverter(conversionService));
        }
        // 这个很有意思,是一个protected的空方法,因此我们发现若我们自己要自定义BeanExpressionResolver,完全可以继承自StandardBeanExpressionResolver
        // 因为我们绝大多数情况下,只需要提供更多的计算环境即可~~~~~
        customizeEvaluationContext(sec);
        this.evaluationCache.put(evalContext, sec);
      }
      return expr.getValue(sec);
    } catch (Throwable ex) {
      throw new BeanExpressionException("Expression parsing failed", ex);
    }
  }
  //Spring留给我们扩展的SPI  
  protected void customizeEvaluationContext(StandardEvaluationContext evalContext) {
  }
}


如上,整个@Value的解析过程至此就全部完成了。可能有小伙伴会问:怎么不见Resource这种注入呢?其实,从上面不难看出,这个是ConversionService去做的事,它能够把一个字符串转换成Resource对象,仅此而已


总得来说@Value它自己做的事本身还是非常单一的:依赖注入,只是它把众多功能都很好的像插件一样插拔进来了,从而对用户很友好的显示了显它的神通广大~


需要注意的是,在整个依赖的解析过程中,有两个非常重要的接口:BeanExpressionResolver和AutowireCandidateResolver都扮演着重要角色,有兴趣的可以深入继续了解~


SpEL中PropertyAccessor的匹配规则和执行顺序


本来这个内容应该放在SpEL章节更为合适,但是就着这个Spring的环境,就放在此处了。

如上StandardEvaluationContext中默认就会给放在上4+1个PropertyAccessor,他们的匹配规则和执行顺序势必会影响到最终的取值,因此这部分就看看他们的决策原理。


备注:这部分的理解对平时的使用几乎没有关系,但对你自定义扩展功能有较大的影响~


默认注册后,他们的顺序如下:

image.png



它选择的代码在此处(SpEL章节说到了,目前只有PropertyOrFieldReference这个AST才会涉及到PropertyAccessor的决策):

// 它是一个AST~
public class PropertyOrFieldReference extends SpelNodeImpl {
  ...
  // 按照本例而言,contextObject就为BeanExpressionContext
  private List<PropertyAccessor> getPropertyAccessorsToTry(@Nullable Object contextObject, List<PropertyAccessor> propertyAccessors) {
    Class<?> targetType = (contextObject != null ? contextObject.getClass() : null);
    List<PropertyAccessor> specificAccessors = new ArrayList<>();
    List<PropertyAccessor> generalAccessors = new ArrayList<>();
    // 此处propertyAccessors默认会有5个值,如上截图
    for (PropertyAccessor resolver : propertyAccessors) {
      Class<?>[] targets = resolver.getSpecificTargetClasses();
      if (targets == null) {
        // generic resolver that says it can be used for any type
        // 如果为null没指定类型,那就相当于这个处理器可议处理任意type
        generalAccessors.add(resolver);
      }
      else if (targetType != null) {
        for (Class<?> clazz : targets) {
          //你的处理器指定的targetType和我的完全一样,我才选中这个处理器,加入到specificAccessors
          // 找到了就立马break~~~ 所以我说  他们顺序是很重要的
          // specificAccessors里的值要么为[]  要么只有一个
          if (clazz == targetType) {
            specificAccessors.add(resolver);
            break;
          } 
          // targetType若是clazz的子类,加入到generalAccessors 作为通用的处理器
          else if (clazz.isAssignableFrom(targetType)) {
            generalAccessors.add(resolver);
          }
        }
      }
    }
    // 这里有些神操作~~~resolvers 作为最终返回  
    // 1、 先从generalAccessors里面移除掉了所有已匹配上的~~~比如此处generalAccessors的size为0~
    // 2、移除好后把它在添加进resolvers里面去~~~~
    List<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors);
    generalAccessors.removeAll(specificAccessors);
    resolvers.addAll(generalAccessors);
    return resolvers;
  }
  ...
}


如此,我们每一种待处理的类型,一般最终得到的都是唯一一个resolver,从而完成对该引用的取值。


相关文章
|
2月前
|
Java 开发者 Spring
【SpringBoot 异步魔法】@Async 注解:揭秘 SpringBoot 中异步方法的终极奥秘!
【8月更文挑战第25天】异步编程对于提升软件应用的性能至关重要,尤其是在高并发环境下。Spring Boot 通过 `@Async` 注解简化了异步方法的实现。本文详细介绍了 `@Async` 的基本用法及配置步骤,并提供了示例代码展示如何在 Spring Boot 项目中创建与管理异步任务,包括自定义线程池、使用 `CompletableFuture` 处理结果及异常情况,帮助开发者更好地理解和运用这一关键特性。
125 1
|
2月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
2月前
|
XML Java 测试技术
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
这篇文章介绍了Spring5框架的三个新特性:支持@Nullable注解以明确方法返回、参数和属性值可以为空;引入函数式风格的GenericApplicationContext进行对象注册和管理;以及如何整合JUnit5进行单元测试,同时讨论了JUnit4与JUnit5的整合方法,并提出了关于配置文件加载的疑问。
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
|
2月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
2月前
|
缓存 Java 数据库连接
Spring Boot奇迹时刻:@PostConstruct注解如何成为应用初始化的关键先生?
【8月更文挑战第29天】作为一名Java开发工程师,我一直对Spring Boot的便捷性和灵活性着迷。本文将深入探讨@PostConstruct注解在Spring Boot中的应用场景,展示其在资源加载、数据初始化及第三方库初始化等方面的作用。
53 0
|
8天前
|
Java Spring 容器
Spring使用异步注解@Async正确姿势
Spring使用异步注解@Async正确姿势,异步任务,spring boot
|
7天前
|
XML Java 数据格式
spring复习03,注解配置管理bean
Spring框架中使用注解配置管理bean的方法,包括常用注解的标识组件、扫描组件、基于注解的自动装配以及使用注解后的注意事项,并提供了一个基于注解自动装配的完整示例。
spring复习03,注解配置管理bean
|
8天前
|
XML 前端开发 Java
控制spring框架注解介绍
控制spring框架注解介绍
|
21天前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
2月前
|
XML Java 数据格式
Spring5入门到实战------2、IOC容器底层原理
这篇文章深入探讨了Spring5框架中的IOC容器,包括IOC的概念、底层原理、以及BeanFactory接口和ApplicationContext接口的介绍。文章通过图解和实例代码,解释了IOC如何通过工厂模式和反射机制实现对象的创建和管理,以及如何降低代码耦合度,提高开发效率。
Spring5入门到实战------2、IOC容器底层原理
下一篇
无影云桌面