全球新资讯:深度解析@Value注解,你真的彻底了解过吗?
一、学习指引Spring中的@Value注解,你真的彻底了解过吗?在实际开发过程中,通常会有这样一种场景:将一些
2023-05-29Spring中的@Value注解,你真的彻底了解过吗?
在实际开发过程中,通常会有这样一种场景:将一些配置项写到配置文件中,在业务逻辑中会读取配置文件中的配置项,取出对应的值进行业务逻辑处理。Spring中提供的@Value注解就可以读取配置文件中的值。另外@Value注解也可以向Bean中的属性设置其他值。本章,就对@Value注解进行简单的介绍。
【资料图】
二、注解说明关于@Value注解的一点点说明~~
@Value注解可以向Spring的Bean的属性中注入数据。并且支持Spring的EL表达式,可以通过${} 的方式获取配置文件中的数据。配置文件支持properties、XML、和YML文件。
1、注解源码@Value注解的源码详见:org.springframework.beans.factory.annotation.Value。
/** * @author Juergen Hoeller * @since 3.0 * @see AutowiredAnnotationBeanPostProcessor * @see Autowired * @see org.springframework.beans.factory.config.BeanExpressionResolver * @see org.springframework.beans.factory.support.AutowireCandidateResolver#getSuggestedValue */@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Value { String value();}
从源码可以看出,@Value注解可以标注到字段、方法、参数和其他注解上,@Value注解中提供了一个String类型的value属性,具体含义如下所示。
value:指定要向Bean的属性中注入的数据,数据可以是配置文件中的配置项,并且支持EL表达式。2、使用场景在实际开发中,项目中难免会有一些配置信息,此时,就可以将这些配置信息统一写到配置文件中。随后使用@Value注解读取配置文件的值来向Spring中Bean的属性设置值。
例如,一些系统环境变量信息,数据库配置,系统通用配置等等,都可以保存到配置文件中,此时就可以使用Spring的EL表达式读取配置文件中的值。
3、用法本节,主要介绍不通过配置文件注入属性和通过配置文件注入属性两种情况来介绍@Value注解的用法。
不通过配置文件注入属性通过@Value可以将外部的值动态注入到Bean中,有如下几种用法。
(1)注入普通字符串。
@Value("normalString")private String normalString;
(2)注入操作系统属性。
@Value("#{systemProperties["os.name"]}")private String osName;
(3)注入表达式的结果信息。
@Value("#{ T(java.lang.Math).random() * 100.0 }")private double randomNum;
(4)注入其他Bean属性。
@Value("#{otherBean.name}")private String name;
(5)注入文件资源。
@Value("classpath:config.properties")private Resource resourceFile;
(6)注入URL资源。
@Value("http://www.baidu.com")private Resource url;
通过配置文件注入属性通过@Value(“${app.name}”)语法将属性文件的值注入到bean的属性中,
@Component@PropertySource({"classpath:config.properties","classpath:config_${anotherfile.configinject}.properties"})public class ConfigurationFileInject{ @Value("${user.id}") private String userId; @Value("${user.name}") private String userName; @Value("${user.address}") private String userAddress; }
@Value中#{...}和${...}的区别这里提供一个测试属性文件:test.properties,大致的内容如下所示。
server.name=server1,server2,server3author.name=binghe
测试类Test:引入test.properties文件,作为属性的注入。
@Component@PropertySource({"classpath:test.properties"})public class Test {}
${...}的用法{}里面的内容必须符合SpEL表达式, 通过@Value(“${spelDefault.value}”)可以获取属性文件中对应的值,但是如果属性文件中没有这个属性,则会报错。可以通过赋予默认值解决这个问题,如下所示。
@Value("${author.name:binghe}")
上述代码的含义表示向Bean的属性中注入配置文件中的author.name属性的值,如果配置文件中没有author.name属性,则向Bean的属性中注入默认值binghe。例如下面的代码片段。
@Value("${author.name:binghe}")private String name;
#{…}的用法(1)SpEL:调用字符串Hello World的concat方法
@Value("#{"Hello World".concat("!")}")private String helloWorld;
(2)SpEL: 调用字符串的getBytes方法,然后调用length属性
@Value("#{"Hello World".bytes.length}")private int length;
${…}和#{…}混合使用${...}和#{...}可以混合使用,如下文代码执行顺序:传入一个字符串,根据 "," 切分后插入列表中,#{}和${}配合使用,注意单引号。
@Value("#{"${server.name}".split(",")}")private List servers;
注意:${}和#{}混合实用时,不能${}在外面,#{}在里面。因为Spring执行${}的时机要早于#{},当Spring执行外层的${}时,内部的#{}为空,会执行失败。
@Value注解用法总结#{…}用于执行SpEl表达式,并将内容赋值给属性。${…}主要用于加载外部属性文件中的值。#{…}和${…}可以混合使用,但是必须#{}外面,${}在里面。三、使用案例@Value的实现案例,我们一起实现吧~~
本节,就基于@Value注解实现向Bean属性中赋值的案例,具体的实现步骤如下所示。
1、新增test.properties配置文件在spring-annotation-chapter-11工程下的resources目录下新增test.properties配置文件,内容如下所示。
db.url=jdbc:mysql://localhost:3306/test
2、新增ValueName类ValueName类的源码详见:spring-annotation-chapter-11工程下的io.binghe.spring.annotation.chapter11.bean.ValueName。
@Componentpublic class ValueName { private String name; public ValueName() { this.name = "binghe"; } public String getName() { return name; } public void setName(String name) { this.name = name; }}
可以看到,ValueName类上标注了@Component注解,说明当Spring的IOC容器启动时,会向IOC容器中注入ValueName类的Bean对象。
3、新增ValueConfig类ValueConfig类的源码详见:spring-annotation-chapter-11工程下的io.binghe.spring.annotation.chapter11.config.ValueConfig。
@Configuration@ComponentScan(value = {"io.binghe.spring.annotation.chapter11"})@PropertySource(value = {"classpath:test.properties"})public class ValueConfig { /** * 注入普通字符串 */ @Value("normalString") private String normalString; /** * 注入操作系统名称 */ @Value("#{systemProperties["os.name"]}") private String osName; /** * 注入表达式的结果 */ @Value("#{ T(java.lang.Math).random() * 100.0 }") private double randomNum; /** * 注入其他Bean的属性 */ @Value("#{valueName.name}") private String name; /** * 注入配置文件中的值 */ @Value("${db.url}") private String dbUrl; @Override public String toString() { return "ValueConfig{" + "normalString="" + normalString + "\"" + ", osName="" + osName + "\"" + ", randomNum=" + randomNum + ", name="" + name + "\"" + ", dbUrl="" + dbUrl + "\"" + "}"; }}
可以看到,在ValueConfig类上标注了@Configuration注解,说明ValueConfig类是Spring的配置类。使用@ComponentScan注解指定了扫描的包名是io.binghe.spring.annotation.chapter11。并且使用@PropertySource注解导入了test.properties配置文件。ValueConfig类的字段通过@Value注解注入对应的属性值,代码中有详细的注释,这里不再赘述。
4、新增ValueTest类ValueTest类的源码详见:spring-annotation-chapter-11工程下的io.binghe.spring.annotation.chapter11.ValueTest。
public class ValueTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueConfig.class); ValueConfig valueConfig = context.getBean(ValueConfig.class); System.out.println(valueConfig.toString()); }}
可以看到,ValueTest类是案例程序的测试类,实现的代码比较简单,这里不再赘述。
5、运行ValueTest类运行ValueTest类的main()方法,输出的结果信息如下所示。
ValueConfig{normalString="normalString", osName="Windows 10", randomNum=60.704013358598715, name="binghe", dbUrl="jdbc:mysql://localhost:3306/test"}
可以看到,在ValueTest类中的各个字段值都输出了正确的结果数据。
说明:使用@Value注解向Bean的属性中正确设置了值。
四、源码时序图结合时序图理解源码会事半功倍,你觉得呢?
本节,就以源码时序图的方式,直观的感受下@Value注解在Spring源码层面的执行流程。本节,会从解析并获取 @Value 修饰的属性、为 @Value 修饰属性赋值和使用@Value获取属性值三个方面分析源码时序图。
注意:本节以单例Bean为例分析源码时序图,并且基于@Value注解标注到类的字段上的源码时序图为例进行分析,@Value注解标注到类的方法上的源码时序图与标注到字段上的源码时序图基本相同,不再赘述。
1、解析并获取@Value修饰的属性本节,就简单介绍下解析并获取@Value修饰的属性的源码时序图,整体如图11-1~11-2所示。
由图11-1~11-2可以看出,解析并获取@Value修饰的属性的流程中涉及到ValueTest类、AnnotationConfigApplicationContext类、AbstractApplicationContext类、DefaultListableBeanFactory类、AbstractBeanFactory类、AbstractAutowireCapableBeanFactory类和AutowiredAnnotationBeanPostProcessor类。具体的源码执行细节参见源码解析部分。
2、为@Value修饰的属性赋值本节,就简单介绍下为@Value修饰的属性赋值的源码时序图,整体如图11-3~11-4所示。
由图11-3~11-4所示,为@Value修饰的属性赋值流程涉及到ValueTest类、AnnotationConfigApplicationContext类、AbstractApplicationContext类、DefaultListableBeanFactory类、AbstractBeanFactory类、AbstractAutowireCapableBeanFactory类、AutowiredAnnotationBeanPostProcessor类、InjectionMetadata类和AutowiredFieldElement类。具体的源码执行细节参见源码解析部分。
3、使用@Value获取属性的值本节,就简单介绍下使用@Value注解获取属性的值的源码时序图,整体如图11-5~11-7所示。
由图11-5~11-7所示,使用@Value获取属性的值的流程涉及到ValueTest类、AnnotationConfigApplicationContext类、AbstractApplicationContext类、DefaultListableBeanFactory类、AbstractBeanFactory类、AbstractAutowireCapableBeanFactory类、AutowiredAnnotationBeanPostProcessor类、InjectionMetadata类、AutowiredFieldElement类、AbstractEnvironment类、AbstractPropertyResolver类、PropertyPlaceholderHelper类和PropertySourcesPropertyResolver类。具体的源码执行细节参见源码解析部分。
五、源码解析源码时序图整清楚了,那就整源码解析呗!
本节,主要分析@Value注解在Spring源码层面的执行流程,同样的,本节也会从解析并获取 @Value 修饰的属性、为 @Value 修饰属性赋值和使用@Value获取属性值三个方面分析源码执行流程,并且结合源码执行的时序图,会理解的更加深刻。
注意:本节以单例Bean为例分析,并且基于@Value注解标注到类的字段上的源码流程为例进行分析,@Value注解标注到类的方法上的源码流程与标注到字段上的源码流程基本相同,不再赘述。
1、解析并获取@Value修饰的属性本节主要对解析并获取 @Value 修饰属性的源码流程进行简单的分析,结合源码执行的时序图,会理解的更加深刻,本节的源码执行流程可以结合图11-1~11-2进行理解。具体分析步骤如下所示。
注意:解析并获取 @Value 修饰属性源码流程的前半部分与第7章5.3节分析源码的流程相同,这里,从AbstractBeanFactory类的doGetBean()方法开始分析。
(1)解析AbstractBeanFactory类的doGetBean(String name, ClassrequiredType, Object[] args, boolean typeCheckOnly)方法
源码详见:org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean(String name, ClassrequiredType, Object[] args, boolean typeCheckOnly)。重点关注如下代码片段。
protected T doGetBean( String name, @Nullable Class requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException { /***********省略其他代码***********/ if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } }); beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } /***********省略其他代码***********/ return adaptBeanInstance(name, beanInstance, requiredType);}
可以看到,在AbstractBeanFactory类的doGetBean()方法中,如果是单例Bean,会调用getSingleton()方法创建单例Bean,实际执行的是Lambda表达式中的createBean()方法来创建单例Bean。
(2)解析AbstractAutowireCapableBeanFactory类的createBean(String beanName, RootBeanDefinition mbd, Object[] args)。
源码详见:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)。
@Overrideprotected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { /**************省略其他代码***************/ try { Object beanInstance = doCreateBean(beanName, mbdToUse, args); if (logger.isTraceEnabled()) { logger.trace("Finished creating instance of bean "" + beanName + """); } return beanInstance; } catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException( mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex); }}
可以看到,在AbstractAutowireCapableBeanFactory类的createBean()方法中,会调用doCreateBean()方法创建Bean对象。
(3)解析AbstractAutowireCapableBeanFactory类的doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)方法
源码详见:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)。此时重点关注创建Bean实例的代码片段,如下所示。
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } /***********省略其他代码**********/ return exposedObject;}
(4)解析AbstractAutowireCapableBeanFactory类的(String beanName, RootBeanDefinition mbd, Object[] args)方法
源码详见:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args)。
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) { // Make sure bean class is actually resolved at this point. Class> beanClass = resolveBeanClass(mbd, beanName); if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Bean class isn"t public, and non-public access not allowed: " + beanClass.getName()); } Supplier> instanceSupplier = mbd.getInstanceSupplier(); if (instanceSupplier != null) { return obtainFromSupplier(instanceSupplier, beanName); } if (mbd.getFactoryMethodName() != null) { return instantiateUsingFactoryMethod(beanName, mbd, args); } boolean resolved = false; boolean autowireNecessary = false; if (args == null) { synchronized (mbd.constructorArgumentLock) { if (mbd.resolvedConstructorOrFactoryMethod != null) { resolved = true; autowireNecessary = mbd.constructorArgumentsResolved; } } } if (resolved) { if (autowireNecessary) { return autowireConstructor(beanName, mbd, null, null); } else { return instantiateBean(beanName, mbd); } } Constructor>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR || mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { return autowireConstructor(beanName, mbd, ctors, args); } ctors = mbd.getPreferredConstructors(); if (ctors != null) { return autowireConstructor(beanName, mbd, ctors, null); } return instantiateBean(beanName, mbd);}
可以看到,createBeanInstance()方法会创建Bean的实例并返回BeanWrapper对象。
(5)返回AbstractAutowireCapableBeanFactory类的doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)方法,此时,重点关注如下代码片段。
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { /*************省略其他代码***************/ synchronized (mbd.postProcessingLock) { if (!mbd.postProcessed) { try { applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", ex); } mbd.markAsPostProcessed(); } } /*************省略其他代码***************/}
可以看到,在AbstractAutowireCapableBeanFactory类的doCreateBean()方法中会调用applyMergedBeanDefinitionPostProcessors()方法的主要作用就是:获取@Value、@Autowired、@PostConstruct、@PreDestroy等注解标注的字段和方法,然后封装到InjectionMetadata对象中,最后将所有的InjectionMetadata对象存入injectionMeatadataCache缓存中。
(6)解析AbstractAutowireCapableBeanFactory类的applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class> beanType, String beanName)方法
源码详见:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class> beanType, String beanName)。
protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class> beanType, String beanName) { for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) { processor.postProcessMergedBeanDefinition(mbd, beanType, beanName); }}
可以看到,在AbstractAutowireCapableBeanFactory类的applyMergedBeanDefinitionPostProcessors()方法中,会调用processor的postProcessMergedBeanDefinition()方法处理BeanDefinition信息。
(7)解析AutowiredAnnotationBeanPostProcessor类postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class> beanType, String beanName)。
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class> beanType, String beanName)
@Overridepublic void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class> beanType, String beanName) { findInjectionMetadata(beanName, beanType, beanDefinition);}
可以看到,在AutowiredAnnotationBeanPostProcessor类postProcessMergedBeanDefinition()方法中会调用findInjectionMetadata()方法来获取标注了注解的字段或者方法。
(8)解析AutowiredAnnotationBeanPostProcessor类的findInjectionMetadata(String beanName, Class> beanType, RootBeanDefinition beanDefinition)
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#findInjectionMetadata(String beanName, Class> beanType, RootBeanDefinition beanDefinition)。
private InjectionMetadata findInjectionMetadata(String beanName, Class> beanType, RootBeanDefinition beanDefinition) { InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); metadata.checkConfigMembers(beanDefinition); return metadata;}
可以看到,在AutowiredAnnotationBeanPostProcessor类的findInjectionMetadata()方法中,调用了findAutowiringMetadata方法来解析并获取@Value、@Autowired、@Inject等注解修饰的属性或者方法。
(9)解析AutowiredAnnotationBeanPostProcessor类的findAutowiringMetadata(String beanName, Class> clazz, PropertyValues pvs)方法
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata(String beanName, Class> clazz, PropertyValues pvs)。
private InjectionMetadata findAutowiringMetadata(String beanName, Class> clazz, @Nullable PropertyValues pvs) { String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { synchronized (this.injectionMetadataCache) { metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { if (metadata != null) { metadata.clear(pvs); } metadata = buildAutowiringMetadata(clazz); this.injectionMetadataCache.put(cacheKey, metadata); } } } return metadata;}
AutowiredAnnotationBeanPostProcessor类的findAutowiringMetadata()方法需要重点关注下,findAutowiringMetadata()方法最核心的功能就是对传递进来的每个类进行筛选判断是否被@Value、@Autowired、@Inject注解修饰的方法或者属性,如果是被 @Value、@Autowired、@Inject注解修饰的方法或者属性,就会将这个类记录下来,存入injectionMetadataCache缓存中,为后续的 DI 依赖注作准备。
首次调用findAutowiringMetadata()方法时,会调用buildAutowiringMetadata()方法来查找使用@Value、@Autowired、@Inject注解修饰的方法或者属性。
(10)解析AutowiredAnnotationBeanPostProcessor类的buildAutowiringMetadata(Class> clazz)方法。
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata(Class> clazz)。方法中会查找@Value、@Autowired、@Inject注解修饰的方法或者属性,这里以查找属性为例,重点关注如下代码片段。
private InjectionMetadata buildAutowiringMetadata(Class> clazz) { if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) { return InjectionMetadata.EMPTY; } List elements = new ArrayList<>(); Class> targetClass = clazz; do { final List currElements = new ArrayList<>(); ReflectionUtils.doWithLocalFields(targetClass, field -> { MergedAnnotation> ann = findAutowiredAnnotation(field); if (ann != null) { if (Modifier.isStatic(field.getModifiers())) { if (logger.isInfoEnabled()) { logger.info("Autowired annotation is not supported on static fields: " + field); } return; } boolean required = determineRequiredStatus(ann); currElements.add(new AutowiredFieldElement(field, required)); } }); /**************省略其他代码****************/ elements.addAll(0, currElements); targetClass = targetClass.getSuperclass(); } while (targetClass != null && targetClass != Object.class); return InjectionMetadata.forElements(elements, clazz);}
可以看到,在AutowiredAnnotationBeanPostProcessor类的buildAutowiringMetadata()方法中,获取到类上所有的字段,然后遍历每个字段,判断是否标注了 @Value、@Autowired和@Inject注解,如果标注了 @Value、@Autowired和@Inject注解,直接封装成 AutowiredFieldElement 对象,然后保存到一个名为 currElements集合中。
指的一提的是,如果解析到的字段是静态字段,则直接返回,这就是为什么Spring不会对类中的静态字段赋值的原因。如下代码片段所示。
if (Modifier.isStatic(field.getModifiers())) { if (logger.isInfoEnabled()) { logger.info("Autowired annotation is not supported on static fields: " + field); } return;}
在AutowiredAnnotationBeanPostProcessor类的buildAutowiringMetadata()方法的最后,则将标注了@Value、@Autowired和@Inject注解的字段封装到 InjectionMetadata 对象中,如下所示。
return InjectionMetadata.forElements(elements, clazz);
最终回到AutowiredAnnotationBeanPostProcessor类的findAutowiringMetadata()方法中,将InjectionMetadata 对象存入injectionMetadataCache缓存中。如下所示。
metadata = buildAutowiringMetadata(clazz);this.injectionMetadataCache.put(cacheKey, metadata);
另外,在AutowiredAnnotationBeanPostProcessor类的buildAutowiringMetadata()方法中,调用了findAutowiredAnnotation()方法来获取注解信息,如下所示。
MergedAnnotation> ann = findAutowiredAnnotation(field);
接下来,看看这个findAutowiredAnnotation()方法。
(11)解析AutowiredAnnotationBeanPostProcessor类的findAutowiredAnnotation(AccessibleObject ao)方法
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#findAutowiredAnnotation(AccessibleObject ao)。
private MergedAnnotation> findAutowiredAnnotation(AccessibleObject ao) { MergedAnnotations annotations = MergedAnnotations.from(ao); for (Class extends Annotation> type : this.autowiredAnnotationTypes) { MergedAnnotation> annotation = annotations.get(type); if (annotation.isPresent()) { return annotation; } } return null;}
可以看到,在AutowiredAnnotationBeanPostProcessor类的findAutowiredAnnotation()方法中,会遍历autowiredAnnotationTypes集合,通过遍历出的每个autowiredAnnotationTypes集合中的元素从annotations中获取MergedAnnotation对象annotation,如果annotation存在,则返回annotation。否则返回null。
这里,需要关注下autowiredAnnotationTypes集合,在AutowiredAnnotationBeanPostProcessor类的构造方法中向autowiredAnnotationTypes集合中添加元素,如下所示。
public AutowiredAnnotationBeanPostProcessor() { this.autowiredAnnotationTypes.add(Autowired.class); this.autowiredAnnotationTypes.add(Value.class); try { this.autowiredAnnotationTypes.add((Class extends Annotation>) ClassUtils.forName("jakarta.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); logger.trace(""jakarta.inject.Inject" annotation found and supported for autowiring"); } catch (ClassNotFoundException ex) { // jakarta.inject API not available - simply skip. } try { this.autowiredAnnotationTypes.add((Class extends Annotation>) ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); logger.trace(""javax.inject.Inject" annotation found and supported for autowiring"); } catch (ClassNotFoundException ex) { // javax.inject API not available - simply skip. }}
可以看到,在AutowiredAnnotationBeanPostProcessor类的构造方法中,向autowiredAnnotationTypes集合中添加了@Autowired注解、@Value注解和@Inject注解。所以,@Autowired注解赋值的流程和@Value注解赋值的流程基本一致。
至此,解析并获取@Value注解修饰的属性的源码流程分析完毕。
2、为@Value修饰的属性赋值本节主要对为@Value修饰的属性赋值的源码流程进行简单的分析,结合源码执行的时序图,会理解的更加深刻,本节的源码执行流程可以结合图11-3~11-4进行理解。具体分析步骤如下所示。
注意:为@Value修饰的属性赋值的源码流程的前半部分与本章5.1节分析源码的流程相同,这里,同样从AbstractAutowireCapableBeanFactory类的doCreateBean()方法开始分析。
(1)解析AbstractAutowireCapableBeanFactory类的doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)方法
源码详见:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)。重点关注如下代码片段。
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { /************省略其他代码*************/ Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } catch (Throwable ex) { if (ex instanceof BeanCreationException bce && beanName.equals(bce.getBeanName())) { throw bce; } else { throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); } } /************省略其他代码*************/ return exposedObject;}
可以看到,在AbstractAutowireCapableBeanFactory类的doCreateBean()方法中,会调用populateBean方法为Bean的属性赋值。
(2)解析AbstractAutowireCapableBeanFactory类的populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw)方法
源码详见:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw)。
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) { /**************省略其他代码*************/ if (hasInstantiationAwareBeanPostProcessors()) { if (pvs == null) { pvs = mbd.getPropertyValues(); } for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) { PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName); if (pvsToUse == null) { return; } pvs = pvsToUse; } } /**************省略其他代码*************/}
可以看到,在populateBean()方法中,会调用InstantiationAwareBeanPostProcessor类的postProcessProperties()方法来处理属性或方法的值。实际上是调用的AutowiredAnnotationBeanPostProcessor类的postProcessProperties()方法。
(3)解析AutowiredAnnotationBeanPostProcessor类的postProcessProperties(PropertyValues pvs, Object bean, String beanName)方法
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties(PropertyValues pvs, Object bean, String beanName)。
@Overridepublic PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try { metadata.inject(bean, beanName, pvs); } catch (BeanCreationException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex); } return pvs;}
可以看到,在AutowiredAnnotationBeanPostProcessor类的postProcessProperties()方法中,会调用findAutowiringMetadata()方法获取注解的元数据信息。
(4)解析AutowiredAnnotationBeanPostProcessor类的findAutowiringMetadata(String beanName, Class> clazz, @Nullable PropertyValues pvs)方法
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata(String beanName, Class> clazz, @Nullable PropertyValues pvs)。
private InjectionMetadata findAutowiringMetadata(String beanName, Class> clazz, @Nullable PropertyValues pvs) { String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { synchronized (this.injectionMetadataCache) { metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { if (metadata != null) { metadata.clear(pvs); } metadata = buildAutowiringMetadata(clazz); this.injectionMetadataCache.put(cacheKey, metadata); } } } return metadata;}
由于在之前解析并获取@Value修饰的属性的代码流程中,已经完成了对@Value 修饰的属性的获取工作。所以,程序执行到findAutowiringMetadata()方法内部时,injectionMetadataCache缓存中已经有数据了。
(5)返回AutowiredAnnotationBeanPostProcessor类的postProcessProperties(PropertyValues pvs, Object bean, String beanName)方法。在AutowiredAnnotationBeanPostProcessor类的postProcessProperties()方法中,调用了metadata对象的inject()方法为属性赋值。
(6)解析InjectionMetadata类的inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs)方法
源码详见:org.springframework.beans.factory.annotation.InjectionMetadata#inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs)。
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { Collection checkedElements = this.checkedElements; Collection elementsToIterate = (checkedElements != null ? checkedElements : this.injectedElements); if (!elementsToIterate.isEmpty()) { for (InjectedElement element : elementsToIterate) { element.inject(target, beanName, pvs); } }}
可以看到,在InjectionMetadata类的inject()方法中,会循环遍历checkedElements集合,调用遍历出的每个InjectedElement对象的inject()方法为属性赋值。
注意:调用InjectedElement对象的inject()方法时,实际上可能会调用AutowiredFieldElement类的inject()方法、AutowiredMethodElement类的inject()方法或者InjectedElement类的inject()方法。这里,以调用AutowiredFieldElement类的inject()方法为例进行说明。
(7)解析AutowiredFieldElement类的inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs)方法
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs)。
@Overrideprotected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { Field field = (Field) this.member; Object value; if (this.cached) { try { value = resolvedCachedArgument(beanName, this.cachedFieldValue); } catch (NoSuchBeanDefinitionException ex) { value = resolveFieldValue(field, bean, beanName); } } else { value = resolveFieldValue(field, bean, beanName); } if (value != null) { ReflectionUtils.makeAccessible(field); field.set(bean, value); }}
可以看到,在AutowiredFieldElement类的inject()方法中,会调用resolveFieldValue()方法来获取对应的属性值,如下所示。
value = resolveFieldValue(field, bean, beanName);
并通过反射向使用@Value注解标注的字段赋值,如下所示。
field.set(bean, value);
(8)返回AbstractAutowireCapableBeanFactory类的doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)方法。再来看下源码:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { /************省略其他代码*************/ Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } catch (Throwable ex) { if (ex instanceof BeanCreationException bce && beanName.equals(bce.getBeanName())) { throw bce; } else { throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); } } /************省略其他代码*************/ return exposedObject;}
可以看到,在AbstractAutowireCapableBeanFactory类的doCreateBean()方法中,为Bean的属性赋值后会调用initializeBean()方法对Bean进行初始化。
(9)解析AbstractAutowireCapableBeanFactory类的initializeBean(String beanName, Object bean, RootBeanDefinition mbd)方法
源码详见:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(String beanName, Object bean, RootBeanDefinition mbd)。
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) { invokeAwareMethods(beanName, bean); Object wrappedBean = bean; if (mbd == null || !mbd.isSynthetic()) { wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); } try { invokeInitMethods(beanName, wrappedBean, mbd); } catch (Throwable ex) { throw new BeanCreationException( (mbd != null ? mbd.getResourceDescription() : null), beanName, ex.getMessage(), ex); } if (mbd == null || !mbd.isSynthetic()) { wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); } return wrappedBean;}
可以看到,在AbstractAutowireCapableBeanFactory类的initializeBean()方法中,会调用applyBeanPostProcessorsBeforeInitialization()方法在初始化之前执行一些逻辑,然后调用invokeInitMethods()执行真正的初始化操作,执行完Bean的初始化,会调用applyBeanPostProcessorsAfterInitialization()方法执行初始化之后的一些逻辑。
至此,为@Value修饰的属性赋值的源码流程分析完毕。
3、使用@Value获取属性的值本节主要对使用@Value获取属性的值的源码流程进行简单的分析,结合源码执行的时序图,会理解的更加深刻,本节的源码执行流程可以结合图11-5~11-7进行理解。具体分析步骤如下所示。
注意:使用@Value获取属性的值的源码流程的前半部分与本章5.2节分析源码的流程相同,这里,从AutowiredFieldElement类的inject()方法开始分析。
(1)解析AutowiredFieldElement类的inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs)方法
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs)。
@Overrideprotected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { Field field = (Field) this.member; Object value; if (this.cached) { try { value = resolvedCachedArgument(beanName, this.cachedFieldValue); } catch (NoSuchBeanDefinitionException ex) { // Unexpected removal of target bean for cached argument -> re-resolve value = resolveFieldValue(field, bean, beanName); } } else { value = resolveFieldValue(field, bean, beanName); } if (value != null) { ReflectionUtils.makeAccessible(field); field.set(bean, value); }}
可以看到,在AutowiredFieldElement类的inject()方法中,会调用resolveFieldValue()方法处理获取属性的值。
(2)解析AutowiredFieldElement类的resolveFieldValue(Field field, Object bean, @Nullable String beanName)方法
源码详见:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#resolveFieldValue(Field field, Object bean, @Nullable String beanName)。
@Nullableprivate Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) { /*************省略其他代码************/ Object value; try { value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); } catch (BeansException ex) { throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); } /*************省略其他代码************/ return value;}
可以看到,在AutowiredFieldElement类的resolveFieldValue()方法中,会调用beanFactory对象的resolveDependency()方法,继续向下分析。
(3)解析DefaultListableBeanFactory类的resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable SetautowiredBeanNames, @Nullable TypeConverter typeConverter)
源码详见:org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable SetautowiredBeanNames, @Nullable TypeConverter typeConverter)。
@Override@Nullablepublic Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { /***************省略其他代码**************/ else { Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(descriptor, requestingBeanName); if (result == null) { result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); } return result; }}
可以看到,在DefaultListableBeanFactory类的resolveDependency()方法中,会调用doResolveDependency()方法进一步处理。
(4)解析DefaultListableBeanFactory类的doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable SetautowiredBeanNames, @Nullable TypeConverter typeConverter)方法
源码详见:org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable SetautowiredBeanNames, @Nullable TypeConverter typeConverter)。
@Nullablepublic Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { /************省略其他代码*************/ Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); if (value != null) { if (value instanceof String strValue) { String resolvedValue = resolveEmbeddedValue(strValue); BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); value = evaluateBeanDefinitionString(resolvedValue, bd); } TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); try { return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor()); } catch (UnsupportedOperationException ex) { // A custom TypeConverter which does not support TypeDescriptor resolution... return (descriptor.getField() != null ? converter.convertIfNecessary(value, type, descriptor.getField()) : converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); } } /************省略其他代码*************/}
可以看到,在DefaultListableBeanFactory类的doResolveDependency()方法中,如果当前获取到的数据是String类型,则调用resolveEmbeddedValue()方法进行处理。
(5)解析AbstractBeanFactory类的resolveEmbeddedValue(@Nullable String value)方法
源码详见:org.springframework.beans.factory.support.AbstractBeanFactory#resolveEmbeddedValue(@Nullable String value)。
@Override@Nullablepublic String resolveEmbeddedValue(@Nullable String value) { if (value == null) { return null; } String result = value; for (StringValueResolver resolver : this.embeddedValueResolvers) { result = resolver.resolveStringValue(result); if (result == null) { return null; } } return result;}
可以看到,在AbstractBeanFactory类的resolveEmbeddedValue()中,会调用遍历出来的StringValueResolver对象的resolveStringValue()方法进行处理。此时,会进入AbstractEnvironment类的resolvePlaceholders(String text)方法。
(6)解析AbstractEnvironment类的resolvePlaceholders(String text)方法
源码详见:org.springframework.core.env.AbstractEnvironment#resolvePlaceholders(String text)。
@Overridepublic String resolvePlaceholders(String text) { return this.propertyResolver.resolvePlaceholders(text);}
可以看到,在AbstractEnvironment类的resolvePlaceholders()方法中,会调用propertyResolver对象的resolvePlaceholders()方法进行处理。
(7)解析AbstractPropertyResolver类的resolvePlaceholders(String text)方法
源码详见:org.springframework.core.env.AbstractPropertyResolver#resolvePlaceholders(String text)。
@Overridepublic String resolvePlaceholders(String text) { if (this.nonStrictHelper == null) { this.nonStrictHelper = createPlaceholderHelper(true); } return doResolvePlaceholders(text, this.nonStrictHelper);}
可以看到,在AbstractPropertyResolver类的resolvePlaceholders()方法中,会调用doResolvePlaceholders()方法进一步处理。
(8)解析AbstractPropertyResolver类的doResolvePlaceholders(String text, PropertyPlaceholderHelper helper)方法
源码详见:org.springframework.core.env.AbstractPropertyResolver#doResolvePlaceholders(String text, PropertyPlaceholderHelper helper)。
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) { return helper.replacePlaceholders(text, this::getPropertyAsRawString);}
可以看到,在AbstractPropertyResolver类的doResolvePlaceholders()方法中,会解析 ${xxx.xxx} 这种占位,最终获取到 key = xxx.xxx,随后根据key去资源文件(xml、application.properties、Environment 等)中查找是否配置了这个key的值。其实是通过调用helper的replacePlaceholders()方法并以Lambda表达式的方式传入getPropertyAsRawString()方法实现的。
(9)解析PropertyPlaceholderHelper类的replacePlaceholders(String value, PlaceholderResolver placeholderResolver)方法
源码详见:org.springframework.util.PropertyPlaceholderHelper#replacePlaceholders(String value, PlaceholderResolver placeholderResolver)。
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { Assert.notNull(value, ""value" must not be null"); return parseStringValue(value, placeholderResolver, null);}
可以看到,在PropertyPlaceholderHelper类的replacePlaceholders()方法中,会调用parseStringValue()方法解析String类型的数据。
(10)解析PropertyPlaceholderHelper类的parseStringValue(String value, PlaceholderResolver placeholderResolver, @Nullable SetvisitedPlaceholders)方法
源码详见:org.springframework.util.PropertyPlaceholderHelper#parseStringValue(String value, PlaceholderResolver placeholderResolver, @Nullable SetvisitedPlaceholders)。
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver, @Nullable Set visitedPlaceholders) { /***************省略其他代码****************/ // Now obtain the value for the fully resolved key... String propVal = placeholderResolver.resolvePlaceholder(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()); propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder); if (propVal == null) { propVal = defaultValue; } } } /**********省略其他代码***********/}
在PropertyPlaceholderHelper类的parseStringValue()方法中重点关注如下代码片段。
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
会调用placeholderResolver对象的resolvePlaceholder()方法传入解析 ${xxx.xxx} 占位符,获取到的key,其中key的形式为xxx.xxx。调用placeholderResolver对象的resolvePlaceholder()方法会最终调用PropertySourcesPropertyResolver类的getPropertyAsRawString()方法。
(11)解析PropertySourcesPropertyResolver类的getPropertyAsRawString(String key)方法
源码详见:org.springframework.core.env.PropertySourcesPropertyResolver#getPropertyAsRawString(String key)。
@Override@Nullableprotected String getPropertyAsRawString(String key) { return getProperty(key, String.class, false);}
可以看到,在getPropertyAsRawString()方法中,会调用getProperty()方法获取属性的值。在调用getPropertyAsRawString()方法时,传入的Key的形式的规则就是:如果使用@Value标注的属性为 ${xxx.xxx} 占位符,则此处传入的Key的形式为xxx.xxx。
(12)解析PropertySourcesPropertyResolver类的getProperty(String key, ClasstargetValueType, boolean resolveNestedPlaceholders)方法
源码详见:org.springframework.core.env.PropertySourcesPropertyResolver#getProperty(String key, ClasstargetValueType, boolean resolveNestedPlaceholders)。
@Nullableprotected T getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { for (PropertySource> propertySource : this.propertySources) { if (logger.isTraceEnabled()) { logger.trace("Searching for key "" + key + "" in PropertySource "" + propertySource.getName() + """); } Object value = propertySource.getProperty(key); if (value != null) { if (resolveNestedPlaceholders && value instanceof String string) { value = resolveNestedPlaceholders(string); } logKeyFound(key, propertySource, value); return convertValueIfNecessary(value, targetValueType); } } } if (logger.isTraceEnabled()) { logger.trace("Could not find key "" + key + "" in any property source"); } return null;}
在PropertySourcesPropertyResolver类的getProperty()方法中,会从 propertySources 资源中获取 key = xxx.xxx 的值,如果获取到一个对应的值,就会直接返回。其中,在propertySources中会封装PropertiesPropertySource、SystemEnvironmentPropertySource和ResourcePropertySource类型的对象,如图11-8所示。
其中,每种类型的对象中封装的信息如下所示。
PropertiesPropertySource:封装 JVM 环境变量中的键值对。SystemEnvironmentPropertySource:封装操作系统环境变量中的键值对。ResourcePropertySource:封装项目中application.properties、yml和xml等文件中的键值对。通过调试PropertySourcesPropertyResolver类的getProperty()方法,可以发现,ResourcePropertySource对象中获取到对应的值,如图11-9所示。
从ResourcePropertySource对象中获取到对应的值就可以设置到被@Value注解标注的字段上。
至此,使用@Value获取属性的值的源码流程分析完毕。
六、总结@Value注解介绍完了,我们一起总结下吧!
本章,详细介绍了@Value注解,首先介绍了@Value注解的源码和使用场景,并且对@Value注解的用法进行了简单的介绍。随后,介绍了@Value的使用案例。接下来,从解析并获取 @Value 修饰的属性、为 @Value 修饰属性赋值和使用@Value获取属性值三个方面分别详细介绍了@Value注解在Spring底层执行的源码时序图和源码流程。
标签:
一、学习指引Spring中的@Value注解,你真的彻底了解过吗?在实际开发过程中,通常会有这样一种场景:将一些
2023-05-29很多人对C919冲上云霄壮观一幕具体是什么情况比较关心,现在让我们一起来瞧瞧具体是什么情况吧!10月24日,
2023-05-29“以前,小区光秃秃的,没一点生机。在小区‘楼栋长’带动下,今年,居民们种下桂花树,还自筹建立‘管...
2023-05-29泉州网5月29日讯(记者游怡冰)受副高稳定控制,泉州市昨天一早便呈现多云到晴的好天气,午后气温抬升迅速
2023-05-291、法拉德,法拉德,法拉德,法拉德,法拉德,法拉德,法拉德。2、意译3、骑自行车娱乐;骑自行车去兜风。4
2023-05-291、《守护甜心之吸血鬼骑士》是在小说阅读网连载的小说。2、作者是雨的等待。文章到此就分享结束,希望对大
2023-05-291、鲎阿米巴样细胞溶解物(LimulusAmebocyteLysate);区内接驳连线(LocalAccessLink);洛杉矶湖人(LosAngel
2023-05-291、现实生活当中,两人应该是同学兼好友,关系非常好。2、但是并不是什么情侣关系。3、不是。4、从《演员请
2023-05-29一碟小菜,传承千年产值百亿,香飘天下今天(5月28日)CCTV1《焦点访谈》用15分钟聚焦眉山在系列节目《土特
2023-05-291、—:要接受占吊机空间的现实中央空调室内机安装在吊顶内,至少要占用20cm层高,这是很多用户头疼的事...
2023-05-291、微信视频是mp4格式的。2、2、首先在手机中打开微信,点击菜单栏中的“发现”,点击上方的“视频号”...
2023-05-291、赵普,1971年4月24日出生于安徽省黄山市,中国内地男主持人。2、毕业于北京电影学院。本文到此分享完毕
2023-05-28刘晓春上海新金融研究院副院长近年来,中国的金融科技发展速度很快,建设成绩也非常显著,目前我国的金融科
2023-05-28“到底蛋里会孵出什么动物呢?”广州市黄埔区香雪山幼儿园教师胡丽莉正为幼儿分享绘本故事《很大很大的...
2023-05-28原标题:视频|第四届琼山福稻节正式开幕点击查看视频(雷亚平)
2023-05-285月28日,中国全球乳业首座全数智化工厂——蒙牛宁夏工厂正式落成投产。蒙牛集团总裁卢敏放表示,这座工...
2023-05-28观点摘编近年来,安徽省联社坚持打造“乡土银行、普惠银行、数字银行、绿色银行”,充分发挥农商银行长...
2023-05-281、乌兰察布职业学院有什么专业怎么样乌兰察布职业学院是公办大学,通常来说,公办大学由于有国家或地方财
2023-05-28参考消息网5月28日报道据美国《华尔街日报》网站5月26日报道,在百岁生日到来之际,亨利·基辛格表示,...
2023-05-28抄写作文网小编为大家提供隶书的古诗有哪些隶书唐诗三百首28字来供大家参考,欢迎阅读。扩展资料隶书的结构
2023-05-28当地时间28日凌晨,沙特与美国发表联合声明,呼吁苏丹冲突双方延长目前的临时停火协议。声明说,协议延长将
2023-05-28中国地震台网正式测定:05月28日11时29分在所罗门群岛(南纬10 00度,东经161 35度)发生5 6级地震,震源深
2023-05-28苹果已经官宣,将在7月26日关闭“我的照片流”服务,并推荐现有用户尽快迁移到“iCloudPhotos”服务上。...
2023-05-28邵帅川观新闻记者宁宁邵明亮视频 图罗顺5月27日,向家坝灌区北总干渠一期一步工程瓦房头渡槽最大单跨浇筑成
2023-05-28王姓,中华姓氏之一,主要源自姬姓,部分源自子姓、妫姓和少数民族改姓。周灵王之子太子晋,称王子晋,因值
2023-05-285月26日,省人大常委会委员、民侨外工委主任姜虹带领省人大常委会旅游发展联动监督调研组到我县调研。市人
2023-05-285月25日,惠民县退役军人事务局党组成员王铭刚,带领惠民县绳网产业经济创新创业高端培训班全体学员到邹平
2023-05-281、不出意外的话他们应该是休息 因为他们是事业单位和正常机关的休息是一样的。2、档案貌似是不可以代领的
2023-05-28挺进梅纳乌!巴黎发赛前海报姆巴佩拉莫斯领衔,向法甲冠军冲刺,法甲,巴黎,梅纳乌,朗斯队,维拉蒂,斯特拉斯堡
2023-05-28IT之家5月27日消息,根据国外科技媒体MacRumors从配件厂商处获得的机模,分享了iPhone15、iPhone15Plus、iP
2023-05-281、之所以错是因为金手指有多种格式。本文就为大家分享到这里,希望小伙伴们会喜欢。
2023-05-28中国国际大数据产业博览会正在举行,贵州再次吸引世界目光。2015年以来,这一行业盛会已在贵州成功举办9届
2023-05-28中超联赛第10轮,武汉三镇客场对阵沧州雄狮。上半场,韦世豪52秒闪击破门;下半场,什科里奇和奥乌苏连扳两
2023-05-272年前难再正常走路的他,如今成了国乒最强拦路虎
2023-05-27孙颖莎在女单比赛中赢得很轻松,以4-0分别横扫了索尔佳、萨马拉、申裕斌、韩莹,不丢一局,强势晋级四强。
2023-05-271、游戏的术语,网游的话很多是游戏内的缩写。2、不过通用的来说,比如BOSS是怪,BUG是程序漏洞,来源英语
2023-05-271、我跟你情况一样,年龄也一样。2、呵呵,我月经本来就不规律。3、只不过我是学医的可能比你知道的多一些
2023-05-27轮辋保护轮胎是指特殊设计能够起到保护轮辋作用的轮胎,有轮辋保护的轮胎作用主要是保护轮辋不受刮蹭,有轮
2023-05-27鞭牛士5月27日消息,根据应用情报提供商data ai最新报告,OpenAI的iOS版ChatGPT应用推出不到一周下载量已超
2023-05-271、由张骞出使西域开辟的以长安(今西安)为起点,经甘肃、新疆,到中亚、西亚。2、并联结地中海各国的陆上
2023-05-27世乒赛:王曼昱0-4陈幸同卫冕失败梁靖崑胜张本三进四强,张本,王曼昱,陈幸同,梁靖崑,世乒赛,樊振东,东京奥运
2023-05-27妻子离婚时才知丈夫年入300万法院:女方分六成财产
2023-05-27出品|子弹财经作者|段楠楠编辑|蛋总美编|倩倩审核|颂文近日,机器人领域又迎来一波关注度。先是科技圈大佬
2023-05-27新海南客户端、南海网、南国都市报5月27日消息(记者丁文文)5月26日晚,由海口市旅文局主办的第26届重庆都
2023-05-275月25日,中国(南京)教体装备产业博览会正式开展,格灵深瞳受邀参展。在展会同期举办的长三角教育与学校
2023-05-27今天小编肥嘟来为大家解答以上的问题。手机公园拍照,手机公园相信很多小伙伴还不知道,现在让我们一起来看
2023-05-27脑内记忆是如何存储和提取的?这是脑科学领域的一大难题。我国科研团队揭示了人脑杏仁核-海马神经环路在工
2023-05-271、用美图秀秀来做很方便的:具体做法就是在类似的头像上边用涂鸦笔着色、描边,再用腮红笔在脸蛋的位置上
2023-05-27近日,瑞昌法院审结一起诈骗案,被告人吴某某因犯诈骗罪被判处有期徒刑一年七个月,并处罚金人民币三万元。
2023-05-27解答:1、目前,凯迪拉克无论是国产还是进口,都没有现成的车型。2、你可以等。再过几个月,国产凯迪拉克AT
2023-05-271、QQ上的默认表情丢失(空白),自定义表情丢失,如下所示:2、如果缺省表达式,建议先注销所有QQ,然后把号
2023-05-27发布一个月之后,360智脑再度升级。一眼望去最显著的变化来自UI界面,升级后的360智脑拥有一个类似chatGPT
2023-05-271、原因及解决方法屏幕不干净造成的屏幕不灵敏:用干净的无尘布将手机上的脏污、水渍擦拭干净;2、手机有贴
2023-05-271、火绒是一款界面简单,功能众多的保护电脑的软件。2、不要被它简单的表面所欺骗。3、点击4、你可以看到三
2023-05-27对于那些想抄底买一台燃油车的消费者们来说,我估计在近一两个月时间里,油车价格得大降。为什么这么说呢?
2023-05-275月24日肯定是个特别的日子,蔚小理三家集体出动:蔚来发布了全新的ES6,小鹏汽车公布了一季度的财报,李想
2023-05-26在标准普尔11年下调美国信用评级的十多年后,美国目前再度面临信用评级被下调的风险。对此,标准普尔的前主
2023-05-26据塞尔维亚南通社26日消息,塞尔维亚总统武契奇当天下令,将塞尔维亚军队的战备状态提升到最高等级,并紧急
2023-05-26出品|搜狐科技作者|郑松毅编辑|杨锦5月26日消息,美国特斯拉公司创始人埃隆·马斯克(ElonMusk)旗下的
2023-05-26国产车巅峰“内讧”:老大哥长城与新一哥比亚迪的恩怨往事,比亚迪,长城汽车,长城,比亚迪秦,哈弗h6
2023-05-26大家好,今日关于【大学生掏鸟案当事人27日刑满释放】迅速上了的热搜榜,受到全网的关注度非常高。那么【大
2023-05-26时隔两个月时间,公募基金总规模重回正增长,并再次站上27万亿台阶。5月25日晚间,中国证券投资基金业协会
2023-05-26▲刘亚娴全国名中医(1944 6-)全国名中医刘亚娴系河北医科大学第四医院教授、主任医师,参考历代中医文献
2023-05-26关于充电,总流传着一些说法:手机电量耗尽再充电,每次要充满,这样才有利于电池保养,随时充会影响电池寿
2023-05-261、思恩腾生物科技集团是一家从事妇女生殖健康研发生产的生物高科技企业,集技术研发,技术推广。2、生物制
2023-05-26本文转自【央视军事】;日前美国“福特”号航母抵达挪威预备停留数日后前往北极参加军演俄罗斯批评美方...
2023-05-26本届“金豪笔编剧之夜”共有来自电影、动画、剧集三大单元的19位优秀编剧和10部作品获得表彰。活动现场...
2023-05-26中国经济网杭州5月26日综合报道据浙报集团官方客户端消息,5月26日上午,浙江省十四届人大常委会第三次会议
2023-05-26眼看着2024年逐渐向我们靠近,有一些生肖在2024年会犯太岁,导致自己在这一年中被霉运纠缠,无论做任何事情
2023-05-26600张演唱会门票,Mini汽车任性送!
2023-05-26记者日前从北京海关获悉,2023年4月,北京大兴国际机场进出境航班596架次,进出境旅客10 1万人次,环比上月
2023-05-26你们好,最近小活发现有诸多的小伙伴们对于问道火系怎么样,问道火系练什么好这个问题都颇为感兴趣的,今天
2023-05-26直播吧5月26日讯 东决G5今日在凯尔特人主场进行。上半场比赛,凯尔特人61-44领先,这半场球,塔图姆打得十
2023-05-26“3天时间,燃气就改造完工了,我们企业能够这么快投入生产,真是多亏了你们啊。”5月8日,摩擦一号制动...
2023-05-26据英国路透社24日报道,一位阿根廷央行的消息人士当天表示,阿根廷央行正在推进续签与中国的货币互换协议,
2023-05-26港股股东权益披露|5月26日
2023-05-261、高考是考试完之后填报,单招是考试前填报。2、高考可以同时填报几个学校来作为自己的第一志愿、第二志愿
2023-05-26欢迎观看本篇文章,小升来为大家解答以上问题。祝酒歌歌词,祝酒歌歌词简单介绍很多人还不知道,现在让我们
2023-05-265月25日,富国美丽中国混合A最新单位净值为2 262元,累计净值为2 362元,较前一交易日下跌0 44%。历史数据
2023-05-26“大家了解什么是民事支持起诉工作吗?”“因年老、疾病、缺乏劳动力不能独立生活或生活困难,请求给付...
2023-05-26来为大家解答以下的问题,国最好的不粘锅,德国不粘锅品牌排行榜这个很多人还不知道,现在让我们一起来看看
2023-05-261、城市名称地区生产总值(亿元)上海15047北京12153广州9138深圳8201天津75
2023-05-26随着互联网金融的发展,网贷已经成为了很多人借款的首选方式。但是,如果你没有按时还款,就有可能被起诉。
2023-05-26AI救人从线上走到线下树洞救援武汉中心挂牌---湖北日报讯(记者龚雪、通讯员柯颖琨)5月25日,“树洞救援武
2023-05-26IT之家5月25日消息,据日本经济新闻及新华社报道,日本山梨大学和早稻田大学的研究团队近日合作开发出了一
2023-05-26中新社重庆5月25日电(张旭)中国(重庆)—欧盟国家经贸人文合作交流会25日在重庆举行。重庆市人大常委会副主
2023-05-25金财互联(002530)05月25日在投资者关系平台上答复了投资者关心的问题。
2023-05-25当前大家对于高聚物改性沥青防水涂料都是颇为感兴趣的,大家都想要了解一下高聚物改性沥青防水涂料,那么小
2023-05-25王楚钦在男单1 8决赛中,对阵的是葡萄牙选手格拉尔多,这名球员27岁,在本届世乒赛表现非常出色。男单1 16
2023-05-251、布封用四十年的时间写出了36册的巨著《自然史》。本文到此分享完毕,希望对大家有所帮助。
2023-05-25前十大成交股中,净买入额居前三的是昆仑万维、迈瑞医疗、中际旭创,分别获净买入2 59亿元、1 33亿元、0 88
2023-05-25①长城汽车发布声明,称比亚迪旗下的两款热销车型存在排放不达标问题,并向相关部门进行了举报。②比亚迪回
2023-05-25近期,各地演唱会集中“开唱”,不少场次开票几秒钟即售罄,观众热情高涨。据统计,在刚刚过去的上个周...
2023-05-25上置集团:接获联交所复牌指引继续停牌:上置集团发布公告,2023年5月19日,公司收到一封来自联交所的信函
2023-05-25电子商务预计将在2021年至2025年间增长96%,交易额达到1200亿美元。伴随这种增长而来的是交付的复杂
2023-05-25[本站新车上市]5月25日,我们从沃尔沃官方微信公众号处了解到,旗下中型SUV――沃尔沃XC60峡湾版正式上市,
2023-05-25幼年时,我曾一度对“百慕大三角”的传说深信不疑。我并不知道哪位牛鬼蛇神能在地球那椭圆形的球体上画...
2023-05-25澳门统计暨普查局资料显示,受惠于入境防疫措施放宽、港澳全面恢复人员往来、内地赴澳团队旅游重启等利好因
2023-05-251、据心理学研究表明:人的表达靠55%的面部表情+38%的声音+7%的言词。2、教师课堂上的教态应该是明朗、活泼
2023-05-25五名分析师“无证上岗”,去年亏损4 59亿元的太平洋证券被责令改正,股权,无证,持股,分析师,证监会,投资...
2023-05-25Copyright © 2015-2022 时代水产网版权所有 备案号: 联系邮箱: 514 676 113@qq.com