游戏电视苹果数码历史美丽
投稿投诉
美丽时装
彩妆资讯
历史明星
乐活安卓
数码常识
驾车健康
苹果问答
网络发型
电视车载
室内电影
游戏科学
音乐整形

三万字盘点SpringSpringBoot的那些常用扩展点

  Spring对于每个Java后端程序员来说肯定不陌生,日常开发和面试必备的。本文就来盘点SpringSpringBoot常见的扩展点,同时也来看看常见的开源框架是如何基于这些扩展点跟SpringSpringBoot整合的。
  话不多说,直接进入正题。FactoryBean
  提起FactoryBean,就有一道著名的面试题说一说FactoryBean和BeanFactory的区别。其实这两者除了名字有点像,没有半毛钱关系
  BeanFactory是Bean的工厂,可以帮我们生成想要的Bean,而FactoryBean就是一种Bean的类型。当往容器中注入class类型为FactoryBean的类型的时候,最终生成的Bean是用过FactoryBean的getObject获取的。
  来个FactoryBean的Demo
  定义一个UserFactoryBean,实现FactoryBean接口,getObject方法返回一个User对象publicclassUserFactoryBeanimplementsFactoryBeanUser{OverridepublicUsergetObject()throwsException{UserusernewUser();System。out。println(调用UserFactoryBean的getObject方法生成Bean:user);returnuser;}OverridepublicClasslt;?getObjectType(){这个FactoryBean返回的Bean的类型returnUser。class;}}复制代码
  测试类:publicclassApplication{publicstaticvoidmain(String〔〕args){AnnotationConfigApplicationContextapplicationContextnewAnnotationConfigApplicationContext();将UserFactoryBean注册到容器中applicationContext。register(UserFactoryBean。class);applicationContext。refresh();System。out。println(获取到的Bean为applicationContext。getBean(User。class));}}复制代码
  结果:调用UserFactoryBean的getObject方法生成Bean:com。sanyou。spring。extension。User396e2f39获取到的Bean为com。sanyou。spring。extension。User396e2f39复制代码
  从结果可以看出,明明注册到Spring容器的是UserFactoryBean,但是却能从容器中获取到User类型的Bean,User这个Bean就是通过UserFactoryBean的getObject方法返回的。FactoryBean在开源框架中的使用1、在Mybatis中的使用
  Mybatis在整合Spring的时候,就是通过FactoryBean来实现的,这也就是为什么在Spring的Bean中可以注入Mybatis的Mapper接口的动态代理对象的原因。
  代码如下,省略了不重要的代码。publicclassMapperFactoryBeanTextendsSqlSessionDaoSupportimplementsFactoryBeanT{mapper的接口类型privateClassTmapperInterface;OverridepublicTgetObject()throwsException{通过SqlSession获取接口的动态搭理对象returngetSqlSession()。getMapper(this。mapperInterface);}OverridepublicClassTgetObjectType(){returnthis。mapperInterface;}}复制代码
  getObject方法的实现就是返回通过SqlSession获取到的Mapper接口的动态代理对象。
  而MapperScan注解的作用就是将每个接口对应的MapperFactoryBean注册到Spring容器的。2、在OpenFeign中的使用
  FeignClient接口的动态代理也是通过FactoryBean注入到Spring中的。classFeignClientFactoryBeanimplementsFactoryBeanObject,InitializingBean,ApplicationContextAware{FeignClient接口类型privateClasslt;?type;OverridepublicObjectgetObject()throwsException{returngetTarget();}OverridepublicClasslt;?getObjectType(){returntype;}}复制代码
  getObject方法是调用getTarget方法来返回的动态代理。
  EnableFeignClients注解的作用就是将每个接口对应的FeignClientFactoryBean注入到Spring容器的。
  一般来说,FactoryBean比较适合那种复杂Bean的构建,在其他框架整合Spring的时候用的比较多。Import注解
  Import注解在项目中可能不常见,但是下面这两个注解肯定常见。Import({SchedulingConfiguration。class})publicinterfaceEnableScheduling{}Import({AsyncConfigurationSelector。class})publicinterfaceEnableAsync{忽略}复制代码
  EnableScheduling和EnableAsync两个注解,一个是开启定时任务,一个是开启异步执行。通过这两个注解可以看出,他们都使用了Import注解,所以真正起作用的是Import注解。并且在很多情况下,EnbaleXXX这种格式的注解,都是通过Import注解起作用的,代表开启了某个功能。Import注解导入的配置类的分类
  Import注解导入的配置类可以分为三种情况:第一种:配置类实现了ImportSelector接口publicinterfaceImportSelector{String〔〕selectImports(AnnotationMetadataimportingClassMetadata);NullabledefaultPredicateStringgetExclusionFilter(){returnnull;}}复制代码
  当配置类实现了ImportSelector接口的时候,就会调用selectImports方法的实现,获取一批类的全限定名,最终这些类就会被注册到Spring容器中。
  UserImportSelector实现了ImportSelector,selectImports方法返回User的全限定名,代表吧User这个类注册容器中publicclassUserImportSelectorimplementsImportSelector{OverridepublicString〔〕selectImports(AnnotationMetadataimportingClassMetadata){System。out。println(调用UserImportSelector的selectImports方法获取一批类限定名);returnnewString〔〕{com。sanyou。spring。extension。User};}}复制代码
  测试:Import注解导入UserImportSelectorImport(UserImportSelector。class)publicclassApplication{publicstaticvoidmain(String〔〕args){AnnotationConfigApplicationContextapplicationContextnewAnnotationConfigApplicationContext();将Application注册到容器中applicationContext。register(Application。class);applicationContext。refresh();System。out。println(获取到的Bean为applicationContext。getBean(User。class));}}复制代码
  结果:调用UserImportSelector的selectImports方法获取一批类限定名获取到的Bean为com。sanyou。spring。extension。User282003e1复制代码
  所以可以看出,的确成功往容器中注入了User这个Bean第二种:配置类实现了ImportBeanDefinitionRegistrar接口publicinterfaceImportBeanDefinitionRegistrar{defaultvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry,BeanNameGeneratorimportBeanNameGenerator){registerBeanDefinitions(importingClassMetadata,registry);}defaultvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){}}复制代码
  当配置类实现了ImportBeanDefinitionRegistrar接口,你就可以自定义往容器中注册想注入的Bean。这个接口相比与ImportSelector接口的主要区别就是,ImportSelector接口是返回一个类,你不能对这个类进行任何操作,但是ImportBeanDefinitionRegistrar是可以自己注入BeanDefinition,可以添加属性之类的。
  来个demo:
  实现ImportBeanDefinitionRegistrar接口publicclassUserImportBeanDefinitionRegistrarimplementsImportBeanDefinitionRegistrar{OverridepublicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry,BeanNameGeneratorimportBeanNameGenerator){构建一个BeanDefinition,Bean的类型为UserAbstractBeanDefinitionbeanDefinitionBeanDefinitionBuilder。rootBeanDefinition(User。class)设置User这个Bean的属性username的值为三友的java日记。addPropertyValue(username,三友的java日记)。getBeanDefinition();System。out。println(往Spring容器中注入User);把User这个Bean的定义注册到容器中registry。registerBeanDefinition(user,beanDefinition);}}复制代码
  测试:导入UserImportBeanDefinitionRegistrarImport(UserImportBeanDefinitionRegistrar。class)publicclassApplication{publicstaticvoidmain(String〔〕args){AnnotationConfigApplicationContextapplicationContextnewAnnotationConfigApplicationContext();将Application注册到容器中applicationContext。register(Application。class);applicationContext。refresh();UseruserapplicationContext。getBean(User。class);System。out。println(获取到的Bean为user,属性username值为:user。getUsername());}}复制代码
  结果:往Spring容器中注入User获取到的Bean为com。sanyou。spring。extension。User6385cb26,属性username值为:三友的java日记复制代码第三种:配置类什么接口都没实现
  这种就不演示了,就是一个普普通通的类。总结
  其实不论是什么样的配置类,主要的作用就是往Spring容器中注册Bean,只不过注入的方式不同罢了。
  这种方式有什么好处呢?
  ImportSelector和ImportBeanDefinitionRegistrar的方法是有入参的,也就是注解的一些属性的封装,所以就可以根据注解的属性的配置,来决定应该返回样的配置类或者是应该往容器中注入什么样的类型的Bean,可以看一下EnableAsync的实现,看看是如何根据EnableAsync注解的属性来决定往容器中注入什么样的Bean。
  Import的核心作用就是导入配置类,并且还可以根据配合(比如EnableXXX)使用的注解的属性来决定应该往Spring中注入什么样的Bean。Bean的生命周期
  第一节讲的FactoryBean是一种特殊的Bean的类型,Import注解是往Spring容器中注册Bean。其实不论是Import注解,还是Component、Bean等注解,又或是xml配置,甚至是demo中的register方法,其实主要都是做了一件事,那就是往Spring容器去注册Bean。
  为什么需要去注册Bean?
  当然是为了让Spring知道要为我们生成Bean,并且需要按照我的要求来生成Bean,比如说,我要Autowired一个对象,那么你在创建Bean的过程中,就得给我Autowired一个对象,这就是一个IOC的过程。所以这就涉及了Bean的创建,销毁的过程,也就是面试常问的Bean的生命周期。
  本节来着重看一下,一个Bean在创建的过程中,有哪些常见的操作Spring在Bean的创建过程中给我们完成,并且操作的顺序是什么样的。
  话不多说,直接测试,基于结果来分析。Bean生命周期的回调先来测试创建LifeCycle类
  创建了一个LifeCycle,实现了InitializingBean、ApplicationContextAware、DisposableBean接口,加了PostConstruct、PreDestroy注解,注入了一个User对象。publicclassLifeCycleimplementsInitializingBean,ApplicationContextAware,DisposableBean{AutowiredprivateUseruser;publicLifeCycle(){System。out。println(LifeCycle对象被创建了);}实现的Aware回调接口paramapplicationContextthrowsBeansExceptionOverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{System。out。println(Aware接口起作用,setApplicationContext被调用了,此时useruser);}PostConstructpublicvoidpostConstruct(){System。out。println(PostConstruct注解起作用,postConstruct方法被调用了);}实现InitializingBean接口throwsExceptionOverridepublicvoidafterPropertiesSet()throwsException{System。out。println(InitializingBean接口起作用,afterPropertiesSet方法被调用了);}通过{linkBeaninitMethod()}来指定throwsExceptionpublicvoidinitMethod()throwsException{System。out。println(BeaninitMethod()起作用,initMethod方法被调用了);}PreDestroypublicvoidpreDestroy()throwsException{System。out。println(PreDestroy注解起作用,preDestroy方法被调用了);}通过{linkBeandestroyMethod()}来指定throwsExceptionpublicvoiddestroyMethod()throwsException{System。out。println(BeandestroyMethod()起作用,destroyMethod方法被调用了);}实现DisposableBean注解throwsExceptionOverridepublicvoiddestroy()throwsException{System。out。println(DisposableBean接口起作用,destroy方法被调用了);}}复制代码声明LifeCycle
  通过Bean声明了LifeCycle,并且initMethod和destroyMethod属性分别指定到了LifeCycle类的initMethod方法和destroyMethod方法Bean(initMethodinitMethod,destroyMethoddestroyMethod)publicLifeCyclelifeCycle(){returnnewLifeCycle();}复制代码测试publicclassApplication{publicstaticvoidmain(String〔〕args){AnnotationConfigApplicationContextapplicationContextnewAnnotationConfigApplicationContext();将LifeCycle注册到容器中applicationContext。register(Application。class);applicationContext。refresh();关闭上下文,触发销毁操作applicationContext。close();}Bean(initMethodinitMethod,destroyMethoddestroyMethod)publicLifeCyclelifeCycle(){returnnewLifeCycle();}BeanpublicUseruser(){returnnewUser();}}复制代码
  执行结果:LifeCycle对象被创建了Aware接口起作用,setApplicationContext被调用了,此时usercom。sanyou。spring。extension。User57d5872cPostConstruct注解起作用,postConstruct方法被调用了InitializingBean接口起作用,afterPropertiesSet方法被调用了BeaninitMethod()起作用,initMethod方法被调用了PreDestroy注解起作用,preDestroy方法被调用了DisposableBean接口起作用,destroy方法被调用了BeandestroyMethod()起作用,destroyMethod方法被调用了复制代码分析结果
  通过测试的结果可以看出,Bean在创建和销毁的过程当我们实现了某些接口或者加了某些注解,Spring就会回调我们实现的接口或者执行的方法。
  同时,在执行setApplicationContext的时候,能打印出User对象,说明User已经被注入了,说明注入发生在setApplicationContext之前。
  这里画张图总结一下Bean创建和销毁过程中调用的顺序。
  红色部分发生在Bean的创建过程,灰色部分发生在Bean销毁的过程中,在容器关闭的时候,就会销毁Bean。
  这里说一下图中的Aware接口指的是什么。其余的其实没什么好说的,就是按照这种方式配置,Spring会调用对应的方法而已。
  Aware接口是指以Aware结尾的一些Spring提供的接口,当你的Bean实现了这些接口的话,在创建过程中会回调对应的set方法,并传入响应的对象。
  这里列举几个Aware接口以及它们的作用
  接口
  作用
  ApplicationContextAware
  注入ApplicationContext
  ApplicationEventPublisherAware
  注入ApplicationEventPublisher事件发布器
  BeanFactoryAware
  注入BeanFactory
  BeanNameAware
  注入Bean的名称
  有了这些回调,比如说我的Bean想拿到ApplicationContext,不仅可以通过Autowired注入,还可以通过实现ApplicationContextAware接口拿到。
  通过上面的例子我们知道了比如说PostConstruct注解、Autowired注解、PreDestroy注解的作用,但是它们是如何在不同的阶段实现的呢?接着往下看。BeanPostProcessor
  BeanPostProcessor,中文名Bean的后置处理器,在Bean创建的过程中起作用。
  BeanPostProcessor是Bean在创建过程中一个非常重要的扩展点,因为每个Bean在创建的各个阶段,都会回调BeanPostProcessor及其子接口的方法,传入正在创建的Bean对象,这样如果想对Bean创建过程中某个阶段进行自定义扩展,那么就可以自定义BeanPostProcessor来完成。
  说得简单点,BeanPostProcessor就是在Bean创建过程中留的口子,通过这个口子可以对正在创建的Bean进行扩展。只不过Bean创建的阶段比较多,然后BeanPostProcessor接口以及他的子接口InstantiationAwareBeanPostProcessor、DestructionAwareBeanPostProcessor就提供了很多方法,可以使得在不同的阶段都可以拿到正在创建的Bean进行扩展。来个Demo
  现在需要实现一个这样的需求,如果Bean的类型是User,那么就设置这个对象的username属性为三友的java日记。
  那么就可以这么写:publicclassUserBeanPostProcessorimplementsBeanPostProcessor{OverridepublicObjectpostProcessAfterInitialization(Objectbean,StringbeanName)throwsBeansException{if(beaninstanceofUser){如果当前的Bean的类型是User,就把这个对象username的属性赋值为三友的java日记((User)bean)。setUsername(三友的java日记);}returnbean;}}复制代码
  测试:publicclassApplication{publicstaticvoidmain(String〔〕args){AnnotationConfigApplicationContextapplicationContextnewAnnotationConfigApplicationContext();将UserBeanPostProcessor和User注册到容器中applicationContext。register(UserBeanPostProcessor。class);applicationContext。register(User。class);applicationContext。refresh();UseruserapplicationContext。getBean(User。class);System。out。println(获取到的Bean为user,属性username值为:user。getUsername());}}复制代码
  测试结果:获取到的Bean为com。sanyou。spring。extension。User21a947fe,属性username值为:三友的java日记复制代码
  从结果可以看出,每个生成的Bean在执行到某个阶段的时候,都会回调UserBeanPostProcessor,然后UserBeanPostProcessor就会判断当前创建的Bean的类型,如果是User类型,那么就会将username的属性设置为三友的java日记。Spring内置的BeanPostProcessor
  这里我列举了常见的一些BeanPostProcessor的实现以及它们的作用
  BeanPostProcessor
  作用
  AutowiredAnnotationBeanPostProcessor
  处理Autowired、Value注解
  CommonAnnotationBeanPostProcessor
  处理Resource、PostConstruct、PreDestroy注解
  AnnotationAwareAspectJAutoProxyCreator
  处理一些注解或者是AOP切面的动态代理
  ApplicationContextAwareProcessor
  处理Aware接口注入的
  AsyncAnnotationBeanPostProcessor
  处理Async注解
  ScheduledAnnotationBeanPostProcessor
  处理Scheduled注解
  通过列举的这些BeanPostProcessor的实现可以看出,SpringBean的很多注解的处理都是依靠BeanPostProcessor及其子类的实现来完成的,这也回答了上一小节的疑问,处理Autowired、PostConstruct、PreDestroy注解是如何起作用的,其实就是通过BeanPostProcessor,在Bean的不同阶段来调用对应的方法起作用的。BeanPostProcessor在Dubbo中的使用
  在Dubbo中可以通过DubboReference(Reference)来引用生产者提供的接口,这个注解的处理也是依靠ReferenceAnnotationBeanPostProcessor,也就是BeanPostProcessor的扩展来实现的。publicclassReferenceAnnotationBeanPostProcessorextendsAbstractAnnotationBeanPostProcessorimplementsApplicationContextAware,BeanFactoryPostProcessor{忽略}复制代码
  当Bean在创建的某一阶段,走到了ReferenceAnnotationBeanPostProcessor这个类,就会根据反射找出这个类有没有DubboReference(Reference)注解,有的话就构建一个动态搭理注入就可以了。
  BeanPostProcessor在SpringBean的扩展中扮演着重要的角色,是SpringBean生命周期中很重要的一部分。正是因为有了BeanPostProcessor,你就可以在Bean创建过程中的任意一个阶段扩展自己想要的东西。BeanFactoryPostProcessor
  通过上面一节我们知道BeanPostProcessor是对Bean的处理,那么BeanFactoryPostProcessor很容易就猜到是对BeanFactory,也就是Spring容器的处理。
  举个例子,如果我们想禁止循环依赖,那么就可以这么写。publicclassMyBeanFactoryPostProcessorimplementsBeanFactoryPostProcessor{OverridepublicvoidpostProcessBeanFactory(ConfigurableListableBeanFactorybeanFactory)throwsBeansException{禁止循环依赖((DefaultListableBeanFactory)beanFactory)。setAllowCircularReferences(false);}}复制代码
  后面只需要将注入到Spring容器中就会生效。
  BeanFactoryPostProcessor是可以对Spring容器做处理的,方法的入参就是Spring的容器,通过这个接口,就对容器进行为所欲为的操作。SpringSPI机制
  SPI全称为(ServiceProviderInterface),是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。
  JDK有内置的SPI机制的实现ServiceLoader,Dubbo也有自己的SPI机制的实现ExtensionLoader。
  这里我们着重讲一下Spring的SPI机制的实现SpringFactoriesLoader。SpringFactoriesLoader
  Spring的SPI机制规定,配置文件必须在classpath路径下的METAINF文件夹内,文件名必须为spring。factories,文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名。但是键和值可以没有任何关系,当然想有也可以有。showmethecode
  这里我自定义一个类,MyEnableAutoConfiguration作为键,值就是UserpublicclassMyEnableAutoConfiguration{}复制代码
  spring。factories文件com。sanyou。spring。extension。spi。MyEnableAutoConfigurationcom。sanyou。spring。extension。User复制代码
  然后放在METAINF底下
  测试:publicclassApplication{publicstaticvoidmain(String〔〕args){ListStringclassNamesSpringFactoriesLoader。loadFactoryNames(MyEnableAutoConfiguration。class,MyEnableAutoConfiguration。class。getClassLoader());classNames。forEach(System。out::println);}}复制代码
  结果:com。sanyou。spring。extension。User复制代码
  可以看出,通过SpringFactoriesLoader的确可以从spring。factories文件中拿到MyEnableAutoConfiguration键对应的值。
  到这你可能说会,这SPI机制也没啥用啊。的确,我这个例子比较简单,拿到就是遍历,但是在Spring中,如果Spring在加载类的话使用SPI机制,那我们就可以扩展,接着往下看。SpringBoot启动扩展点
  SpringBoot项目在启动的过程中有很多扩展点,这里就来盘点一下几个常见的扩展点。1、自动装配
  说到SpringBoot的扩展点,第一时间肯定想到的就是自动装配机制,面试贼喜欢问,但是其实就是一个很简单的东西。当项目启动的时候,会去从所有的spring。factories文件中读取EnableAutoConfiguration键对应的值,拿到配置类,然后根据一些条件判断,决定哪些配置可以使用,哪些不能使用。
  spring。factories文件?键值?不错,自动装配说白了就是SPI机制的一种运用场景。
  EnableAutoConfiguration注解:Import(AutoConfigurationImportSelector。class)publicinterfaceEnableAutoConfiguration{忽略}复制代码
  我擦,这个注解也是使用Import注解,而且配置类还实现了ImportSelector接口,跟前面也都对上了。在SpringBoot中,EnableAutoConfiguration是通过SpringBootApplication来使用的。
  在AutoConfigurationImportSelector中还有这样一段代码
  所以,这段代码也明显地可以看出,自动装配也是基于SPI机制实现的。
  那么我想实现自动装配怎么办呢?很简单,只需两步。
  第一步,写个配置类:ConfigurationpublicclassUserAutoConfiguration{BeanpublicUserFactoryBeanuserFactoryBean(){returnnewUserFactoryBean();}}复制代码
  这里我为了跟前面的知识有关联,配置了一个UserFactoryBean。
  第二步,往spring。factories文件配置一下org。springframework。boot。autoconfigure。EnableAutoConfigurationcom。sanyou。spring。extension。springbootextension。UserAutoConfiguration复制代码
  到这就已经实现了自动装配的扩展。
  接下来进行测试:SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String〔〕args){ConfigurableApplicationContextapplicationContextSpringApplication。run(Application。class);UseruserapplicationContext。getBean(User。class);System。out。println(获取到的Bean为user);}}复制代码
  运行结果:调用UserFactoryBean的getObject方法生成Bean:com。sanyou。spring。extension。User3406472c获取到的Bean为com。sanyou。spring。extension。User3406472c复制代码
  从运行结果可以看出,自动装配起了作用,并且虽然往容器中注入的Bean的class类型为UserFactoryBean,但是最终会调用UserFactoryBean的getObject的实现获取到User对象。
  自动装配机制是SpringBoot的一个很重要的扩展点,很多框架在整合SpringBoot的时候,也都通过自动装配来的,实现项目启动,框架就自动启动的,这里我举个Mybatis整合SpringBoot。
  Mybatis整合SpringBoot的spring。factories文件2、PropertySourceLoaderPropertySourceLoader,这是干啥的呢?
  我们都知道,在SpringBoot环境下,外部化的配置文件支持properties和yaml两种格式。但是,现在不想使用properties和yaml格式的文件,想使用json格式的配置文件,怎么办?
  当然是基于该小节讲的PropertySourceLoader来实现的。publicinterfacePropertySourceLoader{可以支持哪种文件格式的解析String〔〕getFileExtensions();解析配置文件,读出内容,封装成一个PropertySourcelt;?结合返回回去ListPropertySourcelt;?load(Stringname,Resourceresource)throwsIOException;}复制代码
  对于PropertySourceLoader的实现,SpringBoot两个实现
  PropertiesPropertySourceLoader:可以解析properties或者xml结尾的配置文件
  YamlPropertySourceLoader:解析以yml或者yaml结尾的配置文件
  所以可以看出,要想实现json格式的支持,只需要自己实现可以用来解析json格式的配置文件的PropertySourceLoader就可以了。
  动手来一个。实现可以读取json格式的配置文件
  实现这个功能,只需要两步就可以了。第一步:自定义一个PropertySourceLoader
  JsonPropertySourceLoader,实现PropertySourceLoader接口publicclassJsonPropertySourceLoaderimplementsPropertySourceLoader{OverridepublicString〔〕getFileExtensions(){这个方法表明这个类支持解析以json结尾的配置文件returnnewString〔〕{json};}OverridepublicListPropertySourcelt;?load(Stringname,Resourceresource)throwsIOException{ReadableByteChannelreadableByteChannelresource。readableChannel();ByteBufferbyteBufferByteBuffer。allocate((int)resource。contentLength());将文件内容读到ByteBuffer中readableByteChannel。read(byteBuffer);将读出来的字节转换成字符串StringcontentnewString(byteBuffer。array());将字符串转换成JSONObjectJSONObjectjsonObjectJSON。parseObject(content);MapString,ObjectmapnewHashMap(jsonObject。size());将json的键值对读出来,放入到map中for(Stringkey:jsonObject。keySet()){map。put(key,jsonObject。getString(key));}returnCollections。singletonList(newMapPropertySource(jsonPropertySource,map));}}复制代码第二步:配置PropertySourceLoader
  JsonPropertySourceLoader已经有了,那么怎么用呢?当然是SPI机制了,SpringBoot对于配置文件的处理,就是依靠SPI机制,这也是能扩展的重要原因。
  SPI机制加载PropertySourceLoader实现
  spring。factories文件配置PropertySourceLoader
  SpringBoot会先通过SPI机制加载所有PropertySourceLoader,然后遍历每个PropertySourceLoader,判断当前遍历的PropertySourceLoader,通过getFileExtensions获取到当前PropertySourceLoader能够支持哪些配置文件格式的解析,让后跟当前需要解析的文件格式进行匹配,如果能匹配上,那么就会使用当前遍历的PropertySourceLoader来解析配置文件。
  PropertySourceLoader其实就属于策略接口,配置文件的解析就是策略模式的运用。
  所以,只需要按照这种格式,在spring。factories文件中配置一下就行了。org。springframework。boot。env。PropertySourceLoadercom。sanyou。spring。extension。springbootextension。propertysourceloader。JsonPropertySourceLoader复制代码
  到此,其实就扩展完了,接下来就来测试一下。测试
  先创建一个application。json的配置文件
  application。json配置文件
  改造UserpublicclassUser{注入配置文件的属性Value({sanyou。username:})privateStringusername;}复制代码
  启动项目SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String〔〕args){ConfigurableApplicationContextapplicationContextSpringApplication。run(Application。class);UseruserapplicationContext。getBean(User。class);System。out。println(获取到的Bean为user,属性username值为:user。getUsername());}BeanpublicUseruser(){returnnewUser();}}复制代码
  运行结果:获取到的Bean为com。sanyou。spring。extension。User481ba2cf,属性username值为:三友的java日记复制代码
  成功将json配置文件的属性注入到User对象中。
  至此,SpringBoot就支持了以json为结尾的配置文件格式。Nacos对于PropertySourceLoader的实现
  如果你的项目正在用Nacos作为配置中心,那么刚刚好,Nacos已经实现json配置文件格式的解析。
  Nacos不仅实现了json格式的解析,也实现了关于xml格式的配置文件的解析,并且优先级会比SpringBoot默认的xml格式文件解析的优先级高。至于Nacos为啥需要实现PropertySourceLoader?其实很简单,因为Nacos作为配置中心,不仅支持properties和yaml格式的文件,还支持json格式的配置文件,那么客户端拿到这些配置就需要解析,SpringBoot已经支持了properties和yaml格式的文件的解析,那么Nacos只需要实现SpringBoot不支持的就可以了。3、ApplicationContextInitializer
  ApplicationContextInitializer也是SpringBoot启动过程的一个扩展点。
  在SpringBoot启动过程,会回调这个类的实现initialize方法,传入ConfigurableApplicationContext。
  那怎么用呢?
  依然是SPI。
  SPI加载ApplicationContextInitializer
  然后遍历所有的实现,依次调用
  调用initialize
  这里就不演示了,实现接口,按照如下这种配置就行了
  但是这里需要注意的是,此时传入的ConfigurableApplicationContext并没有调用过refresh方法,也就是里面是没有Bean对象的,一般这个接口是用来配置ConfigurableApplicationContext,而不是用来获取Bean的。4、EnvironmentPostProcessor
  EnvironmentPostProcessor在SpringBoot启动过程中,也会调用,也是通过SPI机制来加载扩展的。
  EnvironmentPostProcessor是用来处理ConfigurableEnvironment的,也就是一些配置信息,SpringBoot所有的配置都是存在这个对象的。
  说这个类的主要原因,主要不是说扩展,而是他的一个实现类很关键。
  这个类的作用就是用来处理外部化配置文件的,也就是这个类是用来处理配置文件的,通过前面提到的PropertySourceLoader解析配置文件,放到ConfigurableEnvironment里面。5、ApplicationRunner和CommandLineRunner
  ApplicationRunner和CommandLineRunner都是在SpringBoot成功启动之后会调用,可以拿到启动时的参数。
  那怎么扩展呢?
  当然又是SPI了。
  这两个其实不是通过SPI机制来扩展,而是直接从容器中获取的,这又是为啥呢?
  因为调用ApplicationRunner和CommandLineRunner时,SpringBoot已经启动成功了,Spring容器都准备好了,需要什么Bean直接从容器中查找多方便。
  而前面说的几个需要SPI机制的扩展点,是因为在SpringBoot启动的时候,Spring容器还没有启动好,也就是无法从Spring容器获取到这些扩展的对象,为了兼顾扩展性,所以就通过SPI机制来实现获取到实现类。
  所以要想扩展这个点,只需要实现接口,添加到Spring容器就可以了。SpringEvent事件
  Event事件可以说是一种观察者模式的实现,主要是用来解耦合的。当发生了某件事,只要发布一个事件,对这个事件的监听者(观察者)就可以对事件进行响应或者处理。
  举个例子来说,假设发生了火灾,可能需要打119、救人,那么就可以基于事件的模型来实现,只需要打119、救人监听火灾的发生就行了,当发生了火灾,通知这些打119、救人去触发相应的逻辑操作。
  什么是SpringEvent事件
  那么是什么是SpringEvent事件,就是Spring实现了这种事件模型,你只需要基于Spring提供的API进行扩展,就可以完成事件的发布订阅
  Spring提供的事件api:ApplicationEvent
  ApplicationEvent
  事件的父类,所有具体的事件都得继承这个类,构造方法的参数是这个事件携带的参数,监听器就可以通过这个参数来进行一些业务操作。ApplicationListener
  ApplicationListener
  事件监听的接口,泛型是子类需要监听的事件类型,子类需要实现onApplicationEvent,参数就是事件类型,onApplicationEvent方法的实现就代表了对事件的处理,当事件发生时,Spring会回调onApplicationEvent方法的实现,传入发布的事件。ApplicationEventPublisher
  ApplicationEventPublisher
  事件发布器,通过publishEvent方法就可以发布一个事件,然后就可以触发监听这个事件的监听器的回调。
  ApplicationContext实现了ApplicationEventPublisher接口,所以通过ApplicationContext就可以发布事件。
  那怎么才能拿到ApplicationContext呢?
  前面Bean生命周期那节说过,可以通过ApplicationContextAware接口拿到,甚至你可以通过实现ApplicationEventPublisherAware直接获取到ApplicationEventPublisher,其实获取到的ApplicationEventPublisher也就是ApplicationContext,因为是ApplicationContext实现了ApplicationEventPublisher。话不多说,上代码
  就以上面的火灾为例第一步:创建一个火灾事件类
  火灾事件类继承ApplicationEvent火灾事件publicclassFireEventextendsApplicationEvent{publicFireEvent(Stringsource){super(source);}}复制代码第二步:创建火灾事件的监听器
  打119的火灾事件的监听器:publicclassCall119FireEventListenerimplementsApplicationListenerFireEvent{OverridepublicvoidonApplicationEvent(FireEventevent){System。out。println(打119);}}复制代码
  救人的火灾事件的监听器:publicclassSavePersonFireEventListenerimplementsApplicationListenerFireEvent{OverridepublicvoidonApplicationEvent(FireEventevent){System。out。println(救人);}}复制代码
  事件和对应的监听都有了,接下来进行测试:publicclassApplication{publicstaticvoidmain(String〔〕args){AnnotationConfigApplicationContextapplicationContextnewAnnotationConfigApplicationContext();将事件监听器注册到容器中applicationContext。register(Call119FireEventListener。class);applicationContext。register(SavePersonFireEventListener。class);applicationContext。refresh();发布着火的事件,触发监听applicationContext。publishEvent(newFireEvent(着火了));}}复制代码
  将两个事件注册到Spring容器中,然后发布FireEvent事件
  运行结果:打119救人复制代码
  控制台打印出了结果,触发了监听。
  如果现在需要对火灾进行救火,那么只需要去监听FireEvent,实现救火的逻辑,注入到Spring容器中,就可以了,其余的代码根本不用动。Spring内置的事件
  Spring内置的事件很多,这里我罗列几个
  事件类型
  触发时机
  ContextRefreshedEvent
  在调用ConfigurableApplicationContext接口中的refresh()方法时触发
  ContextStartedEvent
  在调用ConfigurableApplicationContext的start()方法时触发
  ContextStoppedEvent
  在调用ConfigurableApplicationContext的stop()方法时触发
  ContextClosedEvent
  当ApplicationContext被关闭时触发该事件,也就是调用close()方法触发
  在Spring容器启动的过程中,Spring会发布这些事件,如果你需要这Spring容器启动的某个时刻进行什么操作,只需要监听对应的事件即可。Spring事件的传播
  Spring事件的传播是什么意思呢?
  我们都知道,在Spring中有子父容器的概念,而Spring事件的传播就是指当通过子容器发布一个事件之后,不仅可以触发在这个子容器的事件监听器,还可以触发在父容器的这个事件的监听器。上代码publicclassEventPropagateApplication{publicstaticvoidmain(String〔〕args){创建一个父容器AnnotationConfigApplicationContextparentApplicationContextnewAnnotationConfigApplicationContext();将打119监听器注册到父容器中parentApplicationContext。register(Call119FireEventListener。class);parentApplicationContext。refresh();创建一个子容器AnnotationConfigApplicationContextchildApplicationContextnewAnnotationConfigApplicationContext();将救人监听器注册到子容器中childApplicationContext。register(SavePersonFireEventListener。class);childApplicationContext。refresh();设置一下父容器childApplicationContext。setParent(parentApplicationContext);通过子容器发布着火的事件,触发监听childApplicationContext。publishEvent(newFireEvent(着火了));}}复制代码
  创建了两个容器,父容器注册了打119的监听器,子容器注册了救人的监听器,然后将子父容器通过setParent关联起来,最后通过子容器,发布了着火的事件。
  运行结果:救人打119复制代码
  从打印的日志,的确可以看出,虽然是子容器发布了着火的事件,但是父容器的监听器也成功监听了着火事件。源码验证
  从这段源码可以看出,如果父容器不为空,就会通过父容器再发布一次事件。传播特性的一个坑
  前面说过,在Spring容器启动的过程,会发布很多事件,如果你需要有相应的扩展,可以监听这些事件。但是,在SpringCloud环境下,你的这些Spring发布的事件的监听器可能会执行很多次。为什么会执行很多次呢?其实就是跟传播特性有关。
  在SpringCloud的环境下,为了使像FeignClient和RibbonClient这些不同的服务的配置相互隔离,会创建很多的子容器,而这些子容器都有一个公共的父容器,那就是SpringBoot项目启动时创建的容器,事件的监听器都在这个容器中。而这些为了配置隔离创建的子容器,在容器启动的过程中,也会发布诸如ContextRefreshedEvent等这样的事件,如果你监听了这些事件,那么由于传播特性的关系,你的这个事件的监听器就会触发多次。
  如何解决这个坑呢?
  你可以进行判断这些监听器有没有执行过,比如加一个判断的标志;或者是监听类似的事件,比如ApplicationStartedEvent事件,这种事件是在SpringBoot启动中发布的事件,而子容器不是SpringBoot,所以不会多次发这种事件,也就会只执行一次。Spring事件的运用举例1、在Mybatis中的使用
  又来以Mybatis举例了Mybatis的SqlSessionFactoryBean监听了ApplicationEvent,然后判断如果是ContextRefreshedEvent就进行相应的处理,这个类还实现了FactoryBean接口publicclassSqlSessionFactoryBeanimplementsFactoryBeanSqlSessionFactory,InitializingBean,ApplicationListener{OverridepublicvoidonApplicationEvent(ApplicationEventevent){if(failFasteventinstanceofContextRefreshedEvent){failfastcheckallstatementsarecompletedthis。sqlSessionFactory。getConfiguration()。getMappedStatementNames();}}}复制代码
  说实话,这监听代码写的不太好,监听了ApplicationEvent,那么所有的事件都会回调这个类的onApplicationEvent方法,但是onApplicationEvent方法实现又是当ApplicationEvent是ContextRefreshedEvent类型才会往下走,那为什么不直接监听ContextRefreshedEvent呢?
  可以给个差评。
  膨胀了膨胀了2、在SpringCloud的运用
  在SpringCloud的中,当项目启动的时候,会自动往注册中心进行注册,那么是如何实现的呢?当然也是基于事件来的。当web服务器启动完成之后,就发布ServletWebServerInitializedEvent事件。
  然后不同的注册中心的实现都只需要监听这个事件,就知道web服务器已经创建好了,那么就可以往注册中心注册服务实例了。如果你的服务没往注册中心,看看是不是web环境,因为只有web环境才会发这个事件。
  SpringCloud提供了一个抽象类AbstractAutoServiceRegistration,实现了对WebServerInitializedEvent(ServletWebServerInitializedEvent的父类)事件的监听
  一般不同的注册中心都会去继承这个类,监听项目启动,实现往注册中心服务端进行注册。
  Nacos对于AbstractAutoServiceRegistration的继承
  SpringEvent事件在Spring内部中运用很多,是解耦合的利器。在实际项目中,你既可以监听SpringBoot内置的一些事件,进行相应的扩展,也可以基于这套模型在业务中自定义事件和相应的监听器,减少业务代码的耦合。命名空间
  最后来讲一个可能没有留意,但是很神奇的扩展点命名空间。起初我知道这个扩展点的时候,我都惊呆了,这玩意也能扩展?真的不得不佩服Spring设计的可扩展性。
  回忆一下啥是命名空间?
  先看一段配置lt;?xmlversion1。0encodingUTF8?beansxmlns:xsihttp:www。w3。org2001XMLSchemainstancexmlnshttp:www。springframework。orgschemabeansxmlns:contexthttp:www。springframework。orgschemacontextxsi:schemaLocationhttp:www。springframework。orgschemabeanshttp:www。springframework。orgschemabeansspringbeans。xsdhttp:www。springframework。orgschemacontexthttp:www。springframework。orgschemabeansspringcontext。xsdcontext:componentscanbasepackagecom。sanyou。spring。extensionbeans复制代码
  这一段xml配置想必都很熟悉,其中,context标签就代表了一个命名空间。
  也就说,这个标签是可以扩展的。话不多说,来个扩展
  接下来自定义命名空间sanyou,总共分为3步。第一步:定义一个xsd文件
  如下:lt;?xmlversion1。0encodingUTF8standaloneno?!xmlns和targetNamespace需要定义,结尾为sanyou,前面都一样的xsd:schemaxmlnshttp:sanyou。comschemasanyouxmlns:xsdhttp:www。w3。org2001XMLSchematargetNamespacehttp:sanyou。comschemasanyouxsd:importnamespacehttp:www。w3。orgXML1998namespacexsd:complexTypenameBeanxsd:attributenameclasstypexsd:stringuserequiredxsd:complexType!sanyou便签的子标签,类型是Bean,就会找到上面的complexTypeBean类型,然后处理属性xsd:elementnamemybeantypeBeanxsd:schema复制代码
  这个xsd文件来指明sanyou这个命名空间下有哪些标签和属性。这里我只指定了一个标签mybean,mybean标签里面有个class的属性,然后这个标签的目的就是将class属性指定的Bean的类型,注入到Spring容器中,作用跟spring的标签的作用是一样的。
  xsd文件没有需要放的固定的位置,这里我放到METAINF目录下第二步:解析这个命名空间
  解析命名空间很简单,Spring都有配套的东西NamespaceHandler接口,只要实现这个接口就行了。但一般我们不直接实现NamespaceHandler接口,我们可以继承NamespaceHandlerSupport类,这个类实现了NamespaceHandler接口。publicclassSanYouNameSpaceHandlerextendsNamespaceHandlerSupport{Overridepublicvoidinit(){注册解析mybean标签的解析器registerBeanDefinitionParser(mybean,newSanYouBeanDefinitionParser());}privatestaticclassSanYouBeanDefinitionParserextendsAbstractSingleBeanDefinitionParser{OverrideprotectedbooleanshouldGenerateId(){returntrue;}OverrideprotectedStringgetBeanClassName(Elementelement){returnelement。getAttribute(class);}}}复制代码
  SanYouNameSpaceHandler的作用就是将sanyou命名空间中的mybean这个标签读出来,拿到class的属性,然后将这个class属性指定的class类型注入到Spring容器中,至于注册这个环节的代码,都交给了SanYouBeanDefinitionParser的父类来做了。第三步:创建并配置spring。handlers和spring。schemas文件
  先创建spring。handlers和spring。schemas文件
  spring。handlers文件内容http:sanyou。comschemasanyoucom。sanyou。spring。extension。namespace。SanYouNameSpaceHandler复制代码
  通过spring。handlers配置文件,就知道sanyou命名空间应该找SanYouNameSpaceHandler进行解析
  spring。schemas文内容http:sanyou。comschemasanyou。xsdMETAINFsanyou。xsd复制代码
  spring。schemas配置xsd文件的路径
  文件都有了,只需要放到classpath下的METAINF文件夹就行了。
  到这里,就完成了扩展,接下来进行测试测试
  先构建一个applicationContext。xml文件,放到resources目录下lt;?xmlversion1。0encodingUTF8?beansxmlns:xsihttp:www。w3。org2001XMLSchemainstancexmlnshttp:www。springframework。orgschemabeansxmlns:sanyouhttp:sanyou。comschemasanyouxsi:schemaLocationhttp:www。springframework。orgschemabeanshttp:www。springframework。orgschemabeansspringbeans。xsdhttp:sanyou。comschemasanyouhttp:sanyou。comschemasanyou。xsd!使用sanyou标签,配置一个UserBeansanyou:mybeanclasscom。sanyou。spring。extension。Userbeans复制代码
  再写个测试类publicclassApplication{publicstaticvoidmain(String〔〕args){ClassPathXmlApplicationContextapplicationContextnewClassPathXmlApplicationContext(applicationContext。xml);applicationContext。refresh();UseruserapplicationContext。getBean(User。class);System。out。println(user);}}复制代码
  运行结果:com。sanyou。spring。extension。User27fe3806复制代码
  成功获取到User这个对象,说明自定义标签生效了。Spring内置命名空间的扩展
  NameSpaceHandler的spring实现
  通过NameSpaceHandler接口的这些实现类的命名就可以看出来有哪些扩展和这些扩展的作用,比如有处理aop的,有处理mvc的等等之类的。开源框架对命名空间的扩展1、Mybatis的扩展
  这个就是来扫描指定路径的mapper接口的,处理scan标签,跟MapperScan注解的作用是一样的。2、dubbo的扩展
  使用dubbo可能写过如下的配置dubbo:registryaddresszookeeper:192。168。10。119:2181复制代码
  这个dubbo命名空间肯定就是扩展的Spring的,也有对应的dubbo实现的NameSpaceHandler。
  不得不说,dubbo解析的标签可真的多啊,不过功能也是真的多。总结
  到这,本文就接近尾声了,这里画两张图来总结一下本文讲了Spring的哪些扩展点。
  通过学习Spring的这些扩展点,既可以帮助我们应对日常的开发,还可以帮助我们更好地看懂Spring的源码。

烟管裤奶奶靴才是适合50女人的打扮,优雅显瘦,谁穿谁好看直筒裤的搭配真的是属于非常高出镜率和百搭的存在了,不仅可以修饰出来流畅显瘦的腿型状态,同时又可以和任何鞋型搭配起来,而对于50的中年女性而言,直筒裤和奶奶靴的结合穿搭才是真正体……一座布达拉宫就足以然后爱上西藏阳光灿烂的布达拉宫,在蓝天白云下更显辉煌,白色的墙体,深红色的瓦片屋顶,让这座宫殿精美绝伦,无与伦比。真的,布达拉宫太壮美了,而且还有一层神秘的面纱,让你想去揭秘他的神秘……一男子身体自动酿酒?!真没喝酒,还真醉了2016年,比利时一位47岁的男子为了治疗呼吸道感染,吃了一些阿莫西林和莫西沙星以后,身体却发生了变异,经常感觉醉醺醺的,还在一次驾驶过程中,被逮住了酒驾。他坚持自己四天……中国一边费力大量进口原油,一边低价出口成品油,这是为什么呢?中国是一个大型的工业国家,对于工业生产的原材料需求量很大。其中最重要的原材料之一是石油,而中国自己的石油产量是无法满足国内需求的,因此需要大量进口原油。同时,中国也是世界上最大……每周酒典沈园绝唱这是南宋绍兴二十五年(公元1155年)的春天,31岁的陆游仕途坎坷,在闷闷不乐中黯然回到了故乡。他举目看着外面的世界,风和日丽,便决心到外面散散心。走着走着,就不由自主地朝着沈……肉馅的万能做法,才不是包饺子美食原创酿菜yyds美食台夏天吃蒸菜省时又省力,冬瓜酿虾、苦瓜塞肉都是餐桌常客。这种将肉馅填入菜中的做法被称为酿。(常吃的青椒塞肉也是酿菜哦!)油豆腐酿酿菜的起源……买手机买128G容量起步?128G已经过时了手机究竟选择多大内存空间,其中这个事情挺困扰大家的。我记得几年前,选择128G都是大容量内存了,现在都2022年了,选择128G可能也不太够用了。本文所说的不选128G,……为什么晚上睡觉特别容易在凌晨两三点醒来,这到底是怎么回事儿近日,重庆的李先生,有一个睡眠的苦恼。就是虽然能在10点种按时躺床睡觉。但是呢,还没睡多久,就又醒来了。结果一看表,也就才两三点钟,但是呢,仍然非常困,整个人昏昏沉沉的,上班也……50岁孟晚舟穿20万的造型大爆火!10万的胸针配DIOR裙,作为华为长公主的孟晚舟,自从回国后就备受大家的关注,而不久前,华为开展的发布会就请到了孟晚舟出席,作为她回国后的首次正式亮相,孟晚舟的整身装扮也是让人眼前一亮,全身20来万的行……华为不再孤单!又一中企剑指苹果,外媒结局清晰了在华为被美孤立后,苹果就躺平了,一心想要扮演黄雀,坐享其成,这也应了余承东那句华为跌倒,苹果吃饱。但让美始料未及的是,华为不再孤单,又一中企挺身而出,接棒华为,剑指苹果,扛起了……十月的江浙是大闸蟹味道的,连吃一个月,吃够一年的量秋风起,蟹脚痒,苏州阳澄湖大闸蟹闻名遐迩。寒露一大习俗是吃螃蟹,古人诗日:九月团脐十月尖,持螯饮酒菊花天。民间也有九雌十雄的谚语。寒露后的雌蟹,肉嫩味美、黄多油丰,是大饱口福的……排骨还能这样做,酸甜开胃,鲜嫩多汁来源:美食台水果和肉组合,真真是绝妙!初夏正是水果争相上市的时候,今天就用应季的菠萝搭配糖酿梅子做一道烤排骨,品尝属于初夏的心动滋味~果香配肉香,酸甜解腻,天……
文心一言发布,中国人工智能突破关键靠什么新京报快评未来,人类将围绕通用人工智能技术实现大量创新突破。图ICphoto3月16日,被称为中国版ChatGPT的百度文心一言在北京发布。据报道,经历了过去十几年在AI研发……广东四季怎么煲汤?你好,我是厨娘,很高兴回答你问题!广东人煲汤历史悠久,因一年四季气候湿热,广东人喜欢用汤水来改善身体的不适,几乎每家每户都会煲老火靓汤,不管在外面吃饭还是在家吃饭都会煲上……啤酒不管国产还是进口,只要有3种配料,都是难喝的工业啤酒虽然已是立秋,但属于夏季的炎热暑气依然还没消散,所以夜宵大排档依然都是热火朝天,烧烤啤酒小龙虾,样样都是热销款。而说到啤酒,夜宵大排档里的常驻款都是青岛、雪花、百威等知名……中国女排2023年集训名单中国女排2023年首次集训名单公布,李盈莹、袁心玥、龚翔宇领衔,丁霞入选。在海外联赛效力的朱婷和姚迪暂未入选。中国女排2023年集训名单:王云蕗(北京)、李盈莹、袁心玥、王媛媛……夏季减脂食谱,1只鸡腿1根黄瓜,1碗料汁,馋的朋友流口水平时总是一个人吃午饭,是随便凑合一下呢?还是认认真真做一餐呢?如果有时间,估计大多数人都会选择后者吧!夏天已经到了,我最近也在琢磨各种清爽开胃好吃的减脂餐,又想减肥又不想……BVLGARI宝格丽推出Aluminium计时码表杜卡迪特别〔腕表之家品牌新闻〕BVLGARI宝格丽与著名摩托车生产商杜卡迪(Ducati)携手合作,推出一款特别限量版Aluminium计时码表。全新时计以广受欢迎的Aluminium系……让你停不下筷子的麻辣小龙虾,夏天的最爱夏天到了,就想嘬一口麻辣小龙虾的味道,于是旺强买来了四斤小龙虾,喝着汽水吃着小龙虾超级过瘾,我俩边吃边看剧真的太幸福了。By苗儿与旺强用料小龙虾4斤红洋葱半个蒜5瓣……WealthX全球超级富豪人数锐减61。2022上半年,全球净资产超3000万美元的超级富豪人数下降6,至39。241万人,资产净值41。8万亿美元2。北美地区超级富豪下降人数最多3。中国大陆是超级富……演员杨童舒,34岁意外怀孕生子,老公只是一个普通生意人杨童舒出演过很多角色,是《痴情不改》的女主,之后和陈宝国合作《咱爸咱妈》,做过《星光大道》的评委,还有《家有公婆》的的女主,《一生守护》的女主等等。杨舒通是吉林人,因为自……生啤熟啤干啤纯生,啤酒到底分多少种,你知道么?本文图片及部分观点、数据来自于互联网,如有侵权请联系作者删除!天气越来越热了,小龙虾也上市了,喝啤酒的季节正式到来了。如果喝啤酒的时候,你能边喝边说出生啤、熟啤、干啤、原……西红柿去皮别再用开水烫了,教你2招,简单快速不流汁,省时省力大家好,我是小小雯。西红柿大家都比较常见,它和黄瓜一样,既可以当做水果直接吃,也可以当做青菜炒着吃。在我们家乡,都称西红柿为洋柿子,你们那边是怎么个叫法呢?西红柿不仅好吃……煎带鱼时,别再放面粉了,教你多加2味,带鱼外酥里嫩(本账号已与维权骑士签约,本文原创,搬运转载必究)煎带鱼时,别再放面粉了,教你多加2味,带鱼外酥里嫩。平时比较喜欢吃鱼,其中带鱼就是非常爱吃的一道,带鱼的做法多种多样,可……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网