Java代理模式及源码实现

代理模式简介

代理模式是设计模式中非常常用的一种设计模式,在Java和安卓源码中都有涉及。使用代理模式可以对一些事务进行日志处理,权限控制等。
为其他对象提供一种代理以控制对这个对象的访问。代理对象起到中介作用,可去掉功能服务或增加额外的服务。

代理模式的几种实现方式

代理模式一般有三种:

  • 使用继承
  • 使用聚合
  • 动态代理(重点)
    继承和聚合的方式相对于动态代理来说可以认为是静态代理 ,因为这两种方式的代理类是静态实现的,而动态代理类是根据参数的不同动态实现的。

代理的实现

继承实现代理

使用继承的方式实现代,理非常简单。其实原理就是子类继承父类,子类是父类的代理。
构建以下场景:

比如我们有一公共汽车Bus,在汽车启动前需要确认司机身份,司机身份确认后才能开始行驶,并记录汽车行驶时间,那么我们该如何构建类的关系呢?如果再有一辆小轿车也要做相同的事情呢?如果再有火车,马车。。。

最简单的方法就是:定义一个汽车类Bus.java,它有一个move方法,在move调用前进行身份验证,记录时间这些工作。再定义一个轿车类Car.java,它有一个move方法,在move方法调用前进行身份验证,记录这些工作。再定义。。。很明显,我们这样会使代码无限增多,维护性差,扩展性差。那么我们就定义一个接口Moveable.java,所有类型的车的类都要实现这个接口。

Moveable.java

public interface Moveable {
    void move();
}

Bus.java

public class Bus implements Moveable {

    @Override
    public void move() {
        //预处理逻辑
        System.out.println("确认司机身份中...");
        System.out.println("启动...");
        long startTime = System.currentTimeMillis();
        
        //核心逻辑
        System.out.println("moving");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
    }

}

我们实现了一Bus.java类,但足以说明问题了。

可以发现,此时我们的身份验证和时间记录是写死在move方法中的,很明显这是内聚过度,导致了move的核心逻辑和预处理(身份验证)耦合性太强。

这时我们需要把身份验证和move的核心逻辑进行解耦,新建一个代理类ExtendsProxy.java

public class ExtendsProxy extends Bus {

    @Override
    public void move() {
        //预处理逻辑
        System.out.println("确认司机身份中...");
        System.out.println("启动...");
        long startTime = System.currentTimeMillis();
        
        
        super.move();//调用被 代理对象的方法
        
        long endTime = System.currentTimeMillis();
        System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
    }
    
}

这时在Bus.java的move方法中就不需要做预处理操作了,Bus.java如下:

public class Bus implements Moveable {

    @Override
    public void move() {
        
        System.out.println("moving");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们来测试一下:

public class Client {
    public static void main(String[] args) {        
        //新建代理类对象
        Moveable m = new ExtendsProxy();
        //调用代理方法
        m.move();       
    }
}

结果如下:

确认司机身份中...
启动...
moving
车辆行驶时间:175毫秒

非常好!很开心我们实现了一个代理。可是突然又增加了需求,我们不仅要代理Bus了,还要代理小轿车Car,那怎么办呢?有的人说:好办,再新建一个类继承Car.java,用同样的方式实现代理不就可以了吗?这样确实是可以的,但是相信你的leader会不开森,要是代理一百个类,那你还再写一百个代理 类么?这时你突然醒悟过来:继承的方式实现代理有很大的局限性,扩展性不好,那么我们是坚决摒弃这种做法的。

聚合实现代理

聚合方式是通过把被代理对象当作参数传入代理对象中。这种方式是很常用的方式。

我们实现UnionProxy.java,它也需要实现Moveable接口。为什么要实现Moveable接口呢?统一接口,统一调用。

public class UnionProxy implements Moveable {
    private Moveable m;
    public UnionProxy(Moveable m) {
        this.m = m;
    }
    @Override
    public void move() {
        if (m != null) {
            //预处理逻辑
            System.out.println("确认司机身份中...");
            System.out.println("启动...");
            long startTime = System.currentTimeMillis();
            
            //调用被 代理对象的方法
            m.move();
            
            long endTime = System.currentTimeMillis();
            System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
        }
    }
}

这时如果我们再想要代理其实实现了Movealbe的类,比如轿车,马车之类 的,那么我们做的是直接把它们的对象传到UnionProxy的构造参数中即可,解决了继承方式的弊端,真棒!

但是,但是,但是,问题又来了,现在Leader要求,先启动汽车,再确认身份,顺序颠倒了,那么怎么办呢。因为我们的预处理逻辑是硬编码的,要想添加新的逻辑,那么只能再写一个UnionProxy2.java,在move方法中重新定义预处理逻辑,这样好不好呢?没错,正如你所想的那样,肯定是不好的,因为如果我们有一万种预处理逻辑,就要再写一万个代理类吗?我们希望有一种代理类,既可以动态得传入被代理对象,又可以动态得生成代理方法,这就是动态代理!

动态代理

动态代理在JDK中提供了API,我们可以很方便得使用它,下面我们来了解一下动态代理是如何使用的吧。
JDK中在java.long.reflect中提供了两个类:

  • Proxy.java 代理类,用来生成代理对象
  • InvocationHandler.java 实现这个接口,添加预处理逻辑等
    使用流程如下:
  1. 新建xxxHandler实现InvocationHandler接口,添加预处理逻辑
  2. 创建被代理类对象,并获取被代理类对象的ClassLoader和Intefaces
  3. 创建xxxHandler对象
  4. 调用Proxy.newInstance(loader, interfaces, handler);来创建代理类对象
  5. 调用代理类对象的代理方法
    我们这里创建DDdogerHandler.java实现InvocationHandler接口
    DDdogerHandler.java
public class DDdogerHandler implements InvocationHandler {
    //被代理对象
    private Object target;  
    public DDdogerHandler(Object target) {
        super();
        this.target = target;
    }
    /**
     * o  被代理对象
     * m  被代理的方法
     * args 被代理方法的参数
     */
    @Override
    public Object invoke(Object o, Method m, Object[] args) throws Throwable {
        // 预处理逻辑
        System.out.println("确认司机身份中...");
        System.out.println("启动...");
        long startTime = System.currentTimeMillis();
        
        m.invoke(target);//调用被代理方法,并传入被代理对象作为参数
        
        long endTime = System.currentTimeMillis();
        System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
        
        return null;
    }
}

接下来创建代理对象并调用:

    public static void main(String[] args) {
        Moveable target;

        // 新建被代理对象
        target = new Bus();
//      target = new Car();
        // 新建handler对象
        DDdogerHandler handler = new DDdogerHandler(target);
        /**
         * 参数: loader 类加载器 
         * interfaces 被代理对象实现的接口 
         * h InvocationHandler对象
         */
        Moveable m = (Moveable) Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(),
                handler);
        m.move();
    }

运行结果如下:

确认司机身份中...
启动...
The bus is moving
车辆行驶时间:72毫秒

我们先来分析一下,如何使用JDK提供的API实现对任意对象添加任意的代理
正如之前所说:如果我们要对其它类比如Car.java实现先验证司机身份再启动的,最后记录行驶时间的代理,那我们应该怎么做呢?
因为代理的预处理工作不变,所以只需要把传入把Bus对象换成Car对象即可,如下:

    public static void main(String[] args) {
        Moveable target;

        // 新建被代理对象
//      target = new Bus();
        target = new Car();
        // 新建handler对象
        DDdogerHandler handler = new DDdogerHandler(target);
        /**
         * 参数: loader 类加载器 
         * interfaces 被代理对象实现的接口 
         * h InvocationHandler对象
         */
        Moveable m = (Moveable) Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(),
                handler);
        m.move();
    }

运行结果如下:

确认司机身份中...
启动...
The car is moving
车辆行驶时间:770毫秒

怎么样,很简单吧。那么我们如果想要实现先启动,再验证司机身份的预处理逻辑该怎么做呢?那我们只需要把参数传入到handler对象中,根据参数来定义预处理逻辑的顺序即可。这里不再详述。
看来动态代理确实很方便啊,那它内部究竟是如何实现的呢?
下面我们来手动实现动态代理吧~

手动实现动态代理

动态代理,顾名思义,就是动态得生成代理类,再动态得把代理类编译成class文件,最后生成代理类对象并调用的一个过程。所以我们的实现思路如下:

  1. 首先要声明一段源码,要知道源码其实就是一些字符,把这些字符写到文件中就是源码文件了
  2. 编译源码,JDK提供了相关的Complier API,生成字节码文件
  3. 通过反射通过字节码文件生成代理对象
    PS:为了与JDK的API区别开来,以下类使用不同的命名方式。
    首先要有一个代理类DDdogerProxy.java,它有一个方法newInstance()来生成代理类对象。生成对象的过程就是上面的3步。
    话不多说,先上源码!
public class DDdogerProxy {
    /**
     * @param infc 被代理类实现的接口Class对象
     * @return
     * @throws Exception
     */
    public static Object newInstance(Class infc) throws Exception {
        //1 生成源码文件
        //1.1 声明一段源码,这里使用的是UnionProxy的源码进行稍加整理
        //其中Moveable需要根据传入的参数infc来确定
        //构建的代理类名取为$Proxy0,与jdk保持一致,当然也可以换成别的名字
        String sourceStr = "package com.dddoger.proxy;\r\n" + 
                "\r\n" + 
                "public class $Proxy0 implements "+ infc.getName() +" {\r\n" + 
                "   private "+ infc.getName() +" m;\r\n" + 
                "   public $Proxy0("+ infc.getName() +" m) {\r\n" + 
                "       this.m = m;\r\n" + 
                "   }\r\n" + 
                "   @Override\r\n" + 
                "   public void move() {\r\n" + 
                "       if (m != null) {\r\n" + 
                "           //预处理逻辑\r\n" + 
                "           System.out.println(\"确认司机身份中...\");\r\n" + 
                "           System.out.println(\"启动...\");\r\n" + 
                "           long startTime = System.currentTimeMillis();\r\n" + 
                "           \r\n" + 
                "           //调用被 代理对象的方法\r\n" + 
                "           m.move();\r\n" + 
                "           \r\n" + 
                "           long endTime = System.currentTimeMillis();\r\n" + 
                "           System.out.println(\"车辆行驶时间:\" + (endTime - startTime) + \"毫秒\");\r\n" + 
                "       }\r\n" + 
                "   }\r\n" + 
                "}";
        
        //1.2 定义文件名,相对路径,位于工程目录下/bin/com/dddoger/proxy/$Proxy0.java
        String filename = System.getProperty("user.dir") +"/bin/com/dddoger/proxy/$Proxy0.java";
        File file = new File(filename);
        //1.3 这里使用的commons-io方便操作,生成源文件$Proxy0.java
        FileUtils.writeStringToFile(file, sourceStr);
        
        //2 编译源码文件
        //2.1 获取系统编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //2.2 通过cmopiler对象获取文件管理者对象
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        //2.3 获取文件对象
        Iterable fileObjs = fileMgr.getJavaFileObjects(filename);
        //2.4 建立编译任务
        CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, fileObjs);
        //2.5 开始编译任务
        task.call();
        
        //3 加载class文件到内存
        //3.1 获取类加载器
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        //3.2 加载指定的类到内存,我们只需要加载代理类$Proxy
        Class clazz = loader.loadClass("com.dddoger.proxy.$Proxy0");
        
        //4 使用构造器创建代理对象并返回
        //4.1 获取被代理类的构造器,参数为被代理类对象
        Constructor constructor = clazz.getConstructor(infc);
        //4.2 创建代理对象,并传入参数
        Object proxy = constructor.newInstance(new Bus());
        return proxy;
    }
}

注释已经写得很详细了,相信大家理解整体的思路。
然后来测试一下看:

    public static void main(String[] args) throws Exception {
        Moveable target;
        //被代理对象
        target = new Bus();
        //生成代理对象,因为代理类是实现了Movealbe接口的,
        //也就是说代理类和被代理类都实现了同一接口
        Moveable m = (Moveable)DDdogerProxy.newInstance(Moveable.class);
        m.move();
    }

运行结果:

确认司机身份中...
启动...
The Bus is moving
车辆行驶时间:533毫秒

再说明一下中间过程,这样更清晰一些。
在执行完FileUtils.writeStringToFile(file, sourceStr);之后,会生成源码文件$Proxy0.java,使用Navigator可以看到:

image.png

$Proxy0.java内容如下:

package com.dddoger.proxy;
public class $Proxy0 implements com.dddoger.proxy.Moveable {
    private com.dddoger.proxy.Moveable m;
    public $Proxy0(com.dddoger.proxy.Moveable m) {
        this.m = m;
    }
    @Override
    public void move() {
        if (m != null) {
            //预处理逻辑
            System.out.println("确认司机身份中...");
            System.out.println("启动...");
            long startTime = System.currentTimeMillis();
            
            //调用被 代理对象的方法
            m.move();
            
            long endTime = System.currentTimeMillis();
            System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
        }
    }
}

经过执行task.call();后会生成$Proxy0.class

image.png

字节码文件,需要先加载到内存中,再创建对象。
写到这里,动态代理还没有完全实现,因为我位的代理逻辑是硬编码的,需要把这些代理逻辑解耦,该怎么办呢?别忘了,JDK中有一个InvocationHandler的类,叫做事务处理器,代理逻辑是放在这里面的。把InvocationHandler对象传到DDdogerProxy作为参数,再反射调用InvocationHandler的方法不就可以了吗。
DDdogerInvocationHandler.java

public interface DDdogerInvocationHandler {
    /**
     * @param proxy 代理对象
     * @param m 被代理方法
     * @throws Exception 
     */
    void invoke(Object proxy, Method m) throws Exception;
}

实现该接口,并添加代理逻辑
BusHandler.java

public class BusHandler implements DDdogerInvocationHandler {
    private Object target;  
    public BusHandler(Object target) {
        super();
        this.target = target;
    }   
    @Override
    public void invoke(Object proxy, Method m) throws Exception {
        // 预处理逻辑
        System.out.println("确认司机身份中...");
        System.out.println("启动...");
        long startTime = System.currentTimeMillis();
        
        m.invoke(target);
        
        long endTime = System.currentTimeMillis();
        System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
    }
}

我们的代理类DDdogerProxy.java也要做一些修改,添加handler参数,并且动态得生成代理逻辑。
修改后的DDdogerProxy.java如下:

public class DDdogerProxy {

    /**
     * @param infc 被代理类实现的接口Class对象
     * @return
     * @throws Exception
     */
    public static Object newInstance(Class infc, DDdogerInvocationHandler h) throws Exception {
        //1 生成源码文件
        //1.1 声明一段源码,这里使用的是UnionProxy的源码进行稍加整理
        //其中Moveable需要根据传入的参数infc来确定
        //构建的代理类名取为$Proxy0,与jdk保持一致,当然也可以换成别的名字
        String methodStr = "";
        //循环获取被代理类的方法
        for(Method m : infc.getMethods()) {
            methodStr += 
                    "   @Override\r\n" + 
                    "   public void " + m.getName() + "() {\r\n" + 
                    "       try{\r\n" +
                    "           Method md = " + infc.getName() + ".class.getMethod(\"" + m.getName() + "\");" +
                    "           h.invoke(this, md);\r\n" + 
                    "       }catch(Exception e){e.printStackTrace();}\r\n" +
                    "   }";
        }
        
        String sourceStr = "package com.dddoger.proxy;\r\n" + 
                "import java.lang.reflect.Method;\r\n" + 
                "import com.dddoger.proxy.manual.DDdogerInvocationHandler;\r\n" + 
                "public class $Proxy0 implements "+ infc.getName() +" {\r\n" + 
                "   private DDdogerInvocationHandler h;\r\n" +
                "   public $Proxy0(DDdogerInvocationHandler h) {\r\n" + 
                "       this.h = h;\r\n" + 
                "   }\r\n" + 
                    methodStr +
                "}";
        
        //1.2 定义文件名,相对路径,位于工程目录下/bin/com/dddoger/proxy/$Proxy0.java
        String filename = System.getProperty("user.dir") +"/bin/com/dddoger/proxy/$Proxy0.java";
        File file = new File(filename);
        //1.3 这里使用的commons-io方便操作,生成源文件$Proxy0.java
        FileUtils.writeStringToFile(file, sourceStr);
        
        //2 编译源码文件
        //2.1 获取系统编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //2.2 通过cmopiler对象获取文件管理者对象
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        //2.3 获取文件对象
        Iterable fileObjs = fileMgr.getJavaFileObjects(filename);
        //2.4 建立编译任务
        CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, fileObjs);
        //2.5 开始编译任务
        task.call();
        
        //3 加载class文件到内存
        //3.1 获取类加载器
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        //3.2 加载指定的类到内存,我们只需要加载代理类$Proxy
        Class clazz = loader.loadClass("com.dddoger.proxy.$Proxy0");
        
        //4 使用构造器创建代理对象并返回
        //4.1 获取被代理类的构造器,参数为被代理类对象
        Constructor constructor = clazz.getConstructor(DDdogerInvocationHandler.class);
        //4.2 创建代理对象,并传入参数
        Object proxy = constructor.newInstance(h);
        return proxy;
    }
}

主要修改有以下几点:

  1. newInstance方法中添加了事务处理器DDdogerInvocationHandler的h参数,DDdogerInvocationHandler在它的invoke方法中有预处理逻辑并反射调用了代理方法。
  2. 代理类$Proxy0.java要动态实现Moveable接口的所有方法,在方法内部使用反射调用事务处理器的方法。
for(Method m : infc.getMethods()) {
    methodStr += 
        "   @Override\r\n" + 
        "   public void " + m.getName() + "() {\r\n" + 
        "       try{\r\n" +
        "           Method md = " + infc.getName() + ".class.getMethod(\"" + m.getName() + "\");" +
        "           h.invoke(this, md);\r\n" + 
        "       }catch(Exception e){e.printStackTrace();}\r\n" +
        "   }";
}
  1. 生成$Proxy0的对象使用的参数也要对应改变
//4.1 获取被代理类的构造器,参数为被代理类对象
Constructor constructor = clazz.getConstructor(DDdogerInvocationHandler.class);
//4.2 创建代理对象,并传入参数
Object proxy = constructor.newInstance(h);

再测试一下:

target = new Bus();
BusHandler handler = new BusHandler(target);
//也就是说代理类和被代理类都实现了同一接口
Moveable m = (Moveable)DDdogerProxy.newInstance(Moveable.class, handler);
m.move();

输出结果:

确认司机身份中...
启动...
The Bus is moving
车辆行驶时间:137毫秒

这时我们的动态代理就基本实现了,只不过我们实现的事务处理器的invoke方法少一个args参数,返回值也为void,但是不影响我们整体的,我们暂时不再过多讨论。

附上源码链接:https://pan.baidu.com/s/1GDKw9wKbSrkzTwiinkqc0A

好了动态代理我们就说到这里,欢迎勘误,不胜感激!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 本文首发于个人博客:Lam's Blog - 谈谈23种设计模式在Android源码及项目中的应用,文章由Mark...
    格子林ll阅读 4,536评论 1 105
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 参考资料:菜鸟教程之设计模式 设计模式概述 设计模式(Design pattern)代表了最佳的实践,通常被有经验...
    Steven1997阅读 1,127评论 1 12
  • 文|心路独行 1. 历经19年流亡历程的晋文公,不但在秦国的帮助下登临君位,还在四年后通过城濮之战击败强楚,成为新...
    吕拾春秋阅读 737评论 0 4
  • 阳春三月,胜日寻芳。十年之后,再访杭州。千年古城,春意盎然。参天大树,直指云霄。垂柳依依,繁花似锦,人面桃花相映红...
    newjulie阅读 324评论 1 0