• 2007-05-31

    Google Guice Get-Starting - [Java]

    Tag:Java

    Guice(pronounced "juice")发布了1.0版本.
    首先看看Guice的定位:an ultra-lightweight, next-generation dependency injection container for Java 5 and later.
    这是一个基于Java 5及后续版本的轻量级依赖注入容器。Martin Fowler关于依赖注入的精彩讨论参见:http://www.martinfowler.com/articles/injection.html#InversionOfControl
    在这篇讨论中涉及到几种注入方式:Constructor Injection、Setter Injection、Interface Injection;
    实际应用开发中依赖的注入问题自始至终都伴随着我们,当我们走过了最简单直接的手动注入、运用模式进行注入,到依赖于框架,当前成熟的框架众多,例如:Spring IOC、PicoContainer、Hivemind等;那么Guice作为一个依赖注入的framework,又是怎么定位自己的呢?其special的地方又在哪儿呢?我们首先看一个非常简单的例子:
    对于注入描述的类如下:
    public class MyModule implements Module {
    public void configure(Binder binder) {
    binder.bind(Service.class).to(ServiceImpl.class).in(Scopes.SINGLETON);
    }
    }
    依赖注入发生的地方:
    public class Client {

    private final Service service;

    @Inject
    public Client(Service service) {
    this.service = service;
    }

    public void go() {
    service.go();
    }
    }
    从这个例子我们可以清晰看出,Guice的依赖注入已经不同于以往的三种注入模式了,Okey,你已经看到了Guice充分使用了Java的新特性annotation来进行依赖注入的,已经完全跨越了Constructor Injection、Setter Injection、Interface Injection的概念了。这样,依赖注入只是需要一个或者多个annotation而已,一切搞定。
    下一步我看看Guice这个Framework是要如何进行依赖注入的?
    我们首先看看Guice的Architecture.
    public class MyModule implements Module {
    public void configure(Binder binder) {
    // Bind Foo to FooImpl. Guice will create a new
    // instance of FooImpl for every injection.
    binder.bind(Foo.class).to(FooImpl.class);

    // Bind Bar to an instance of Bar.
    Bar bar = new Bar();
    binder.bind(Bar.class).toInstance(bar);
    }
    }
    该部分代码的sequence diagram如下:

    运行时Guice的依赖注入的绑定器structure如下:

    很明显的看到,一个注入的绑定关联主键Key、Scope、Provider;其中Key包含了依赖的Type和Annotation.
    看到了Guice的简单绑定,一定会有一个疑问:就是一个类型的多个绑定怎么实现?Guice是运行Annotation binding来解决这个问题的。
    例如绑定代码如下:
    public class BindingModule implements Module {

    public void configure(Binder binder) {
    binder.bind(IService.class).annotatedWith(Blue.class).to(BlueService.class);

    }

    }
    注入代码如下:
    public class BindingClient {
    // @Inject
    // @Blue
    // private IService service;

    private IService service;

    @Inject
    public void injectService(@Blue IService service) {
    this.service = service;
    }

    public void go() {
    this.service.go();
    }
    }
    annotation部分代码:
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @BindingAnnotation
    public @interface Blue {

    }
    如果再复杂一些,Guice还支持有属性的annotation:
    绑定代码
    public class NamedModule implements Module {

    /*
    * (non-Javadoc)
    *
    * @see com.google.inject.Module#configure(com.google.inject.Binder)
    */
    public void configure(Binder binder) {
    binder.bind(IPerson.class).annotatedWith(new NamedAnnotation("Bob")).to(Bob.class);
    }

    }
    注入代码:
    public class NamedClient {

    @Inject
    @Named("Bob")
    private IPerson person;

    public void say(){
    this.person.say();
    }
    }
    annotation代码:
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @BindingAnnotation
    public @interface Named {
    String value();
    }

    public class NamedAnnotation implements Named {

    private String value;

    public NamedAnnotation(String value){
    this.value = value;
    }
    /*
    * (non-Javadoc)
    *
    * @see com.neusoft.anno.Named#value()
    */
    public String value() {
    return value;
    }

    public int hashCode() {
    return 127 * "value".hashCode() ^ value.hashCode();
    }

    public boolean equals(Object o) {
    if (!(o instanceof Named))
    return false;
    Named other = (Named) o;
    return value.equals(other.value());
    }

    public String toString() {
    return "@" + Named.class.getName() + "(value=" + value + ")";
    }

    /*
    * (non-Javadoc)
    *
    * @see java.lang.annotation.Annotation#annotationType()
    */
    public Class annotationType() {
    return Named.class;
    }

    }
    通过这几个例子,可以清晰看到,Guice应用annotation进行注入的关联的,用module将依赖关系组装起来,提供给客户端代码进行使用;而且annotation是可以自由定制的,支持灵活扩展的。

    Guice本身也支持实现类的直接绑定:
    public class MixerModule implements Module {

    /*
    * (non-Javadoc)
    *
    * @see com.google.inject.Module#configure(com.google.inject.Binder)
    */
    public void configure(Binder binder) {
    binder.bind(Concrete.class);

    }

    }
    Guice有一种隐式绑定,其方法是这样,针对一个接口,如果缺少显式绑定,Guice 会寻找一个指向具体实现的@ImplementedBy标注。
    @ImplementedBy(GoodNightImpl.class)
    public interface IGoodNight {

    void goodNight();
    }

    @Singleton
    public class GoodNightImpl implements IGoodNight {

    /*
    * (non-Javadoc)
    *
    * @see com.neusoft.nobinding.IGoodNight#goodNight()
    */
    public void goodNight() {
    System.out.println("Good night!");
    }

    }

    那么,Provider是怎么回事呢?“有时对于每次注入,客户代码需要某个依赖的多个实例。其它时候,客户可能不想在一开始就真地获取对象,而是等到注入后的某个时候再获取。对于任意绑定类型 T,你可以不直接注入 T 的实例,而是注入一个 Provider,然后在需要的时候调用 Provider.get()”因此,可以使用Provider去真正的来创建依赖的对象实例,直到需要的时候才创建客户代码需要的实例。

    Provider等绑定代码:
    public class WidgetModule implements Module {

    /*
    * (non-Javadoc)
    *
    * @see com.google.inject.Module#configure(com.google.inject.Binder)
    */
    public void configure(Binder binder) {
    binder.bind(IService.class).to(WidgetService.class).in(Scopes.SINGLETON);
    binder.bind(Widget.class).toProvider(WidgetProvider.class);
    }

    }
    Provider的注入:
    public class Widget {
    private IService service;

    @Inject Provider provider;

    public Widget(IService service){
    this.service = service;
    }

    public void go(){
    this.service.go();
    }
    }
    Provider的实现:
    public class WidgetProvider implements Provider {

    final IService service;

    @Inject
    WidgetProvider(IService service) {
    this.service = service;
    }

    public Widget get() {
    return new Widget(service);
    }

    }
    Service的接口和实现:
    public interface IService {

    void go();

    }
    public class WidgetService implements IService {

    /* (non-Javadoc)
    * @see com.neusoft.service.IService#go()
    */
    public void go() {
    System.out.println("Widget Service Hello.");
    }

    }
    客户代码:
    public class WidgetApplication {

    /**
    * @param args
    */
    public static void main(String[] args) {
    Injector injector = Guice.createInjector(new WidgetModule());
    Widget widget = injector.getInstance(Widget.class);
    widget.go();
    }

    }
    这个例子完全表达出了Provider的价值所在。
    另外,Guice也提供了Constant Values以及Converting Stings的支持:
    Guice会针对Primitive types、primitive wrapper types、Strings、Enums、Classes类型进行类型的自动判断绑定机制。对于Primitive types、primitive wrapper types,如果 Guice 仍然无法找到一个的显式绑定,它会去找一个拥有相同绑定标注的常量 String 绑定,并试图将字符串转换到相应的值。
    这就是一个Guice如何进行依赖注入的一个基本的思路,可以看到Guice的代码量十分少,是一个不错的依赖注入的方案,也将会推动依赖注入的进步。
    参考:
    Guice项目:http://code.google.com/p/google-guice/
    Guice中文文档:http://docs.google.com/View?docid=dqp53rg_3hjf3ch

  • 2007-05-20

    重返Miami - [随笔]

    Tag:随笔
    为什么重返Miami,在自己内心深处,不知道什么是出路的时候,这不是一种逃避,是一种面对,但不是“重新”,寻找到内心很多寂寥,若有若无的种种境地。
    在飞机在北太平洋上空,在强气流,在阳光所洒在的云层中穿梭的时候,我多么强烈的意识到一个人是必须要面对人的一生必须要面对的许许多多事情的。如同人最后一定要离开一个世界一样,无所谓失败,无所谓失去,无所谓面对。我想我是太执著于在自己过去的阴影之下,太过于执著于一种心情。
    “许多人生活在‘其实我很希望。。。只是很可惜。。。’的模式中,找借口完全接触不到你的生命,使你不能经历成长的喜悦。只有真刀真枪地面对自己,才了解生命和幸福的深度。”当我看到一个朋友的签名的时候,不禁也想起另外一个朋友的话,“如果你要找借口,一百个你都能找到。”
    (翻开我的blog和自己的心情,就如同一个朋友曾经一语道破为如同“失恋”的心态。悲观的心态可能有积极的人生吗?)“先知,先觉,先行”,王阳明先生“知行合一”。无论多少心情,多少道理,都不如行动来的猛烈。
    有梦想,有目标,究竟每一天能有多少东西能够沉淀下来?为了梦想,为了目标。““无产阶级失去的只是枷锁”,如果人的一生有枷锁,就是一个人的心;最难改变的是性格中的不肯改变。准备好了吗,改变一切不肯改变的?
    重返Miami,在于能够释放自己,释放自己平和稳定的心境,放下、放松;真刀真枪的面对自己;改变不肯改变的自己;在自己的内心寻找到自己。
  • Java 5发布有一段时间了,Instrumentation这个feature是Java 5新提供的。其方式是通过修改字节码的方式使得Java开发人员能够操作类。官方文档说主要是给工具提供修改应用的状态、行为使用的:)
    先来个简单的例子看看到底什么是Instrumentation:
    在JSE 1.5.0的Javadoc的看到java.lang.instrument仅有两个接口ClassFileTransformer和Instrumentation。我们就看看着两个接口的用法:
    public class Greeting implements ClassFileTransformer {

    //字节码转换在这个方法中进行。
    public byte[] transform(ClassLoader arg0, String classname, Class arg2,
    ProtectionDomain arg3, byte[] arg4)
    throws IllegalClassFormatException {
    System.out.printf("hello:" + classname);
    return new byte[]{};
    }

    //options是通过命令行传递给虚拟机的参数。
    public static void premain(String options, Instrumentation ins) {
    if (options != null) {
    System.out.printf(" I've been called with options: \"%s\"\n",
    options);
    } else
    System.out.println(" I've been called with no options.");
    ins.addTransformer(new Greeting());
    }

    }

    public class Sample {

    /**
    * @param args
    */
    public static void main(String[] args) {
    (new Sample()).hello();

    }

    public void hello() {
    for (int i = 0; i < 10000; i++) {
    int index =0;
    index++;
    }
    }

    }

    使用命令行参数的命令行:java -javaagent:Greeting.jar="Hello, Sample" Sample
    因此下一步是需要打个jar包,jar包中包含META-INF/MANIFEST.MF和Class文件。
    其中META-INF/MANIFEST.MF的内容如下:
    Manifest-Version: 1.0
    Premain-Class: Timing
    包含的类文件有:Greeting.class和Sample.class
    打包:jar cvfM greeting.jar *
    adding: Greeting.class(in = 1774) (out= 869)(deflated 51%)
    adding: META-INF/(in = 0) (out= 0)(stored 0%)
    adding: META-INF/MANIFEST.MF(in = 44) (out= 46)(deflated -4%)
    adding: Sample.class(in = 556) (out= 371)(deflated 33%)
    运行命令行:java -javaagent:greeting.jar="Hello,Sample" Greeting
    控制台输出:I've been called with options: "Hello,Sample"
    运行命令行:java -javaagent:greeting.jar="Hello,Sample" Sample
    控制台输出:
    I've been called with options: "Hello,Sample"
    hello:Sample
    通过这个例子估计Instrutment API使用的方法已经基本上有了个理解了。
    下面就是举一个用apache bcel构造bytecode的Instrutment的实际的例子:

    使用 instrumentation ,使用Apache 开源项目 BCEL修改bytecode,实现用于计算一个方法运行时间的功能。这种方式,用于性能测量的语句与业务逻辑完全分离,同时也可以用于测量任意类的任意方法的 运行时间,提高了代码的重用性。
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.lang.instrument.Instrumentation;

    import org.apache.bcel.Constants;
    import org.apache.bcel.classfile.ClassParser;
    import org.apache.bcel.classfile.JavaClass;
    import org.apache.bcel.classfile.Method;
    import org.apache.bcel.generic.ClassGen;
    import org.apache.bcel.generic.ConstantPoolGen;
    import org.apache.bcel.generic.InstructionConstants;
    import org.apache.bcel.generic.InstructionFactory;
    import org.apache.bcel.generic.InstructionList;
    import org.apache.bcel.generic.MethodGen;
    import org.apache.bcel.generic.ObjectType;
    import org.apache.bcel.generic.PUSH;
    import org.apache.bcel.generic.Type;

    public class Timing implements ClassFileTransformer {

    private String methodName;

    private Timing(String methodName) {
    this.methodName = methodName;
    System.out.println(methodName);
    }

    public byte[] transform(ClassLoader loader, String className, Class cBR,
    java.security.ProtectionDomain pD, byte[] classfileBuffer)
    throws IllegalClassFormatException {
    try {
    ClassParser cp = new ClassParser(new java.io.ByteArrayInputStream(
    classfileBuffer), className + ".java");
    JavaClass jclas = cp.parse();
    ClassGen cgen = new ClassGen(jclas);
    Method[] methods = jclas.getMethods();
    int index;
    for (index = 0; index < methods.length; index++) {
    if (methods[index].getName().equals(methodName)) {
    break;
    }
    }
    if (index < methods.length) {
    addTimer(cgen, methods[index]);
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    cgen.getJavaClass().dump(bos);
    return bos.toByteArray();
    }
    System.err.println("Method " + methodName + " not found in "
    + className);
    System.exit(0);

    } catch (IOException e) {
    System.err.println(e);
    System.exit(0);
    }
    return null; // No transformation required
    }

    private static void addTimer(ClassGen cgen, Method method) {

    // set up the construction tools
    InstructionFactory ifact = new InstructionFactory(cgen);
    InstructionList ilist = new InstructionList();
    ConstantPoolGen pgen = cgen.getConstantPool();
    String cname = cgen.getClassName();
    MethodGen wrapgen = new MethodGen(method, cname, pgen);
    wrapgen.setInstructionList(ilist);

    // rename a copy of the original method
    MethodGen methgen = new MethodGen(method, cname, pgen);
    cgen.removeMethod(method);
    String iname = methgen.getName() + "_timing";
    methgen.setName(iname);
    cgen.addMethod(methgen.getMethod());
    Type result = methgen.getReturnType();

    // compute the size of the calling parameters
    Type[] parameters = methgen.getArgumentTypes();
    int stackIndex = methgen.isStatic() ? 0 : 1;
    for (int i = 0; i < parameters.length; i++) {
    stackIndex += parameters[i].getSize();
    }

    // save time prior to invocation
    ilist.append(ifact.createInvoke("java.lang.System",
    "currentTimeMillis", Type.LONG, Type.NO_ARGS,
    Constants.INVOKESTATIC));
    ilist.append(InstructionFactory.createStore(Type.LONG, stackIndex));

    // call the wrapped method
    int offset = 0;
    short invoke = Constants.INVOKESTATIC;
    if (!methgen.isStatic()) {
    ilist.append(InstructionFactory.createLoad(Type.OBJECT, 0));
    offset = 1;
    invoke = Constants.INVOKEVIRTUAL;
    }
    for (int i = 0; i < parameters.length; i++) {
    Type type = parameters[i];
    ilist.append(InstructionFactory.createLoad(type, offset));
    offset += type.getSize();
    }
    ilist.append(ifact.createInvoke(cname, iname, result, parameters,
    invoke));

    // store result for return later
    if (result != Type.VOID) {
    ilist
    .append(InstructionFactory.createStore(result,
    stackIndex + 2));
    }

    // print time required for method call
    ilist.append(ifact.createFieldAccess("java.lang.System", "out",
    new ObjectType("java.io.PrintStream"), Constants.GETSTATIC));
    ilist.append(InstructionConstants.DUP);
    ilist.append(InstructionConstants.DUP);
    String text = "Call to method " + methgen.getName() + " took ";
    ilist.append(new PUSH(pgen, text));
    ilist
    .append(ifact.createInvoke("java.io.PrintStream", "print",
    Type.VOID, new Type[] { Type.STRING },
    Constants.INVOKEVIRTUAL));
    ilist.append(ifact.createInvoke("java.lang.System",
    "currentTimeMillis", Type.LONG, Type.NO_ARGS,
    Constants.INVOKESTATIC));
    ilist.append(InstructionFactory.createLoad(Type.LONG, stackIndex));
    ilist.append(InstructionConstants.LSUB);
    ilist.append(ifact.createInvoke("java.io.PrintStream", "print",
    Type.VOID, new Type[] { Type.LONG }, Constants.INVOKEVIRTUAL));
    ilist.append(new PUSH(pgen, " ms."));
    ilist
    .append(ifact.createInvoke("java.io.PrintStream", "println",
    Type.VOID, new Type[] { Type.STRING },
    Constants.INVOKEVIRTUAL));

    // return result from wrapped method call
    if (result != Type.VOID) {
    ilist.append(InstructionFactory.createLoad(result, stackIndex + 2));
    }
    ilist.append(InstructionFactory.createReturn(result));

    // finalize the constructed method
    wrapgen.stripAttributes(true);
    wrapgen.setMaxStack();
    wrapgen.setMaxLocals();
    cgen.addMethod(wrapgen.getMethod());
    ilist.dispose();
    }

    public static void premain(String options, Instrumentation ins) {
    if (options != null) {
    ins.addTransformer(new Timing(options));
    } else {
    System.out
    .println("Usage: java -javaagent:Timing.jar=\"class:method\"");
    System.exit(0);
    }

    }
    }
    打jar包:
    >jar cvfM timing.jar *
    adding: META-INF/(in = 0) (out= 0)(stored 0%)
    adding: META-INF/MANIFEST.MF(in = 44) (out= 46)(deflated -4%)
    adding: Sample.class(in = 556) (out= 371)(deflated 33%)
    adding: Timing.class(in = 7372) (out= 3442)(deflated 53%)
    运行命令行:
    >java -classpath bcel-5.2.jar -javaagent:timing.jar="hello" Sample
    hello
    Call to method hello_timing took 2047 ms.

    >java -classpath bcel-5.2.jar -javaagent:timing.jar="main" Sample
    main
    Call to method main_timing took 2469 ms.
    通过这段代码,基本能够了解Instrument的用处之一了:)

    参考:http://www.ibm.com/developerworks/cn/java/j-lo-instrumentation/

  • 2007-03-13

    一篇没有写完的BLOG - [随笔]

    Tag:随笔

    2006已经过去,2007已经开始。
    过去的一年,有高兴,有激动,有不少难以忘记的时刻,有很多痛苦的思考。当我面对和回首自己过去的一年,我不知道自己除了时间之外究竟还失去了什么,自己除了痛苦之外还有多少收获?
    人是可以活的洒脱些,就看一个人究竟要的是什么?就看你付出了多少?
    要行动,要思考,要计划,要不断的沟通。
    技术的狂热,当我在Miami Java User Group第一次聚会上看到Michael Feather的依然这么执着,依然的狂热,对于C++,对于functional programming,一个普通的程序员的极限就是这样吗。

    一个Senior Programmer

    首先是一个人,要做一个什么样子的人?
    “观身如身,观心如心。”
    如果是一个人,一个人的架构一定要比程序员的架构高。我们要一个什么样的身,基本的是健康的,就要经常锻炼了;中国一向有一个修身的传统,可见修身包括很多,怎么去修身,社会有社会的价值,个人有个人的志向。修身对谁都是一个既迷茫又痛苦的话题,“求仁得仁”,回到一个很糟乱的话题了,一个人到底想要得是什么?为了想要的得到底付出了什么?包括痛苦,包括性命,“求仁得仁”这个成语本身,伯夷、叔齐并没有欢天喜地的结局,相反,太执著,太倔强,为了得到可以牺牲了权利,身份,温饱,生命。一个人想来想去,问来问去,问的都是自己,都是自己的心。屈原先生应当是这方面的最有权威的专家了。能研究做学问的学问--哲学的人智商肯定不能低,“观身如身,观心如心。”这句话理解起来费力;还说了“观身观心”,痛苦的时候观自己的心;走投无路的时候寻找去观他人的心。
    如果人一生有幸福和痛苦的话,那痛苦和幸福一定有一个起点,那是不是凡事都有起点。如果一生有起点的话,这个起点在哪儿?一个人能不能自己选择起点?
    技术,技术是什么?什么是技术?如果技术是一个世界,技术之外呢?技术之外的世界呢?
    人类也不知道生命出现之前的状态,一个人是不知道前世的;也无法将来到这个世界的最初的感觉的存储在自己的大脑的。那么一个人还是能想象死亡的。
    一个人最痛苦的是什么,失去生命吗?如果说失去生命是最痛苦的,那是为什么呢,有人体验过吗?肯定没有。为什么人要惧怕死亡呢,因为要失去一些东西吗还是人的状态要发生了转变还是人类无法预知死亡之后的东西?如果是这样,恐惧的是失去,恐惧的是巨变,恐惧的是未知。如果一个人要恐惧失去,就应该恐惧死亡;如果一个人恐惧巨变,就应该恐惧死亡;如果一个人恐惧未知,就应该恐惧死亡。同样,一个人恐惧死亡,肯定会恐惧失去,恐惧巨变,恐惧未知。

  • 2007-03-13

    On the Open Way - [随笔]

    Tag:随笔
    偶尔看一下中央二套的两会节目,竟然在讨论青少年上网的社会/教育问题。
    抨击我国现状教育的不在少数,学校教育大体上就是一种“推”的教育方式,是一种社会培养人才的最重要的体系,是对未来人才的储备机制,正如宣传所说青少年就是花儿的年华。人生只有一次,家长,社会的期望终归是一种期望,孩子有孩子的乐趣,孩子的年华,孩子的思想。
    就针对个人而言,回想数十年“寒窗读书”有意思的好像真没多少,或许是背了些英语单词,记了些简单的英语句子;还有就是几篇语文的文章名字或者几个名句;还有几个数理化的专业术语。如果说是从业研究方向,可能还正能捡起些东西来。一般人对于孩子教育不外乎认为培养兴趣,培养性格等几个方面,当我们进入社会,发现做人本身就不是件容易的事情,何况培养人,培养人才。
    一提到问题/隐患,很多人不约而同的想到改革这个话题,我有个朋友谈到,教育抓得是人的未来,医疗抓得人的命,谁都无法逃脱其中,改革这两项何其难啊。这真是一阵见血的评论。
    培养性格是很重要的一方面,培养什么样的人才至少需要前瞻性的眼界了解社会的人才需求方向,所以做大师就不是那么简单的事情了。
    一个人走出校门的同时进入社会,肯定能强烈感觉到社会需要什么样的人才的,很多时候就是社会在“拉”了,在这个过程中人类表现了异常聪明的一面,总是在做自我培养,提高自身。中国哲学的本身很大成分上都是和自己斗争的过程,大体上是自我培养的哲学体系,可见中国人对于社会还是异常重视尊重的。学校教育带来的很大的弊端就是自闭,一不小心就走入了以自我为中心的圈子;同时容易让人抑郁。估计很多人在人生的某些阶段都会有抑郁,自闭的问题;要提高自我必须要认清出这个问题,解决这个问题。
    中国人大体上不怎么提倡Open这种方式,我们在社会中愈来愈发现Open的重要性,所以更多人都宁愿自己是一个心理,眼睛,思维都很Open的人。
    我自身已近从事软件开发三年多,如果让我回首认为该毕业的时候如何策划培养自己,除了性格之外有三点:
    1。沟通能力,做好任何一件事情,寻找任何一个机会,每一次自我的提高,都是从沟通开始的。世界变得越来越平,沟通的空间时间也在增长。
    2。语言(掌控)能力,能够多掌握一门语言,相当于给自己多了一个世界,机会,眼界,空间。。。。。。
    3。技术能力,这是一个从事软件开发人员的立身基础,也是对于行业知识的积累。
    世界如此美丽,希望每一个人都要好好的珍惜。