一起学设计模式-02 工厂模式

写在前面

如果你还没了解六大软件设计原则的话,建议先谷歌下,再来学习设计模式。这对理解设计模式和记忆设计模式的经典写法都会有很大帮助。学习设计模式,光是记写法没有意义,要学习的是设计思想,为什么要这么做,这么做的好处是什么。怀着这两个问题来学习设计模式,你会发现六大软件设计原则在设计模式中的应用,相应的也会加强你对六大软件设计原则的记忆。

工厂模式的经典疑问

很多人,包括我自己在看工厂模式的时候都会问一个问题,工厂模式的好处到底是什么?有人说抽象?我觉得那是依赖倒置的好处,工厂模式遵循了依赖倒置原则,自然有抽象的特点,但这并不是根本好处,但凡遵循这一原则的都会具备这个特点。所以在我们在学习某个知识点的时候,要多看几篇文章,反复对比斟酌,不要希望说“XXX看这一篇就够了”(很多博客的标题都会这么起),包括这篇文章,也仅仅是作为读者的一个参考。那么工厂模式的好处到底是啥,我个人认为工厂模式最根本的好处是:

工厂模式最直接的好处就是屏蔽了对象的创建细节,将使用者和对象的创建过程隔离开来。特别是对那些创建过程非常复杂的对象,使用者不再需要关心这些对象是如何创建的,这么做简化了使用者的操作,降低了调用难度。另外,由于创建的过程被工厂封装,也避免了创建对象的代码,散落在各个模块的情况,同时也减少了代码开发工作量。当然工厂模式也具备多态的特性,这是因为工厂模式遵循依赖倒置原则,只是一个附带的特性。最后,多了这层封装也为实例创建的集中管理提供了便捷。

假如我需要一个ThinkPad 的Computer电脑实例来办公,这个ThinkPad电脑实例是由A Screen显示屏,B KeyBoard键盘,C CPU 处理器, D RAM内存条构成,且有一个非常复杂的组装过程。好了,作为一个使用者,我可能需要这么写:

Screen aScreen = new AScreen(a);
KeyBoard bKeyborad = new BKeyBoard(b);
CPU ccpu = new CCPU(c);
RAM dRam = new DRAM(d);
Computer computer = new ThinkPad(aScreen, bKeyborad, cCpu, dRam);

我作为电脑的使用者,我并不关心电脑是由哪些部分构成的,是怎么组装的。这些内容对使用者来说,显然是多余的。而且假如我在多处需要使用这个ThinkPad的实例,这段构造ThinkPad的代码会出现在多个地方。某天ThinkPad的构成变了,使用的是fRAM,那我还要把散落在各个模块的构造代码挑出来,把参数dRam改为fRam。

有的人可能会说,那我提供一个无参构造函数,把创建对象的细节都放里面,这样不也可以避免当入参发生变化时,不需要到处修改代码,而且也屏蔽了创建细节。这样确实是可以,但是实际上有多少个类的是可以无参构造的呢,这种情况就非常极端了。设计模式只是一个解决实际问题的参考,有没有用要不要用都是结合实际情况决定,当然因为其广泛的适用性,所以被总结为一个模式,类构成非常简单且产品单一,当然可以不用工厂模式,没必要钻牛角尖。

下面我们从简单工厂模式开始,一步步深入的学习简单工厂模式,工厂方法模式,抽象工厂模式,看看工厂模式是怎么样一步步演变的。

简单工厂模式

简单工厂模式通常由三部分构成,经典写法就是switch语句来决定返回的对象。

  • 抽象的产品接口(遵循依赖倒置原则,所有产品都实现了产品抽象接口)
  • 具象的产品类
  • 具象的工厂类
//抽象接口
public interface Computer {
    public void run();
}

//具象类
public class Mac implements  Computer {
    @Override
    public void run() {
        System.out.println("Mac Run");
    }
}

public class ThinkPad implements Computer {
    @Override
    public void run() {
        System.out.println("ThinkPad Run");
    }
}

//简单工厂-告诉工厂想要什么类型的电脑,工厂就给你什么类型的电脑,新款电脑需要修改工厂类增加case分支
public class SimpleFactory {
    public static Computer createComputer(String Type){
        Computer computer = null;
        switch (Type){
            case "MAC": return new Mac();
            case "ThinPad":return new ThinkPad();
            default: throw new IllegalArgumentException("不存在的Computer类型");
        }
    }
}

//客户端代码
public class TestComputer {
    public static void main(String[] args)  {
        SimpleFactory.createComputer("ThinPad").run();
    }
}

在产品不多的情况下,我们可以接受使用简单工厂模式。但是在产品类型比较多的时候,简单工厂模式就不再适用。想象下,每增加一个产品,我们都需要修改简单工厂(违背开闭原则),而且我们不停的往这个简单工厂里面添加代码,越来越多,非常臃肿,可读性和可维护性会变差。而且,我们需要不停的修改简单工厂的代码,违背了开闭原则(只扩展不修改原代码)。这个时候,工厂方法模式就来了。

注1:顺便提一下使用反射来创建实例可以实现不违反开闭原则的简单工厂

    public static Computer createComputerByReflect(String type) 
            throws IllegalAccessException, 
                    InstantiationException, 
                   ClassNotFoundException {
        Class computerClass = Class.forName(type);
        return (Computer)computerClass.newInstance();
    }

    //客户端代码
    public class TestComputer {
    public static void main(String[] args) throws IllegalAccessException, ClassNotFoundException, InstantiationException {
        SimpleFactory.createComputerByReflect("test.open.Mac").run();
    }
}

注2:简单工厂模式又称静态工厂模式,使用静态方法,无需实例化简单工厂。至于工厂方法模式和抽象工厂模式为啥就不用静态方法,个人理解是因为这两个模式对工厂进行了抽象(定了工厂接口),如果用了静态方法,这些方法无法被重写。

工厂方法模式

工厂方法模式,可以理解为简单工厂模式的加强版,对具象的工厂类做了一层抽象(构成多了一层工厂接口),不同产品的创建不再集中在一个工厂内,而是由各自专门的工厂类负责。新增一个产品,无需改动原有工厂类,只需要新增一个新的工厂类即可,在继承了简单工厂模式的优点之余,也克服了简单工厂模式违背开闭原则的问题。当然缺点就是类变多了,开发量增加了。

  • 抽象的产品接口
  • 抽象的工厂接口(为了遵循开闭原则而做的抽象)
  • 具象的产品类
  • 具象的工厂类
//抽象的产品接口
public interface Computer {
    public void run();
}

//具象的产品类
public class Mac implements  Computer {

    @Override
    public void run() {
        System.out.println("Mac Run");
    }
}

public class ThinkPad implements Computer {

    @Override
    public void run() {
        System.out.println("ThinkPad Run");
    }
}

//抽象的工厂接口
public interface AbMethodFactory {
    Computer createComputer();
}

//具象的工厂类
public class MacFactory implements AbMethodFactory{
    @Override
    public Computer createComputer() {
        return new Mac();
    }
}

//具象的工厂类
public class ThinkPadFactory implements AbMethodFactory {

    @Override
    public Computer createComputer() {
        return new ThinkPad();
    }
}

//客户端代码
public class TestComputer {
    public static void main(String[] args){
        AbMethodFactory abMethodFactory = new MacFactory();
        Computer mac = abMethodFactory.createComputer();
        mac.run();
    }
}

抽象工厂模式

前面两种工厂,生产的都是单一产品。实际上,有的时候我们会有生成一系列产品的需求,比如我要生产的是一个电脑套装(一个产品系列),包含电脑,鼠标,键盘。当我们面临多个产品组合协作的场景时,使用抽象工厂,我们可以保证客户端使用的产品,都是如我们所希望的出自同一系列(比如 Mac电脑要接Type-C接口的鼠标),我只要切换工厂,就能实现配套措施的全部切换,改变整个系统的行为。缺点是如果我要在产品族里面再加一个产品,那么我需要修改工厂接口,增加一个获取新产品的方法抽象方法定义,所有具象工厂也要增加对应实现,违背了开闭原则。

抽象工厂的构成和工厂方法模式一致,但是其工厂内会提供多个方法用于获取同一个系列的不同产品。

  • 抽象的产品接口
  • 抽象的工厂接口(定义多个方法生产同系列不同产品)
  • 具象的产品类
  • 具象的工厂类(提供多个方法生产同系列不同产品)

代码只贴工厂接口和其中一个工厂吧,不然篇幅太冗长了。

public interface AbMethodFactory {
    //生产电脑
    Computer createComputer();
    //生产鼠标
    Mouse createMouse();
}

public class MacFactory implements AbMethodFactory{
    @Override
    public Computer createComputer() {
        return new Mac();
    }

    @Override
    public Mouse createMouse() {
        return new TypeCMouse();
    }

}

参考文章:

https://blog.csdn.net/fmyzc/article/details/79614944

0

软件设计原则之——依赖倒置原则

什么是依赖倒置?直接上概念的话,非常不好理解。首先要知道什么是依赖?我们上班要坐车,吃饭需要用碗筷,我需要使用这些东西才能达成某项目的,那么我就是依赖于这些东西。反映在代码内就是A类使用B类。下面的代码Worker工人就是依赖于Tool工具。

public class Worker {
    private void doWork(Tool tool){
        tool.doSometing();
    }
}

然后我们看一个例子,程序员小李子写代码就会需要用各种IDE。假设小李子需要写Java代码,那他就需要一个Eclipse来敲代码,小李子依赖的是Eclipse这一具体的工具,代码可以这么写。

public class XiaoLiZi {
    private void coding(Eclipse eclipse){
        eclipse.edit();
    }

    public static void main(String[] args){
        //让小李子写Java代码
        XiaoLiZi xiaoliZi = new XiaoLiZi();
        Eclipse eclipse = new Eclipse();
        xiaoliZi.coding(eclipse);
    }
}

由于公司人力紧张,小李子被临时抽调到了前端项目组做前端开发了,这时候他需要一个VsCode来敲代码。由于小李子之前用的是Eclipse,我们除了调整调用处的main方法内的代码,还不得不对小李子的类进行改写。调整coding方法的入参类型为VsCode,这个时候小李子依赖的是另一个具体的工具VsCode。

public class XiaoLiZi {
    private void coding(VsCode vsCode){
        vsCode.edit();
    }

    public static void main(String[] args){
        //让小李子写前端代码
        XiaoLiZi xiaoliZi = new XiaoLiZi();
        VsCode vsCode = new VsCode();
        xiaoliz.coding(vsCode);
    }
}

在金三银四的日子里,小李子的同事跳槽了,这个时候小李子不得不再兼顾回java的开发,现在小李子需要同时编写java代码和前端代码了。现在可就不再是简单调整coding方法的入参类型就能解决的了,XiaoliZi类现有的结构显然不满足让小李同时敲java代码和前端代码的功能,于是我们需要对XiaoLiZi类进行大改,把coding方法拆分成codeJava和codeHtml两个方法,分别传入Eclipse和VsCode。

public class XiaoLiZi {
    private void codeHtml(VsCode vsCode){
        vsCode.edit();
    }

    private void codeJava(Eclipse eclipse){
        eclipse.edit();
    }
    
    public static void main(String[] args){
        XiaoLiZi xiaoliZi = new XiaoLiZi();
        //让小李子写前端代码
        VsCode vsCode = new VsCode();
        xiaoliZi.codeHtml(vsCode);
        //让小李子写java代码
        Eclipse eclipse = new Eclipse();
        xiaoliZi.codeHtml(eclipse);
    }
}

每次变更,我们都需要调整XiaoliZi类的代码,使用了XiaoLiZi类的代码也需要调整。一个小小的改动,影响的范围可能扩散到整个模块,有很大的影响扩散风险,原因也很简单,因为XiaoLiZi这个类和具体的IDE类Eclipse和VsCode是高度耦合的。而抽象是一种很好的解耦方法。什么是抽象?我的理解抽象其实就是概括了某一些事物总体特质的一个范围。汽车是宝马奔驰的抽象,IDE是Eclipse,VsCode的抽象。

现在我们使用抽象的思想来重构代码,满足上面的场景。我们先定义IDE的抽象接口,不管是Eclispe和VsCode都实现了IDE接口,因为他们都有edit功能。

public interface IDE {
    void edit();
}
public class Eclipse implements  IDE{
    @Override
    public void edit() {
        System.out.println("Eclispe 写Java");
    }
}
public class VsCode implements IDE{
    @Override
    public void edit() {
        System.out.println("VsCode 写前端");
    }
}

这个时候,如果我们想让小李子写Java代码可以这么写,注意这个时候XiaoLiZi类依赖的不再是具体的编辑器类,而是IDE这个抽象接口。

public class XiaoLiZi {
    //传入的是IDE,依赖的不再是具体的编辑器类,而是IDE接口
    private void coding(IDE ide){
        ide.edit();
    }

    public static void main(String[] args){

        XiaoLiZi xiaoliZi = new XiaoLiZi();
        //让小李子写java代码,使用了抽象句柄IDE而不是具体类的句柄Eclipse
        IDE eclipse = new Eclipse();
        xiaoliZi.coding(eclipse);
    }
}

当我想让小李子写前端代码时,我只需要给XiaoLiZi一个VsCode实例就行,不需要调整XiaoLiZi类的代码。

public class XiaoLiZi {
    //传入的是IDE,依赖的不再是具体的编辑器类,而是IDE接口
    private void coding(IDE ide){
        ide.edit();
    }

    public static void main(String[] args){

        XiaoLiZi xiaoliZi = new XiaoLiZi();
        //让小李子写java代码,使用了抽象句柄IDE而不是具体类的句柄Eclipse
        IDE eclipse = new Eclipse();
        xiaoliZi.coding(eclipse);

        //让小李子写前端代码,只需要新增两行代码,不改动原有代码
        IDE vsCode = new VsCode();
        xiaoliZi.coding(vsCode);
    }
}

对比下来,第二种写法,对原有功能模块的影响显然是更低的,因为他不需要改动XiaoLiZi类的定义,不管我以后加多少种编辑器,都不需要改动XiaoLiZi的代码。这一改进,得益于我们把XiaoLiZi对具体编辑器的依赖关系给“颠覆”了,XiaoLiZi不依赖于具体的编辑器(Eclipse,VsCode),而是依赖于编辑器的抽象(IDE)。我们放弃了对具体实现细节(类)的依赖关系,转而去依赖抽象(接口)的思想,就是依赖倒置思想。

现在我们可以看下维基百科对“依赖倒置”的定义了:

依赖倒置的概念

我的译文:

在面向对象的程序设计中,“依赖倒置”是软件模块解耦的一种具体实现形式。这个原则颠覆了传统的上层业务模块对底层业务模块的依赖关系,致使上层代码不再依赖于底层模块代码的实现细节(而是依赖于底层的抽象)。这个原则要求以下两点:
1.上层模块不能依赖于底层模块(的具体实现细节),他们都要依赖于抽象(接口)。
2.抽象不能依赖于实现细节。实现细节(具体的实现类)需要依赖于抽象(接口)。

Dependency inversion principle wikipedia
In object-oriented design, the dependency inversion principle is a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details. The principle states:[1]
1.High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).
2.Abstractions should not depend on details. Details (classes) should depend on abstractions.

倒置还是颠覆?

我们在看各种设计模式的书籍,包括学习Spring AOP的过程中提起IOC的时候,经常会遇到这么一个名词“依赖倒置”。很多人包括我自己在理解这个原则的时候,经常会纠结于“倒置”这个词语,看了好几篇都不能get到这个“倒置”到底是倒置在哪。其实我个人觉得这完全就是一个翻译问题,更好的翻译我觉得应该是“依赖颠覆”。我们传统的直接的思维都是面向实现编程,而面向抽象编程就是对我们传统的面向实现编程的一种颠覆。依赖倒置,说穿了就是面向接口编程,行为类定义尽量是派生自抽象,使用时尽量用抽象的句柄。

抽象的好处

抽象的好处,对大项目而言是非常明显的。抽象是共有特质的概括范围,有了抽象,具体实现细节不会超出抽象的划定范围,不管被依赖的模块是否被完全开发完,依赖他的模块都可以并行的开发。实际上,项目的需求细节的变更也是经常的事情,使用抽象可以减少细节变更导致的代码改动量。具象的细节是多变的,但是基本上不会偏离抽象划定的范围,这样一对比,抽象明显更稳定,更适合作为依赖,就像盖房子一样,根基要稳,抽象就是根基。

参考文章:

https://www.cnblogs.com/yulang314/p/3551586.html

https://blog.csdn.net/briblue/article/details/75093382

https://www.cnblogs.com/fuchongjundream/p/3873073.html

0

一起学设计模式-01:单例模式

单例模式,说穿了就是整个程序的生命周期内,不管怎么调用,只会创建一个实例。都有哪些场景用了单例呢:

  • 数据库连接池是单例的(注意是管理连接的池,不是连接)
  • Spirng的Bean是单例的
  • 网站的计数器是单例的

单例模式有多种写法,这里挑选比较有代表性的“懒汉式”和“饿汉式”和“双重检查式”来讲。笔者一开始接触的时候,只知道死记写法,这样的后果就是忘得非常快。所以一定还是要理解,融会贯通了才能实现记忆持久化。实际上,这三种写法,其写法总结起来就是三部分构成:

  • 1.私有的静态成员变量 (最小化可见性)
  • 2.私有的构造方法 (杜绝类外构造的可能)
  • 3.获取类实例的唯一静态方法getInstance(唯一入口)

饿汉式写法:

饿汉式,由于其实例的创建是写在静态代码内,在类首次加载的时候就会被创建,因此饿汉式有天然的线程安全,调用效率高(实例一早就被创建好了,不存在并发创建出多个实例的情况)。同时由于只要加载类就创建实例,就像个饿鬼一样,不管吃不吃一定要有吃的,因此饿汉式的缺点在于非延迟加载,可能造成空间浪费。如果不在意空间,这个当然是最简便的实现单例的方式。

public class HungrySingleton{
    //私有的静态成员变量,一上来就实例化
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    //私有的构造方法
    private HungrySingleton(){}

    //获取实例的唯一入口
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

}

懒汉式一般线程安全写法:(不推荐)

懒汉式支持延迟加载,也就是说实例的创建会一直拖着,直到实例需要被使用的时候才做第一次创建。懒汉式并不具备天然线程安全特性,因此需要使用sysnchronized关键字来保证同步。一般线程安全写法调用效率相对较低,对性能影响较大。

public class LazySingleton{
    //私有的静态成员变量
    private static LazySingleton lazySingleton;
    //私有的构造方法
    private LazySingleton(){}

    //获取实例的唯一入口, 方法上加了synchronized关键字来保证同步
    public static synchronized LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

}

懒汉式双重检查写法(推荐)

双重检查式,可以看做改良的懒汉式写法,是比较推荐的写法。和普通懒汉式写法相比,其写法差异在于:

  • 使用了volatile关键字对私有单例类变量进行了修饰,禁止重排序
  • synchronized 加锁在代码段而不是整个方法
  • 且多出了null值一次检查。

public class DoubleCheckSingleton{
    //私有的静态成员变量,使用volatile禁止重排序
    private volatile static DoubleCheckSingleton doubleCheckSingleton;
    //私有的构造方法
    private DoubleCheckSingleton(){}

    //获取实例的唯一入口
    public static  DoubleCheckSingleton getInstance(){

        if(doubleCheckSingleton == null){
            //同步加锁的位置在方法内,且在第一重判断内
            //这样只有在第一次创建对象时需要加锁,其他时间都会在第一重判断处就返回,不会进入同步代码块
            synchronized (DoubleCheckSingleton.class){
                //在同步代码块内进行第二重判断
                if (doubleCheckSingleton == null){
                    doubleCheckSingleton = new DoubleCheckSingleton();
                }
            }
        }

        return doubleCheckSingleton;
    }

}

这样在保证线程安全的前提下,降低了synchronized关键字造成的性能下降问题,因为加锁只会发生在实例第一次创建的时候,其他时候都不会进入同步的代码块,是单例模式的推荐写法。

关于双重检查式写法,大家通常会有两个问题:

为啥使用volatile关键字和为啥进行两次检查。我们一个一个说。

1.为啥使用volatile关键字:

一个对象的创建,会经过下面的步骤:

  1. 内存分配
  2. 对象初始化
  3. 返回对象在堆的引用

而JVM有时候为了提高效率,会对指令进行重排,打乱这个顺序变成 内存分配 >> 返回对象在堆的引用 >> 对象初始化。如果不加volatile关键字禁止重排,可能会出现其他线程获取到了一个未初始化完成的实例的引用,导致程序出错。

我们看下下面这种情况,由于重排,对象的创建顺序被调整,变为132。此时线程A刚好返回singleton的引用,但是singleton并未完成初始化。此时线程B刚好执行了singleton==null的判断,返回false,于是线程B就得到未完成初始化的singleton的引用,在使用过程中就会发异常。

CPU时间线程A线程B
1给singleton分配内存getInstance()
2返回对象singleton在堆的引用 进行singleton == null判断 返回false
3.实例初始化return singleton 返回了一个未完成初始化的实例引用并使用
2.为啥需要两次检查:

第一层null检查前面说过了,是为了减少进入synchronized的次数,确保只在实例第一次初始化时需要进入同步代码块,提升程序性能。

现在我们把getInstance方法的第二层检查去掉,代码变成下面这样

    public static  DoubleCheckSingleton getInstance(){
        if(doubleCheckSingleton == null){
            synchronized (DoubleCheckSingleton.class){
                //去掉了第二重判断
                doubleCheckSingleton = new DoubleCheckSingleton();
            }
        }
        return doubleCheckSingleton;
    }

假设有两个线程同时通过了第一重校验,执行到了synchronized (DoubleCheckSingleton.class)这一行。此时,线程A获得了锁,线程B由于没有获得锁,因此必须等待A执行完同步代码块后再进入同步代码块。A执行完同步代码块,创建了一个singleton实例,并释放锁。此时B获得锁,由于没有第二重校验,B也会创建一个singleton实例,这就不再是单例了。

CPU时间线程A线程B
1获得锁,进入同步块等待锁
2创建singleton对象获得锁,进入同步块
3.释放锁创建singleton对象
0

tcpdump: eth1: No such device exists(SIOCGIFHWADDR: No such device)

问题背景:

使用命令抓包

sudo tcpdump -i eth1 host 123.123.123.123 and  port 9527 -w xxx.cap

提示 tcpdump: eth1: No such device exists(SIOCGIFHWADDR: No such device)

解决办法:

原因是不存在名称为eth1的网卡,执行命令查看本机的网卡名称

ifconfig -a

可以看到是 eth0

把命令里面的网卡名称 eth1替换成本机网卡名 eth0 ,问题解决。

0

使用Java ftpclient 下载文件损坏无法打开

问题背景:

最近做ftp对接的需求,文件下载下来后,打开提示文件损坏。直接用ftp命令登录下载下来的文件就没问题。说明是java程序的问题。百度一下很快就找到了解决方案。

解决方案:

在下载或上传文件之前,设置文件编码类型为二进制文件,问题解决。(我是放在ftpclient初始化的时候就设置好)

ftpClient.setFileType(FTP.BINARY_FILE_TYPE);

补充姿势:

common-net的ftpclient默认是使用ASCII_FILE_TYPE,文件会经过ASCII编码转换,所以可能会造成文件损坏。所以我们需要手动指定其文件类型为二进制文件,屏蔽ASCII转换的操作,避免文件在转换的过程中受损。

下面是common-net 3.6 的API对这两种文件格式的说明

common-net 3.6 API地址:http://commons.apache.org/proper/commons-net/apidocs/

0

FTP登录成功但无法LIST和下载文件的问题排查

问题背景:

最近有一个系统对接需求,采用了古老的ftp交换文件方式来对接。于是我用了commons-net包的3.6版本来进行ftp的连接和文件的传输。连接ftp成功,登录也没问题,但是在传输文件的时候会卡住,程序没有往下走,一段时间后抛异常。传输文件的代码如下(顺便提一下如果你连都连不上,那先理清架构,问下你们运维是不是用了代理,如果用了代理,java代码里面需要设置使用代理连接):

        //初始化ftpclient
        initFtpClient();
        //切换路径
        ftpClient.changeWorkingDirectory(pathname);
        InputStream inputStream = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] dataBytes;
        try{
            //卡在下面这一行!!
            FTPFile[] ftpFiles = ftpClient.listFiles();
            for(FTPFile file : ftpFiles){
                if(file.getName().startsWith(filenamePrefix)){
                    inputStream = ftpClient.retrieveFileStream(pathname+file.getName());
                    break;
                }
            }

            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1 ) {
                baos.write(buffer, 0, len);
            }
            dataBytes = baos.toByteArray();
            return dataBytes;

        }catch (Exception e){
            //....
        }finally {
            //....
        }

排查过程:

1.登录应用服务器,执行ftp各种操作排除架构,代理服务器配置等环境问题

由于公司的网络架构有非常严格的安全限制,所以实际上,应用服务器和目标ftp服务器之间往往还要经过各种代理服务器,所以首先要做的就是登录到应用服务器使用ftp命令连接ftp服务器(具体的ip和端口号不一定就是对方直接暴露给你的ip和端口号,需要咨询运维确认)执行目录切换,文件下载,文件上传的操作,排除架构上和代理服务器配置上可能存在的问题。附这几个操作的ftp命令:

#切换到xxx目录
cd xxx 
#下载abcd.xls文件
get abcd.xls
#上传当前目录下的yyy.txt文件,当前目录可以用lcd查看
put yyy.txt

一顿操作下来,目录切换和文件上传下载都正常,那么架构环境代理配置有问题的可能性就比较小了。命令可以正常下载文件而程序又不行,那就可能是程序的问题。可是程序只是卡住,抛出了一个空指针异常,没有任何指导意义,这种时候只能祭出最终兵器:抓包。

2.抓包分析程序的行为和命令行行为上的区别,找到问题所在

由于异常信息非常有限,我们只能使用抓包的命令(命令:tcpdump),分析我写的java程序连ftp传输文件和我直接使用ftp命令连接服务器传输文件所发出的命令的区别。把抓到的包用wireshark打开,只看协议为ftp的数据(过滤条件:ftp or ftp-data)。按时间顺序看下来,可以看到ftp客户端发送了port命令,使用的是主动模式,客户端在发出LIST请求后,服务端返回了连接失败。

问题原因:

到这里,问题的原因就很明显了:因为我的ftp客户端使用了主动模式(如果你想了解ftp的主动被动模式的区别,可以拉到最后),ftp服务器会主动尝试连接ftp客户端port命令所指定的随机端口建立数据传输连接,但是由于我的ftp客户端与ftp服务器之间还隔着防火墙,ftp客户端的防火墙没有开通(这么多随机端口想必要开防火墙也够喝一壶了),因此就出现了425 Failed to establish connection的返回。

解决办法:

使用主动模式还是被动模式,都是由客户端来决定的。除非你愿意开放一堆端口,不然的话最佳的解决办法就是设置客户端使用被动模式。我用的commons-net包的3.6版本,在初始化ftp客户端的时候就可以直接在代码中设定客户端使用被动模式,代码如下,设置被动模式后,问题解决。

//设置二进制文件传输模式
ftpClient.setFileTransferMode(ftpClient.BINARY_FILE_TYPE);
//设置被动传输模式
ftpClient.enterLocalPassiveMode();

扩展阅读:

ftp主动模式和被动模式:https://www.cnblogs.com/mawanglin2008/articles/3607767.html

0