调研目标
- 项目结构,项目配置,启动脚本的变化
- 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启动.
- 这里会读取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
流程大约是:
- 在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
其它注意点
api里面的service.thrift需要额外定义group(这个应该是和nacos的dataId命名空间有关)
- 启动基类使用@PropertySource来指定spring boot的配置文件
- 此外maven命令行里面的spring.config.location貌似也是相同作用.实测下来两者可以都不配,有默认路径兜底,如果配错则都会报错.
nacos(feign)如何注册和发现服务
- 调研@EnableFeignClients原理
- 调研@EnableDiscoveryClient原理
- 调研@FeignClient原理