Fork me on GitHub

Trouble Shooting —— CAS Server集群环境下TGC验证问题排查,需要开启会话保持

问题现象

CAS部署结构:

两台cas server通过nginx做负载均衡,两个cas serverticket registry配置的jpa方式,指向同一个库。两个cas servertomcat做了TomcatRedisSessionManager,使用redis集中存储session

目前的现象:

页面上请求cas登录地址,登录过后频繁刷新登录页面,有时返回已登录,有时返回未登录,当返回未登录时去后台查看日志发现有如下错误,验证cookie发现请求的源IP与第一次访问的源IP不一致。这个很明显是cas集群环境下的问题。

2018-03-16 10:02:44,418 DEBUG [org.apereo.cas.web.support.TGCCookieRetrievingCookieGenerator] - <Invalid cookie. Required remote address does not match ${ip}>
java.lang.IllegalStateException: Invalid cookie. Required remote address does not match ${ip}
	at org.apereo.cas.web.support.DefaultCasCookieValueManager.obtainCookieValue(DefaultCasCookieValueManager.java:84) ~[cas-server-support-cookie-5.0.4.jar:5.0.4]
	at org.apereo.cas.web.support.CookieRetrievingCookieGenerator.retrieveCookieValue(CookieRetrievingCookieGenerator.java:93) ~[cas-server-support-cookie-5.0.4.jar:5.0.4]
	at org.apereo.cas.web.support.CookieRetrievingCookieGenerator$$FastClassBySpringCGLIB$$25dba342.invoke(<generated>) ~[cas-server-support-cookie-5.0.4.jar:5.0.4]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:133) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:121) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.apereo.cas.web.support.CookieRetrievingCookieGenerator$$EnhancerBySpringCGLIB$$10d36968.retrieveCookieValue(<generated>) ~[cas-server-support-cookie-5.0.4.jar:5.0.4]
	at org.apereo.cas.logging.web.ThreadContextMDCServletFilter.doFilter(ThreadContextMDCServletFilter.java:83) ~[cas-server-core-logging-5.0.4.jar:5.0.4]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]
	at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]
	at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:107) ~[spring-boot-actuator-1.4.2.RELEASE.jar:1.4.2.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]
	at org.springframework.boot.web.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:117) ~[spring-boot-1.4.2.RELEASE.jar:1.4.2.RELEASE]
	at org.springframework.boot.web.support.ErrorPageFilter.access$000(ErrorPageFilter.java:61) ~[spring-boot-1.4.2.RELEASE.jar:1.4.2.RELEASE]
	at org.springframework.boot.web.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:92) ~[spring-boot-1.4.2.RELEASE.jar:1.4.2.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.boot.web.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:110) ~[spring-boot-1.4.2.RELEASE.jar:1.4.2.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]
	at org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:71) ~[log4j-web-2.6.2.jar:2.6.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:110) ~[catalina.jar:7.0.85]
	at com.r.tomcat.session.management.RequestSessionHandlerValve.invoke(RequestSessionHandlerValve.java:30) ~[TomcatRedisSessionManager-1.0.jar:?]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169) ~[catalina.jar:7.0.85]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103) ~[catalina.jar:7.0.85]
	at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:962) ~[catalina.jar:7.0.85]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116) ~[catalina.jar:7.0.85]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:445) ~[catalina.jar:7.0.85]
	at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1115) ~[tomcat-coyote.jar:7.0.85]
	at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637) ~[tomcat-coyote.jar:7.0.85]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1775) ~[tomcat-coyote.jar:7.0.85]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1734) ~[tomcat-coyote.jar:7.0.85]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_162]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_162]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-coyote.jar:7.0.85]
	at java.lang.Thread.run(Thread.java:748) [?:1.8.0_162]

网上查询资料:google group,相同的问题,但是没有看到具体的解决方法。

看到的tomcat RemoteIpValue也只是tomcat请求ip限制的方法,跟我们要的不匹配

根据异常查看CAS代码,如下:

 public String obtainCookieValue(Cookie cookie, HttpServletRequest request)
  {
    String cookieValue = (String)this.cipherExecutor.decode(cookie.getValue());
    LOGGER.debug("Decoded cookie value is [{}]", cookieValue);
    if (StringUtils.isBlank(cookieValue))
    {
      LOGGER.debug("Retrieved decoded cookie value is blank. Failed to decode cookie [{}]", cookie.getName());
      return null;
    }
    String[] cookieParts = cookieValue.split(String.valueOf('@'));
    if (cookieParts.length != 3) {
      throw new IllegalStateException("Invalid cookie. Required fields are missing");
    }
    String value = cookieParts[0];
    String remoteAddr = cookieParts[1];
    String userAgent = cookieParts[2];
    if ((StringUtils.isBlank(value)) || (StringUtils.isBlank(remoteAddr)) || 
      (StringUtils.isBlank(userAgent))) {
      throw new IllegalStateException("Invalid cookie. Required fields are empty");
    }
    if (!remoteAddr.equals(request.getRemoteAddr())) {
      throw new IllegalStateException("Invalid cookie. Required remote address does not match " + request.getRemoteAddr());
    }
    String agent = WebUtils.getHttpServletRequestUserAgent(request);
    if (!userAgent.equals(agent)) {
      throw new IllegalStateException("Invalid cookie. Required user-agent does not match " + agent);
    }
    return value;
  }

TGC中包含了user-agent信息,会根据requestuser-agent去跟decode后的cookie中的user-agent对比,而且这个验证是在cas 4.1版本就已经加了这个验证信息了,如果我们修改源码去掉这个user-agent验证可能还会引发其他问题。

解决方案

因此我们选用负载均衡的保持会话来解决这个问题了。

  1. 如果使用的是阿里云的SLB需要开启会话保持的选项。
  2. 如果使用nginx需要在upstream中增加ip_hash保持会话。

这样就可以让相同的客户端ip将会话永远路由到相同的一台后端cas server上去。

经过验证解决了上述的问题。

所以这里需要说明一下,在对cas server做集群实现无状态化,需要注意一下几点:

  1. casticket需要做到集中存储,可以使用redisjpa、或者其他方式,这个官方文章上有详细介绍:ticket-registry
  2. cassession信息需要做到集中存储,如果使用的是tomcat可以使用TomcatRedisSessionMananger插件来通过redis做session集中存储。
  3. 还有一个就是上面遇到的问题,客户端cookie信息:TGCTGC采用cookie方式存在客户端,因此需要开启会话保持,使得相同客户端每次都会被路由到同一个cas server上去做TGC验证。
  4. 最后一个就是需要接入ssoclient应用端的session信息也需要做集中存储,因此cas server会和client进行通信去验证ticket,验证完后会生成信息并存储到sesson中,因此也需要使用TomcatRedisSessionMananger插件来通过redissession集中存储。

世界和平、Keep Real!

Json序列化、反序列化支持泛型,Dubbo对泛型参数方法进行反射调用

Published on:

最近在对Dubbo接口进行反射调用时,遇到了参数类型较为复杂的情况下,使用反射方式无法调用的问题。

由于Dubbo使用了proxy代理对象,因此在反射上调用是存在一定的问题,从反射对象上获取的方法和参数类型可能会导致无法正常的调用。

首先先让我们看一个复杂参数的接口定义

public String testMethod(Map<String,ResourceVo> map, List<Map<String,ResourceVo>> list) throws BizException;

Gson反序列化复杂类型

在对参数进行反序列化时,内部的类型容易丢失,我们可以使用gson的Type进行反序列化得到正确的参数值,让我们看一下gson反序列化的两个方法

  /**
   * This method deserializes the specified Json into an object of the specified class. It is not
   * suitable to use if the specified class is a generic type since it will not have the generic
   * type information because of the Type Erasure feature of Java. Therefore, this method should not
   * be used if the desired type is a generic type. Note that this method works fine if the any of
   * the fields of the specified object are generics, just the object itself should not be a
   * generic type. For the cases when the object is of generic type, invoke
   * {@link #fromJson(String, Type)}. If you have the Json in a {@link Reader} instead of
   * a String, use {@link #fromJson(Reader, Class)} instead.
   *
   * @param <T> the type of the desired object
   * @param json the string from which the object is to be deserialized
   * @param classOfT the class of T
   * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null}.
   * @throws JsonSyntaxException if json is not a valid representation for an object of type
   * classOfT
   */
  public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
    Object object = fromJson(json, (Type) classOfT);
    return Primitives.wrap(classOfT).cast(object);
  }

 /**
   * This method deserializes the specified Json into an object of the specified type. This method
   * is useful if the specified object is a generic type. For non-generic objects, use
   * {@link #fromJson(String, Class)} instead. If you have the Json in a {@link Reader} instead of
   * a String, use {@link #fromJson(Reader, Type)} instead.
   *
   * @param <T> the type of the desired object
   * @param json the string from which the object is to be deserialized
   * @param typeOfT The specific genericized type of src. You can obtain this type by using the
   * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for
   * {@code Collection<Foo>}, you should use:
   * <pre>
   * Type typeOfT = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
   * </pre>
   * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null}.
   * @throws JsonParseException if json is not a valid representation for an object of type typeOfT
   * @throws JsonSyntaxException if json is not a valid representation for an object of type
   */
  @SuppressWarnings("unchecked")
  public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException {
    if (json == null) {
      return null;
    }
    StringReader reader = new StringReader(json);
    T target = (T) fromJson(reader, typeOfT);
    return target;
  }

让我们测试一下复杂接口参数在使用这两个方法反序列化会有什么不同

String json = "{\"name\":\"name\",\"value\":{\"service\":\"test1\",\"url\":\"test\",\"action\":\"GET\",\"enabled\":true,\"isPublic\":false,\"appId\":8,\"menuId\":30001}}";
Class clazz = Map.class;
Map map = gson.fromJson(json, clazz);

上面代码反序列化后的map对象实际是com.google.gson.internal.LinkedTreeMap<K, V>,这个是gson中自定义的Map实现类,而且内部的对象也都是LinkedTreeMap,当我们换成HashMap时,返回的结果都是HashMap,但是我们的方法上使用的是Map<String,ResourceVo>,如何才能反序列化得到这个类型的对象呢?让我们看一下使用Type后的情况。

String json = "{\"name\":\"name\",\"value\":{\"service\":\"test1\",\"url\":\"test\",\"action\":\"GET\",\"enabled\":true,\"isPublic\":false,\"appId\":8,\"menuId\":30001}}";
Type type = new TypeToken<ResourceVo>(){}.getType();
Map<ResourceVo> map = gson.fromJson(json, type);

通过使用TypeToken生成的Type对象可以得到Map<String,ResourceVo>这个类型的实例,但是当我们在反射调用方法时,由于不知道参数是什么类型,也不能够import自定义的对象来使用TypeToken来获取type对象,那我们应该怎么做呢?接着往下看

ps.类型:List<ResourceVo>和List<Map<Object,ResourceVo>>这样的类型一样使用Type来进行反序列化

String json = "{\"name\":\"name\",\"value\":{\"service\":\"test1\",\"url\":\"test\",\"action\":\"GET\",\"enabled\":true,\"isPublic\":false,\"appId\":8,\"menuId\":30001}}";
Class clazz = Class.forName("com.package.JavaBean");
String methodName = "testMethod";
Method[] methods = clazz.getMethods();
for (Method m : methods) {
	if (m.getName().equals(methodName)) {
		Type[] paramTypes = m.getGenericParameterTypes();
		for (int j = 0; j < paramTypes.length; j++) {
			gson.fromJson(json, paramTypes[j]);
		}
	}
}

可以通过method.getGenericParameterTypes()获取参数的Type对象。

但是需要注意的是,当使用Proxy代理对象通过上面的方式获取的Type对象全都是java.lang.Class

那如何解决代理对象获取的Type不正确的问题呢?

正确的做法就是放弃通过Proxy对象来进行反射,使用Class.forName获取Class对象进行反射。

可以通过Class.forName的方式获取Class对象,再获取Method对象,最后通过Method.getGenericParameterTypes()获取正确的Type对象,这个步骤是构造方法的参数类型和参数值。但是通过这个方式构造出来的参数类型和参数值,无法通过proxy对象来进行method.invoke,其原因就是原始接口的方法参数定义和代理对象的方法参数定义不同导致。这让我们如何是好。

继续往下看。

Dubbo泛化调用

通过Dubbo的官网文档找到Dubbo支持GenericService泛化调用,什么是泛化调用?

泛化接口调用方式主要用于客户端没有 API 接口及模型类元的情况,参数及返回值中的所有 POJO 均用 Map 表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现。

ps. GenericService实际上是Dubbo提供的通用接口,解决使用通用接口调用任何服务方法

这样我们就可以使用前面说到的参数反序列化方式来获取方法的参数类型和参数值,传入GenericService通用接口来对目标方法进行调用。

首先先让我们看一下Dubbo的泛化调用如何使用。

import com.alibaba.dubbo.rpc.config.ApplicationConfig;
import com.alibaba.dubbo.rpc.config.RegistryConfig;
import com.alibaba.dubbo.rpc.config.ConsumerConfig;
import com.alibaba.dubbo.rpc.config.ReferenceConfig;

Class clazz = Class.forName("com.package.JavaBean");
String method = "testMethod"
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("yyy");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("10.20.130.230:9090");
// 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
// 引用远程服务
ReferenceConfig reference = new ReferenceConfig(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
reference.setInterface(clazz);
reference.setVersion("1.0.0");
reference.setRetries(0);
reference.setCluster("failfast");
reference.setTimeout(12001);
reference.setGeneric(true);
GenericService genericService = (GenericService) reference.get();
Object result = genericService.$invoke(method, parameterTypes, parameterValues);

只要给reference设置generictrue就可以使用GenericService通用接口来进行方法调用。

这样我们就可以顺利的完成任何参数类型方法的反射调用。

从而避免了通过Proxy代理类获取到不正确的参数Type导致反序列化参数失败,这个原因前面也说了是因为原始接口的方法参数定义和代理对象的方法参数定义不同导致。

接下来让我们看一下具体的实现

@SuppressWarnings({"unchecked", "rawtypes", "static-access"})
private Object callDubbo(SampleResult res) {
    ApplicationConfig application = new ApplicationConfig();
    application.setName("DubboSample");
    
    // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
    ReferenceConfig reference = new ReferenceConfig();
    // 引用远程服务
    reference.setApplication(application);
    RegistryConfig registry = null;
    
    String protocol = getProtocol();
    if ("zookeeper".equals(protocol)) {
        // 连接注册中心配置
        registry = new RegistryConfig();
        registry.setProtocol("zookeeper");
        registry.setAddress(getAddress());
        reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
    } else {
        StringBuffer sb = new StringBuffer();
        sb.append(protocol).append("://").append(getAddress()).append("/").append(getInterface());
        log.info("rpc invoker url : " + sb.toString());
        reference.setUrl(sb.toString());
    }
    try {
        Class clazz = Class.forName(getInterface());
        reference.setInterface(clazz);
        reference.setRetries(Integer.valueOf(getRetries()));
        reference.setCluster(getCluster());
        reference.setVersion(getVersion());
        reference.setTimeout(Integer.valueOf(getTimeout()));
        reference.setGeneric(true);
        GenericService genericService = (GenericService) reference.get();
        Method method = null;
        String[] parameterTypes = null;
        Object[] parameterValues = null;
        List<MethodArgument> args = getMethodArgs();
        List<String> paramterTypeList = null;
        List<Object> parameterValuesList = null;
        Method[] methods = clazz.getMethods();
		for (int i = 0; i < methods.length; i++) {
			Method m = methods[i];
			Type[] paramTypes = m.getGenericParameterTypes();
			paramterTypeList = new ArrayList<String>();
			parameterValuesList = new ArrayList<Object>();
			log.info("paramTypes.length="+paramTypes.length+"|args.size()="+args.size());
			if (m.getName().equals(getMethod()) && paramTypes.length == args.size()) {
				//名称与参数数量匹配,进行参数类型转换
				for (int j = 0; j < paramTypes.length; j++) {
					paramterTypeList.add(args.get(j).getParamType());
					ClassUtils.parseParameter(paramTypes[j], parameterValuesList, args.get(j));
				}
				if (parameterValuesList.size() == paramTypes.length) {
					//没有转换错误,数量应该一致
					method = m;
					break;
				}
			}
		}
        if (method == null) {
            res.setSuccessful(false);
            return "Method["+getMethod()+"] Not found!";
        }
        //发起调用
        parameterTypes = paramterTypeList.toArray(new String[paramterTypeList.size()]);
        parameterValues = parameterValuesList.toArray(new Object[parameterValuesList.size()]);
        Object result = null;
		try {
			result = genericService.$invoke(getMethod(), parameterTypes, parameterValues);
			res.setSuccessful(true);
		} catch (Throwable e) {
			log.error("接口返回异常:", e);
			res.setSuccessful(false);
			result = e;
		}
        return result;
    } catch (Exception e) {
        log.error("调用dubbo接口出错:", e);
        res.setSuccessful(false);
        return e;
    } finally {
        if (registry != null) {
            registry.destroyAll();
        }
        reference.destroy();
    }
}

## ClassUtils.parseParameter方法代码

public static void parseParameter(Type type,
		List<Object> parameterValuesList, MethodArgument arg)
		throws ClassNotFoundException {
	String className = getClassName(type);
	if (className.equals("int")) {
		parameterValuesList.add(Integer.parseInt(arg.getParamValue()));
	} else if (className.equals("double")) {
		parameterValuesList.add(Double.parseDouble(arg.getParamValue()));
	} else if (className.equals("short")) {
		parameterValuesList.add(Short.parseShort(arg.getParamValue()));
	} else if (className.equals("float")) {
		parameterValuesList.add(Float.parseFloat(arg.getParamValue()));
	} else if (className.equals("long")) {
		parameterValuesList.add(Long.parseLong(arg.getParamValue()));
	} else if (className.equals("byte")) {
		parameterValuesList.add(Byte.parseByte(arg.getParamValue()));
	} else if (className.equals("boolean")) {
		parameterValuesList.add(Boolean.parseBoolean(arg.getParamValue()));
	} else if (className.equals("char")) {
		parameterValuesList.add(arg.getParamValue().charAt(0));
	} else if (className.equals("java.lang.String")
			|| className.equals("String") || className.equals("string")) {
		parameterValuesList.add(String.valueOf(arg.getParamValue()));
	} else if (className.equals("java.lang.Integer")
			|| className.equals("Integer") || className.equals("integer")) {
		parameterValuesList.add(Integer.valueOf(arg.getParamValue()));
	} else if (className.equals("java.lang.Double")
			|| className.equals("Double")) {
		parameterValuesList.add(Double.valueOf(arg.getParamValue()));
	} else if (className.equals("java.lang.Short")
			|| className.equals("Short")) {
		parameterValuesList.add(Short.valueOf(arg.getParamValue()));
	} else if (className.equals("java.lang.Long")
			|| className.equals("Long")) {
		parameterValuesList.add(Long.valueOf(arg.getParamValue()));
	} else if (className.equals("java.lang.Float")
			|| className.equals("Float")) {
		parameterValuesList.add(Float.valueOf(arg.getParamValue()));
	} else if (className.equals("java.lang.Byte")
			|| className.equals("Byte")) {
		parameterValuesList.add(Byte.valueOf(arg.getParamValue()));
	} else if (className.equals("java.lang.Boolean")
			|| className.equals("Boolean")) {
		parameterValuesList.add(Boolean.valueOf(arg.getParamValue()));
	} else {
		parameterValuesList.add(JsonUtils.formJson(arg.getParamValue(),
				type));
	}
}

## JsonUtils.formJson方法代码

public static <T> T formJson(String json, Type type) {
	try {
		return gson.fromJson(json, type);
	} catch (JsonSyntaxException e) {
		logger.error("json to class[" + type.getClass().getName()
				+ "] is error!", e);
	}
	return null;
}

总结

  1. 复杂参数类型:Map<Object, ResourceVo>List<ResourceVo>List<Map<Object,ResourceVo>>使用gson.fromJson(json, classOfT)反序列化会丢失内部的类型。通过使用gson.fromJson(json, type)方式可以得到正确的类型。
  2. 通过Proxy对象的method.getGenericParameterTypes()获取的Type值全部为java.lang.Class,我们需要的是java.util.Map<com.package.ResourceVo>
  3. 使用Class.forName得到Class,再获取Method,再通过method.getGenericParameterTypes()获取我们想要的参数Type是:java.util.Map<com.package.ResourceVo>
  4. 通过Class.forName得到Class,再获取Method,再通过method.getGenericParameterTypes()构造出来的参数类型和参数值,无法通过Proxy代理对象来进行method.invoke,其原因是:原始接口的方法参数定义和代理对象的方法参数定义不同导致。
  5. 放弃通过Proxy对象的method.invoke方式调用接口,通过Dubbo的通用服务接口(GenericService)来调用任何服务接口方法:GenericService.$invoke(method, parameterTypes, args)

参数对照参考表如下

Java类型 paramType paramValue
int int 1
double double 1.2
short short 1
float float 1.2
long long 1
byte byte 字节
boolean boolean true或false
char char A,如果字符过长取值为:”STR”.charAt(0)
java.lang.String java.lang.String或String或string 字符串
java.lang.Integer java.lang.Integer或Integer或integer 1
java.lang.Double java.lang.Double或Double 1.2
java.lang.Short java.lang.Short或Short 1
java.lang.Long java.lang.Long或Long 1
java.lang.Float java.lang.Float或Float 1.2
java.lang.Byte java.lang.Byte或Byte 字节
java.lang.Boolean java.lang.Boolean或Boolean true或false
JavaBean com.package.Bean {“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}
java.util.Map以及子类 java.util.Map以及子类 {“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}
java.util.Map<String,JavaBean> java.util.Map {“name”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},“value”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}}
java.util.HashMap<Object,Object> java.util.HashMap {“name”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},“value”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}}
java.util.Collection以及子类 java.util.Collection以及子类 [“a”,“b”]
java.util.List<String> java.util.List [“a”,“b”]
java.util.List<JavaBean> java.util.List [{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}]
java.util.List<Map<Object, JavaBean>> java.util.List [{“name”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},“value”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}},{“name”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},“value”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}}]
java.util.List<Long> java.util.List [1,2,3]
java.util.ArrayList<Object> java.util.ArrayList [“ny”,1,true]

New Version V1.2.0, Dubbo Plugin for Apache JMeter

项目地址

jmeter-plugin-dubbo项目已经transfer到dubbo group下

github: jmeter-plugin-dubbo

码云: jmeter-plugin-dubbo

V1.2.0

  1. 使用gson进行json序列化、反序列化
  2. 使用dubbo泛化调用方式重构反射调用方式
  3. 支持复杂类型、支持泛型,例如:”java.lang.List,Map map,List> list”

本次版本主要对反射参数类型进行了增强,支持复杂类型、支持参数泛型,可以参考如下的参数对照表:

Java类型 paramType paramValue
int int 1
double double 1.2
short short 1
float float 1.2
long long 1
byte byte 字节
boolean boolean true或false
char char A,如果字符过长取值为:”STR”.charAt(0)
java.lang.String java.lang.String或String或string 字符串
java.lang.Integer java.lang.Integer或Integer或integer 1
java.lang.Double java.lang.Double或Double 1.2
java.lang.Short java.lang.Short或Short 1
java.lang.Long java.lang.Long或Long 1
java.lang.Float java.lang.Float或Float 1.2
java.lang.Byte java.lang.Byte或Byte 字节
java.lang.Boolean java.lang.Boolean或Boolean true或false
JavaBean com.package.Bean {“service”:“test1”,“url”:“test-${__RandomString(5,12345,ids)}“,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}
java.util.Map以及子类 java.util.Map以及子类 {“service”:“test1”,“url”:“test-${__RandomString(5,12345,ids)}“,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}
java.util.Map<String,JavaBean> java.util.Map {“name”:{“service”:“test1”,“url”:“test-${__RandomString(5,12345,ids)}“,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},“value”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}}
java.util.HashMap<Object,Object> java.util.HashMap {“name”:{“service”:“test1”,“url”:“test-${__RandomString(5,12345,ids)}“,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},“value”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}}
java.util.Collection以及子类 java.util.Collection以及子类 [“a”,“b”]
java.util.List<String> java.util.List [“a”,“b”]
java.util.List<JavaBean> java.util.List [{“service”:“test1”,“url”:“test-${RandomString(5,12345,ids)}“,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},{“service”:“test1”,“url”:“test-${RandomString(5,12345,ids)}“,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}]
java.util.List<Map<Object, JavaBean>> java.util.List [{“name”:{“service”:“test1”,“url”:“test-${RandomString(5,12345,ids)}“,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},“value”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}},{“name”:{“service”:“test1”,“url”:“test-${RandomString(5,12345,ids)}“,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001},“value”:{“service”:“test1”,“url”:“test”,“action”:“GET”,“enabled”:true,“isPublic”:false,“appId”:8,“menuId”:30001}}]
java.util.List<Long> java.util.List [1,2,3]
java.util.ArrayList<Object> java.util.ArrayList [“ny”,1,true]

Python项目生成requirements.txt的多种方式,用于类库迁移必备

Published on:

我相信任何软件程序都会有依赖的类库,尤其现在开源如此的火爆,很多轮子可以拿来直接使用不再需要自己再去开发(拿来主义者),这样大大的提高开发效率。NPM就是轮子最多的地方,哈哈!开个玩笑!

我们做开发时为何需要对依赖库进行管理?当依赖类库过多时,如何管理类库的版本?

我相信大家都知道怎么回答这个问题,为了更加规范管理项目结构,提高开发效率所以我们需要对依赖库进行管理,不管使用任何开发语言,如今都有依赖库的管理工具。

例如:JavaMavenGradleJSNPMPythonpipeasy_installLinuxapt-getyun 等。

我们这里就对Python的依赖库管理来进一步说一说。

Python提供通过requirements.txt文件来进行项目中依赖的三方库进行整体安装导入。

那首先让我们看一下requirements.txt的格式

requests==1.2.0
Flask==0.10.1

Python安装依赖库使用pip可以很方便的安装,如果我们需要迁移一个项目,那我们就需要导出项目中依赖的所有三方类库的版本、名称等信息。

接下来就看Python项目如何根据requirements.txt文件来安装三方类库

方法一:pip freeze

pip freeze > requirements.txt

pip freeze命令输出的格式和requirements.txt文件内容格式完全一样,因此我们可以将pip freeze的内容输出到文件requirements.txt中。在其他机器上可以根据导出的requirements.txt进行包安装。

如果要安装requirements.txt中的类库内容,那么你可以执行

pip install -r requirements.txt

注意:pip freeze输出的是本地环境中所有三方包信息,但是会比pip list少几个包,因为pipwheelsetuptools等包,是自带的而无法(un)install的,如果要显示所有包可以加上参数-all,即pip freeze -all

方法二:pipreqs

使用pipreqs生成requirements.txt

首先先安装pipreqs

pip install pipreqs

使用pipreqs生成requirements.txt

pipreqs requirements.txt

注意:pipreqs生成指定目录下的依赖类库

上面两个方法的区别?

使用pip freeze保存的是当前Python环境下所有的类库,如果你没有用virtualenv来对Python环境做虚拟化的话,类库就会很杂很多,在对项目进行迁移的时候我们只需关注项目中使用的类库,没有必要导出所有安装过的类库,因此我们一般迁移项目不会使用pipreqspip freeze更加适合迁移整个python环境下安装过的类库时使用。

不知道virtualenv是什么或者不会使用它的可以查看:《构建Python多个虚拟环境来进行不同版本开发之神器-virtualenv》

使用pipreqs它会根据当前目录下的项目的依赖来导出三方类库,因此常用与项目的迁移中。

这就是pip freezepipreqs的区别,前者是导出Python环境下所有安装的类库,后者导出项目中使用的类库。

Bug Fix Version V1.1.0, Dubbo Plugin for Apache JMeter

首先先感谢网友 @流浪的云 提的bug,让我感觉到写这个工具没有白费还有点价值,非常感谢,

他在使用jmeter-plugin-dubbo插件时发现GUI中输入的信息无法使用Jmeter变量${var}与函数来进行参数化,以下是我修复这个问题的记录。

项目地址

jmeter-plugin-dubbo项目已经transfer到dubbo group下

github: jmeter-plugin-dubbo

码云: jmeter-plugin-dubbo

问题描述

  1. jmeter-plugin-dubbo插件GUI输入的信息无法使用${var}变量来进行参数化

问题修复

Jmeter的输出要想使用用户自定义变量、CSV变量、BeanShell、函数来进行参数化,必须将输入的参数通过JMeterProperty的子类addJmeter管理。如果使用的是SwingBean绑定机制可以很好的支持变量与函数参数化,如果是手写的GUI与Sample就需要注意这一点,可能写出来的插件不能使用变量${var}参数化。

我之前在处理参数值在GUI和Sample之间传递时,没有使用org.apache.jmeter.testelement.property.JMeterProperty系列子类来处理参数,因此变量无法支持,让我们来看一下区别。

先让我们看一下org.apache.jmeter.testelement.property.JMeterProperty都有哪些子类。

我们之前使用的参数赋值是这样的:

public String getVersion() {
    return this.getPropertyAsString(FIELD_DUBBO_VERSION, DEFAULT_VERSION);
}
public void setVersion(String version) {
    this.setProperty(FIELD_DUBBO_VERSION, version);
}

这种方式是无法支持使用${var}变量来参数化赋值的(也就是动态赋值)。

我们应该给setProperty传入JMeterProperty的子类来支持变量参数化,如下:

public String getVersion() {
    return this.getPropertyAsString(FIELD_DUBBO_VERSION, DEFAULT_VERSION);
}
public void setVersion(String version) {
    this.setProperty(new StringProperty(FIELD_DUBBO_VERSION, version));
}

ps.注意setProperty的使用不一样,这里使用的是new StringProperty

上面的参数还相对简单的普通字符串参数,当我们遇到集合或更加复杂的参数类型时如何处理?

我本以为使用JMeterProperty的子类CollectionProperty是可以让集合参数支持变量参数化的,结果测试下来没有任何用,传入的${var}变量,在运行的时候还是变量没有变成相应的值。

于是又换成MapPropertyObjectProperty一样无法支持变量参数化。

查看Jmeter PluginsHttp Sample源码,看他是如何处理的。

org.apache.jmeter.protocol.http.util.HTTPArgument源码

package org.apache.jmeter.protocol.http.util;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.LinkedList;
import java.util.List;
import org.apache.jmeter.config.Argument;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.testelement.property.BooleanProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

public class HTTPArgument
  extends Argument
  implements Serializable
{
  private static final Logger log = ;
  private static final long serialVersionUID = 240L;
  private static final String ALWAYS_ENCODE = "HTTPArgument.always_encode";
  private static final String USE_EQUALS = "HTTPArgument.use_equals";
  private static final EncoderCache cache = new EncoderCache(1000);
  
  public HTTPArgument(String name, String value, String metadata)
  {
    this(name, value, false);
    setMetaData(metadata);
  }
  
  public void setUseEquals(boolean ue)
  {
    if (ue) {
      setMetaData("=");
    } else {
      setMetaData("");
    }
    setProperty(new BooleanProperty("HTTPArgument.use_equals", ue));
  }
  
  public boolean isUseEquals()
  {
    boolean eq = getPropertyAsBoolean("HTTPArgument.use_equals");
    if ((getMetaData().equals("=")) || ((getValue() != null) && (getValue().length() > 0)))
    {
      setUseEquals(true);
      return true;
    }
    return eq;
  }
  
  public void setAlwaysEncoded(boolean ae)
  {
    setProperty(new BooleanProperty("HTTPArgument.always_encode", ae));
  }
  
  public boolean isAlwaysEncoded()
  {
    return getPropertyAsBoolean("HTTPArgument.always_encode");
  }
  
  public HTTPArgument(String name, String value)
  {
    this(name, value, false);
  }
  
  public HTTPArgument(String name, String value, boolean alreadyEncoded)
  {
    this(name, value, alreadyEncoded, "UTF-8");
  }
  
  public HTTPArgument(String name, String value, boolean alreadyEncoded, String contentEncoding)
  {
    setAlwaysEncoded(true);
    if (alreadyEncoded) {
      try
      {
        if (log.isDebugEnabled()) {
          log.debug("Decoding name, calling URLDecoder.decode with '" + name + "' and contentEncoding:" + "UTF-8");
        }
        name = URLDecoder.decode(name, "UTF-8");
        if (log.isDebugEnabled()) {
          log.debug("Decoding value, calling URLDecoder.decode with '" + value + "' and contentEncoding:" + contentEncoding);
        }
        value = URLDecoder.decode(value, contentEncoding);
      }
      catch (UnsupportedEncodingException e)
      {
        log.error(contentEncoding + " encoding not supported!");
        throw new Error(e.toString(), e);
      }
    }
    setName(name);
    setValue(value);
    setMetaData("=");
  }
  
  public HTTPArgument(String name, String value, String metaData, boolean alreadyEncoded)
  {
    this(name, value, metaData, alreadyEncoded, "UTF-8");
  }
  
  public HTTPArgument(String name, String value, String metaData, boolean alreadyEncoded, String contentEncoding)
  {
    this(name, value, alreadyEncoded, contentEncoding);
    setMetaData(metaData);
  }
  
  public HTTPArgument(Argument arg)
  {
    this(arg.getName(), arg.getValue(), arg.getMetaData());
  }
  
  public HTTPArgument() {}
  
  public void setName(String newName)
  {
    if ((newName == null) || (!newName.equals(getName()))) {
      super.setName(newName);
    }
  }
  
  public String getEncodedValue()
  {
    try
    {
      return getEncodedValue("UTF-8");
    }
    catch (UnsupportedEncodingException e)
    {
      throw new Error("Should not happen: " + e.toString());
    }
  }
  
  public String getEncodedValue(String contentEncoding)
    throws UnsupportedEncodingException
  {
    if (isAlwaysEncoded()) {
      return cache.getEncoded(getValue(), contentEncoding);
    }
    return getValue();
  }
  
  public String getEncodedName()
  {
    if (isAlwaysEncoded()) {
      return cache.getEncoded(getName());
    }
    return getName();
  }
  
  public static void convertArgumentsToHTTP(Arguments args)
  {
    List<Argument> newArguments = new LinkedList();
    for (JMeterProperty jMeterProperty : args.getArguments())
    {
      Argument arg = (Argument)jMeterProperty.getObjectValue();
      if (!(arg instanceof HTTPArgument)) {
        newArguments.add(new HTTPArgument(arg));
      } else {
        newArguments.add(arg);
      }
    }
    args.removeAllArguments();
    args.setArguments(newArguments);
  }
}

org.apache.jmeter.protocol.http.gui.HTTPArgumentsPanel源码

package org.apache.jmeter.protocol.http.gui;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Iterator;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Argument;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.config.gui.ArgumentsPanel;
import org.apache.jmeter.protocol.http.util.HTTPArgument;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.gui.GuiUtils;
import org.apache.jorphan.gui.ObjectTableModel;
import org.apache.jorphan.reflect.Functor;

public class HTTPArgumentsPanel
  extends ArgumentsPanel
{
  private static final long serialVersionUID = 240L;
  private static final String ENCODE_OR_NOT = "encode?";
  private static final String INCLUDE_EQUALS = "include_equals";
  
  protected void initializeTableModel()
  {
    this.tableModel = new ObjectTableModel(new String[] { "name", "value", "encode?", "include_equals" }, HTTPArgument.class, new Functor[] { new Functor("getName"), new Functor("getValue"), new Functor("isAlwaysEncoded"), new Functor("isUseEquals") }, new Functor[] { new Functor("setName"), new Functor("setValue"), new Functor("setAlwaysEncoded"), new Functor("setUseEquals") }, new Class[] { String.class, String.class, Boolean.class, Boolean.class });
  }
  
  public static boolean testFunctors()
  {
    HTTPArgumentsPanel instance = new HTTPArgumentsPanel();
    instance.initializeTableModel();
    return instance.tableModel.checkFunctors(null, instance.getClass());
  }
  
  protected void sizeColumns(JTable table)
  {
    GuiUtils.fixSize(table.getColumn("include_equals"), table);
    GuiUtils.fixSize(table.getColumn("encode?"), table);
  }
  
  protected HTTPArgument makeNewArgument()
  {
    HTTPArgument arg = new HTTPArgument("", "");
    arg.setAlwaysEncoded(false);
    arg.setUseEquals(true);
    return arg;
  }
  
  public HTTPArgumentsPanel()
  {
    super(JMeterUtils.getResString("paramtable"));
    init();
  }
  
  public TestElement createTestElement()
  {
    Arguments args = getUnclonedParameters();
    super.configureTestElement(args);
    return (TestElement)args.clone();
  }
  
  public Arguments getParameters()
  {
    Arguments args = getUnclonedParameters();
    return (Arguments)args.clone();
  }
  
  private Arguments getUnclonedParameters()
  {
    stopTableEditing();
    
    Iterator<HTTPArgument> modelData = this.tableModel.iterator();
    Arguments args = new Arguments();
    while (modelData.hasNext())
    {
      HTTPArgument arg = (HTTPArgument)modelData.next();
      args.addArgument(arg);
    }
    return args;
  }
  
  public void configure(TestElement el)
  {
    super.configure(el);
    if ((el instanceof Arguments))
    {
      this.tableModel.clearData();
      HTTPArgument.convertArgumentsToHTTP((Arguments)el);
      for (JMeterProperty jMeterProperty : ((Arguments)el).getArguments())
      {
        HTTPArgument arg = (HTTPArgument)jMeterProperty.getObjectValue();
        this.tableModel.addRow(arg);
      }
    }
    checkButtonsStatus();
  }
  
  protected boolean isMetaDataNormal(HTTPArgument arg)
  {
    return (arg.getMetaData() == null) || (arg.getMetaData().equals("=")) || ((arg.getValue() != null) && (arg.getValue().length() > 0));
  }
  
  protected Argument createArgumentFromClipboard(String[] clipboardCols)
  {
    HTTPArgument argument = makeNewArgument();
    argument.setName(clipboardCols[0]);
    if (clipboardCols.length > 1)
    {
      argument.setValue(clipboardCols[1]);
      if (clipboardCols.length > 2)
      {
        argument.setAlwaysEncoded(Boolean.parseBoolean(clipboardCols[2]));
        if (clipboardCols.length > 3)
        {
          Boolean useEqual = BooleanUtils.toBooleanObject(clipboardCols[3]);
          
          argument.setUseEquals(useEqual != null ? useEqual.booleanValue() : true);
        }
      }
    }
    return argument;
  }
  
  private void init()
  {
    JTable table = getTable();
    JPopupMenu popupMenu = new JPopupMenu();
    JMenuItem variabilizeItem = new JMenuItem(JMeterUtils.getResString("transform_into_variable"));
    variabilizeItem.addActionListener(new ActionListener()
    {
      public void actionPerformed(ActionEvent e)
      {
        HTTPArgumentsPanel.this.transformNameIntoVariable();
      }
    });
    popupMenu.add(variabilizeItem);
    table.setComponentPopupMenu(popupMenu);
  }
  
  private void transformNameIntoVariable()
  {
    int[] rowsSelected = getTable().getSelectedRows();
    for (int selectedRow : rowsSelected)
    {
      String name = (String)this.tableModel.getValueAt(selectedRow, 0);
      if (StringUtils.isNotBlank(name))
      {
        name = name.trim();
        name = name.replaceAll("\\$", "_");
        name = name.replaceAll("\\{", "_");
        name = name.replaceAll("\\}", "_");
        this.tableModel.setValueAt("${" + name + "}", selectedRow, 1);
      }
    }
  }
}

能发现它使用的是继承Argument来处理和GUI之间的参数传递,使用继承ArgumentsPanel来处理GUI页面,这个就是我们上面说的,通过SwingBean绑定机制来进行开发,很遗憾我们没有使用这种方式,如果要改成这种方式,整个代码结构都要修改,成本太大。

但是我们发现像StringInteger等这种普通类型的参数通过使用JMeterProperty的子类可以很好的支持变量参数化,那我们能不能将集合参数拉平来直接使用普通类型的参数来处理,我承认这种方式有点恶心。

解决方式

首先我们的集合参数有索引下标和总行数,每一行有两列,那就修改集合参数的赋值,代码如下:

//标记集合参数前缀
public static String FIELD_DUBBO_METHOD_ARGS = "FIELD_DUBBO_METHOD_ARGS";
//集合参数总数
public static String FIELD_DUBBO_METHOD_ARGS_SIZE = "FIELD_DUBBO_METHOD_ARGS_SIZE";

public List<MethodArgument> getMethodArgs() {
	int paramsSize = this.getPropertyAsInt(FIELD_DUBBO_METHOD_ARGS_SIZE, 0);
	List<MethodArgument> list = new ArrayList<MethodArgument>();
	for (int i = 1; i <= paramsSize; i++) {
		String paramType = this.getPropertyAsString(FIELD_DUBBO_METHOD_ARGS + "_PARAM_TYPE" + i);
		String paramValue = this.getPropertyAsString(FIELD_DUBBO_METHOD_ARGS + "_PARAM_VALUE" + i);
		MethodArgument args = new MethodArgument(paramType, paramValue);
		list.add(args);
	}
	return list;
}
public void setMethodArgs(List<MethodArgument> methodArgs) {
	int size = methodArgs == null ? 0 : methodArgs.size();
	this.setProperty(new IntegerProperty(FIELD_DUBBO_METHOD_ARGS_SIZE, size));
	if (size > 0) {
		for (int i = 1; i <= methodArgs.size(); i++) {
			this.setProperty(new StringProperty(FIELD_DUBBO_METHOD_ARGS + "_PARAM_TYPE" + i, methodArgs.get(i-1).getParamType()));
			this.setProperty(new StringProperty(FIELD_DUBBO_METHOD_ARGS + "_PARAM_VALUE" + i, methodArgs.get(i-1).getParamValue()));
		}
	}
}

上面的代码就是将集合参数拉平来进行传递,大致的结果如下:

FIELD_DUBBO_METHOD_ARGS_SIZE = 2
FIELD_DUBBO_METHOD_ARGS_SIZE_PARAM_TYPE_1 = xx${var1}xx 
FIELD_DUBBO_METHOD_ARGS_SIZE__PARAM_VALUE_1 = xx${var2}xx 
FIELD_DUBBO_METHOD_ARGS_SIZE_PARAM_TYPE_2 = xx${var3}xx 
FIELD_DUBBO_METHOD_ARGS_SIZE__PARAM_VALUE_2 = xx${var4}xx 

让我们测试一下是否可用。

测试结果GUI上所有的输入框均可以支持Jmeter变量${var}参数化.

我觉得应该还是更加完美的解决办法只不过我没有找到,有空了再细致研究一下Jmeter的插件开发的细节看看能否找到突破口。

再次感谢网友 @流浪的云 提的bug,非常感谢!感谢使用插件的朋友多提rp和bug,让我们来一起完善起来,感谢这个开放的世界,最后还是一句老话:世界和平,Keep Real!

Java中内部类使用注意事项,内部类对序列化与反序列化的影响

现在很多服务架构都是微服务、分布式架构,开发模式也都是模块化开发,在分布式的开发方式下服务之间的调用不管是RPC还是RESTful或是其他SOA方案,均离不开序列化与反序列化,尤其是使用Java开发,Bean实现序列化接口几乎已经是必备的要求,而且这个要求已经纳入到很多大厂公司的开发规范中,开发规范中强制要求实现序列化接口和重写toStringhashCode方法。

前面提到了序列化与反序列化,那序列化与反序列化的对象就是开发人员写的java bean,不同的java bean会给序列化反序列化带来什么问题呢?接下来就让我们看一下内部类对序列化反序列化的影响。

在这之前我们先看一下常用的序列化工具:

  • JavaSerialize
  • fastjson
  • dubbo json
  • google gson
  • google protoBuf
  • hessian
  • kryo
  • Avro
  • fast-serialization
  • jboss-serialization
  • jboss-marshalling-river
  • protostuff
  • msgpack-databind
  • json/jackson/databind
  • json/jackson/db-afterburner
  • xml/xstream+c
  • xml/jackson/databind-aalto

工具太多了这里就不列了,让我们先做一个测试。

测试

常规java bean

测试类:

import java.io.Serializable;

public class Test implements Serializable {
	private static final long serialVersionUID = 2010307013874058143L;
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

调用序列化与反序列化:

public static String toJson(Object obj) {
    try {
        return JSON.json(obj);
    } catch (IOException e) {
        log.error("class to json is error!", e);
    }
    return null;
}
public static <T> T formJson(String json, Class<T> classOfT) {
    try {
        return JSON.parse(json, classOfT);
    } catch (ParseException e) {
        log.error("json to class is error! "+classOfT.getName(), e);
    }
    return null;
}
public static void main(String[] args) {
	Test test = new Test();
	System.out.println(toJson(test));
	String json = "{\"name\":\"test\"}";
	test = formJson(json, Test.class);
	System.out.println(test.getName());
}

输出:

{"name":null}
test

我们能看到不管是序列化还是反序列化都没有任何问题,我们这里测试使用了常用的fastjsondubbo json做了测试。

有内部类的java bean

测试类:

import java.io.Serializable;

public class Test implements Serializable {
	private static final long serialVersionUID = 2010307013874058143L;
	private String name;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public C1 c1;
	public C1 getC1() {
		return c1;
	}
	public void setC1(C1 c1) {
		this.c1 = c1;
	}
	public class C1 {
		public String name;
		public C1() {
		}
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = name;
		}
	}
}

调用序列化与反序列化:

public static String toJson(Object obj) {
    try {
        return JSON.json(obj);
    } catch (IOException e) {
        log.error("class to json is error!", e);
    }
    return null;
}
public static <T> T formJson(String json, Class<T> classOfT) {
    try {
        return JSON.parse(json, classOfT);
    } catch (ParseException e) {
        log.error("json to class is error! "+classOfT.getName(), e);
    }
    return null;
}
public static void main(String[] args) {
	Test test = new Test();
	System.out.println(toJson(test));
	String json = "{\"name\":\"test\",\"c1\":{\"name\":\"c1\"}}";
	test = formJson(json, Test.class);
	System.out.println(test.getC1().getName());
}

输出:

{"c1":null,"name":null,"C1":null}
Exception in thread "main" java.lang.NullPointerException
ERROR   2018-03-06 15:19:05.418 [xxx] (): json to class is error! Test
com.alibaba.dubbo.common.json.ParseException: java.lang.InstantiationException: Test$C1
java.lang.InstantiationException: Test$C1
	at java.lang.Class.newInstance(Class.java:359)
	at com.alibaba.dubbo.common.json.J2oVisitor.objectBegin(J2oVisitor.java:119)
	at com.alibaba.dubbo.common.json.JSON.parse(JSON.java:745)
	at com.alibaba.dubbo.common.json.JSON.parse(JSON.java:227)
	at com.alibaba.dubbo.common.json.JSON.parse(JSON.java:210)

可以成功序列化,但是反序列化报错了:无法创建实例Test$C1,这是什么问题?为什么会有这个错误?接下来我们分析一下

错误分析(java.lang.InstantiationException: Test$C1)

通过使用fastjson和dubbo json的错误代码跟踪,找到了J2oVisitor.objectBegin(J2oVisitor.java:119)这个地方,代码如下:

//下面是com.alibaba.dubbo.common.json.J2oVisitor的方法
public void objectBegin() throws ParseException
{
	mStack.push(mValue);
	mStack.push(mType);
	mStack.push(mWrapper);

	if( mType == Object.class || Map.class.isAssignableFrom(mType) )
	{
		if (! mType.isInterface() && mType != Object.class) {
			try {
				mValue = mType.newInstance();
			} catch (Exception e) {
				throw new IllegalStateException(e.getMessage(), e);
			}
		} else if (mType == ConcurrentMap.class) {
			mValue = new ConcurrentHashMap<String, Object>();
		} else {
			mValue = new HashMap<String, Object>();
		}
		mWrapper = null;
	} else {
		try {
			mValue = mType.newInstance();
			mWrapper = Wrapper.getWrapper(mType);
		} catch(IllegalAccessException e){ 
			throw new ParseException(StringUtils.toString(e)); 
		} catch(InstantiationException e){ 
			throw new ParseException(StringUtils.toString(e)); 
		}
	}
}
//下面是Class的方法
public T newInstance()
        throws InstantiationException, IllegalAccessException
{
    if (System.getSecurityManager() != null) {
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);
    }

    // NOTE: the following code may not be strictly correct under
    // the current Java memory model.

    // Constructor lookup
    if (cachedConstructor == null) {
        if (this == Class.class) {
            throw new IllegalAccessException(
                "Can not call newInstance() on the Class for java.lang.Class"
            );
        }
        try {
            Class<?>[] empty = {};
            final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
            // Disable accessibility checks on the constructor
            // since we have to do the security check here anyway
            // (the stack depth is wrong for the Constructor's
            // security check to work)
            java.security.AccessController.doPrivileged(
                new java.security.PrivilegedAction<Void>() {
                    public Void run() {
                            c.setAccessible(true);
                            return null;
                        }
                    });
            cachedConstructor = c;
        } catch (NoSuchMethodException e) {
            throw new InstantiationException(getName());
        }
    }
    Constructor<T> tmpConstructor = cachedConstructor;
    // Security check (same as in java.lang.reflect.Constructor)
    int modifiers = tmpConstructor.getModifiers();
    if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
        Class<?> caller = Reflection.getCallerClass();
        if (newInstanceCallerCache != caller) {
            Reflection.ensureMemberAccess(caller, this, null, modifiers);
            newInstanceCallerCache = caller;
        }
    }
    // Run constructor
    try {
        return tmpConstructor.newInstance((Object[])null);
    } catch (InvocationTargetException e) {
        Unsafe.getUnsafe().throwException(e.getTargetException());
        // Not reached
        return null;
    }
}

代码中使用的是tmpConstructor.newInstance((Object[])null)不带参数的构造器,查看我们的原类,我们的内部类也是无参数的构造器,那为什么无法实例化呢?

我们来看一下我们的java源代码中内部类生成的class字节码文件,通过反编译工具查看如下:

public class Test$C1
{
  public String name;
  
  public Test$C1(Test paramTest) {}
  
  public String getName()
  {
    return this.name;
  }
  
  public void setName(String name)
  {
    this.name = name;
  }
}

我们是空构造器为什么生成的确是带参数的构造器而且参数paramTest的类型是Test,这是为什么呢?

我们来看一下JDK doc关于Constructor.newInstance它的解释

Uses the constructor represented by this Constructor object to create and initialize a new instance of the constructor's declaring class, with the specified initialization parameters. Individual parameters are automatically unwrapped to match primitive formal parameters, and both primitive and reference parameters are subject to method invocation conversions as necessary. 
If the number of formal parameters required by the underlying constructor is 0, the supplied initargs array may be of length 0 or null. 

If the constructor's declaring class is an inner class in a non-static context, the first argument to the constructor needs to be the enclosing instance; see The Java Language Specification, section 15.9.3. 

If the required access and argument checks succeed and the instantiation will proceed, the constructor's declaring class is initialized if it has not already been initialized. 

If the constructor completes normally, returns the newly created and initialized instance.

具体关注这句:If the constructor’s declaring class is an inner class in a non-static context, the first argument to the constructor needs to be the enclosing instance; see The Java Language Specification, section 15.9.3

意思是说:如果构造函数的声明类是一个非静态(non-static)上下文中的内部类,则构造函数的第一个参数需要是封闭实例;参见Java语言规范,第15.9.3节。

15.9.3节具体看:15.9.3. Choosing the Constructor and its Arguments的说明

到这里我们应该清楚内部类在没有修饰符static和有修饰符static的区别了吧,就是non-static的内部类在生成的时候构造器第一个参数是parent实例,用来共享parent的属性访问的,那让我们将内部类修改为static再做一次测试验证。

验证

测试类:

import java.io.Serializable;

public class Test implements Serializable {
	private static final long serialVersionUID = 2010307013874058143L;
	private String name;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public C1 c1;
	public C1 getC1() {
		return c1;
	}
	public void setC1(C1 c1) {
		this.c1 = c1;
	}
	public static class C1 {
		public String name;
		public C1() {
		}
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = name;
		}
	}
}

ps.内部类C1增加了static修饰符

调用序列化与反序列化:

public static String toJson(Object obj) {
    try {
        return JSON.json(obj);
    } catch (IOException e) {
        log.error("class to json is error!", e);
    }
    return null;
}
public static <T> T formJson(String json, Class<T> classOfT) {
    try {
        return JSON.parse(json, classOfT);
    } catch (ParseException e) {
        log.error("json to class is error! "+classOfT.getName(), e);
    }
    return null;
}
public static void main(String[] args) {
	Test test = new Test();
	System.out.println(toJson(test));
	String json = "{\"name\":\"test\",\"c1\":{\"name\":\"c1\"}}";
	test = formJson(json, Test.class);
	System.out.println(test.getC1().getName());
}

输出:

{"c1":null,"name":null,"C1":null}
c1

结果可以正常的序列化了,以上测试使用的是fastjsondubbo json进行测试。

总结

按照规范内部类是不太推荐使用的,如果要用尽量使用static修饰符修饰内部类,这个问题其实就是Java的基本功,尽量一个Java文件中只保留一个类,这样在大多数序列化与反序列化工具中都不会出现问题,也比较符合当下模块化开发的规范,内部类改为static修饰符修饰还可以有效的避免内存泄漏,很多大厂的性能建议文档与Java开发规范文档都可以看到对内部类使用的注意事项,有空多看看大厂的经验总结。

使用Googlegson进行测试,non-static的内部类可以正常序列化,Google出的工具包就是强大兼容了各种使用方式,从gsonapi还发现可以通过参数来disableenableinner class序列化的支持,具体查看如下代码:

测试类:

import java.io.Serializable;

public class Test implements Serializable {
	private static final long serialVersionUID = 2010307013874058143L;
	private String name;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public C1 c1;
	public C1 getC1() {
		return c1;
	}
	public void setC1(C1 c1) {
		this.c1 = c1;
	}
	public class C1 {
		public String name;
		public C1() {
		}
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = name;
		}
	}
}

ps.注意我这里的内部类C1是non-static的

gson开启内部类序列化

public static void main(String[] args) {
	Gson gson = new GsonBuilder().serializeNulls().create();
	Test test = new Test();
	test.setName("序列化参数name");
	System.out.println(gson.toJson(test));
	String json = "{\"name\":\"test\",\"c1\":{\"name\":\"c1\"}}";
	test = gson.fromJson(json, Test.class);
	System.out.println(test.getC1() == null ? "null" : test.getC1().getName());
}

ps.默认InnerClassSerialization就是开启的

输出:

{"name":"序列化参数name","c1":null}
c1

gson禁用内部类序列化

public static void main(String[] args) {
	Gson gson = new GsonBuilder().serializeNulls().disableInnerClassSerialization().create();
	Test test = new Test();
	test.setName("序列化参数name");
	System.out.println(gson.toJson(test));
	String json = "{\"name\":\"test\",\"c1\":{\"name\":\"c1\"}}";
	test = gson.fromJson(json, Test.class);
	System.out.println(test.getC1() == null ? "null" : test.getC1().getName());
}

ps.调用GsonBuilder.disableInnerClassSerialization()禁用InnerClassSerialization

输出:

{"name":"序列化参数name"}
null

从而能看出Google出的工具包就是强大兼容各种使用方式,Google出的都是精品,从guava就可以看出。

好了到这里整个文章就介绍完了,最后还是一句老话:世界和平、Keep Real!

Trouble Shooting —— Docker rancher/agent-instance cannot start automatically

今天发现一个docker机器莫名其妙的无工作了,于是进入宿主机查看信息如下:

docker@xxx:~$ docker ps
be4238200956        rancher/agent:v1.0.2                          "/run.sh run"            5 months ago        Up 34 minutes                                                              rancher-agent

发现只有一个rancher/agent容器是启动的,其余的都没有启动,查看rancher控制台,服务都在转圈圈Restaring状态,而且长时间一直这个状态没有变化。

这是什么问题呢?

查看机器上所有的容器

docker@xxx:~$ docker ps -a
CONTAINER ID        IMAGE                                         COMMAND                  CREATED             STATUS                        PORTS               NAMES
d9da7f16ef2d        192.168.0.34:5000/saas-erp:latest             "./entrypoint.sh"        4 days ago          Exited (0) 50 minutes ago                         r-erp_erp-dubbo_1
79e8e475db19        192.168.0.34:5000/tms2job:latest              "./entrypoint.sh"        4 weeks ago         Exited (0) 50 minutes ago                         r-tms_tms2-job_1
0995dabe324b        192.168.0.34:5000/customer-mq:latest          "catalina.sh run"        8 weeks ago         Exited (143) 7 weeks ago                          r-customer_customer-mq_1
65492930b132        192.168.0.34:5000/saas-account:latest         "./entrypoint.sh"        9 weeks ago         Exited (0) 50 minutes ago                         r-account_account-dubbo_1
248514cd635a        192.168.0.34:5000/saas-erp-http-main:latest   "./entrypoint.sh"        4 months ago        Exited (0) 50 minutes ago                         r-erp_erp-http-main_1
94e51332cc40        192.168.0.34:5000/zookeeper:elevy             "/entrypoint.sh zkSer"   5 months ago        Exited (0) 50 minutes ago                         db61a2f2-9b47-4d97-97a3-b6e0764208ca
d72c359c2d5e        192.168.0.34:5000/mysql:5.6.30                "docker-entrypoint.sh"   5 months ago        Exited (0) 50 minutes ago                         c7638fa0-f263-45bd-85d7-2e3b7407ad2f
0c8d3edbc53d        rancher/agent-instance:v0.8.3                 "/etc/init.d/agent-in"   5 months ago        Exited (128) 50 minutes ago                       e505b911-a391-4d1c-8ef2-7bbb306df8eb
be4238200956        rancher/agent:v1.0.2                          "/run.sh run"            5 months ago        Up 11 minutes                                     rancher-agent

发现服务全都是Exited状态,Rancher控制台上Network Agent容器也是一直转圈圈Restarting状态。

因此断定应该是Network Agent服务没有启动导致的所有服务无法恢复自动启动。

那为什么会出现这个问题?这个问题是什么原因导致的呢?

在解决这个问题之前先看一下Rancher的网络+负载均衡 实现与说明

Rancher网络+负载均衡的实现与说明

依赖镜像:rancher/agent-instance:v0.8.3

Rancher网络是采用SDN技术所建容器为虚拟ip地址,各host之间容器采用ipsec隧道实现跨主机通信,使用的是udp的500和4500端口。

启动任务时,在各个host部署容器之前会起一个Network Agent容器,负责组建网络环境。

网络全都靠agent-instance容器实现,网络没有准备好其余的容器当然也不会自动恢复。

那我们的这个问题就是agent-instance容器没有起来导致的,那让我们启动agent-instance容器。

docker@xxx:~$ docker start 0c8d3edbc53d
Error response from daemon: rpc error: code = 2 desc = "oci runtime error: exec format error"
Error: failed to start containers: 0c8d3edbc53d

很遗憾提示错误无法启动,那让我们看一下日志中的错误是什么?

docker@xxx:~$ docker logs --tail=200 -f 0c8d3edbc53d
.......省略其他的
INFO: Sending agent-instance-startup applied 3-0f669dbfe83bbb7389a0c2129247f633575904e41d665e311051de2ce1b85737
Starting monit daemon with http interface at [localhost:2812]
The system is going down NOW!
Sent SIGTERM to all processes
Sent SIGKILL to all processes
Requesting system reboot
INFO: Downloading agent http://192.168.0.34:8080/v1/configcontent/configscripts

发现The system is going down NOW!这个错误,什么情况?无法启动要求重启系统。

于是查看rancher官方相关这个问题的issues,也没看出个所以然来,跟我的系统版本和agent、agent-instance版本都一致也有很多人无法启动或者启动报错。

最终无解尝试暴力做法,删除以前的agent-instance容器,然后重新创建重启

删除rancher/agent-instance:v0.8.3容器

docker@xxx:~$ docker rm 0c8d3edbc53d
0c8d3edbc53d

查看有没有rancher/agent-instance:v0.8.3这个镜像

docker@xxx:~$ docker images
REPOSITORY                             TAG                 IMAGE ID            CREATED             SIZE
192.168.0.34:5000/saas-erp             latest              0ad78488245a        4 days ago          275.4 MB
192.168.0.34:5000/tms2job              latest              caa888ff603f        4 weeks ago         236.8 MB
192.168.0.34:5000/customer-mq          latest              db319e29bd7f        8 weeks ago         431.8 MB
192.168.0.34:5000/saas-account         latest              004999746d2c        9 weeks ago         181.9 MB
192.168.0.34:5000/saas-erp-http-main   latest              9a5f8be5ef8d        4 months ago        200.8 MB
192.168.0.34:5000/messer               1.0                 74e9ec4742cc        7 months ago        184.8 MB
192.168.0.34:5000/tomcat               7                   830387a4274c        19 months ago       357.8 MB
rancher/agent-instance                 v0.8.3              b6b013f2aa85        20 months ago       331 MB
192.168.0.34:5000/rancher/agent        v1.0.2              860ed2b2e8e3        20 months ago       454.3 MB
rancher/agent                          v1.0.2              860ed2b2e8e3        20 months ago       454.3 MB
192.168.0.34:5000/mysql                5.6.30              2c0964ec182a        21 months ago       329 MB
192.168.0.34:5000/zookeeper            elevy               d2805d0326a9        2 years ago         131.8 MB

有镜像,根据镜像重新创建rancher/agent-instance:v0.8.3容器

docker@xxx:~$ docker run -d b6b013f2aa85
0060edfa2594

ps.-d, –detach Run container in background and print container ID,后台运行容器并且打印出容器ID

OK创建好了,再ps查看一下其余的容器是否都自动恢复了

docker@xxx:~$ docker ps
CONTAINER ID        IMAGE                                         COMMAND                  CREATED             STATUS              PORTS                                                  NAMES
854fa1039e76        192.168.0.34:5000/zookeeper:elevy             "/entrypoint.sh zkSer"   33 minutes ago      Up 33 minutes       2888/tcp, 3888/tcp, 0.0.0.0:2181->2181/tcp, 9010/tcp   r-zookeeper_zookeeper-2_1
47c189dbd5c6        b6b013f2aa85                                  "/etc/init.d/agent-in"   37 minutes ago      Up 37 minutes                                                              drunk_tesla
0060edfa2594        rancher/agent-instance:v0.8.3                 "/etc/init.d/agent-in"   37 minutes ago      Up 37 minutes       0.0.0.0:500->500/udp, 0.0.0.0:4500->4500/udp           e505b911-a391-4d1c-8ef2-7bbb306df8eb
d9da7f16ef2d        192.168.0.34:5000/saas-erp:latest             "./entrypoint.sh"        4 days ago          Up 37 minutes       0.0.0.0:20833->20833/tcp                               r-erp_erp-dubbo_1
79e8e475db19        192.168.0.34:5000/tms2job:latest              "./entrypoint.sh"        4 weeks ago         Up 37 minutes       0.0.0.0:50831->50831/tcp                               r-tms_tms2-job_1
65492930b132        192.168.0.34:5000/saas-account:latest         "./entrypoint.sh"        9 weeks ago         Up 37 minutes       0.0.0.0:20834->20834/tcp                               r-account_account-dubbo_1
248514cd635a        192.168.0.34:5000/saas-erp-http-main:latest   "./entrypoint.sh"        4 months ago        Up 37 minutes       0.0.0.0:20902->20902/tcp                               r-erp_erp-http-main_1
d72c359c2d5e        192.168.0.34:5000/mysql:5.6.30                "docker-entrypoint.sh"   5 months ago        Up 37 minutes       0.0.0.0:3306->3306/tcp                                 c7638fa0-f263-45bd-85d7-2e3b7407ad2f
be4238200956        rancher/agent:v1.0.2                          "/run.sh run"            5 months ago        Up About an hour                                                           rancher-agent

很好全都恢复了,Status全都是Up。早知道删除重建就不需要这么麻烦去Issues中找答案,以后记住了只要Network Agent容器(rancher/agent-instance:v0.8.3)出问题先尝试start,如果无法start就删除了重新创建容器。

构建Python多个虚拟环境来进行不同版本开发之神器-virtualenv

Published on:

我们都知道Python的类库很多,但是大多支持的版本还是Python2.x系列,Python3支持的类库相对较少,因此我们在开发的时候经常还使用的Python2系列的版本,Python3对语法进行了比较大的重构,Python3中将一些Python2的模块名称做了修改,虽然兼容Python2但还是需要我们做一些处理来保证代码在不同Python版本中能够正常运作,如果我们想同时使用Python2Python3,这个时候大家最常用的做法就是机器上配置多个版本,虽然可以解决问题但是配合多个项目的各种杂乱的包依赖情况,问题就变的非常复杂了,可能升级某一个第三方依赖库会对很多项目产生影响。

我们都知道在安装Python类库的时候它默认会安装到Python的目录下,有编程洁癖的人都会因此苦恼,因为它污染了Python的目录,并且在开发的时候不同的项目使用的类库差异也蛮大,为了使多个项目之间互相不影响,我们能不能根据项目来区分开Python环境目录?

当然可以,virtualenv就能帮助我们解决上面的苦恼,它是一个可以创建多个隔绝Python环境的工具,virtualenv可以创建一个包含所有必要的可执行的文件夹,用来使用Python工程所需要的包,同时还不污染Python的原安装目录。

这个工具简直就是给有开发洁癖的人送福音的。画外音:专业送快递

上面大致说了一下我们使用virtualenv的初衷,接下来让我们看一下virtualenv如何使用,在使用之前先正式的了解一下virtualenv

什么是virtualenv?

Virtualenv是一个用来创建独立的Python环境的工具

为什么我们需要一个独立的Python环境?

引用virtualenv的文档

virtualenv is a tool to create isolated Python environments.
The basic problem being addressed is one of dependencies and versions, and indirectly permissions. Imagine you have an application that needs version 1 of LibFoo, but another application requires version 2. How can you use both these applications? If you install everything into /usr/lib/python2.7/site-packages (or whatever your platform’s standard location is), it’s easy to end up in a situation where you unintentionally upgrade an application that shouldn’t be upgraded.
Or more generally, what if you want to install an application and leave it be? If an application works, any change in its libraries or the versions of those libraries can break the application.
Also, what if you can’t install packages into the global site-packages directory? For instance, on a shared host.
In all these cases, virtualenv can help you. It creates an environment that has its own installation directories, that doesn’t share libraries with other virtualenv environments (and optionally doesn’t access the globally installed libraries either).

上面这段话的意思大致是这样的,我们需要处理的基本问题是包的依赖、版本和间接权限问题。想象一下,你有两个应用,一个应用需要libfoo的版本1,而另一应用需要版本2。如何才能同时使用这些应用程序?如果您安装到的/usr/lib/python2.7/site-packages(或任何平台的标准位置)的一切,在这种情况下,您可能会不小心升级不应该升级的应用程序。或者更广泛地说,如果您想要安装一个应用程序并离开它呢?如果应用程序工作,其库中的任何更改或这些库的版本都可以破坏应用程序。另外,如果您不能将包安装到全局站点包目录中,该怎么办?例如,在共享主机上。在所有这些情况下,virtualenv可以帮助您。它创建了一个有自己的安装目录的环境,它不与其他virtualenv环境共享库(也不可能访问全局安装的库)。

简单地说,你可以为每个项目建立不同的/独立的Python环境,你将为每个项目安装所有需要的软件包到它们各自独立的环境中。

到这里我相信我们已经很清晰的知道了virtualenv是什么,能做什么,那接下来就让我们来用一用它。

使用virtualenv

安装

pip install virtualenv

画外音:pip安装非常简单,简直就是傻瓜式的

使用

virtualenv安装完毕后,可以通过运行下面的命令来为你的项目创建独立的Python环境:

mkdir my_project_dir
cd my_project_dir
virtualenv --distribute my_venv
# my_venv为虚拟环境目录名,目录名自定义

OK,执行成功,上面发生了什么?

它会在my_project_dir目录中创建一个文件夹(my_venv),包含了Python可执行文件,以及 pip 库的一份拷贝,这样就能安装其他包了。虚拟环境的名字(my_venv)可以是任意的;不写名字会使用当前目录创建。

我们再来看看输出:

1 New python executable in my_venv/bin/python2.7
2 Also creating executable in my_venv/bin/python
3 Installing Setuptools......done.
4 Installing Pip...........done.

--distribute 选项使virtualenv使用新的基于发行版的包管理系统而不是 setuptools 获得的包。 你现在需要知道的就是 --distribute 选项会自动在新的虚拟环境中安装 pip ,这样就不需要手动安装了。 当你成为一个更有经验的Python开发者,你就会明白其中细节。

当然还有很多参数配置,例如:-p参数指定Python解释器程序路径,这里就过多介绍了,通过help去查看。

到这里这个虚拟环境就创建好了,但是要真正使用还需要激活,通过如下命令激活。

my_project_dir\my_venv\Scripts\activate

激活后输出如下:

# window下
(my_venv) yourpath\venv\Scripts>

# linux下
(my_venv)[root@docker-x my_venv]#

从现在起,任何你使用pip安装的包将会放在 my_venv文件夹中,与全局安装的Python隔绝开,是不是很赞,想怎么装怎么装。

就像平常一样安装包,例如:

pip install flask

上面启用激活,有激活那就有停用,如果你在当前虚拟环境中暂时完成了工作,可以使用如下命令停用它:

my_project_dir\my_venv\Scripts\deactivate

这将会回到系统默认的Python解释器,包括已安装的库也会回到默认的。要删除一个虚拟环境,只需删除它的文件夹。(执行 rm -rf venv )。

思考

让我们看看激活与停用virtualenv,调用python/pip命令有什么不一样。先停用virtualenv,如下:

[root@docker-x ~]# which python
/usr/bin/python
[root@docker-x ~]# which pip
/usr/local/bin/pip

让我们激活virtualenv后,再来一次!看看有什么不同。如下:

[root@docker-x ~]# which python
/usr/local/my_venv/bin/python

[root@docker-x ~]# which pip
/usr/local/my_venv/bin/pip

virtualenv拷贝了Python可执行文件的副本,并创建一些有用的脚本和安装了项目需要的软件包,你可以在项目的整个生命周期中安装/升级/删除这些包。 它也修改了一些搜索路径,例如PYTHONPATH,以确保:

  1. 当安装包时,它们被安装在当前活动的virtualenv里,而不是系统范围内的Python路径。
  2. 当import代码时,virtualenv将优先采取本环境中安装的包,而不是系统Python目录中安装的包。

还有一点比较重要,在默认情况下,所有安装在系统范围内的包对于virtualenv是可见的。这意味着如果你将simplejson安装在您的系统Python目录中,它会自动提供给所有的virtualenvs使用。这种行为可以被更改,在创建virtualenv时增加 --no-site-packages 选项,virtualenv就不会读取系统包,如下:

virtualenv my_venv --no-site-packages

virtualenvwrapper

有的时候virtualenv也会带来一些问题,由于virtualenv的启动、停止脚本都在特定文件夹,可能一段时间后,你可能会有很多个虚拟环境散落在系统各处,你可能忘记它们的名字甚至忘记它的位置。怎么来管理virtualenv?

鉴于virtualenv不便于对虚拟环境集中管理,所以推荐直接使用virtualenvwrapper

virtualenvwrapper提供了一系列命令使得和虚拟环境工作变得便利。它把你所有的虚拟环境都放在一个地方。

安装

pip install virtualenvwrapper
pip install virtualenvwrapper-win  #Windows使用该命令

注意:安装virtualenvwrapper之前首先确保virtualenv已安装

安装完成后,在~/.bashrc写入以下内容

export WORKON_HOME=~/Envs
source /usr/local/bin/virtualenvwrapper.sh

#读入配置文件,立即生效
source ~/.bashrc

说明:第一行:virtualenvwrapper存放虚拟环境目录,第二行:virtrualenvwrapper会安装到python的bin目录下,所以该路径是python安装目录下bin/virtualenvwrapper.sh

使用

使用如下命令创建虚拟环境:

mkvirtualenv my_venv_py3

这样会在WORKON_HOME变量指定的目录下新建名为 my_venv_py3 的虚拟环境。

若想指定Python版本,可通过--python指定Python解释器

mkvirtualenv --python=/usr/local/python3.5.3/bin/python my_venv_py3

查看当前的虚拟环境目录

[root@docker-x ~]# workon
my_venv_py2
my_venv_py3

切换到虚拟环境

[root@docker-x ~]# workon my_venv_py3
(my_venv_py3) [root@docker-x ~]# 

退出虚拟环境

(my_venv_py3) [root@docker-x ~]# deactivate
[root@docker-x ~]# 

删除虚拟环境

rmvirtualenv my_venv_py3

到这里 virtualenvsvirtualenvwrapper 就讲完了,是不是 so easy!跟着步骤来,一切都是顺理成章的。而且功能也很强大,使Python的开发环境配置起来变得非常简单。尤其是扩展工具virtualenvwrapper 使得构建出来的虚拟环境可以更好的管理起来。感谢这个世界,世界和平,Keep Real!

RESTful访问权限管理实现思路,采用路径匹配神器之AntPathMatcher

Published on:

我们经常在写程序时需要对路径进行匹配,比如说:资源的拦截与加载、RESTful访问控制、审计日志采集、等,伟大的SpringMVC在匹配Controller路径时是如何实现的?全都归功于ant匹配规则。

Spring源码之AntPathMatcher,这个工具类匹配很强大,采用的是ant匹配规则。

什么是ant匹配规则?

字符wildcard 描述
? 匹配一个字符(matches one character)
* 匹配0个及以上字符(matches zero or more characters )
** 匹配0个及以上目录directories(matches zero or more ‘directories’ in a path )

这个匹配规则很简单,采用简洁明了的方式来进行匹配解析,简化版本的正则。

结合官方的示例来理解一下

Pattern 匹配说明
com/t?st.jsp 匹配: com/test.jsp , com/tast.jsp , com/txst.jsp
com/*.jsp 匹配: com文件夹下的全部.jsp文件
com/**/test.jsp 匹配: com文件夹和子文件夹下的全部.jsp文件
org/springframework/*/.jsp 匹配: org/springframework文件夹和子文件夹下的全部.jsp文件
org/**/servlet/bla.jsp 匹配: org/springframework/servlet/bla.jsp , org/springframework/testing/servlet/bla.jsp , org/servlet/bla.jsp

如何实现RESTful访问权限管理?

在微服务和前后端分离的开发模式下,往往会使用RESTful来开发后端服务,那服务的访问权限控制就是一个问题,那下来我们就说一下如何实现RESTful访问权限管理。

权限资源类型

资源分为如下两种类型:

  • public(公有):public为不控制访问的资源
  • private(私有):private为需要被控制访问的资源

ps.这种方式资源管理的相对严格一些,如果想管理的粗矿一些,可以不需要public,只要在private中未找到的资源就是不控制访问的资源即可,实现时可以根据自己的业务场景来调整。

匹配原则

基础匹配规则:使用ant匹配规则

SpringMVC的路径匹配原则中有一个原则是:最长匹配原则(has more characters)

什么是最长匹配原则(has more characters)?

最长匹配原则(has more characters)简单的理解就是目标URL有多个pattern都可以匹配上就取最长的那个pattern

例如:请求的URL/app/dir/file.jsp,有两个pattern /**/*.jsp/app/dir/*.jsp都可以匹配成功,那么会根据pattern的长度来控制是否采用哪一个,这里使用/app/dir/*.jsp来匹配。

为什么要使用最长匹配原则?我的理解是长的pattern更符合目标URL格式,短的pattern往往是范围较广的,匹配取最适合的pattern也是比较符合预期的。

根据服务名分类

在做资源访问权限时往往会有多个服务可能会出现相同的资源路径,因此增加一级服务名来对资源进行分类。

例如:GET /v1/service1/product/1GET /v1/service2/product/1,根据二级目录service名称来对服务进行模块化分割。/v1为RESTful版本号

ps.服务名就是为了做资源分类

权限验证逻辑

  • 验证public资源
    • 去除末尾"/"
    • 验证service服务名,服务名为空返回没有权限
    • 获取服务名下enabled=true的资源表,结果进行cache,结果为空没有权限
    • 根据pattern长度倒序
    • 匹配method,匹配成功进行下一步匹配
    • 匹配请求的url,匹配成功返回有权限,反之返回没有权限
  • 验证private资源
    • 去除末尾"/"
    • 验证service服务名,服务名为空返回没有权限
    • 获取服务名下用户角色对应的资源列表聚合结果,结果进行cache,结果为空返回没有权限
    • 根据pattern长度倒序
    • 匹配method,匹配成功进行下一步匹配,反之continue
    • 匹配请求的url,匹配成功进行下一步匹配,反之continue
    • 检查匹配成功的url是否为禁用状态,如果禁用返回无权限,反之进行下一步匹配
    • 匹配成功的url对应的角色列表进行登录用户的角色匹配
    • 角色匹配成功返回有权限,反之返回没有权限

ps.method是GETPOSTPUTPATCHDELETEservice是服务模块名

缓存结构

  • private资源数据
    • 结构:hash
    • cache key=${APPNAME}.METADATA.RESOURCEfield=${RESOURCE_ID}value=Resource对象
  • public资源数据
    • 结构:hash
    • cache key=${APPNAME}.METADATA.RESOURCE.PUBLICfield=${SERVICE}value=List<Resource>
  • 用户关联角色数据
    • 结构:hash
    • cache key=${APPNAME}.METADATA.ROLEfield=${USER_ID}value=List<ROLE_ID>
  • 角色关联的资源数据
    • 结构:hash
    • cache key=${APPNAME}.METADATA.MAPPINGfield=${SERVICE}value=List<Metadata<Resource,List<ROLE_ID>>>
    • 这里存储的数据结构是反向的,获取服务下的资源列表,每个资源数据中会有拥有这个资源的角色列表。

ps.缓存可以使用分布式的redisredisson、如果单机可以使用jvm cache

缓存控制

  • private资源数据发生变更时
    • 调用MetadataCache.invalidResources(),失效cache key=${APPNAME}.METADATA.RESOURCE下所有数据
  • public资源数据发生变更时
    • 调用MetadataCache.invalidPublicResource(service)失效服务名下的public资源集合,失效cache key=${APPNAME}.METADATA.RESOURCE.PUBLIC下的某个${SERVICE}数据
    • 调用MetadataCache.invalidPublicResource()失效所有服务名下的public资源集合,失效cache key=${APPNAME}.METADATA.RESOURCE.PUBLIC下所有数据
  • 用户关联角色数据发生变更时
    • 调用MetadataCache.invalidUserRoles(userId)失效用户下的角色集合,失效cache key=${APPNAME}.METADATA.ROLE下所有数据
  • 角色关联的资源数据发生变更时
    • 调用MetadataCache.invalidMetadata(service)失效服务名下的资源角色聚合对象,失效cache key=${APPNAME}.METADATA.MAPPING下的某个${SERVICE}数据
    • 调用MetadataCache.invalidMetadata()失效所有服务名下的资源角色聚合对象,失效cache key=${APPNAME}.METADATA.MAPPING下所有数据

ps.在以上触发点上对缓存数据进行更新,这里采用失效再加载方式

缓存加载

  • private资源数据,在系统启动加载,加载所有私有资源,如果失效了,会在private匹配的时再进行加载
  • public资源数据,在public匹配时加载,通过服务名加载,如果失效了,会在public匹配时再进行加载
  • 用户关联角色数据,在private匹配时加载,如果失效了,会在private匹配时再进行加载
  • 角色关联的资源数据,在private匹配时加载,如果失效了,会在private匹配时再进行加载

ps.资源数据加载触发点

pattern配置建议

  • 配置资源时,将不需要配置权限的url配置为public资源
  • 每个服务名下建议配置一个**(双星)通配符给超级管理员使用,例如:/v1/products/**
  • 每个url的第二级目录要与服务名一致,例如:/v1/products/{pid},服务名为products
  • url的目录结构必须大于两级目录,例如:/v1/products/{pid},不允许为:/v1/{pid}
  • url与权限通配符映射关系,前面url,后面pattern
    • 例如:/v1/products/{pid} -> /v1/products/*
    • 例如:/v1/products/{pid}/skus/{sid} -> /v1/products/*/skus/*
    • 例如:/v1/products/enabled -> /v1/products/enabled
    • 例如:/v1/products/**,匹配products目录下所有目录

以上就是一种RESTful资源管理的实现思路,能控制到RESTful的方法级别,在前后端分离的项目可以使用这种方式来控制访问权限。

扩展Disconf支持Global共享配置,简化业务应用参数配置

Published on:
Tags: disconf ucm

当我们使用统一配置中心(UCM)后或许都会出现这种烦恼,项目中的配置项目多,当项目引用到基础中间件时都要增加基础中间件的配置,例如:zk参数、redis参数、rpc参数、loadbalance参数、mq参数、等。这些配置都是基础的中间件配置,应该做成共享的方式让所有APP都共享,而并不是在用的时候再去APP中添加,Global的配置基础中间件团队维护即可。

为什么要有公共共享的配置?

因为在APP配置中有很多是公共的配置,如果没有Global就需要在自己的APP中配置这些配置信息,导致APP中配置信息过多不好维护,公共的配置信息修改需要通知各业务APP修改自己APP中的配置,没有达到一处修改,各处使用的目标。

这时候有朋友就会问我了如果做成全局共享配置,那不同项目需要修改全局某个参数怎么办呢?

这个需求也很正常,比如loadbalance参数确实需要根据不同项目的具体情况去配置参数,对于这种问题其实很好解决,我们可以使用APP中的配置去覆盖Global配置,也就是说当APP中的配置项与Global配置项相同的情况下,以APP的配置为主即可。

这样一来APP的配置生效的优先级为:Local conf > Project conf > Global conf,当出现相同配置项以APP自身的配置为主去覆盖。

增加了Global的支持后,APP中的配置减少了,避免了一些由于配置导致的错误,也可以通过Global的方式去规范APP的配置,让业务开发不关心公共配置的细节,在使用的时候直接使用无需维护。

Disconf作为一个比较老牌的UCM在这方面支持的并不好,它并没有共享配置这个概念,这样一来公共的配置就需要在每个APP中都要配置一份,操作起来很烦人。

那我们如何来解决这个问题?我们能否扩展Disconf让其支持Global共享配置呢?

扩展思路

在加载properties的时候,也就是ReloadablePropertiesFactoryBean的locations,给前面默认加一个GlobalProp项目的索引项:global(使用disconf的新建配置项,而不是配置文件),这个索引项的值是所有global配置文件的名称,使用”,“分隔,例如:

global-dubbo.properties,global-redis.properties,global-zookeeper.properties,global-sso.properties,global-mq.properties,global-fastdfs.properties,global-elasticsearch.properties 

让disconf下载配置文件的时候优先下载global的配置文件,在properties加载的时候优先加载global的配置,这样当发生重复项时后加载的会覆盖前面的信息,从而达到了我们上面的需求,当APP中修改了某个global配置应该以APP的配置项为主。

接下来就让我们看一下具体扩展了哪些类?

Disconf的扩展点做的不是那么的好,因此扩展起来有些麻烦,我使用的是比较暴力的方式,直接使用原包的类在名称后加Ext然后修改代码,使用的时候使用Ext的类替代即可,这种方式的弊端是升级Disconf的时候很麻烦。

Disconf扫描管理

com.baidu.disconf.client.DisconfMgrBean
扩展一个
com.baidu.disconf.client.DisconfMgrBeanExt

com.baidu.disconf.client.DisconfMgrBeanSecond
扩展一个
com.baidu.disconf.client.DisconfMgrBeanSecondExt

Reloadable Properties

com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean
扩展一个
com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBeanExt

可以增加一个开关从而支持启用global的自由度,默认是开启的。

下面来看一下扩展后的具体使用方法如下

项目地址

disconf-client-ext

   

disconf-client-ext的使用

  • 依赖disconf版本:2.6.32
  • pom中引入disconf-client-ext依赖
  • 修改disconf配置
    • 替换com.baidu.disconf.client.DisconfMgrBean –> com.baidu.disconf.client.DisconfMgrBeanExt
    • 替换com.baidu.disconf.client.DisconfMgrBeanSecond –> com.baidu.disconf.client.DisconfMgrBeanSecondExt
    • 替换com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean –> com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBeanExt
    • 修改locations中配置文件,只保留项目自己的配置文件,例如
<bean id="disconfNotReloadablePropertiesFactoryBean" class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBeanExt">
	<property name="locations">
		<list>
			<value>classpath:/jdbc.properties</value>
		</list>
	</property>
</bean>
  • 关闭global共享配置(默认是开启的)
<bean id="disconfNotReloadablePropertiesFactoryBean" class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBeanExt">
	<property name="locations">
		<list>
			<value>classpath:/jdbc.properties</value>
		</list>
	</property>
	<property name="globalShareEnable" value="false" />
</bean>
  • 最后一步添加global项目到Disconf

世界和平,Keep Real!