陈公子的博客

文字可以宣泄过往,但终究写不出流年

dubbo源码研究之config-spring模块

dubbo-config-spring模块是dubbo-config的Extension。

Dubbo的扩展点加载从JDK标准的SPI(Service Provider Interface)扩展点发现机制加强而来。
Dubbo改进了JDK标准的SPI的以下问题:
JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,
但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,
当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。
增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。
dubbo的spi约定:在扩展类的jar包内,放置扩展点配置文件:META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔。

schema.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14

@SPI
public interface ExtensionFactory {

/**
* Get extension.
*
* @param type object type.
* @param name object name.
* @return object instance.
*/
<T> T getExtension(Class<T> type, String name);

}

dubbo的扩展点接口ExtensionFactory ,该接口有三个实现

dubbo的扩展实现的具体类是ExtensionLoader。
ExtensionLoader加载扩展点时,会检查扩展点的属性(通过set方法判断),如该属性是扩展点类型,则会注入扩展点对象。
因为注入时不能确定使用哪个扩展点(在使用时确定),所以注入的是一个自适应扩展(一个代理)。自适应扩展点调用时,选取一个真正的扩展点,并代理到其上完成调用。
Dubbo是根据调用方法参数(上面有调用哪个扩展点的信息)来选取一个真正的扩展点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

@SuppressWarnings("unchecked")
private T createExtension(String name) {
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && wrapperClasses.size() > 0) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}

private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}

可以看出ExtensionLoader持有某一类扩展点的所有扩展,并且扩展以class为key,一个扩展类只有一个实例。
如果扩展点使用了Adaptive则会通过字节码生成一个自适应的扩展类。
通过createAdaptiveExtensionClassCode方法返回class的code,然后通过Compiler接口返回class对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

/**
* Compiler. (SPI, Singleton, ThreadSafe)
*
* @author william.liangf
*/
@SPI("javassist")
public interface Compiler {

/**
* Compile java source code.
*
* @param code Java source code
* @param classLoader TODO
* @return Compiled class
*/
Class<?> compile(String code, ClassLoader classLoader);

}

Compiler dubbo支持jdk和Javassist两种实现,特别注意一点,JdkCompiler的java版本通过硬编码指定版本为1.6。
JdkCompiler首先通过JavaFileObject接口生成java文件,然后通过JavaCompiler接口编译成class文件,最后通过ClassLoader加载生成的class文件。
spring名称空间扩展
dubbo通过扩展spring的名称空间来读取xml配置。
dubbo.xsd是spring schma的扩展文件。作用是定义相关xml元素和名称空间。
spring.handlers,spring.schemas是spring在解析xml的时候根据spi机制读取对象的NamespaceHandler和xsd文件位置。
很多程序猿使用dubbo开发的时候,会发现ide工具报错,因为xml上面dubbo名称空间的链接打不开,其实这个报错可以无视的,
因为spring解析的xml的时候这个网络地址其实是指向spi配置文件里面的一个java类。
spring.handlers

1
2

http\://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

DubboNamespaceHandler则注册了相关自定义元素的解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

/**
* DubboNamespaceHandler
*
* @author william.liangf
* @export
*/
public class DubboNamespaceHandler extends NamespaceHandlerSupport {

static {
Version.checkDuplicate(DubboNamespaceHandler.class);
}

public void init() {
registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
}

}

dubbo源码研究之config模块

dubbo模块说明

  • dubbo-common 公共逻辑模块,包括Util类和通用模型
  • dubbo-remoting 远程通讯模块,相当于Dubbo协议的实现,如果RPC用RMI协议则不需要使用此包。
  • dubbo-rpc 远程调用模块,抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理
  • dubbo-cluster 集群模块,将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。
  • dubbo-registry 注册中心模块,基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。
  • dubbo-monitor 监控模块,统计服务调用次数,调用时间的,调用链跟踪的服务。
  • dubbo-config 配置模块,是Dubbo对外的API,用户通过Config使用Dubbo,隐藏Dubbo所有细节。
  • dubbo-container 容器模块,是一个Standlone的容器,以简单的Main加载Spring启动,因为服务通常不需要Tomcat/JBoss等Web容器的特性,没必要用Web容器去加载服务。

今天开始从dubbo入口,config模块开始研究。
config模块和Service模块是API,其他为SPI实现。
config模块uml类图

AbstractConfig类提供几个主要的方法 appendAnnotation,appendProperties,appendParameters,appendAttributes
其他子类提供相关自身的属性。
AbstractConfig类里面大量通过 反射和javabean约定获取相关值,特别注意的是toString方法注释提到的防御性容错

1
2
3
4
5
6

catch (Throwable t) { // 防御性容错
logger.warn(t.getMessage(), t);
return super.toString();
}

防御性容错在此处使用,提升代码健壮性。
AnnotationBean是子类里面一个比较特殊的类。因为该类是处于com.alibaba.dubbo.config.spring包下面。
该类属于config模块的spring 扩展,支持通过注解来配置dubbo。
AnnotationBean实现了DisposableBean, BeanFactoryPostProcessor, BeanPostProcessor, ApplicationContextAware这四个接口。
DisposableBean:资源清理接口
BeanFactoryPostProcessor,BeanPostProcessor:作用类似,都是在bean创建之后的扩展接口。
ApplicationContextAware:获取spring上下文接口。
通过该类可以看出,dubbo注解实际上是spring注解的扩展,也就是说使用dubbo注解的前提是使用spring作为dubbo的容器。
@servcie暴露dubbo服务配置的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (! isMatchPackage(bean)) {
return bean;
}
Class<?> clazz = bean.getClass();
if(isProxyBean(bean)){
clazz = AopUtils.getTargetClass(bean);
}
Service service = clazz.getAnnotation(Service.class);
if (service != null) {
ServiceBean<Object> serviceConfig = new ServiceBean<Object>(service);
if (void.class.equals(service.interfaceClass())
&& "".equals(service.interfaceName())) {
if (clazz.getInterfaces().length > 0) {
serviceConfig.setInterface(clazz.getInterfaces()[0]);
} else {
throw new IllegalStateException("Failed to export remote service class " + clazz.getName() + ", cause: The @Service undefined interfaceClass or interfaceName, and the service class unimplemented any interfaces.");
}
}
if (applicationContext != null) {
serviceConfig.setApplicationContext(applicationContext);
if (service.registry() != null && service.registry().length > 0) {
List<RegistryConfig> registryConfigs = new ArrayList<RegistryConfig>();
for (String registryId : service.registry()) {
if (registryId != null && registryId.length() > 0) {
registryConfigs.add((RegistryConfig)applicationContext.getBean(registryId, RegistryConfig.class));
}
}
serviceConfig.setRegistries(registryConfigs);
}
if (service.provider() != null && service.provider().length() > 0) {
serviceConfig.setProvider((ProviderConfig)applicationContext.getBean(service.provider(),ProviderConfig.class));
}
if (service.monitor() != null && service.monitor().length() > 0) {
serviceConfig.setMonitor((MonitorConfig)applicationContext.getBean(service.monitor(), MonitorConfig.class));
}
if (service.application() != null && service.application().length() > 0) {
serviceConfig.setApplication((ApplicationConfig)applicationContext.getBean(service.application(), ApplicationConfig.class));
}
if (service.module() != null && service.module().length() > 0) {
serviceConfig.setModule((ModuleConfig)applicationContext.getBean(service.module(), ModuleConfig.class));
}
if (service.provider() != null && service.provider().length() > 0) {
serviceConfig.setProvider((ProviderConfig)applicationContext.getBean(service.provider(), ProviderConfig.class));
} else {

}
if (service.protocol() != null && service.protocol().length > 0) {
List<ProtocolConfig> protocolConfigs = new ArrayList<ProtocolConfig>();
// modified by lishen; fix dubbo's bug
for (String protocolId : service.protocol()) {
if (protocolId != null && protocolId.length() > 0) {
protocolConfigs.add((ProtocolConfig)applicationContext.getBean(protocolId, ProtocolConfig.class));
}
}
serviceConfig.setProtocols(protocolConfigs);
}
try {
serviceConfig.afterPropertiesSet();
} catch (RuntimeException e) {
throw (RuntimeException) e;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
serviceConfig.setRef(bean);
serviceConfigs.add(serviceConfig);
serviceConfig.export();
}
return bean;
}

主要是先获取类上面的@servcie注解,然后new 一个对应的ServiceBean,ServiceBean继承于ServiceConfig,同时实现了spring bean的相关接口 InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware。
最后该serviceBean保存到线程安全的容器里面去。
@service解析是在 postProcessAfterInitialization里面
@Reference解析是在postProcessBeforeInitialization里面,两者的时机是不一样的。

1
2
3
4

String key = reference.group() + "/" + interfaceName + ":" + reference.version();
ReferenceBean<?> referenceConfig = referenceConfigs.get(key);

从该代码可以看出,服务接口的唯一性,由group,interfaceName 和version一起决定。

浅谈分布式项目日志监控

目前公司项目采用dubbo服务化升级之后,原先大而全的几个主要应用,拆散重构成多个分布式服务。
这个公司业务架构和系统架构实现一次升级,并发和业务开发效率得到提升。
但是事情是两面的,引入dubbo服务化之后,导致业务链路过长,日志分散。不能在使用原来的日志处理方式了。
分布式情况下,每个日志分散到各自服务所在机器,
日志的收集和分析使用原来古老的模式,肯定是过时了,集群和服务规模小还好,数量一大,我想不管是运维人员还是开发人员都会头疼。
目前处理这个需求最为火热的中间套件,自然首选是ELK,ELK是java技术栈的。
也符合目前公司需求。ELK的安装就不讲述了,感兴趣的可以查看官网或者自行百度,资料还是挺多的。
确定了日志收集和分析的中间件,剩下一个就是日志埋点和怎么把日志串起来了。
以前单个应用的时代,系统级别的日志可以通过aop解决。在分布式情况下对每一个独立服务而言,自身的日志系统还是通过aop解决,唯一需要的就是怎么把分散到各自不同应用的日志串起来。
这个有个高大上的说法叫做业务链监控。
目前国内开源的产品有大众点评的cat,是整套业务链监控解决方案。
对于我公司目前来说太重了,我这边日志已经有elk,就没必要在额外引入cat。那如何自己实现呢。
既然是链路,那自然有入口有出口。我们需要做的就是在入口出生成一个全局唯一的traceId,然后把这个traceId按照业务链路传递到各个服务中去。
traceId就是一根线,把各个服务的日志串起来。注意一点,服务的时间要同步,因为是根据来时间排序的。
traceId的生成,简单方案可以采用uuid,其次推荐使用twiiter的snowflake算法。
traceId的传递,需要根据rpc框架来实现了。dubbo框架采用dubbo的fiter来实现,参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

// 调用过程拦截
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
//异步获取serviceId,没获取到不进行采样
String serviceId = tracer.getServiceId(RpcContext.getContext().getUrl().getServiceInterface());
if (serviceId == null) {
Tracer.startTraceWork();
return invoker.invoke(invocation);
}
long start = System.currentTimeMillis();
RpcContext context = RpcContext.getContext();
boolean isConsumerSide = context.isConsumerSide();
Span span = null;
Endpoint endpoint = null;
try {
endpoint = tracer.newEndPoint();
endpoint.setServiceName(serviceId);
endpoint.setIp(context.getLocalAddressString());
endpoint.setPort(context.getLocalPort());
if (context.isConsumerSide()) { //是否是消费者
Span span1 = tracer.getParentSpan();
if (span1 == null) { //为rootSpan
span = tracer.newSpan(context.getMethodName(), endpoint, serviceId);//生成root Span
} else {
span = tracer.genSpan(span1.getTraceId(), span1.getId(), tracer.genSpanId(), context.getMethodName(), span1.isSample(), null);
}
} else if (context.isProviderSide()) {
Long traceId, parentId, spanId;
traceId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.TID));
parentId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.PID));
spanId = TracerUtils.getAttachmentLong(invocation.getAttachment(TracerUtils.SID));
boolean isSample = (traceId != null);
span = tracer.genSpan(traceId, parentId, spanId, context.getMethodName(), isSample, serviceId);
}
invokerBefore(invocation, span, endpoint, start);//记录annotation
RpcInvocation invocation1 = (RpcInvocation) invocation;
setAttachment(span, invocation1);//设置需要向下游传递的参数
Result result = invoker.invoke(invocation);
if (result.getException() != null){
catchException(result.getException(), endpoint);
}
return result;
}catch (RpcException e) {
if (e.getCause() != null && e.getCause() instanceof TimeoutException){
catchTimeoutException(e, endpoint);
}else {
catchException(e, endpoint);
}
throw e;
}finally {
if (span != null) {
long end = System.currentTimeMillis();
invokerAfter(invocation, endpoint, span, end, isConsumerSide);//调用后记录annotation
}
}
}

dubbo通过invocation.setAttachmen来在消费者和调用者之间传递traceId。
如果是http接口调用实现的rpc建议采用在request的head里面传递traceId。
在本地服务里面通过threadlocal变量来传递traceId。
如果想打印sql语句,通过orm框架的拦截器机制实现,以下是mybatis的参考代码

schema.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }) })
public class MidaiLogMybatisPlugn implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = null;
//从当前线程获取trace
MidaiLogTrace trace = MidaiLogTraceService.getMidaiLogTrace();
if(trace !=null){
Object[] arguments = invocation.getArgs();
MidaiLogTraceService.traceSqlLog(trace.getTraceId(), getSqlStatement(arguments));
}
try {
result = invocation.proceed();
} catch (Exception e) {
throw e;
}
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this); // mybatis提供的包装工具类
}
@Override
public void setProperties(Properties properties) {
}
private String getSqlStatement(Object[] arguments) {
MappedStatement mappedStatement = (MappedStatement) arguments[0];
Object parameter = null;
if (arguments.length > 1) {
parameter = arguments[1];
}
String sqlId = mappedStatement.getId();
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
Configuration configuration = mappedStatement.getConfiguration();
String sql = showSql(configuration, boundSql);
StringBuilder str = new StringBuilder(100);
str.append(sqlId);
str.append(":");
str.append(sql);
str.append(":");
return str.toString();
}
public String showSql(Configuration configuration, BoundSql boundSql) {
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
}
}
}
}
return sql;
}
private static String getParameterValue(Object obj) {
String value = null;
if (obj instanceof String) {
value = "‘" + obj.toString() + "‘";
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
value = "‘" + formatter.format(new Date()) + "‘";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "";
}

}
return value;
}
}

当系统并发达到一定数量级,log4j日志打印本身会成为瓶颈,这个时候需要mq来解耦了,
不在打印日志,而是发送mq消息,由mq消费端处理。因为目前公司项目并发数量还不足以导致该问题,因此尚未采用。
elk收集日志之后,通过kibana可以提供搜索。

剩下最后的工作量就是提供一个web界面来更好的分析和展示数据。

基于jquery把表单转成成json对象

最近前端框架修改,小伙伴希望能像以前写jsp一样使用 对象.属性作为表单元素的name值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<form id="test-form">
<input name="a.b.c" value="abc">
<input name="a.b.d" value="abd">
<input name="x" value="x">
<input name="a.f" value="af">
<input name="y" value="0">
<input name="y" value="y2">
<input name="m[0].c" value="m0c">
<input name="m[0].d" value="m0d">
<input name="m[1].c" value="m1c">
<input name="m[1].d" value="m1d">
<input type="button" id="test-btn">
</form>

我这种懒人第一反应肯定去找度娘,发现度娘居然没有,只好自己造了轮子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

function form2Json(params){
var selector=params.form;
var values= $(selector).serializeArray();
var obj={};
for (var index = 0; index < values.length; ++index)
{
var temp=obj; //上一级
var n=values[index].name;
if(n.indexOf(".")>-1){
var arr=n.split(".");
for(var i=0;i<arr.length-1;i++){
if(arr[i].indexOf("[")>-1){
var a=arr[i].substring(0,arr[i].indexOf("["));
temp[a]=temp[a]||[];
var y=arr[i].substring(arr[i].indexOf("[")+1,arr[i].indexOf("]"));
temp[a][y]=temp[a][y]||{};
temp=temp[a][y];
}else{
temp[arr[i]]=temp[arr[i]] || {};
temp=temp[arr[i]];
}
}
temp[arr[arr.length-1]]=values[index].value;
}else{
if(obj[n] !==undefined && obj[n]!=null){
if( !$.isArray(obj[n])){
var v=obj[n];
obj[n]=[];
obj[n].push(v);
}

obj[n].push(values[index].value);
}else{
obj[n]=values[index].value;
}
}
}
return obj;


该轮子支持复杂对象,支持对象数组,支持简单属性

1
2
3
4
5
6
7
8
9

$("#test-btn").on("click",function(){
var json=form2Json({
form:"#test-form"
});
console.log(json);
console.log(JSON.stringify(json));
});

最终结果如下

1
2
3

{"a":{"b":{"c":"abc","d":"abd"},"f":"af"},"x":"x","y":["0","y2"],"m":[{"c":"m0c","d":"m0d"},{"c":"m1c","d":"m1d"}]}

jQuery基于json对象自动给表单元素赋值

为了提高前端小伙伴的开发效率,造了个基于json对象根据表单元素的name属性自动赋值的轮子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

json2form:function(obj){
var nodeParent = null;
var value = undefined;
var $el = null;
var nodeName = "";
for(var i in obj){
value= obj[i] ;
if(value === undefined || value===null){
continue;
}
if(typeof value == 'object'){
nodeParent=obj.nodeParent;
value.nodeParent=nodeParent?nodeParent+"."+i : i;
if(value instanceof Array){
for(var mm=0;mm<value.length;mm++){
var ms=value[mm];
if(typeof ms == 'object'){
nodeParent=ms.nodeParent;
ms.nodeParent=ms.nodeParent?ms.nodeParent+"."+i+"["+mm+"]":i+"["+mm+"]";
arguments.callee(ms);
}
}
$el=$("[name='"+i+"']");
if($el.is(":checkbox")){
$el.each(function(){
if($(this).val() == value){
$(this).prop("checked",true);
}
})
}
else if($el.is(":radio")){
$el.each(function(){
if($(this).val() == value){
$(this).prop("checked",true);
}
})
}
}else{ //递归
arguments.callee(value);
}
}
else{
nodeName=obj.nodeParent?obj.nodeParent+"."+i : i ;
$el=$("[name='"+nodeName+"']");
if($el.length > 0){
// console.log("匹配数据名称:"+nodeName+"值:"+value);
if($el.is(":text")||$el.attr("type")=="hidden"){
if($el.data("money") && $el.data("money") == "money"){
value = outputdollars(value);
}
$el.val(value);

}else if($el.is(":radio")){
$el.each(function(){
if($(this).val()==value){
$(this).prop("checked",true);
}
})
}
else if($el.is("select")){
$el.find("option").filter(function(){return $(this).val() == obj[i];}).prop("selected",true);
}else if($el.is("textarea")){
$el.val(value)
}
}
}
}

}

注意: 表单的name属于与json对象的属性名为一致,保持继承链。
例如 input name=’a.b.c’ 表示json对象里面的a属性里面的b属性的c属性。

dubbo源码研究之container模块

dubbo-container模块是dubbo启动顺序中的第一个模块,
dubbo-container模块是容器模块,通过dubbo-container模块读取dobbo-config模块的相关配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

/**
* Container. (SPI, Singleton, ThreadSafe)
*
* @author william.liangf
*/
@SPI("spring")
public interface Container {

/**
* start.
*/
void start();

/**
* stop.
*/
void stop();
}

container接口非常简洁,只有两个方法,start,stop。注意两个方法都是void并且不抛出受检查异常。
细心的童鞋也可能发现上面的注释,spi,Singleton,ThreadSafe。 说明该接口是基于spi机制,单例,并且线程安全的。

Container 接口的主要实现

其中JavaConfigContainer是基于spring的javaconfig,其中javaconfig是spring4之后主推的配置模式,
使用spring boot的童鞋如果和dubbo整合使用零配置的方式可以考虑这个类。
SpringContainer主要指定了默认的配置文件路径,通过ClassPathXmlApplicationContext来启动spring容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
    
/**
* SpringContainer. (SPI, Singleton, ThreadSafe)
*
* @author william.liangf
*/
public class SpringContainer implements Container {

private static final Logger logger = LoggerFactory.getLogger(SpringContainer.class);

public static final String SPRING_CONFIG = "dubbo.spring.config";

public static final String DEFAULT_SPRING_CONFIG = "classpath*:META-INF/spring/*.xml";

static ClassPathXmlApplicationContext context;

public static ClassPathXmlApplicationContext getContext() {
return context;
}

public void start() {
String configPath = ConfigUtils.getProperty(SPRING_CONFIG);
if (configPath == null || configPath.length() == 0) {
configPath = DEFAULT_SPRING_CONFIG;
}
context = new ClassPathXmlApplicationContext(configPath.split("[,\\s]+"));
context.start();
}

public void stop() {
try {
if (context != null) {
context.stop();
context.close();
context = null;
}
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
}

}

其他几个实现类似,重点讲下com.alibaba.dubbo.container.Main。这个是dubbo自带的main函数入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

public static void main(String[] args) {
try {
if (args == null || args.length == 0) {
String config = ConfigUtils.getProperty(CONTAINER_KEY, loader.getDefaultExtensionName());
args = Constants.COMMA_SPLIT_PATTERN.split(config);
}

final List<Container> containers = new ArrayList<Container>();
for (int i = 0; i < args.length; i ++) {
containers.add(loader.getExtension(args[i]));
}
logger.info("Use container type(" + Arrays.toString(args) + ") to run dubbo serivce.");

if ("true".equals(System.getProperty(SHUTDOWN_HOOK_KEY))) {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
for (Container container : containers) {
try {
container.stop();
logger.info("Dubbo " + container.getClass().getSimpleName() + " stopped!");
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
synchronized (Main.class) {
running = false;
Main.class.notify();
}
}
}
});
}

for (Container container : containers) {
container.start();
logger.info("Dubbo " + container.getClass().getSimpleName() + " started!");
}
System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]").format(new Date()) + " Dubbo service server started!");
} catch (RuntimeException e) {
e.printStackTrace();
logger.error(e.getMessage(), e);
System.exit(1);
}
synchronized (Main.class) {
while (running) {
try {
Main.class.wait();
} catch (Throwable e) {
}
}
}
}

该方法通过钩子 Runtime.getRuntime().addShutdownHook来实现优雅停机,同时还支持通过启动参数来加载扩展点,
containers.add(loader.getExtension(args[i]))。但是没有类似spring boot 的CommandLineRunner 接口,
在容器启动完毕之后执行一些初始化动作。建议使用dubbo的童鞋如没有特殊需求,可以使用该类作为程序启动类。

dubbo源码研究之rpc模块

dubbo作为一个服务化框架,rpc模块是dubbo整个框架的核心部分。我们来通过dubbo来了解rpc调用的本质。

dubbo的rpc模块以Invocation和Result为中心,扩展接口为Protocol、Invoker和Exporter。
Protocol是服务域,它是Invoker暴露和引用的主功能入口,它负责Invoker的生命周期管理。
Invoker是实体域,它是Dubbo的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起invoke调用,
它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
invoker接口提供两个方法。
Class getInterface(); 获取调用的接口
Result invoke(Invocation invocation) throws RpcException; 执行调用
说明rpc就两件事,第一,定位要调用的是哪个接口,第二发起调用返回结果。值得注意的是invoker需要Invocation提供的信息,Invocation 支持有本地invoker。invoker不参与Invocation的序列化。Invocation转成request对象通过网络传递并序列化和反序列化。

rpccontext是一个静态工具类,通过threadlocal来保存单次调用的上下文。
Protocol负载invoker的暴露与引用。

一个refer的invoker就会存在一个export的invoker,如果引用的服务不存在,则需要设置
暴露服务顺序

引用服务时序

dubbo的rpc模块也遵守dubbo的整体设计原则采用Microkernel + Plugin模式,Microkernel只负责组装Plugin。
invoker是核心模型,服务方把invoker对象暴露成Exporter对象,并把url描述写入注册中心,注册中心推送到消费方。消费方通过url转成invoker对象。

dubbo源码研究之dubbo-registry模块

dubbo-registry注册中心模块,基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。
registry模块顶层接口为RegistryService和NotifyListener以及一个工厂接口RegistryFactory。
RegistryService接口包含4个方法。

  • void register(URL url); 注册服务
  • void unregister(URL url); 取消注册
  • void subscribe(URL url, NotifyListener listener); 订阅服务(推)
  • void unsubscribe(URL url, NotifyListener listener);取消订阅
  • List lookup(URL url); 订阅服务(拉)

Registry模块使用经典的消费者生成者模式,dubbo消费者订阅服务,dubbo服务者注册服务。
AbstractRegistry为RegistryService接口实现的一个抽象类,提供一些默认实现。

schema.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public AbstractRegistry(URL url) {
setUrl(url);
// 启动文件保存定时器
syncSaveFile = url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY, false);
String filename = url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getHost() + ".cache");
File file = null;
if (ConfigUtils.isNotEmpty(filename)) {
file = new File(filename);
if(! file.exists() && file.getParentFile() != null && ! file.getParentFile().exists()){
if(! file.getParentFile().mkdirs()){
throw new IllegalArgumentException("Invalid registry store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
}
}
}
this.file = file;
loadProperties();
notify(url.getBackupUrls());
}

可以看出AbstractRegistry类在构建的时候,会去本地磁盘文件读取文件,转成properties对象。
文件默认路径是user.home/.dubbo/dubbo-registry-host.cache
读取完之后会把这个url存到本地缓存文件里面去,但是该方法并非递增修改,而且是全部重写文件。

schema.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

public void doSaveProperties(long version) {
if(version < lastCacheChanged.get()){
return;
}
if (file == null) {
return;
}
Properties newProperties = new Properties();
// 保存之前先读取一遍,防止多个注册中心之间冲突
InputStream in = null;
try {
if (file.exists()) {
in = new FileInputStream(file);
newProperties.load(in);
}
} catch (Throwable e) {
logger.warn("Failed to load registry store file, cause: " + e.getMessage(), e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
// 保存
try {
newProperties.putAll(properties);
File lockfile = new File(file.getAbsolutePath() + ".lock");
if (!lockfile.exists()) {
lockfile.createNewFile();
}
RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
try {
FileChannel channel = raf.getChannel();
try {
FileLock lock = channel.tryLock();
if (lock == null) {
throw new IOException("Can not lock the registry cache file " + file.getAbsolutePath() + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.registry.file=xxx.properties");
}
// 保存
try {
if (! file.exists()) {
file.createNewFile();
}
FileOutputStream outputFile = new FileOutputStream(file);
try {
newProperties.store(outputFile, "Dubbo Registry Cache");
} finally {
outputFile.close();
}
} finally {
lock.release();
}
} finally {
channel.close();
}
} finally {
raf.close();
}
} catch (Throwable e) {
if (version < lastCacheChanged.get()) {
return;
} else {
registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
}
logger.warn("Failed to save registry store file, cause: " + e.getMessage(), e);
}
}

如果是两个不同的注册中心,不能使用一个缓存文件。
值得注意一点的是,多个dubbo服务公用一个cache文件的时候,如果一起启动,可能会出现Failed to load registry store File 这类异常信息,
这是因为当时该文件其他服务占用了。这种情况开发环境比较多,可以无视掉。因为会重复去注册的,下面会说明,
但是尽量还是手动指定注册缓存文件比较好。尽量不使用同一个缓存文件。

1
<dubbo:registry address="${zookeeper.address}" file=".cache/dubbo-registry-car.cache" />

这样指定的话,缓存文件会在当前工程目录下。
RegistryService接口的所有方法参数都有URL对象,说明dubbo服务最终是转成URL来注册的。通过URL来表示服务的全部信息。
FailbackRegistry是AbstractRegistry类的子类,通过模式设计模式扩展,重写了AbstractRegistry类一些实现,并新增模板方法,交个子类去实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public FailbackRegistry(URL url) {
super(url);
int retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
public void run() {
// 检测并连接注册中心
try {
retry();
} catch (Throwable t) { // 防御性容错
logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t);
}
}
}, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS);
}

FailbackRegistry构建的时候通过ScheduledExecutorService来延迟执行连接注册中心,默认延迟周期为5秒。
其中retry()方法从set集合里面取出注册失败的url,然后不断去重新注册。如果注册中心当机了,但是只要注册中心重新启动之后,dubbo还是会去重新注册的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

@Override
public void register(URL url) {
super.register(url);
failedRegistered.remove(url);
failedUnregistered.remove(url);
try {
// 向服务器端发送注册请求
doRegister(url);
} catch (Exception e) {
Throwable t = e;

// 如果开启了启动时检测,则直接抛出异常
boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
&& url.getParameter(Constants.CHECK_KEY, true)
&& ! Constants.CONSUMER_PROTOCOL.equals(url.getProtocol());
boolean skipFailback = t instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if(skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
} else {
logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
}

// 将失败的注册请求记录到失败列表,定时重试
failedRegistered.add(url);
}
}

register方法把具体的注册实现交给子类的doRegister实现。注册失败的都会记录到失败的set集合里面去,值得注意一个check 配置,当设置时,记录失败注册和订阅请求,后台定时重试。
zookeeper注册中心是dubbo推荐使用的注册中心,本身zookeeper的基于是Paxos,一个分布式一致性算法。

流程:

  1. 服务提供者启动时向/dubbo/com.foo.BarService/providers目录下写入URL
  2. 服务消费者启动时订阅/dubbo/com.foo.BarService/providers目录下的URL向/dubbo/com.foo.BarService/consumers目录下写入自己的URL
  3. 监控中心启动时订阅/dubbo/com.foo.BarService目录下的所有提供者和消费者URL

java多线程基础

线程安全性

当多个线程访问莫个类时,这个类始终都能表现出正确的行为,那么这个类就是线程安全的。
在线程安全的类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。
无状态对象一定是线程安全的。

保证线程安全性的三种方式

  1. 不在线程之间共享状态变量
  2. 将状态变量修改为不可变
  3. 访问状态变量时使用同步

原子性

在单核CPU中, 能够在一个指令中完成的操作都可以看作为原子操作, 因为中断只发生在指令间。
java中对于非long和double基本数据类型的”简单操作”都可以看作是原子的。
long和double都是64位,jmm不保证操作的原子性。

可见性

变量读取到的值,总是任何线程对该变量最后写入的值。
加锁不只局限于互斥行为,还包括了内存可见性。为了确保所有线程都能共享到变量最新的值,因此读或者写都必须在一个锁上同步。

顺序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序

线程封闭和栈封闭

线程封闭:变量只属于某个线程,例如通过 ThreadLocal来保持的线程变量。
栈封闭是线程封闭的一种特例,在栈封闭中,只有通过局部对象才能访问对象。

不变性

不可变的对象一定是线程安全的。不可变不等于final,即使对象所有的域都是final的,对象也依然可能是可以变得。
final 修饰引用类型只代表该引用不可变,不代表引用的对象不可变。final修饰简单值类型能保证是不可变的。
满足一下条件对象才是不可变的。

  • 对象创建以后不可以修改
  • 对象创建以后不可以修改
  • 对象是正确创建的(this没有溢出)

dubbo源码研究之extension模块

dubbo的扩展采用spi机制实现,
spi(Service Provider Interface)是指一些提供给你继承、扩展,完成自定义功能的类、接口或者方法。
spi把控制权利交个调用方,调用方来决定使用该spi的哪个实现。
dubbo扩展机制的核心类是ExtensionLoader,该类通过静态方法getExtensionLoader获取一个指定接口的ExtensionLoader实例。

ExtensionLoader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if(!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if(!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}

ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}

该方法要求通过spi实现的接口上必须包含@spi注解,并且一个接口的ExtensionLoader是唯一的,保存在静态容器EXTENSION_LOADERS(ConcurrentHashMap)中。
ExtensionLoader提供实例方法getExtension获取该接口的具体实现。

ExtensionLoader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
return getDefaultExtension();
}
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}

具体流程如下

该方法通过大量缓存容器来优化性能,并且每个扩展点都是单例存在,所以扩展dubbo框架的时候要注意该扩展点的线程安全性。
ExtensionFactory为spi接口实现实例在注入属性(injectExtension)时提供注入的属性.

该工厂有三个实现,分别支持从spring ,spi,Adaptive里面获取对象,注入Extension对象中。

0%