谈对象诞生,从new到beanDefinition

1,071 阅读10分钟

写在最开始

本文从【对象诞生】这个小的切入口开始聊。从最基本的new,到反射,到spring框架中的怎么实例一个bean,到springboot的自动注入AutoConfig机制,最后再深入到springCloud中的openFeign,看看一切是怎么串联起来的。

一起来感受java的魅力。

1: 从new说起

new是每个java人最早接触的语法之一。作用是构建一个对象。下面的语句会实例化一个2岁的名字叫Molly的对象。

Animal a = new Animal("Molly","2")

new背后的原理其实涉及的点还是挺多的。有类的加载机制,内存空间管理,初始化和实例化等等。我们用javap反编译一下这段代码,看看最原生的字节码。

public static void main(java.lang.String[]);

    descriptor: ([Ljava/lang/String;)V

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

      stack=4, locals=2, args_size=1

         0: new           #2                  // class com/galaxy/helloworld/entity/Animal

         3: dup

         4: ldc           #3                  // String Molly

         6: ldc           #4                  // String 2

         8: invokespecial #5                  // Method com/galaxy/helloworld/entity/Animal."<init>":(Ljava/lang/String;Ljava/lang/String;)V

        11: astore_1

        12: return

      LineNumberTable:

        line 15: 0

        line 16: 12

      LocalVariableTable:

        Start  Length  Slot  Name   Signature

            0      13     0  args   [Ljava/lang/String;

           12       1     1     a   Lcom/galaxy/helloworld/entity/Animal;

    MethodParameters:

      Name                           Flags

      args

}

字节码的指令上可以看到经历了new,dup(含义是复制,duplicate),ldc(含义是加载常量,load constants),invokeSpecial, astroe(存储指令,store),return的过程。大致讲讲这个过程。

new new我们都知道是一个本地指令,是用来开辟内存空间并实例化一个对象的。但是其实java虚拟机再接受到new指令以后发生的事情远不止于此。大致可以看作如下:

  • 检验。类是不是被加载,解析和初始化了。字节码中new右边的class路径是com/galaxy/helloworld/entity/Animal。显然是已经加载过了。简单说下类加载的机制也很简单,大致是把.class文件加载到内存-->链接验证-->把.class文件的静态储存结构转化成方法区数据结构,并生成Class对象--> 调用进行类的初始化。

截屏2021-07-16 下午5.28.05.png

  • 开辟内存空间。从堆上分配一块大小确定的java内存空间给对象。这里涉及到oom,gc等知识点。

  • 初始化和对象头。虚拟机会对对象进行必要的设置,存在对象头中。这里面主要会有对象hash码,锁,分代等信息。

截屏2021-07-16 下午5.29.12.png

2: dup/ldc/invokeSpecial

dup本来的含义是通过复制栈顶的一个元素再压入栈进行对栈顶内容备份。new后面为什么接dup,一种说法是invokeSpecial会弹出栈顶的操作数[引用],但是对象访问后还是需要引用的,所以这个时候复制了引用就有作用了。

ldc是载入常数。由于Animal的构造中需要一个string表示名字,一个int表示年龄。这里载入了常数区的Molly和2。

invokeSpecial右边的注释是Method com/galaxy/helloworld/entity/Animal.""。这里调用了指令,把上一步获取的Molly和2注入对象。也代表着一个对象经过了实例化【new】,初始化【init】以后终于完成了创建,诞生在了内存中。

2: 聊聊spring中的对象诞生

明白了new背后的原理,其实对我们写业务一点帮助都没有。因为实际开发中的对象是委托给spring框架管理的。spring中用bean来指代一个比对象更大的数据结构bean。

有时候会想把一个bean实例化出来有Class就行了啊,哪怕你不用new,你用反射拿到构造器不也可以实例化bean,为什么spring不能用Class来建立bean呢? 后来才明白在spring中,class提供的信息远远不够。除了用构造器创建一个bean,spring管理这个bean还需要诸如bean的作用域,注入模型,是不是懒加载等信息,这些是class中没有的。spring中有个数据结构来描述实例化bean的模型,叫做beanDefinition,又有人叫做spring bean的建模对象。只有先转换成了beanDefinition,一个bean中才能在spring中被实例化,初始化,在容器中通过getBean得到。

捋一捋spring中的bean怎么实例化的。简单概括就是磁盘上的类通过spring扫描,然后实例化,然后初始化,继而放到容器当中的过程。

具体的过程非常复杂,由于我们的目的是希望在feign中探索。所以不展开讲spring的注入。建议去看子路的spring文章,链接在文后。 这里只简单总结一下过程

  • classPath等转化成resource。spring调用bean工厂后置处理器完成扫描。Resorce通过beanDefinitionReader等循环解析扫描出来的信息保存在beanFactory中的BeanDefinitionMap.

  • BeanFactory根据BeanDefinitionMap来循环调用doGetBean。在这个过程推断构造方法,因为spring实例化对象是通过构造方法反射。之后spring调用构造方法反射实例化一个对象。在AbstractApplicationContext中的finishBeanFactoryInitialization()找到beanFactory.preInstantiateSingletons(),可以看到这段逻辑。

spring实例化的过程.png

3: springboot中的自动注入autoConfig

当然明白spring中对象怎么注入,仍然毫无卵用,因为在实际开发中只需要简单加上@component,@service等注解表明需要把这个bean委托给spring容器管理,bean就自动扫描,实例化,初始化了。开发者即便不知道这里面的原理,也照样可以开发。那么我们学到这些知识有什么用呢?

有用。

因为springboot中的组件就是通过这种方式自动注入的。扫描,为@configuration标记的bean生成beanDefinition,registerBeanDefinition,推断并调用构造,交给beanFactory投入到单例池。一个一个springboot的组件就自动加载到容器中供我们使用了。了解了这后面的原理,我们可以通过自己的方式来注册组件了。有两种方式可以注入组件,分别是springboot-starter和@import + ImportBeanDefinitionRegistrar接口。

下面介绍一个自定义的springboot starter的大致写法。

springboot-starter自动注入

  • 写一个configuration类并配上bean标签作为注入的组件。
/**
 * 支付自动装配
 *
 * @author yanghaolei
 * @date 2019/9/29 下午5:58
 */
@AllArgsConstructor
@Configuration
@EnableConfigurationProperties(value = {GsxPayProperties.class})
public class GsxpayAutoConfiguration {

   @Resource
   private final GsxPayProperties gsxPayProperties;

   @Bean
   @ConditionalOnMissingBean
   GsxpayService gsxpayService() {
      GsxpayService gsxpayService = new GsxpayService();
      List<GsxPayProperties.PayConfig> payConfigs = gsxPayProperties.getConfigs();
      gsxpayService.setConfigStorageMap(payConfigs.stream()
         .map(payConfig -> CloneUtils.clone(payConfig, GsxPayConfigStorage.class))
         .collect(Collectors.toMap(GsxPayConfigStorage::getAppId, g -> g)));
      gsxpayService.setCallbackUrl(gsxPayProperties.getCallbackUrl());
      
      return gsxpayService;
   }

}
  • 在META-INF下的spring.factories表明需要扫描的类。springboot会自动扫描。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.baijia.xiaozao.starter.gsxpay.GsxpayAutoConfiguration

有兴趣也可以在jar的lib中搜autoconfig。可以看到springboot的特性从何而来。

截屏2021-07-16 下午6.35.22.png

通过@import注解+ImportBeanDefinitionRegistrar接口来完成注入

  • 定义一个注解@EnablePush在application的启动器上打开这个功能的开关。
/**
 * 注解标签
 *
 * @author haolei
 * @date 2021/6/24 下午3:27
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({InitConfiguration.class, PushScanConfiguration.class})
@Documented
public @interface EnablePush {

    String[] pushPackages() default {""};
}
  • 在import标签中引入PushScanConfiguration来实现ImportBeanDefinitionRegistrar这个接口。目的是为scanner中扫描到的class生成beanDefinition。继而通过beanFactory实例化和初始化组件,放入容器。很多的框架中这个都是以registar结尾的。

/**
 * 类的描述
 *
 * @author haolei
 * @date 2021/6/24 下午3:17
 */
public class PushScanConfiguration implements ImportBeanDefinitionRegistrar, BaseConfiguration {

    private final Class ANNOTATION_IMPORT = EnablePush.class;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        if (!annotationMetadata.hasAnnotation(ANNOTATION_IMPORT.getName())) {
            throw new BaseException("error when import class by @EnableRocketMqOns !");
        }
        Set<String> annons = annotationMetadata.getAnnotationTypes();
        if (Objects.nonNull(annons) && annons.size() > 0) {
            for (String annoName : annons) {
                if (annoName.equals(ANNOTATION_IMPORT.getName())) {
                    Map<String, Object> annoFieldMap = annotationMetadata.getAnnotationAttributes(ANNOTATION_IMPORT.getName());
                    String[] pushPackages = (String[]) annoFieldMap.get(PUSH_PACKAGE);
                    if (pushPackages == null) {
                        throw new BaseException("error when import class by @EnableRocketMqOns ! factoryPackages is null, no field!");
                    }
                    setAttributes(PUSH_PACKAGE, pushPackages);
                    pushScan(beanDefinitionRegistry);
                }
            }
        }
    }

    private void pushScan(BeanDefinitionRegistry beanDefinitionRegistry) {

        DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanDefinitionRegistry;
        PushAnnoScanner pushAnnoScanner = new PushAnnoScanner(factory);
        pushAnnoScanner.doScan((String[]) getAttributes(PUSH_PACKAGE));

    }
}

可以看到registerBeanDefinitions完成了从class到beanDefinition的转化,又交给beanFactory的默认实现类DefaultListableBeanFactory去完成注入。PushAnnoScanner会去扫描@import注解在的包和子包中所有被注解push标注的class。

4: SpringCloud OpenFeign中的组件注入和原理

如果掌握了springboot中组件自动注入的知识,其实很多spring体系的组件源码就已经可以看明白了。又尤其是spring cloud中的openFeign,ribbon,gateway,security等都有特别明显的风格一致性。

下面主要看下openFeign的源码。

  • 通过@EnableFeignClient引入了@import标签和beanRegister类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}

FeignClientsRegistrar类实现了前文所说的ImportBeanDefinitionRegistrar接口,实现class到beanDefinition的转型。

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   registerDefaultConfiguration(metadata, registry);
   registerFeignClients(metadata, registry);
}

registerFeignClients代码中实现了扫描类scanner,会扫描包下所有含有@feignClient注解的class,然后向容器注册bean。

public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   ClassPathScanningCandidateComponentProvider scanner = getScanner();
   scanner.setResourceLoader(this.resourceLoader);

   Set<String> basePackages;

   Map<String, Object> attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
   AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
         FeignClient.class);
   final Class<?>[] clients = attrs == null ? null
         : (Class<?>[]) attrs.get("clients");
   if (clients == null || clients.length == 0) {
      scanner.addIncludeFilter(annotationTypeFilter);
      basePackages = getBasePackages(metadata);
   }
   else {
      final Set<String> clientClasses = new HashSet<>();
      basePackages = new HashSet<>();
      for (Class<?> clazz : clients) {
         basePackages.add(ClassUtils.getPackageName(clazz));
         clientClasses.add(clazz.getCanonicalName());
      }
      AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
         @Override
         protected boolean match(ClassMetadata metadata) {
            String cleaned = metadata.getClassName().replaceAll("\$", ".");
            return clientClasses.contains(cleaned);
         }
      };
      scanner.addIncludeFilter(
            new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
   }

   for (String basePackage : basePackages) {
      Set<BeanDefinition> candidateComponents = scanner
            .findCandidateComponents(basePackage);
      for (BeanDefinition candidateComponent : candidateComponents) {
         if (candidateComponent instanceof AnnotatedBeanDefinition) {
            // verify annotated class is an interface
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            Assert.isTrue(annotationMetadata.isInterface(),
                  "@FeignClient can only be specified on an interface");

            Map<String, Object> attributes = annotationMetadata
                  .getAnnotationAttributes(
                        FeignClient.class.getCanonicalName());

            String name = getClientName(attributes);
            registerClientConfiguration(registry, name,
                  attributes.get("configuration"));

            registerFeignClient(registry, annotationMetadata, attributes);
         }
      }
   }
}

我们代码中写的feign组件,就需要带上feignClient注解。

/**
 * 微信远程调用接口
 *
 * @author yanghaolei
 * @date 2019/08/12 上午10:06
 */
@FeignClient(contextId = "RemoteWxService", value = FlowAppConstant.APPLICATION_WECHAt_NAME)
public interface RemoteWxService {

    /**
     * 通过unionId获取code
     *
     * @return true/false
     */
    @PostMapping("/wx/user/getUnionIdByCode")
    R<String> getUnionIdByCode(@Valid @RequestBody StringXO param);

    /**
     * 通过openId获取user
     *
     * @return string
     */
    @PostMapping("/wx/user/getUserByOpenId")
    R<WxUserGetVO> getUserByOpenId(@Valid @RequestBody StringXO param); }
    

RemoteWxService被扫描以后,会根据beanDefinition生成一个bean交给容器管理。这个时候我们就可以使用RemoteWxService类型的bean来用接口中的函数去调用远程模块。比如去微信服务中通过openid获取wxUser。

值得一提的时候,feign实际执行中是用的生成的bean的jdk代理对象是实现的,并不是直接调用bean。可能是为了统一代码函数的实现,在invocationHandler中连真实对象的方法,method.invoke都没有执行,而是直接通过封装好的methodHandler,接受参数args生成了http模版,把请求发了出去。

static class FeignInvocationHandler implements InvocationHandler {

  private final Target target;
  private final Map<Method, MethodHandler> dispatch;

  FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
    this.target = checkNotNull(target, "target");
    this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("equals".equals(method.getName())) {
      try {
        Object otherHandler =
            args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
        return equals(otherHandler);
      } catch (IllegalArgumentException e) {
        return false;
      }
    } else if ("hashCode".equals(method.getName())) {
      return hashCode();
    } else if ("toString".equals(method.getName())) {
      return toString();
    }

    return dispatch.get(method).invoke(args);
  }

在ReflectiveFeign中有一个内部类生产了invocationHandler。可以看到做完校验之后直接通过methodHandler完成了剩下的逻辑。这个map里面的key值就是RemoteWxService中的方法[getUserByOpenId,getUnionIdByCode]。value是一个默认的methodHandler[SynchronousMethodHandler]。把参数,其实主要是接口上的地址,拼接成http模版,然后发送出去的逻辑就是在这个handler中完成的,给大家看看这段代码。尤其是看到client.execute(request)简直恍然大悟。

@Override
public Object invoke(Object[] argv) throws Throwable {
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Options options = findOptions(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      return executeAndDecode(template, options);
    } catch (RetryableException e) {
   }
     
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
  Request request = targetRequest(template);
  Response response;
  
  try {
    response = client.execute(request, options);
  } catch (IOException e) {
  
}

写在最后

java人升级打怪,从p5到p8有没有规律可循?写好一个接口,跟好一个迭代,hold住一个模块,own住一个产品,维护一个框架。踏踏实实走好每一步。

参考

1: # 为什么java对象new之后要进行dup操作

2: # 子路springbean

3: # Feign