调研目标

  • 项目结构,项目配置,启动脚本的变化
  • dapeng-sc的主要修改点
  • nacos(feign)如何注册和发现服务

项目结构,项目配置,启动脚本的变化

项目结构

  • dapeng-demo就是我们的dapeng项目,包括api和service,它们可以集成到一个modules统一管理,也可以像现行这样分离开来.

  • sc-demo就是spring cloud项目.可以和dapeng service互相发现调用.

  • dapeng-api没有变化,仍然使用thrift和相应插件生成service和client.

  • dapeng-service已经是spring boot架构了,有controller和启动基类Application(不过这个类仍然由dapeng container来调用).

  • 配置文件也从services.xml变成application.yml(也可以是application.properties或者bootstrap.yml,读取文件的功能是由spring boot实现的)

项目配置

application.yml :

server:
  port: 9203
spring:
  application:
    name: account-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
dapeng:
  scan:
    base-packages: com.dapeng.demo.account
  • 除了前缀为dapeng.scan的配置都是springboot和nacos的配置.
  • 前缀为dapeng.scan的配置项是dapeng-sc新增的自定义配置项,作用是指定扫描哪些路径来获取含@DapengService的服务类.(目前也仅自定义了该项配置)
  • 配置了dapeng.scan.base-packages之后,就不需要在Application类里面加上@DapengComponentScan注解了,两者的作用应该是相同的

maven命令行

新:

compile com.github.dapeng-soa:dapeng-maven-plugin:2.2.0-SNAPSHOT:run2 -Dsoa.freq.limit.enable=false -Dsoa.transactional.enable=false -Dspringboot.main.class=com.dapeng.demo.account.AccountAnnotationApplication -Dsoa.apidoc.port=9876 -Dsoa.container.port=9071 -Dspring.config.location=E:/scala-project/mydapeng-demo/dapeng-demo/account-service/src/main/resources/application.yml

旧:

compile com.github.dapeng:dapeng-maven-plugin:2.1.1.1:run -Dsoa.zookeeper.kafka.host=127.0.0.1:2181 -Dsoa.kafka.host=127.0.0.1:9092 -Dsoa.zookeeper.host=127.0.0.1:2181 -Dsoa.transactional.enable=false -Dsoa.container.port=11001 -Dsoa.apidoc.port=12001 -Dsoa.container.ip=localhost
  • -D开头的都会写入到系统配置里面去,例如-Dsoa.apidoc.port=9876,那么就有System.getProperty(“soa.apidoc.port”)=9876
  • 所以可以看到,新增了两个系统配置springboot.main.class和spring.config.location
  • dapeng-maven-plugin:2.2.0-SNAPSHOT:run2    这里的run2对应dapeng-maven-plugin下新增的RunContainer2Plugin类(还没看实现逻辑)

dapeng-sc的主要修改点

  • 在Dapeng Container中,spring,zookeeper,netty等第三方组件被封装成plugin(实现了start和stop接口)
  • spring plugin的实现类是SpringApplicationLoader,替换了原有的SpringAppLoader.

SpringAppLoader

@Override  
public void start() {  
    LOGGER.warn("Plugin::" + getClass().getSimpleName() + "::start");  
    String configPath = "META-INF/spring/services.xml";  
  
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();  
    for (ClassLoader appClassLoader : appClassLoaders) {  
        try {  
  
            // ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new Object[]{xmlPaths.toArray(new String[0])});  
            // context.start();            Class<?> appCtxClass = appClassLoader.loadClass("org.springframework.context.support.ClassPathXmlApplicationContext");  
            Class<?>[] parameterTypes = new Class[]{String[].class};  
            Constructor<?> constructor = appCtxClass.getConstructor(parameterTypes);  
  
            Thread.currentThread().setContextClassLoader(appClassLoader);  
            Object springCtx = getSpringContext(configPath, appClassLoader, constructor);  
  
            springCtxs.add(springCtx);  
  
            Method method = appCtxClass.getMethod("getBeansOfType", Class.class);  
  
            Map<String, SoaServiceDefinition<?>> processorMap = (Map<String, SoaServiceDefinition<?>>)  
                    method.invoke(springCtx, appClassLoader.loadClass(SoaServiceDefinition.class.getName()));  
  
            //获取所有实现了lifecycle的bean  
            LifecycleProcessorFactory.getLifecycleProcessor().addLifecycles(((Map<String, LifeCycleAware>)  
                    method.invoke(springCtx, appClassLoader.loadClass(LifeCycleAware.class.getName()))).values());  
  
  
            //TODO: 需要构造Application对象  
            Map<String, ServiceInfo> appInfos = toServiceInfos(processorMap);  
            Application application = new DapengApplication(new ArrayList<>(appInfos.values()), appClassLoader);  
  
            //Start spring context  
            LOGGER.info(" start to boot app");  
            Method startMethod = appCtxClass.getMethod("start");  
            startMethod.invoke(springCtx);  
  
            // IApplication app = new ...  
            if (!application.getServiceInfos().isEmpty()) {  
                // fixme only registerApplication  
                Map<ProcessorKey, SoaServiceDefinition<?>> serviceDefinitionMap = toSoaServiceDefinitionMap(appInfos, processorMap);  
                container.registerAppProcessors(serviceDefinitionMap);  
  
                container.registerAppMap(toApplicationMap(serviceDefinitionMap, application));  
                //fire a zk event  
                container.registerApplication(application);  
            }  
  
            LOGGER.info(" ------------ SpringClassLoader: " + ContainerFactory.getContainer().getApplications());  
  
  
        } catch (Exception e) {  
            LOGGER.error(e.getMessage(), e);  
        } finally {  
            Thread.currentThread().setContextClassLoader(classLoader);  
        }  
    }  
}

关键实现点:

  • 从"META-INF/spring/services.xml"中读取配置并获得上下文.
  • 通过method.invoke()分别加载dapeng service bean和spring自己的bean.
  • dapeng service bean的class是SoaServiceDefinition(很关键),spring bean的class是LifeCycleAware

这里的Container是dapeng容器的主要结构

/**  
 * 大鹏容器的主结构,负责管理容器相关的监听器,插件,应用程序。  
 *  
 * 所有的组件的注册,卸载动作都应该由Container来负责,  
 */  
public interface Container {  
    /**  
     * 容器状态  
     */  
    int STATUS_UNKNOWN = 0;  
    int STATUS_CREATING = 1;  
    int STATUS_RUNNING = 2;  
    int STATUS_SHUTTING = 3;  
    int STATUS_DOWN = 4;  
  
    /**  
     * 注册应用程序监听器,  
     * @param listener  
     */    void registerAppListener(AppListener listener);  
  
    /**  
     * 卸载用用程序监听器  
     * @param listener  
     */    void unregisterAppListener(AppListener listener);  
  
    /**  
     * 注册应用程序(保存容器具体的应用信息)  
     * @param app  
     */    void registerApplication(Application app);  
  
    /**  
     * 卸载应用程序  
     * @param app  
     */    void unregisterApplication(Application app);  
  
    /**  
     * 注册插件(like: Zookeeper,netty..etc.)  
     * @param plugin  
     */    void registerPlugin(Plugin plugin);  
  
    /**  
     * 卸载插件  
     * @param plugin  
     */    void unregisterPlugin(Plugin plugin);  
  
    /**  
     * 注册Filter(like: monitor)  
     */    void registerFilter(Filter filter);  
  
    /**  
     * 卸载Filter  
     * @param filter  
     */    void unregisterFilter(Filter filter);  
  
    /**  
     * 获取应用程序的相关信息  
     * @return  
     */  
    List<Application> getApplications();  
  
    List<Plugin> getPlugins();  
  
    Map<ProcessorKey, SoaServiceDefinition<?>> getServiceProcessors();  
  
    // fixme @Deprecated  
    void registerAppProcessors(Map<ProcessorKey, SoaServiceDefinition<?>> processors);  
  
    Application getApplication(ProcessorKey key);  
  
    // fixme @Deprecated  
    void registerAppMap(Map<ProcessorKey,Application> applicationMap);  
  
    Executor getDispatcher();  
  
    List<Filter> getFilters();  
  
    void startup();  
  
    /**  
     * 0:unknow;     * 1:creating;     * 2:running;     * 3:shutting     * 4:down     *     * @return status of container  
     */    int status();  
  
    /**  
    * 容器内未完成的请求计数  
    */  
    AtomicInteger requestCounter();  
}

SpringApplicationLoader

这个类是SpringAppLoader和SpringBootAppLoader的结合,当services.xml存在时,则通过传统的方式加载spring.否则通过spring boot的方式启动spring
  • 这里首先尝试用传统方式,读取services.xml来启动spring.当services.xml不存在时就会抛出异常转而使用spring boot启动.

trySpringBoot

  • 这里会读取maven命令行配置项springboot.main.class来获得启动基类
  • 获得启动基类后,就像我们以前使用spring boot一样,通过invoke调用spring boot的main方法即可启动spring容器.
  • spring容器启动完之后,新旧版本都会调用afterSpringApplicationStart(),把dapeng service注册到dapeng container里,这里没变.

spring容器启动之后,主要的改动在dapeng-springboot-project下的两个项目.

dapeng-spring-boot-autoconfigure用于读取dpeng自定义的配置(也就是dapeng.scan.base-packages定义的dapeng-service路径),并把配置传给dapeng-config-spring下的ServiceAnnotationBeanPostProcessor来处理包含@DapengService的服务类

DapengSpringAutoConfiguration

这里就是读取dapeng.scan.base-packages的配置(需要扫描的目录),并传到ServiceAnnotationBeanPostProcessor中处理

ServiceAnnotationBeanPostProcessor

ServiceAnnotationBeanPostProcessor

流程大约是:

  • 在dapeng.scan.base-packages指定的目录下扫描获取含有@DapengService的类
  • 然后根据这些类生成service bean并注册到spring容器里.

这里的关键是:

//自定义拦截哪些注解
scanner.addIncludeFilter(new AnnotationTypeFilter(DapengService.class));

public class DapengClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
    public DapengClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, Environment environment, ResourceLoader resourceLoader) {
        //这个类主要就是为了让useDefaultFilters=false
        this(registry, false, environment, resourceLoader);

    }
}

看一下ClassPathBeanDefinitionScanner的源码吧:

	/**
	 * Create a new {@code ClassPathBeanDefinitionScanner} for the given bean factory.
	 * <p>If the passed-in bean factory does not only implement the
	 * {@code BeanDefinitionRegistry} interface but also the {@code ResourceLoader}
	 * interface, it will be used as default {@code ResourceLoader} as well. This will
	 * usually be the case for {@link org.springframework.context.ApplicationContext}
	 * implementations.
	 * <p>If given a plain {@code BeanDefinitionRegistry}, the default {@code ResourceLoader}
	 * will be a {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}.
	 * <p>If the passed-in bean factory also implements {@link EnvironmentCapable} its
	 * environment will be used by this reader.  Otherwise, the reader will initialize and
	 * use a {@link org.springframework.core.env.StandardEnvironment}. All
	 * {@code ApplicationContext} implementations are {@code EnvironmentCapable}, while
	 * normal {@code BeanFactory} implementations are not.
	 * @param registry the {@code BeanFactory} to load bean definitions into, in the form
	 * of a {@code BeanDefinitionRegistry}
	 * @param useDefaultFilters whether to include the default filters for the
	 * {@link org.springframework.stereotype.Component @Component},
	 * {@link org.springframework.stereotype.Repository @Repository},
	 * {@link org.springframework.stereotype.Service @Service}, and
	 * {@link org.springframework.stereotype.Controller @Controller} stereotype annotations
	 * @see #setResourceLoader
	 * @see #setEnvironment
	 */


	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
			Environment environment, @Nullable ResourceLoader resourceLoader) {

		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		this.registry = registry;
		//useDefaultFilters默认是true
		if (useDefaultFilters) {
			registerDefaultFilters();
		}
		setEnvironment(environment);
		setResourceLoader(resourceLoader);
	}

	protected void registerDefaultFilters() {
		this.includeFilters.add(new AnnotationTypeFilter(Component.class));
		ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
		try {
			this.includeFilters.add(new AnnotationTypeFilter(
					((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
			logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
		}
		catch (ClassNotFoundException ex) {
			// JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
		}
		try {
			this.includeFilters.add(new AnnotationTypeFilter(
					((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
			logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
		}
		catch (ClassNotFoundException ex) {
			// JSR-330 API not available - simply skip.
		}
	}
  • spring boot默认拦截的是@Component,@Repository,@Service,@Controller,这里改成了只拦截@DapengService.

service bean注册过程:

private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder, BeanDefinitionRegistry registry, DapengClassPathBeanDefinitionScanner scanner) {
    //服务实现类名称.
    String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();
    //根据给定的服务实现类 注册其对应的  Processor Bean -> SoaServiceDefinition.
    AbstractBeanDefinition processorDefinition = buildServiceProcessorDefinition(annotatedServiceBeanName);
    String processorBeanName = annotatedServiceBeanName + SERVICE_PROCESSOR_NAME_SUFFIX;
    registerBeanDefinition(processorBeanName, processorDefinition, registry, scanner);
}


    private AbstractBeanDefinition buildServiceProcessorDefinition(String serviceBeanName) {
        BeanDefinitionBuilder builder = rootBeanDefinition(ServiceBeanProcessorFactory.class);
        // Set 原先的 beanName 到构造函数中去.
        builder.addConstructorArgReference(serviceBeanName);
//        builder.addConstructorArgValue(serviceBeanName);
        return builder.getBeanDefinition();
    }
  • HelloService最终注册的bean name = helloServiceImpl_Processor
  • 这里注册使用的工厂类是dapeng自定义的ServiceBeanProcessorFactory,得到的bean type是SoaServiceDefinition

其它注意点

自定义group

api里面的service.thrift需要额外定义group(这个应该是和nacos的dataId命名空间有关)

PropertySource

  • 启动基类使用@PropertySource来指定spring boot的配置文件
  • 此外maven命令行里面的spring.config.location貌似也是相同作用.实测下来两者可以都不配,有默认路径兜底,如果配错则都会报错.

nacos(feign)如何注册和发现服务

  • 调研@EnableFeignClients原理
  • 调研@EnableDiscoveryClient原理
  • 调研@FeignClient原理