kurnek学习笔记

GoF23

周末将《设计模式之禅》大致过了一遍,算是补完了欠债多年的GoF23,附上一个粗浅的思维导图以表到此一游。

最后还要吐槽一下该书作者虽然尽可能地想把例子举得更加贴近生活显得生动易懂一些,但是在读者我看来,许多例子举得并不好懂,我还要结合网上别人举得例子才能明白是什么意思。

所以如果有人想学习设计模式的话,不太推荐这本书……

undefined

源码剖析JDK动态代理

上篇文章介绍了JDK的动态代理的用法,结尾处挖了个坑,这次文章就顺着两个问题来剖析JDK动态代理的源码。

首先来看第一个问题,Proxy.newProxyInstance做了些什么?

这里我们结合源码来分析,源码如下所示,关键部位已经做了中文注释:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * Look up or generate the designated proxy class.
     */
    // 关键代码1:根据传入的接口列表创建动态代理类
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
        // 关键代码2:获取动态代理类的构造方法
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        // 关键代码3:调用动态代理类的构造方法,传入InvocationHandler,创建动态代理实例
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}

其中最关键的代码是以下这行:

getProxyClass0(loader, intfs); 

这行代码传入的第一个参数是需要实现的接口的ClassLoader,第二个参数是需要实现的接口列表。

这个方法会先在缓存里面查找是否已经创建过该代理类,如果没有,则会动态创建一个代理类。

再深入一点取看 getProxyClass0 的源码的话,会发现它最终调用的是 ProxyClassFactory.apply() 方法来创建代理类,以下是完整代码,关键部分也加上了中文注释。

private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
    // prefix for all proxy class names
    // 动态代理类的类名前缀
    private static final String proxyClassNamePrefix = "$Proxy";

    // next number to use for generation of unique proxy class names
    // 原子计数器,用于动态代理类的类名后缀
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        // 检查需要实现的接口是否存在,是否存在重复
        for (Class<?> intf : interfaces) {
            /*
             * Verify that the class loader resolves the name of this
             * interface to the same Class object.
             */
            Class<?> interfaceClass = null;
            try {
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
            /*
             * Verify that the Class object actually represents an
             * interface.
             */
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            /*
             * Verify that this interface is not a duplicate.
             */
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        String proxyPkg = null;     // package to define proxy class in
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        /*
         * Record the package of a non-public proxy interface so that the
         * proxy class will be defined in the same package.  Verify that
         * all non-public proxy interfaces are in the same package.
         */
        // 根据接口的可见性,来决定动态代理的包名
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            // if no non-public proxy interfaces, use com.sun.proxy package
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        /*
         * Choose a name for the proxy class to generate.
         */
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        /*
         * Generate the specified proxy class.
         */
        // 关键代码 1:创建动态代理类的字节码
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            // 关键代码 2:定义动态代理类
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
             * A ClassFormatError here means that (barring bugs in the
             * proxy class generation code) there was some other
             * invalid aspect of the arguments supplied to the proxy
             * class creation (such as virtual machine limitations
             * exceeded).
             */
            throw new IllegalArgumentException(e.toString());
        }
    }
}

其中最关键的代码是以下这句:

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);

这个方法第一个参数是代理类的类名,第二个参数是需要实现的接口列表,第三个参数是访问控制Flag,返回的是一个byte数组。

返回的byte数组就是动态代理类的字节码,也就是说jdk创建了一个新的类。

下一行关键代码是:

defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);

这一句代码第一个参数是classLoader,第二个参数是类名,第三个参数是字节码,作用是动态地将刚才创建的字节码定义成了一个新的类,也就是说JDK在运行时生成了一个新类,是不是突然有了一股动态语言的味道了。


接下来回答第二个问题,即以下代码为什么会报 ClassCastException

Person proxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), Person.class.getInterfaces(), handler);

其实看过前面的源码以后,这个问题就已经非常明显了,因为 Proxy.newProxyInstance 返回的是一个新的类,这个类实现了Person类实现的所有接口,但是跟Person类不是同一个类,因此无法Cast成Person类。

通过IDE的调试功能我们可以更加直观地理解这个现象。

undefined

在下方的变量监控界面可以看到 proxy 变量的类名是 $Proxy12 ,这个 $Proxy12 就是JDK运行时动态创建出来的新类,并不是我们在编写代码时定义的。

$Proxy12 虽然实现了IPerson接口,但是由于跟Person类没有继承关系,因此如果将proxy对象cast到Person类的话,就会报错。

JDK动态代理的基础知识基本就是这些了。

浅析JAVA中的动态代理

众所周知在设计模式中有一个非常经典的模式叫做代理模式,而在JAVA语言中对代理模式又做了一个重要的补充,即动态代理。

由于有了动态代理,甚至出现了一种新的编程模式:面向切面编程,即所谓的AOP。

所以本文想要讨论的就是我们如何使用JAVA语言的动态代理特性来进行编码。

普通代理

先来温习一下什么是普通的代理模式,普通的代理模式的UML视图如下所示:

 undefined

图中3个角色分别为:

  • ISubject : 抽象主题角色,例如我们定义的业务抽象接口
  • RealSubject:具体主题角色,是被代理的对象,例如我们定义的业务具体的实现类
  • Proxy:代理主题角色,在代理模式中,我们都会使用代理角色来完成调用,而非直接使用具体主题角色。

说定义总是抽象的,我们还是结合实际场景和代码来描述。

假设有一个叫做张三,他去参加聚会想要结交新朋友,但是他这个人不会说话,于是他就找了自己的好哥们李四在他自我介绍的时候帮下忙做补充。

这下我们就有了3个角色+1个场景,分别是:

  • 人:具体点说是要去参加聚会的人,这是一个抽象主题角色,即ISubject。
  • 张三:张三是要去参加聚会的人,这是一个具体主题角色,即RealSubject。
  • 李四:是张三拉过来帮自己忙做介绍的,我们可以理解为李四代理张三做介绍,即Proxy。
  • 场景:一个聚会,参加聚会的人会做自我介绍。

用代码来描述的话,如下所示:

抽象主题:人

public interface IPerson {
    public void introduce();
}

具体主题:张三

public class Person implements IPerson {
    public Person(IPerson proxy) throws Exception {
        if (proxy == null) {
            throw new Exception("我需要有个好哥们当代理");
        }
    }
    public void introduce() {
        System.out.println("我的名字叫张三。");
    }
}

代理主题:李四

public class ProxyPerson implements IPerson {
    IPerson realSubject;
    public ProxyPerson() {
        try {
            // 好哥们张三由我李四代理
            this.realSubject = new Person(this);
        } catch (Exception e) {}
    }
    public void introduce() {
        System.out.println("初次见面");
        realSubject.introduce();
        System.out.println("可以请你喝杯东西吗?");
    }
}

场景:聚会

public class Party {
    public static void main(String[] args) {
        IPerson clientProxy = new ProxyPerson();
        clientProxy.introduce();
    }
}

运行Party的main函数,结果如下:

初次见面
我的名字叫张三。
可以请你喝杯东西吗?

可以看到,在李四的帮助下,张三的自我介绍变得丰富了起来,不再是只有干巴巴的一句“我的名字叫张三”。

这也是代理模式希望解决的问题,在不修改具体实现类的情况下实现一些增强操作。

聊完了普通代理模式以后,我们能发现普通代理模式有一个比较明显的特点,就是代理类的编写比较复杂,目前我们的IPerson接口只有1个方法,如果IPerson中有10个方法的话,由于我们的Proxy类实现了IPerson,因此也要实现10个方法,这样就为Proxy类的编写带来了困难。

动态代理

动态代理特性则很好地解决了上述问题,那么上面这个例子用动态代理来编写的话,是如下这样的:

首先自定义一个InvocationHandler

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {
    Object realSubject;

    public PersonInvocationHandler(IPerson realSubject) {
        this.realSubject = realSubject;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName() + "方法执行前...");
        Object result = method.invoke(this.realSubject, args);
        System.out.println(method.getName() + "方法执行后...");
        return result;
    }
}

为了展示跟前面例子的区别,我们稍稍修改下IPerson接口和Person类,这次张三不仅自我介绍,还要喝上一杯了。

IPerson

public interface IPerson {
    public void introduce();

    public void drink();
}

Person

public class Person implements IPerson {
    public Person() {}

    public void introduce() {
        System.out.println("我的名字叫张三。");
    }

    public void drink() {
        System.out.println("我先干为敬!");
    }
}

最后是聚会场景Party类

import java.lang.reflect.Proxy;

public class Party {
    public static void main(String[] args) {
        PersonInvocationHandler handler = new PersonInvocationHandler(new Person());
        IPerson proxy = (IPerson) Proxy.newProxyInstance(Person.class.getClassLoader(), Person.class.getInterfaces(), handler);
        proxy.introduce();
        proxy.drink();
    }
}

最后的运行结果如下所示

introduce方法执行前...
我的名字叫张三。
introduce方法执行后...
drink方法执行前...
我先干为敬!
drink方法执行后...

可以看到我们这次没有自己去手动编写代理类了,自然也无需手动实现introduce方法和drink方法,通过java反射包中提供的InvocationHandler接口以及Proxy类,我们动态地创建出了一个代理对象,并且在执行Person类的方法前后都做了一些处理,看到这里,是否就觉得有AOP那味儿了?

那么关于JAVA的动态代理初步介绍就先到这里为止,最后给自己留点思考题到下一篇文章中解答,也算是挖个坑。

如果将代码

IPerson proxy = (IPerson) Proxy.newProxyInstance(Person.class.getClassLoader(), Person.class.getInterfaces(), handler);

改成

Person proxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), Person.class.getInterfaces(), handler);

就会出现ClassCastException,这底下的原理是什么呢?

InvocationHandler是什么?

Proxy.newProxyInstance做了些什么?

Redis分布式锁之惑

Redis毫无疑问是目前业界使用最广泛的缓存方案之一,由于单线程机制以及高性能,许多时候程序员们也会用它来实现分布式锁。

来看一个典型的分布式锁代码(JAVA描述):

package org.owl;

import java.util.ArrayList;
import java.util.List;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

public class RedisLock {
    private Jedis client;
    private String releaseLockScriptSHA;

    private void loadReleaseLockScript() {
        StringBuilder script = new StringBuilder();
        script.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] then").append("\n");
        script.append("    return redis.call(\"del\",KEYS[1])").append("\n");
        script.append("else").append("\n");
        script.append("    return 0").append("\n");
        script.append("end");
        releaseLockScriptSHA = client.scriptLoad(script.toString());
    }

    public RedisLock(String host, int port) {
        client = new Jedis(host, port);
        loadReleaseLockScript();
    }

    public boolean acquireLock(String key, String uniqueValue, int timeout) {
        SetParams params = new SetParams();
        params.ex(timeout);
        params.nx();
        return "OK".equalsIgnoreCase(client.set(key, uniqueValue, params));
    }

    public void releaseLock(String key, String uniqueValue) {
        List<String> keys = new ArrayList<>();
        List<String> argvs = new ArrayList<>();
        keys.add(key);
        argvs.add(uniqueValue);
        client.evalsha(releaseLockScriptSHA, keys, argvs);
    }
}

加锁部分代码通过set命令去设置一个键值对,KEY为锁名,VALUE为锁的值,锁的值是个唯一值,用于后续释放锁时避免错误地释放了其他客户端申请的锁,并且指定NX和EX参数来保证只有一个客户端能拿到锁并且在超时后自动解锁防止死锁。

释放锁部分代码则是执行了一段lua代码,这段代码会对比传入的锁值跟Redis中的值是否一致,一致则认为该锁是本客户端所申请的,使用del命令来解锁,否则不处理。

示例图如下

 undefined

这段代码跟网上流传的Redis锁实现大同小异,在单Redis节点的情况下,这段代码可以说是完全正确的。

但在实际使用中,我们往往不会只使用单个Redis实例,而是多个实例同时运行,使用类似一致性哈希算法来将不同的key散列到不同实例上。

因此情况就会变成了下面这样:

 undefined

考虑一个情况,如果在Client1拿到锁以后,Instance1挂了,这时候Client2再去申请锁时,由于Client1挂了,所以申请锁的请求就会发送到其他的Redis实例,假设是Instance2吧,因为Instance2上面没有key1,set成功,因此Client2也拿到了锁,这时候就产生了冲突了。如下图所示:

 undefined

那么,在多个Redis实例的情况下,如何保证锁的正确性呢?

Redis官方给出了一个算法:The Redlock algorithm

假设我们现在有N个Redis实例,这些实例是完全独立的,不会进行数据同步。我们已经知道怎么在单个实例上正确地申请锁和释放锁,那么如何在多个实例上正确地使用锁呢?我们举例目前有5个实例,也就是N=5,那么客户端在拿锁的时候,要执行以下几步操作:

  1. 获取当前毫秒级时间戳
  2. 用同一个key和value,按顺序向每一个实例申请锁,客户端在请求实例时的超时时间要小于锁的自然过期时间,比方说锁的自然过期时间是10秒,那么客户端请求实例的超时时间可以设置为5~50毫秒,这是为了避免在请求实例时被堵塞太长时间以至于前面申请的锁自然失效,在请求Redis实例超时或者失败的时候应该尽快向下一个实例发起请求。
  3. 客户端在申请锁的同时还要统计耗时,也就是发起第一个请求之后的耗时时间,因为我们一共有5个实例,因此某个客户端在(N/2+1)个,即3个实例上成功写入锁值,并且耗时没有超过锁的自然过期时间,就可以认为该客户端拿到了锁。
  4. 该锁的有效时间为 锁的自然过期时间 减去 请求各个实例的耗时。
  5. 如果客户端因为某些原因无法获取到锁(比方说没能在N/2+1个实例上获取到锁又或者耗时超过了锁的自然过期时间),那么客户端应该尽快释放掉所有已经申请到的锁。

示例图如下所示:

undefined

假设锁的自然过期时间是TTL,发起第一个请求的时间戳是T1,收到最后一个响应的时间戳是T2,如果成功拿到了锁,并且确保 T2 - T1,也就是获取锁的耗时尽可能小于TTL,那么起码在 TTL - (T2 - T1) 这段时间内,这个锁是可用的。

乍一看,这个算法确实像那么回事,如果出现了实例失效的情况,也能保证锁的正常运作,但是该算法也存在不少问题,比如说一旦出现网络分区,即某客户端一直只能访问到一个实例,那么这个客户端就永远拿不到锁了,又例如某个持有锁的值的实例发生了重启,如果该实例做了持久化,那么在restart过程中会产生明显的延迟(如果不持久化,则会导致多个客户端同时拿到锁)。

详细的讨论可以参考Redis官方的专题讨论 -> The Redlock algorithm

看来用Redis来做分布式锁还是要相当谨慎才行啊。

PHP性能分析工具 - xdebug profiler

在工作中我们不得不关注的一点就是程序的执行性能,但是面对日益复杂的程序,单纯靠人工code review去分析性能瓶颈,显然不是那么有效率的事情,因此找到合适的工具用来分析性能消耗变得尤为重要,这次就来介绍下PHP性能分析工具XDEBUG。

XDEBUG

官网:https://xdebug.org/

XDEBUG是历史悠久的PHP扩展之一,用于DEBUG和分析PHP程序,该项目一直有在维护,目前支持PHP5到7的版本,基本上涵盖了我们常用版本。

安装XDEBUG

官方文档:https://xdebug.org/docs/install

基本上跟安装其他扩展没有什么区别,下载源码后用phpize + configure + make && make install 即可。

需要注意的是如果安装了多个PHP版本,那么在configure的时候要带上指定版本的php-config可执行文件。

配置XDEBUG

官方文档:https://xdebug.org/docs/all_settings

XDEBUG并不只有profiler功能,因此它有非常多的配置选项,包括函数trace,GC监控等等,本文由于主要将性能分析所以只启用profiler功能,在php.ini中添加以下几行配置:

zend_extension=/pathToExtension/xdebug.so
xdebug.profiler_enable=0
xdebug.profiler_enable_trigger=1 xdebug.profiler_output_dir=/apps/xdebug

xdebug.profiler_enable参数为1时默认对所有请求都采集性能数据,这里我们设置为0,也就是默认不采集。

xdebug.profiler_enable_trigger参数为1时,仅在GET/POST/COOKIE参数中带有XDEBUG_PROFILE时才采集。

配置好以后,重启PHPFPM,通过HTTP或者其他手段访问PHPFPM进程,就会在配置的xdebug.profiler_output_dir目录下找到类似 cachegrind.out.6191 的文件了,文件后面的数字是进程PID。

注意:如果需要跟OPCACHE共用的话,则要在OPCACHE加载后再加载XDEBUG,具体可以查阅官方文档。

分析结果

XDEBUG采用了Cachegrind格式来记录分析结果,该文件格式可以用kcachegrind工具来生成可视化分析视图,除此以外XDEBUG作者还提供了几种不同的分析工具,不过我这边看了下那些工具都不是那么好使,google后发现PHPSTORM也支持XDEBUG的分析结果文件,因此就直接使用PHPSTORM来进行分析。

需要注意的是XDEBUG不同版本的分析结果对PHPSTORM版本也是有要求的,我用XDEBUG 2.8输出的分析结果,用PHPSTORM 2016版无法解析,用PHPSTORM 2019版就可以解析成功,各位如果解析不成功的话,可以尝试更换不同版本的XDEBUG或PHPSTORM。

打开PHPSTORM,菜单上选择Tools,选择Analyze Xdebug Profiler Snapshot,再选择我们之前采集的文件,即可看到可视化的性能分析报告。

如下图所示:

undefined

点击右边的TAB还能以树状来显示(当然像lavaral这样的套娃框架,用树状来分析显然不是一个好主意…)

undefined

在分析报告中,我们可以看到是哪些函数耗时或者消耗的内存较多,进而找到可优化的地方。

当然由于Xdebug的profiling功能还是存在一些局限性,比如只能统计执行时间和内存消耗,无法知道CPU消耗。

如果需要知道更加详细的数据,那就只能用更强大的工具Tideway了。

所以下次文章会介绍如何使用tideway来搭建一套生产可用的性能监控系统。

对象Mapping工具Orika简介

最近几天在工作中遇到了一个场景,是将一个对象转换成另外一个结构类似的对象,如下所示:

Class Person {
	private String id;
	private String name;
	private String birthday;
	//getters and setters
}

Class PersonDto {
	private String id;
	private String name;
	private Date birthday;
	//getters and setters
}

比方说上面两个类的对象,想要将Person转换为PersonDto,最直接的写法当然是直接用setter:

Class Mapper {
	public PersonDto map(Persion p) {
		PersonDto pd = new PersonDto();
		pd.setId(p.getId());
		pd.setName(p.getName());
		pd.setBirthday(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(p.getBirthday));
                return pd;
	}
}

但如果Person对象和PersonDto对象的属性非常多,而且还存在对象嵌套呢?

再用直接赋值的方法,代码会显得非常难看臃肿,非常地不便利,这时我就想是否存在一个工具能做这样的转换,于是google了一会,果然有不少提供了对象Mapping功能的工具,其中一个佼佼者就是本文想跟大家介绍的Orika。

Orika简介

GitHub

官方文档

Orika是基于Apache License 2.0的开源项目,项目目标是为了让开发者们从冗长的对象转换逻辑中脱身,将时间放在编写真正有价值的业务代码上。

Orika会自动收集类的元信息,并且以此来生成mapper,用于之后的对象转换和复制,并且是支持递归转换的。

引入

POM引入

<dependency>
   <groupId>ma.glasnost.orika</groupId>
   <artifactId>orika-core</artifactId>
   <version>1.4.2</version><!-- or latest version -->
</dependency>

如果不用maven的话,可以下载jar包,但是还要自行引入依赖javassist/slf4j/paranamer,版本要求见官方文档。

基本用法

//创建mapper工厂
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

//注册mapper
mapperFactory.classMap(PersonSource.class, PersonDestination.class)
   .field("firstName", "givenName")
   .field("lastName", "sirName")
   .byDefault()
   .register();

//获取mapper门面
MapperFacade mapper = mapperFactory.getMapperFacade();
//创建PersonSource对象
PersonSource source = new PersonSource();
// 给source对象设置一些字段
...
// 将PersonSource对象转换为PersonDest
PersonDest destination = mapper.map(source, PersonDest.class);

主要看注册mapper的部分:

  • classMap方法注册了类的映射关系。
  • field方法可以将源对象的字段映射到目标对象的字段,也就是将PersonSource对象映射到PersonDestination对象的时候,PersonSource.firstName会复制到PersonDestination.givenName,lastName同理映射到sirName。
  • byDefault方法则是告诉mapper使用默认mapping策略,基本上来说就是mapper会找同名同类型的字段进行映射。
  • register方法用于完成mapper的注册。

在完成mapper的注册以后,只要用工厂获取mapper门面,然后用map方法就可以完成转换了,省去了自己new对象以及逐个字段赋值的大段代码。

另外如果需要映射的类只有一对,即mapper只用来将PersonSource映射到PersonDestination,而不会再配置一个映射是从PersonSource到PersonDestination2的话,那么更好的实践是采用BoundMapperFacade,即约束mapper,具体代码如下

BoundMapperFacade<PersonSource, PersonDest> boundMapper = mapperFactory.getMapperFacade(PersonSource.class, PersonDest.class);
 
PersonSource source = new PersonSource();
// 为PersonSource对象设置一些字段
...
PersonDest destination = boundMapper.map(source);
//反向映射
PersonSource source2 = boundMapper.mapReverse(destination);

BoundMapper对比标准Mapper提供了更好的性能,使用BoundMapper时如果希望反向映射的话,可以使用mapReverse方法。

OK,以上就是Orika的基本介绍了,当然这还只是皮毛,Orika还提供了更多的高级特性,比方说可以自定义mapper的转换逻辑的custom converters,自定义对象工厂的object factories,自定义mapper,映射过滤器filters等等,基本上能满足绝大部分对象转换的场景,在官方文档上有更加详细的介绍。

聊聊重构

引言

重构(Refactor)这个词语我想在程序员圈子里面属于超高频词汇了,对于任何系统,只要它有问题,许多程序员就会想:“我们得找时间重构它的代码”。仿佛重构是一种魔法,可以解决一切问题,包括我开始也是这么想的。

近期因为工作关系,重构二字可以说是天天都听到了,因此国庆假期抽空通读了知名读物《重构 改善既有代码的设计》,书中对重构的观点和指导方法,着实让我开了眼界,因此写篇笔记来记录一下感想。

定义

重构作为名词时的意思是:在不改变软件可观察行为(也可理解不改变原有功能)的前提下,重构是对程序内部结构的一种调整,这种调整的目的是提高代码的可理解性和降低修改成本

重构作为动词时的意思是:在不改变软件可观察行为的前提下,通过一系列重构手法(如提炼函数,使用多态等),来调整代码结构。

看完定义以后,是不是觉得跟以前自己所理解的“重构”的定义有点似是而非,接下来说下我之前对重构理解的几个误区。

误区

重构=重写

很多人认为一个接口/程序的代码写得不好,我们对它进行了完全的重写,这就叫做重构。实际上这里有两个误会:

1、重构实际上是调整代码的组织结构,来达到两个目的:提升可理解性和降低修改成本。调整结构是指在原代码的基础上做局部的调整,完全重写理论上是抛弃原有代码重新开发,两者是完全不同的概念。

2、重构肯定需要重写部分代码,但是这里的重写应该是按照一系列重构手法(如上文提到的提炼函数,使用多态等,下文不再赘述),来达到提高可理解性和降低修改成本。如果重写时,没有采用重构手法去进行代码结构的设计,那么这次重写实际上并没有达到重构的目标。

重构=提高性能

重构的目标是提升可理解性和降低修改成本,性能方面很可能是反而下降的。举个例子:switch语句改用多态去实现,很可能需要创建更多对象,降低了性能。

但是即便如此,我们也不应该以性能为借口而去放弃重构,否则“软件工程”就无法存在了。实际上,在我们工作中,程序消耗的80%~90%性能都在那10%~20%的代码块中,因此性能优化这件事情应该去找到那10%~20%的代码来着重优化(这时候采用的就不一定是重构手法了),对于剩下的大部分代码,我们则应该尽量提高可理解性和降低修改难度来节省开发成本和提高工程质量。

重构需要专人/专门的时间去做

“这代码太烂了,我们以后重构掉他吧!”

相信类似这句话,大家听过太多了,我自己也想过或者说过好多次,实际上这个误区跟重写是类似的。

由于代码的设计很可能一旦写完,到下一次迭代就开始腐化了(因为我们几乎无法预测之后要加什么功能),如果每次迭代都只是单纯地加代码,而不去调整原有的结构来优化设计,只想着以后再调整,那么假以时日这些代码就会变得难以理解和修改,维护成本水涨船高,程序员苦不堪言。

这里就涉及到了重构的时机问题,书中作者的观点是重构应该是在每次迭代时都进行的,也就是每次加功能时,如果发现原先的设计已经腐化了,就尽快进行调整,以保证代码始终保持合理健康的设计。

当然这里涉及到了一个问题,就是调整结构不可避免地会动到以前实现完成的功能,如何在不改变可观察行为的前提下去完成这件事,后续会讲解。

何时重构

其实在前文就已经提到了,重构这件活实际上应该放在软件的迭代周期中一直做,换句话说,也就是开发过程中,如果需要涉及到的代码的设计已经腐化了,那么就应该及时重构它,通过这样的局部重构迭代,来保证整体代码设计是合理的。

重构的主要挑战

延缓新功能开发

这是大部分人不愿意重构的理由,事实上从短期来看确实如此,但如果一个程序需要长期迭代,那么持续重构反而会提高开发效率。

回顾我们的实际工作,在我们开始一个新项目时,开发功能总是非常快的,但越到项目的后期,想要往代码库中加功能就会变得举步维艰,但如果能保持一直重构的话,那么代码始终会保持合理的设计,新增功能时的困难也会更加小,两者的对比可以用下图来说明。

undefined

举个夸张的例子,比如代码仓库A,从不考虑提炼函数,同一个功能始终都是复制粘贴代码,那么在N个版本后,该仓库到处都是重复代码,一旦要修改,就要整个代码仓库大改,那么开发工作将举步维艰,而代码仓库B,则总将可复用代码聚合到函数中,那么修改时只要修改其中一个代码片段即可,开发工作能顺利进行。

引用书中的一句话,重构其实是从经济效益出发的,是为了提高程序员的开发效率而存在的工具。所以如果是长期迭代的项目,重构不仅不是延缓新功能开发的事情,反而是保持良好开发效率的法宝。

自测试不完备

这应该是很多项目难以重构的原因,前文提到因为要调整代码结构,势必会涉及已有功能,如果希望由人工测试来覆盖所有修改到的地方都是正确的,那从成本角度来看是不现实的,搞不好还会惹得QA同事一肚子怨气。

如果有完备的自测试代码,情况就不同了,重构完成后只要执行自测试代码,保证结果OK,那么我们重构时就可以放心去干了。

另外这里提到的自测试代码,跟单元测试还是有些许差别,自测试代码是更加高层的测试,换句话说,自测试代码是为了确保修改前后,程序的可观察行为没有发生改变,即黑盒测试。而单元测试更多是偏向白盒测试,两者不能一概而论。

结语

最后非常建议没有看过《重构 改善既有代码的设计》的同学去读一下这本书,毕竟我文笔拙劣,并不能将原书精华100%还原。

书的前三章就能让人对“重构”这件事情有新的见解,第四章的测试部分,则详细介绍了应该如何去设计有效的自测试代码,之后就全都是实战环节,教授各种重构手法,颇有重构独孤九剑的感觉,非常值得一看。

如何在PHPUNIT中Mock静态方法?

我们写的类、方法常常存在依赖,为这些类编写单元测试的时候,我们要想尽可能地覆盖更多的测试用例(Test Case),难以避免地就要对依赖类进行Mock。

想象一个场景,我们现在编写一个吃货类(Foodaholic),这个吃货只有一个功能,就是吃,但是他自己不会煮,所以要找个厨师给他做饭吃,而且他还很挑剔,只吃蛋糕(cake)、牛排(steak)和披萨(pizza),代码如下:

class Foodaholic {
    private static $eatable = ["cake","steak","pizza"];
    public function eat() {
        $food = Cook::provideMeal();
        if (!in_array($food,self::$eatable)) {
            echo "Not eatable";
        } else {
            echo "Eating ".$food;
        }
    }
}

好了,我们现在打算对吃货类的eat方法编写单元测试,测试他是不是真的只吃蛋糕、牛排和披萨,理所当然地,我们开始编写厨师类(Cook)的Mock类。

但是此时我们发现,厨师类提供了一个静态方法,而吃货类是调用该静态方法来对厨师类进行依赖,那么我们如何mock呢?

最简单的方法就是在AutoLoader找到Cook类之前就手动引入我们编写好的Mock厨师类,参考以下代码:

//mockStaticCakeCook.php
class Cook {
    public static function provideMeal() {
        return "cake";
    }
}
require_once "mockStaticCakeCook.php";

use PHPUnit\Framework\TestCase;

class FoodaholicTest extends TestCase {
    public function testEat() {
        $this->expectOutputString('Eating cake');
        $f = new Foodaholic();
        $f->eat();
    }
}

这方法肯定是可行的,我们mock了一个做蛋糕的厨师,并且测试了吃蛋糕的用例,但这个方法实在称不上优雅。

那么PHPUNIT有没有提供mock功能呢,答案是肯定的,看下面代码:

// 修改自phpunit文档的示例
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function test()
    {
        // Create a mock object
        $mockObject = $this->getMockBuilder("Cook")->getMock();

        // Configure
        $mockObject ->method("provideMeal")->willReturn("cake");
        
        $this->assertEquals("cake", $mockObject->provideMeal());
    }
}

上面的代码是参考官方范例,结合本文场景改写的,我们mock了一个厨师类对象,并且mock了它的provideMeal方法返回cake。

眼尖的小伙伴估计一下就发现问题了:“可这mock的不像是静态方法呀?”

没错,上面确实mock的不是静态方法,因为phpunit并不支持mock静态方法,在官方文档也明确提到这一点。

那么估计这里就有小伙伴要骂我了:“搞半天,原来PHPUNIT不支持啊!你这不是标题党吗?!”

好吧,事已至此,我也只能承认本文确实有点标题党…… 不过各位不好奇为什么PHPUNIT不支持mock静态方法吗?

接下来,我就从本人理解来解读下为什么PHPUNIT不去支持mock静态方法。


首先,我们沿着PHPUNIT的思路走,如果我们要用PHPUNIT的mock方式来对我们的代码进行单元测试,那么我们要怎么改写吃货类呢?我先来举个例子,看以下代码:

单元测试

use PHPUnit\Framework\TestCase;
class FoodaholicTest extends TestCase {
    public function testEat() {
        // Create a mock object
        $mockCook = $this->getMockBuilder("Cook")->getMock();

        // Configure
        $mockCook->method("provideMeal")->willReturn("cake");

        //test
        $foodaholic = new Foodaholic($mockCook);
        $this->expectOutputString('Eating cake');
        $foodaholic->eat();
    }
}

吃货类

class Foodaholic {
    private static $eatable = ["cake","steak","pizza"];
    private $cook;
    // 因为我们的吃货没有厨师做饭就活不下去了
    // 所以在构造的时候就要传入一个厨师
    public function __construct(Cook $cook) {
        $this->cook = $cook;
    }
    public function eat() {
        $food = $this->cook->provideMeal();
        if (!in_array($food,self::$eatable)) {
            echo "Not eatable";
        } else {
            echo "Eating ".$food;
        }
    }
}

我们将厨师作为吃货类构造方法的参数传入,这样就可以顺利使用PHPUNIT的mock功能去对我们的吃货类进行测试了。

那么改写后的代码跟原本的调用静态方法的吃货类最根本的区别是什么呢?

没错,是依赖的引入方式。

使用静态方法调用时,实际上是将吃货类跟厨师类强耦合在一起了,也就是说,假如哪天我们想给吃货换个厨师,那么就必须要改吃货类的代码。

而如果采用构造方法传参的方式,那么依赖就是从外部传入的,我们想要更换厨师,只要给吃货传入另外一个厨师就行了,不需要修改吃货的代码,这时候吃货类跟厨师类就是松耦合了。

说到这里,可能又有小伙伴要问了,在构造方法要求传入Cook类,那不还是耦合了Cook类吗?没错,确实是这样,所以让我们再稍微改一下这个构造方法:

interface ICook {
    public function provideMeal();
}
class Foodaholic {
    private $cook;
    public function __construct(ICook $cook) {
        $this->cook = $cook;
    }
    ...
}

这样一来,只要是会做饭的人(实现了provideMeal方法的类),我们都认为他可以是一个厨师(也就是ICook接口的实现类),这时候我们的吃货对厨师的依赖关系就变得非常合理了,我们可以随时给吃货换厨师,这个厨师可以是保姆,妈妈,爸爸等等,而且在更换厨师的过程中,不需要修改吃货的代码,吃货只要专注吃这件事就可以了,可谓是非常地高内聚了。

同时,我们的单元测试也变得非常简单,以下代码测试吃货是不是只吃蛋糕、牛排和披萨:

class FoodaholicTest extends TestCase {
    /**
    * @dataProvider dp
    */
    public function testEat($food,$expectOutput) {
        // Create a mock object
        $mockCook = $this->getMockBuilder("ICook")->getMock();
        // Configure
        $mockCook->method("provideMeal")->willReturn($food);
        //test
        $foodaholic = new Foodaholic($mockCook);
        $this->expectOutputString($expectOutput);
        $foodaholic->eat();
    }

    public function dp() {
        return [
            ["cake","Eating cake"],
            ["steak","Eating steak"],
            ["pizza","Eating pizza"],
            ["rice","Not eatable"]
        ];
    }
}

结合PHPUNIT提供的 dataProvider 注解,我们轻松优雅地完成了吃货类的测试,真是可喜可贺~


回到问题,为什么PHPUNIT不支持mock静态方法?

我想到的答案是,PHPUNIT希望我们在引入依赖的时候,都能遵循“依赖倒置原则”(Dependence Inversion Principle, DIP)。DIP的原文表达得比较含蓄,我站在PHP这门语言(跟JAVA应该也一样)的角度来解读如何遵循DIP:

  1. 类之间的依赖应该通过抽象来进行,实现类之间不应该直接发生依赖,而是通过接口或抽象类来产生依赖
  2. 接口或抽象类不能依赖实现类
  3. 实现类依赖接口或抽象类

看完上面3句话,再想想前面讲了半天的吃货和厨师的例子,应该明白DIP是个怎么回事了吧!

实际上如果用过最优雅的PHP框架“Laravel”的开发者,都会发现官方给出的大量示例代码都遵循了依赖倒置原则,这也是实现服务容器(Service Container)的基础,当然这是另外一个故事了。

所以,PHPUNIT不去支持静态类的mock,我个人认为是很合理的,反而是我们在实际编写代码的过程中,如果过分贪图快速,而不分场景地去编写静态类,依赖静态类,最后我们的代码大概只会越来越难测试和维护了吧。

Go并发模式:context上下文

在用gin框架写一个go web app的时候,发现浏览器关闭时gin的handler没有立刻结束,为了实现让浏览器关闭时,gin的handler也及时关闭,遂google发现了context包,于是顺手翻译一下这篇介绍context包的博客。

翻译自:https://blog.golang.org/context (需科学上网)

介绍

在Go服务器中,每一个请求都有一个负责处理它的请求处理器(也是一个goroutine),请求处理器经常会启动额外的goroutine去访问后端,如数据库、RPC服务等,这些由请求处理器启动的goroutine常常需要获取对应请求携带的值,如用户ID、授权token、请求的超时时间等,当请求被取消或者超时,这些goroutine也应该要尽快结束,以便系统可以尽快回收它们占用的资源。

因此Google开发了context包,使跨API将请求作用域(request-scoped)变量、取消信号、超时时间传递给请求处理器启动的goroutine变得简单起来。这篇文章就是讲述如何使用context包以及给出了一个完整示例。

Context(上下文)

context包的核心是 Context 类型:

// Context携带超时时间,取消信号,请求作用域变量横跨API边界
// Context的方法是并发安全的
type Context interface {
// Done 返回一个通道,当Context取消或超时,该通道关闭
Done() <-chan struct{}

// Err 在Done通道关闭后给出该Context取消的原因
Err() error

// Deadline 如果Context有超时限制,该方法会返回Context将被取消的时间
Deadline() (deadline time.Time, ok bool)

// Value 返回该Context携带的变量
Value(key interface{}) interface{}
}

(代码以及描述经过精简,推荐阅读 godoc)

Done方法返回一个通道用于传递Context的取消信号:当Done通道关闭的时候,跟该Context相关的方法应该放弃工作并且返回。Err方法返回Context被取消的原因。另外这篇文章(Pipelines and Cancelation)详细讨论了Done通道的用法。

Context类型没有Cancel方法和Done通道只读的原因是相同的:接收取消信号的方法一般都不会是发送取消信号的那个,比方说当父任务启动goroutine去执行子任务时,子任务不应该能够取消父任务,相反,WithCancel方法(下面会讲)提供了取消Context实例的途径。

Context是并发安全的,因此可以在代码中将一个Context实例传递给任意个goroutine并且取消这个Context来通知所有goroutine。

Deadline方法允许程序去决定是否要开始工作,如果Context距离剩余的时间不足以完成工作,那开始工作的开销就不那么划算了,代码中也可以根据超时期限来设定IO操作的超时时间。

Value方法允许Context示例携带请求作用域的数据,这些数据必须得是并发安全的。

Derived contexts(派生上下文)

context包提供了从已有Context实例派生新实例的方法,这些实例会形成一棵树:当一个Context实例被取消,所有由它派生的Context都会被取消

后台(Background)是所有Context树的根,它是永远不会被取消的:

// Background 返回一个空的Context。它永远不会被取消,也没有超时,并且不携带任何值,
// Background 通常作为顶级的Context用在main方法、init方法和tests中
func Background() Context

WithCancel和WithTimeout方法从给定的Context派生出新的Context,新的Context可以在父Context结束前被取消。跟请求关联的Context通常会在请求返回的时候被取消。WithCancel在取消多余的请求时很有用,WithTimeout则在需要给后端请求设置超时时很有用:

// WithCancel 返回父Context的拷贝和cancel方法,当父Context的Done通道关闭或cancel方法被调用时,该拷贝的Done通道被关闭
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// CancelFunc取消Context
type CancelFunc func()

// WithTimeout 返回父Context的拷贝和cancel方法,当父Context的Done通道关闭或cancel方法被调用或达到设定的超时期限时,该拷贝的Done通道关闭
// 该拷贝的超时期限为now + timeout或父Context的超时期限,当该拷贝被取消时,会回收timer所占用的资源
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue提供了将请求作用域的变量关联到Context上的途径:

// WithValue 返回携带了传入的key-value的父Context的拷贝
func WithValue(parent Context, key interface{}, val interface{}) Context

了解Context的作用最好的方法还是看实际例子。

Example: Google Web Search(示例:google 搜索)

本示例是一个HTTP服务器,处理类似这样的URL:/search?q=golang&timeout=1s,将query中的"golang"发送给 Google Web Search API (译者注:该API已经凉了,因此译者最后给出的完整示例代码做了些许更改)并且将返回结果输出,timeout参数决定HTTP服务器在多久收取消这个请求。

代码分了3个包:

  • server:main方法,处理/search请求
  • userip:用来获取用户的IP并且关联到Context上用于发送给Google Search API
  • google:发送请求到Google Search API的相关代码

The server program

server程序处理/search?q=golang请求,它将handleSearch方法注册到/search路径上,该请求处理器会初始化变量名为ctx的Context,当请求返回时会安排取消掉ctx,如果URL中带timeout参数,则超时后立刻取消ctx:

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // ctx.Done channel, which is the cancellation signal for requests
    // started by this handler.
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // The request has a timeout, so create a context that is
        // canceled automatically when the timeout expires.
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // Cancel ctx as soon as handleSearch returns.

请求处理器利用userip包的方法获得用户的IP,因为IP参数是后端服务需要的参数(google search会根据用户所在国家返回对应结果),因此绑定到ctx上:

    // Check the search query.
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Store the user IP in ctx for use by code in other packages.
    userIP, err := userip.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ctx = userip.NewContext(ctx, userIP)

请求处理器以ctx和查询语句为参数调用google.Search:

    // Run the Google search and print the results.
    start := time.Now()
    results, err := google.Search(ctx, query)
    elapsed := time.Since(start)

如果搜索成功,请求处理器输出结果:

    if err := resultsTemplate.Execute(w, struct {
        Results          google.Results
        Timeout, Elapsed time.Duration
    }{
        Results: results,
        Timeout: timeout,
        Elapsed: elapsed,
    }); err != nil {
        log.Print(err)
        return
    }

Package userip

userip包提供获取用户IP以及将IP关联到Context的方法。Context提供了key-value的映射表,key和value都是interface{}类型,因为key的类型需要支持等式比较(support equality),value则必须是并发安全的,userip包隐藏了映射的细节并且使用自定义的类型获取Context的值。

为了避免key冲突,userip定义了一个非导出的私有类型 type key ,并且用该类型的值作为context值的key:

// The key type is unexported to prevent collisions with context keys defined in
// other packages.
type key int

// userIPkey is the context key for the user IP address.  Its value of zero is
// arbitrary.  If this package defined other context keys, they would have
// different integer values.
const userIPKey key = 0

FromRequest从http.Request中获取用户IP:

func FromRequest(req *http.Request) (net.IP, error) {
    ip, _, err := net.SplitHostPort(req.RemoteAddr)
    if err != nil {
        return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
    }

NewContext返回携带了用户IP的新的Context:

func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

FromContext从Context中获取用户IP:

func FromContext(ctx context.Context) (net.IP, bool) {
    // ctx.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil.
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}

Package google

google.Search方法发送一个HTTP请求到Google Web Search API并且解析JSON结果,该方法接收一个Context类型参数ctx,当ctx被取消时,中止这次HTTP请求。

Google Web Search API 的请求参数包含搜索关键字和用户IP:

func Search(ctx context.Context, query string) (Results, error) {
    // Prepare the Google Search API request.
    req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
    if err != nil {
        return nil, err
    }
    q := req.URL.Query()
    q.Set("q", query)

    // If ctx is carrying the user IP address, forward it to the server.
    // Google APIs use the user IP to distinguish server-initiated requests
    // from end-user requests.
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()

Search方法使用httpDo方法来发送HTTP请求,当ctx.Done关闭时取消该请求,无论该请求目前是在发送中还是响应中。Search传递一个闭包函数给httpDo来处理HTTP响应:

    var results Results
    err = httpDo(ctx, req, func(resp *http.Response, err error) error {
        if err != nil {
            return err
        }
        defer resp.Body.Close()

        // Parse the JSON search result.
        // https://developers.google.com/web-search/docs/#fonje
        var data struct {
            ResponseData struct {
                Results []struct {
                    TitleNoFormatting string
                    URL               string
                }
            }
        }
        if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
            return err
        }
        for _, res := range data.ResponseData.Results {
            results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
        }
        return nil
    })
    // httpDo waits for the closure we provided to return, so it's safe to
    // read results here.
    return results, err

httpDo方法启动新的goroutine来发送HTTP请求和处理响应,在goroutine结束前,它会在ctx.Done关闭时取消这个请求:

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

Adapting code for Contexts

许多框架如Gorilla和Tomb之类的都支持context云云… 懒得翻了这段。

Conclusion(结论)

在谷歌,我们要求Go程序员将Context作为第一参数传递给所有在请求路径中方法。这使得Go代码在多个团队间能够很好地交互。它提供了一个简单时间控制和取消方法,并且保证关键的数据如安全认证可以在Go程序中正确地流转。

服务器框架如果想基于Context来构建的话,应该要实现用Context来桥接框架的包以及其他需要Context参数的包,他们的client类库可以从调用代码接收Context变量。通过发布请求作用域参数以及取消方案的通用接口Context,使得开发者可以更简单地分享代码来构建可伸缩服务。

原作者: Sameer Ajmani

---------------------------------------------------------------------------------------------------------------------------------

完整可运行的代码示例

代码组织形式

-- context_test

----main.go

----userip

------userip.go

----google

------google.go

 

main.go

package main

import (
	"context_test/google"
	"context_test/userip"
	"context"
	"time"
	"net/http"
	"log"
	"encoding/json"
)

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // ctx.Done channel, which is the cancellation signal for requests
    // started by this handler.
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // The request has a timeout, so create a context that is
		// canceled automatically when the timeout expires.
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
	defer cancel() // Cancel ctx as soon as handleSearch returns.
    // Check the search query.
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Store the user IP in ctx for use by code in other packages.
    userIP, err := userip.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
	ctx = userip.NewContext(ctx, userIP)
    // Run the Google search and print the results.
    start := time.Now()
    results, err := google.Search(ctx, query)
	elapsed := time.Since(start)
	output,err := json.Marshal(struct {
        Results          google.Results
        Timeout, Elapsed time.Duration
    }{
        Results: results,
        Timeout: timeout,
        Elapsed: elapsed,
    })
	if err != nil {
        log.Print(err)
        return
	}
    w.Write(output)
}

func main() {
	http.HandleFunc("/search",handleSearch)
	log.Println("start..")
	http.ListenAndServe(":8090",nil)
}

userip.go

package userip

import (
	"fmt"
	"context"
	"net"
	"net/http"
)

// The key type is unexported to prevent collisions with context keys defined in
// other packages.
type key int

// userIPkey is the context key for the user IP address.  Its value of zero is
// arbitrary.  If this package defined other context keys, they would have
// different integer values.
const userIPKey key = 0

func FromRequest(req *http.Request) (net.IP, error) {
    ip, _, err := net.SplitHostPort(req.RemoteAddr)
    if err != nil {
        return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
	}
	return net.ParseIP(ip),nil
}

func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

func FromContext(ctx context.Context) (net.IP, bool) {
    // ctx.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil.
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}

google.go

package google

import (
	"context_test/userip"
	"net/http"
	"context"
    "encoding/json"
)

type Result struct {
	Title string
	URL   string
}

type Results []Result

func Search(ctx context.Context, query string) (Results, error) {
    // the Google Search API is nolonger available, so replace it with an example API
    req, err := http.NewRequest("GET", "http://blog.cngal.org/context_test.php", nil)
    if err != nil {
        return nil, err
    }
    q := req.URL.Query()
    q.Set("q", query)

    // If ctx is carrying the user IP address, forward it to the server.
    // Google APIs use the user IP to distinguish server-initiated requests
    // from end-user requests.
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
	req.URL.RawQuery = q.Encode()
    var results Results
    err = httpDo(ctx, req, func(resp *http.Response, err error) error {
        if err != nil {
            return err
        }
        defer resp.Body.Close()

        // Parse the JSON search result.
        var data struct {
            ResponseData struct {
                Results []struct {
                    TitleNoFormatting string
                    URL               string
                }
            }
        }
        if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
            return err
        }
        for _, res := range data.ResponseData.Results {
            results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
        }
        return nil
    })
    // httpDo waits for the closure we provided to return, so it's safe to
    // read results here.
	return results, err
}

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}
Home ← 旧文章