• 三大工厂模式深入解析


    原文地址  http://blog.ximu.site/factory-pattern/

    最常见的工厂模式,工厂模式细分下来有三大类:

    1. 简单工厂 2. 工厂模式 3. 抽象工厂模式

    他们的目标都是一样的:封装对象的创建。但是实现手段和使用场景却是不相同。使用的时候三个模式也可以互相替换使用,导致很容易混淆三者。

    下面我们来具体看看三者的使用。


    简单工厂模式

    准确的说简单工厂不是一个模式,而是一种编程习惯。但是平时使用的非常多,我们就把他归到模式一类了。

    1、定义

    提供一个创建对象实例的功能,而无需关心具体实现。被创建的类型可以使接口、抽象类、具体类。

    2、UML结构图及说明

    image

    obstractClass:可以实现为抽象类或者具体接口,看实际需要选择,定义具体类需要实现的功能 
    concreteClass:实现抽象类所定义功能的具体类,可能会有多个 
    simpleFactory:简单工厂,选择合适的具体类来创建对象返回 
    client:通过simplefactory来获取具体的对象

    如果对UML图不了解,可以先看看这篇文章:UML类图几种关系的总结

    3、实际场景运用

    3.1、需求

    假设我们要实现一个电脑组装的功能,组装电脑很重要的一个地方就是根据客户指定的cpu类型来安装。假设我们有三种类型的cpu供客户选择:apple,intel,AMD。

    3.2、普通实现

    在客户端加入如下方法:

    client.m文件  
    =====================
    
    #import "simpleFactory.h"
    #import "interCpu.h"
    #import "appleCpu.h"
    #import "AMDCpU.h"
    
    @implementation client
    
    -(Cpu *)selectCpuWithType:(NSString *)type{
        Cpu *cpu = nil;
        if ([type isEqualToString:@"intel"]) {
            cpu = [interCpu new];
    
        }else if([type isEqualToString:@"AMD"]){
            cpu = [AMDCpU new];
    
        }else{
            cpu = [appleCpu new];
    
        }
        return  cpu;
    }
    
    @end
    

    比如像使用inter类型的cpu,只需要如下代码:

    [self selectCpuWithType@"interCpu"];
    

    这里我只是展现了核心代码,忽略了其他代码。你需要创建一个CPU的父类,然后创建三个子类继承它,分别是interCpu、AMDCpu、appleCpu。

    上面的代码可以完成功能,根据客户传入的type类型来创建相应的cpu具体对象。

    3.3、问题

    虽然上述代码可以完成功能,但是有如下问题:

    1、如果要加入其他cpu类型,或者更改cpu类型,那么必须修改客户端代码。违反了开闭原则(不了解的童鞋可以去看设计模式开篇漫谈

    2、客户端知道所有的具体cpu类,耦合度太高。客户端必须知道所有具体的cpu类,那么任何一个类的改动都可能会影响到客户端。

    3.4、解决问题

    客户端必须了解所有的具体cpu类才能创建对象,但是这会导致上述一系列问题。那么解决办法就是把这些对象的创建封装起来,对客户端不可见,那么之后如何改动具体类都不会影响到客户端。这可以通过简单工厂来实现。

    下面我们来看看使用简单工厂重写后的代码

    引入简单工厂类:

    simpleFactory.h文件
    
    =======================
    
    #import <Foundation/Foundation.h>
    #import "Cpu.h"
    
    @interface simpleFactory : NSObject
    -(Cpu *)selectCpuWithType:(NSString *)type;
    
    @end
    
    
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    simpleFactory.m文件
    
    =======================
    
    
    #import "simpleFactory.h"
    #import "interCpu.h"
    #import "appleCpu.h"
    #import "AMDCpU.h"
    
    @implementation simpleFactory
    
    -(Cpu *)selectCpuWithType:(NSString *)type{
        Cpu *cpu = nil;
        if ([type isEqualToString:@"intel"]) {
            cpu = [interCpu new];
    
        }else if([type isEqualToString:@"AMD"]){
            cpu = [AMDCpU new];
    
        }else{
            cpu = [appleCpu new];
    
        }
        return  cpu;
    }
    
    @end
    

    客户端调用代码:

    #import <Foundation/Foundation.h>
    #import "simpleFactory.h"
    #import "Cpu.h"
    
    int main(int argc, const char * argv[]) {  
        @autoreleasepool {
            simpleFactory *factory = [simpleFactory new];
            Cpu *cpu = [factory selectCpuWithType:@"interCpu"];
            [cpu installCpu];
        }
        return 0;
    }
    

    此时不管是增加还是减少或者修改cpu类型,客户端代码都不用改动,降低了客户端和具体cpu类的耦合,也遵循了开闭原则

    4、反思

    细心的一点的童鞋可能发现,你这不是逗我吗,仅仅是把本来客户端的代码移到了简单工厂类而已,有什么改变吗?

    理解这个问题的关键在于理解简单工厂所在的位置。

    前面我们把创建具体cpu对象的代码放在客户端,导致一系列问题。我们的目标就是让客户端从创建具体对象中解耦出来,让客户端不知道对象创建的具体过程。而简单工厂就是和具体对象封装在一起,算是一个封装体内,所以简单工厂知道具体的实现类是没有关系的。现在客户端只要知道简单工厂和一个抽象类cpu,就可以创建具体对象了,实现了解耦。

    5、改进

    虽然上面使用简单工厂后,让客户端实现了解耦,但是如果实现类改变了,我们还是需要需改简单工厂。有没有什么办法做到即使实现类改变也不需要改变简单工厂的代码呢?

    在java中可以使用反射或者IoC/DI来实现,在iOS种我们有更简单的方法,一个方法足矣,具体见代码

    -(Cpu *)selectCpuWithType:(NSString *)type{
        Cpu *cpu = (Cpu *)[NSClassFromString(type)new];
        if ([cpu isKindOfClass:[Cpu class]] && cpu) {
            return  cpu;
        }else{
            return nil;
        }
    }
    

    客户端代码不需要改动,是不是简单了很多?

    6、简单工厂优缺点

    • 优点

      1. 帮助封装

        简单工厂虽然简单,但是帮我们实现了封装对象创建的过程,让我们可以实现面向接口编程。

      2. 解耦

        客户端不需要知道具体实现类,也不需要知道创建过程。只需要知道简单工厂类就可以创建具体对象,实现了解耦

    • 缺点

      1.增加客户端复杂度

      如果是通过参数来选择创建具体的对象,那么客户端就必须知道每个参数的含义,也就暴露了内部实现

      2.不方便扩展

      如果实现类改变,那么还是需要修改简单工厂,可以通过文中的方法来避免这个问题。或者使用下节我们讲的工厂方法来解决

    7、简单工厂本质

    简单工厂的本质:选择实现

    简单的工厂的本质在于选择,而不是实现,实现是由具体类完成的,不要在简单工厂完成。简单工厂的目的是让客户端通过自己这个中介者来选择具体的实现,从而让客户端和具体实现解耦,任何实现方面的变化都被简单工厂屏蔽,客户端不会知道。

    简单工厂的实现难点在于如何“选择实现”,前面讲到的是静态传递参数。其实还可以在运行过程中从内存或者数据库动态选择参数来实现,具体代码就不演示了,只是读取参数的方式不同,其他都一样。

    8、何时使用简单工厂

    1. 想完全封装隔离具体实现

    让外部只能通过抽象类或者接口来操作,上面的例子中,就是只能操作抽象类cpu,而不能操作具体类。此时可以使用简单工厂,让客户端通过简单工厂来选择创建具体的类,不需要创建的具体过程。

    1. 想把创建对象的职责集中管理起来

    一个简单工厂可以创建许多相关或者不相关的对象,所以可以把对象的创建集中到简单工厂来集中管理。

    完整代码见文末。


    工厂模式

    1、问题

    让我们回到最原始的代码:

    client.m文件  
    =====================
    
    #import "simpleFactory.h"
    #import "interCpu1179.h"
    #import "appleCpu1179.h"
    #import "AMDCpU1179.h"
    
    @implementation client
    
    -(Cpu *)selectCpuWithType:(NSString *)type{
        Cpu *cpu = nil;
        if ([type isEqualToString:@"intel1179"]) {
            cpu = [interCpu1179 new];
    
        }else if([type isEqualToString:@"intel753"]){
            cpu = [interCpu753 new];
    
        }else if([type isEqualToString:@"AMD1179"]){
            cpu = [AMDCpU1179 new];
    
        }else if([type isEqualToString:@"AMD753"]){
            cpu = [AMDCpu753 new];
    
        }else if([type isEqualToString:@"apple1179"]){
            cpu = [appleCpu1179 new];
    
        }else if([type isEqualToString:@"apple753"]){
            cpu = [appleCpu753 new];
    
        }else{
            return nil;
        }return  cpu;
    }
    
    @end
    

    仔细看这段代码,就会发现一个问题:依赖于具体类。因为必须在这里完成对象创建,所以不得不依赖于具体类:interCpu、appleCpu、AMDCpu。

    这会导致什么问题呢?简单来说就是违反了依赖倒置原则,让高层组件client依赖于底层组件cpu。违反这个原则的后果就是一旦底层组件改动,那么高层组件也就必须改动,违反了开闭原则。联系到上面的这个例子就是如果增加或者修改一个cpu子类,那么就必须改动上面的代码,即使使用了简单工厂模式,还是要修改简单工厂的代码。

    我们先来看看什么是依赖导致原则:

    定义:

    要依赖抽象,不要依赖具体

    展开来说就是:不能让高层组件依赖低层组件,而且不管高层还是低层组件,都应该依赖于抽象。

    那么如何才能避免违反这一原则呢?下面有三条建议可以参考下:

    • 变量不可以持有具体类的引用,比如new一个对象
    • 不要让类派生自具体类,不然就会依赖于具体类,最好派生自抽象类
    • 不要覆盖基类中已经实现的方法,如果覆盖了基类方法,就说明该类不适合做基类,基类方法应该是被子类共享而不是覆盖。

    但是要完全遵守上面三条,那就没法写代码了。所以合适变通才是,而工厂模式就是为了遵循依赖倒置原则而生的。

    下面就来看看使用工厂模式如何解决这个问题。


    2、定义

    定义了一个创建对象的接口,由子类决定实例化哪一个类,让类的实例化延迟到子类执行。

    3、UML结构图及说明

    image

    先记住工厂模式实现了依赖倒置原则,至于如何实现的,暂且按下不表,我们先来看代码

    4、实际场景运用

    还是和简单工厂的同样的需求,但是我们根据cpu的针脚个数增加了cpu的分类,比如intelCpu1179、intelCpu753。另外两个类型的cpu也是如此,分为1179和753两个类型的cpu。但是这次我们用工厂模式来实现。

    定义一个工厂基类,定义一个工厂方法

    #import <Foundation/Foundation.h>
    #import "Cpu.h"
    
    @interface factory : NSObject
    -(Cpu*)createCpuWithType:(NSInteger)type;
    
    @end
    
    
    =============================
    #import "factory.h"
    
    @implementation factory
    -(Cpu *)createCpuWithType:(NSInteger)type{
        @throw ([NSException exceptionWithName:@"继承错误" reason:@"子类必须重写该方法" userInfo:nil]);
        return nil;
    }
    @end
    

    下面是具体工厂,继承自工厂基类,实现工厂方法来创建具体的cpu对象

    #import <Foundation/Foundation.h>
    #import "factory.h"
    
    @interface intelFactory : factory
    
    @end
    
    ===========================
    
    #import "intelFactory.h"
    #import "interCpu753.h"
    #import "interCpu1179.h"
    #import "Cpu.h"
    
    @implementation intelFactory
    -(Cpu *)createCpuWithType:(NSInteger)type{
        Cpu *cpu = nil;
        if (type == 753) {
            cpu = [interCpu753 new];
        }else{
            cpu = [interCpu1179 new];
        }
        return cpu;
    }
    @end
    

    上面演示的是intelCpu工厂,另外的AMD和apple的cpu具体工厂类类似,就不贴代码了。

    客户端调用:

    #import <Foundation/Foundation.h>
    #import "factory.h"
    #import "Cpu.h"
    #import "intelFactory.h"
    #import "appleFactory.h"
    #import "AMDFactory.h"
    
    int main(int argc, const char * argv[]) {  
        @autoreleasepool {
            factory *factory = nil;
            factory = [intelFactory new];
            Cpu *cpu1 = [factory createCpuWithType:753];
            [cpu1 installCpu];
            Cpu *cpu2 = [factory createCpuWithType:1179];
            [cpu2 installCpu];
    
            factory = [AMDFactory new];
            Cpu *cpu3 = [factory createCpuWithType:753];
            [cpu3 installCpu];
            Cpu *cpu4 = [factory createCpuWithType:1179];
            [cpu4 installCpu];
    
    
        }
        return 0;
    }
    

    如果此时又多了一个cpu类型,比如高通的cpu,那么只需要新建一个高通cpu的工厂类,继承自factory类,然后实现工厂方法,就可以了。客户端也可以根据自己的需要选择使用哪个工厂,不用修改原有代码。符合开闭原则:对修改关闭,对扩展开放。

    5、如何遵循依赖倒置原则

    我们先来看看没有使用工厂方法,各个类之间的依赖关系

    image

    可以看到高层组件client依赖于具体的低层组件cpu类,违反了依赖倒置原则。一般我们把功能的使用者归到高层组件,把功能的提供者归到低层组件。

    再来看看使用工厂方法后各个类之间的依赖关系

    image

    可以看到高层组件client依赖于抽象类cpu,低层组件也就是各种cpu具体类也依赖于抽象类factory,符合依赖倒置原则。其实说白了,就是要针对接口编程,而不是针对实现编程

    那么倒置在哪里呢?

    对比两个图,就会发现具体cpu类的箭头从原来向下变成了向上,也就是说依赖关系发生了倒置。我们来看看为什么会这样。

    第一个图里面,因为我们直接在client里面去初始化各个cpu类,倒置client就必须依赖这些具体类,依赖关系向下。

    第二个图里面,每个cpu具体类,都继承自抽象cpu类,并且实现了抽象cpu的方法installCpu,此时具体cpu类就依赖于抽象cpu类,依赖关系向上。

    现在明白为什么叫做依赖倒置了吧?这一切都是工厂方法的功劳。

    有人要说,这个用简单工厂也可以实现的呀。是的没错,简单工厂也能实现,其实如果直接在工厂方法的抽象cpu类里面实现对象的创建,那么此时工厂模式就是简单工厂。但是工厂模式有一个简单工厂模式没有的功能:遵循开闭原则。如果此时要增加或者修改一个cpu具体类,那么简单工厂的代码就必须修改,而工厂方法只需要扩展就行了,不用修改原有代码。

    6、工厂模式优缺点

    • 优点

      1. 可以在不知道具体实现的情况下编程

        工厂模式可以让你在实现功能时候,不需要关心具体对象,只需要使用对象的抽象接口即可,上面例子中client使用的就是cpu抽 象类,而不是具体的cpu类。

      2. 更容易扩展新版本

        如果需要加入新的实现,只需要扩展一个新类,然后继承抽象接口实现工厂方法即可。遵循了开闭原则。

    • 缺点

      具体产品和工厂方法耦合,因为在工厂方法中需要创建具体实例,所以它们会耦合

    7、何时使用工厂模式

    通过工厂模式定义我们知道,工厂模式主要是把对象的创建延迟到子类执行。如何实现的呢?

    拿上面的例子来说,当我们调用抽象类factory的方法createCpuWithType的时候,真正执行的是factory的子类,比如intelFactory。做到这点是面向对象语言的基本特征之一:多态,它可以实现父类的同一个方法在不同的子类中有不同的表现。

    了解了工厂模式的本质,我们就知道在上面情况下可以使用它了

    • 一个类不想知道它所需要创建的对象所属的类,比如client不需要知道intelCpu1179这个具体类
    • 一个类希望由他的子类来指定它所创建的对象,比如factory希望IntelFactory创建具体cpu对象

    抽象工厂

    1、业务场景

    假设我们写了一套系统,底层使用了两套数据库:sqlserver和access数据库。但是针对业务逻辑的代码不可能写两套,这样非常麻烦,也不方便扩展新的数据库。我们需要提供一个统一的接口给业务层操作,切换数据库也不需要修改业务层逻辑。

    简化下需求,假设我们每个数据库都有user和department两张表,业务逻辑代码如下:

            //业务逻辑
            [user insert:@"张三"];
            [user getUser];
            [deparment insert:@"财务"];
            [deparment getDepartment];
    

    下面我们就来看看如何使用抽象工厂来实现这个需求

    2、需求实现

    2.1、创建抽象工厂接口

    我们先创建一个抽象接口,在iOS里面我们使用协议实现。

    IFactory.h文件  
    ========================
    @class IUser;
    @class IDepartment;
    
    @protocol IFactory <NSObject>
    @required
    -(IUser*)createUser;
    -(IDepartment *)createDepartment;
    
    @end
    

    2.2、创建具体工厂

    下面我们来创建两个具体的工厂,分别针对两个数据库,实现抽象工厂的方法,来创建具体的表对象

    #import <Foundation/Foundation.h>
    #import "IFactory.h"
    #import "IUser.h"
    
    @interface SqlServerFactory : NSObject<IFactory>
    
    @end
    
    ======================
    
    #import "SqlServerFactory.h"
    #import "SqlServerUser.h"
    #import "SqlServerDepartment.h"
    
    @implementation SqlServerFactory
    
    -(IUser *)createUser{
        return [SqlServerUser new];
    }
    
    -(IDepartment *)createDepartment{
        return [SqlServerDepartment new];
    }
    @end
    

    AccessFactory类创建方法类似。

    2.3、创建产品

    现在我们需要创建具体工厂需要的产品,这里是两张表:user和department。但是这两张表有分为两个体系,sqlserver的user和department表,access的user和department表。

    我们把user表抽象为基类,下面分别实现sqlserver和access的子类user表。department表同理,不再贴代码了。

    抽象产品类

    #import <Foundation/Foundation.h>
    
    @interface IUser : NSObject
    -(void)insert:(NSString *)user;
    -(void)getUser;
    @end
    
    =======================
    #import "IUser.h"
    
    @implementation IUser
    -(void)insert:(NSString *)user{
        @throw ([NSException exceptionWithName:@"继承错误" reason:@"子类没有实现父类方法" userInfo:nil]);
    }
    
    -(void)getUser{
        @throw ([NSException exceptionWithName:@"继承错误" reason:@"子类没有实现父类方法" userInfo:nil]);
    }
    @end
    

    具体产品类

    #import <Foundation/Foundation.h>
    #import "IUser.h"
    
    @interface SqlServerUser : IUser
    
    @end
    
    ==================
    
    
    #import "SqlServerUser.h"
    
    @implementation SqlServerUser
    -(void)insert:(NSString *)user{
        NSLog(@"向sqlserver数据库插入用户:%@", user);
    }
    
    -(void)getUser{
        NSLog(@"从sqlserver数据库获取到一条用户数据");
    }
    @end
    
    #import <Foundation/Foundation.h>
    #import "IUser.h"
    
    @interface AccessUser : IUser
    
    @end
    
    =========================
    
    #import "AccessUser.h"
    
    @implementation AccessUser
    -(void)insert:(NSString *)user{
        NSLog(@"向access数据库插入用户:%@", user);
    }
    
    -(void)getUser{
        NSLog(@"从access数据库获取到一条用户数据");
    }
    
    @end
    

    2.4、客户端调用

    #import <Foundation/Foundation.h>
    #import "IFactory.h"
    #import "IUser.h"
    #import "IDepartment.h"
    #import "SqlServerFactory.h"
    #import "AccessFactory.h"
    
    
    int main(int argc, const char * argv[]) {  
        @autoreleasepool {
            id<IFactory> DBFactory = [AccessFactory new];
            IUser *user = [DBFactory createUser];
            IDepartment *deparment = [DBFactory createDepartment];
    
            //业务逻辑
            [user insert:@"张三"];
            [user getUser];
            [deparment insert:@"财务"];
            [deparment getDepartment];
    
        }
        return 0;
    }
    

    输出:

    2016-11-22 17:38:30.667 抽象工厂模式[56330:792839] 向access数据库插入用户:张三  
    2016-11-22 17:38:30.668 抽象工厂模式[56330:792839] 从access数据库获取到一条用户数据  
    2016-11-22 17:38:30.668 抽象工厂模式[56330:792839] 向access数据库插入部门:财务  
    2016-11-22 17:38:30.668 抽象工厂模式[56330:792839] 从access数据库获取到一条部门数据
    

    此时如果需要切换到sqlserver数据库,只需要更改如下代码

    id<IFactory> DBFactory = [AccessFactory new];  
    改为:
    id<IFactory> DBFactory = [SqlServerFactory new];
    

    但是抽象工厂有个缺点:你想下,如果此时我想增加一张工资表,那么就必须修改抽象工厂接口类IFactory和每个具体工厂类SqlServerFactory、AccessFactory,违反了开闭原则。但是总体来瑕不掩瑜。

    3、 实现原理分析

    通过上面的例子,我想大家已经认识到抽象工厂的优雅之处,那么它是如何完成的呢?

    我们来把上面的例子做成UML图,这样看的更加清晰。

    image

    可以看到我们创建了两个具体工厂,分别是sqlserverFactory和AccessFactory。我们的产品有两个user和department,每个产品也分为两个体系:sqlserver的access的。

    如果选择sqlserverFactory,那么对应的两个工厂方法就生成sqlserver的user和department表。选择accessFactory也是如此。

    所以我们可以很方便在两个数据库之间切换,而不影响业务逻辑,因为业务逻辑都是面向抽象编程。再看下业务逻辑的代码

            id<IFactory> DBFactory = [AccessFactory new];
            IUser *user = [DBFactory createUser];
            IDepartment *deparment = [DBFactory createDepartment];
    
            //业务逻辑
            [user insert:@"张三"];
            [user getUser];
            [deparment insert:@"财务"];
            [deparment getDepartment];
    

    可以看到业务逻辑都是针对抽象类IUesr和IDepartment编程,所以他们的子类如何变化,不会影响到业务逻辑。

    4、 抽象工厂定义

    提供一个创建一系列相关或者相互依赖的接口,而无需依赖具体类。

    好好分析这句话,关键的地方就是:一系列相关或者相互依赖的接口。这决定了我们使用抽象工厂的初衷,抽象工厂定义了一系列接口,这些接口必须是相互依赖或者相关的,而不是把一堆没有什么关联的接口放到一起。

    回头看看我们上面的抽象工厂类IFactory定义的接口,是用来创建两张表,这两张表是属于同一个数据库的,他们之间是相互关联和依赖的。

    后面一句“无需依赖具体类”是怎么做到的呢?

    可以看到抽象工厂类只是定义了接口,而真正去实现这些接口产生具体对象的是具体工厂。客户端面向的也是抽象工厂类编程,所以无需依赖具体类。

    我们可以把抽象工厂的定义的方法看做工厂方法,然后具体工厂去实现这些工厂方法,这不就是工厂模式吗? 所以说抽象工厂包含了具体工厂。

    5、思考

    工厂模式和抽象工厂模式最大的区别在于,后者的一系列工厂方法是相互依赖或者相关的,而工厂模式虽然也可以定义一些列工厂方法,但是他们之间是没有关联的。这是区分他们的重要依据。

    其实如果抽象工厂里面只定义一个工厂方法,也就是只实现一个产品,那么久退换为工厂方法了。

    记住:

    工厂模式创建一种类型的产品,抽象工厂创建一些列相关的产品家族。

    6、何时使用抽象工厂

    • 客户端只希望知道抽象接口,而不关心具体产品的实现的时候
    • 一个系统需要有多个产品系列中的一个来配置的时候。也就是说可以动态切换产品系列,比如上面的切换两个数据库
    • 需要强调一系列产品的接口有关联的时候,以便联合使用它们。

    三个模式对比

    • 抽象工厂模式和工厂模式

      工厂模式针对单独产品的创建,而抽象工厂注重一个产品系列的创建。如果产品系列只有一个产品的 话,那么抽象工厂就退换到工厂模式了。在抽象工厂中使用工厂方法来提供具体实现,这个时候他们联 合使用。

    • 工厂模式和简单工厂

      两者非常类似,都是用来做选择实现的。不同的地方在于简单工厂在自身就做了选择实现。而工厂模式 则是把实现延迟到子类执行。如果把工厂方法的选择实现直接在父类实现,那么此时就退化为简单工厂 模式了。

    • 简单工厂和抽象工厂 简单工厂用于做选择实现,每个产品的实现之间没有依赖关系。而抽象工厂实现的一个产品系列,相互 之间有关联。这是他们的区别


    Demo下载地址

    简单工厂

    工厂模式

    抽象工厂模式

  • 相关阅读:
    构建之法:第二次心得
    构建之法:第一次心得
    tomcat配置限制ip和建立图片服务器
    tomcat8.5优化配置
    java 操作 csv文件
    jsoup教学系列
    (转)js实现倒计时效果(年月日时分秒)
    本地启动tomcat的时候报java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError: PermGen space
    使用mybatis执行oracle存储过程
    java 获取web登录者的ip地址
  • 原文地址:https://www.cnblogs.com/fendou0320/p/6098141.html
Copyright © 2020-2023  润新知