ramostear.comramostear.com 谭朝红的技术分享博客

格言 编程是一门技术,也是一门艺术 !

Java 8 Optional源码及案例分析

Java 8 Optional源码及案例分析

java.util.optional是从JDK 8开始引的类,Optional是一个包含了空值(NULL)或非空值(NOT NULL)的对象容器,用于判断方法的返回类型是否有值,Optional的好处是可以避免由于NULL带来的异常情况,如NullPointerException。一般地,如果一个方法的返回类型是Optional,则该方法应该经量避免返回NULL,对应的应该返回一个包含了NULL对象的Optional实例。

提示:本文编写时使用的是JDK 8

Optional源码分析

Optional是一个对象容器,你可以在java.util包中找到该类。接下来,将分析Optional类中的构造器,属性和方法。Optional类的源码和分析如下:

public final class Optional<T> {
    /**
     * EMPTY代表NULL值的Optional对象实例
     */
    private static final Optional<?> EMPTY = new Optional<>();

    /**
     * 泛型类型的对象实例
     */
    private final T value;

    /**
     * 私有的Optional空构造函数
     */
    private Optional() {
        this.value = null;
    }

    /**
     * 返回内部的EMPTY实例
     */
    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }

    /**
     * 通过value实例化Optional对象,如果value为空,则抛出空指针异常
     */
    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

    /**
     * 通过value实例化Optional对象,如果value为空,则抛出空指针异常
     */
    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }

    /**
     * 通过value实例化Optional对象,如果value为空,则返回EMPTY,
     * 如果value不为空,则调用Optional::of(value)方法
     */
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

    /**
     * 如果value的值不为空,则返回value,否则抛出NoSuchElementException异常
     */
    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }

    /**
     * 如果value的值不为空,返回true,否则返回false
     */
    public boolean isPresent() {
        return value != null;
    }

    /**
       * 如果value不为NULL,则使用value调用消费者函数式接口的消费方法Consumer::accept()    
     */
    public void ifPresent(Consumer<? super T> consumer) {
        if (value != null)
            consumer.accept(value);
    }

    /**
     * 如果存在值,并且值与给定谓词匹配,则返回描述该值的值,否则返回空值
     */
    public Optional<T> filter(Predicate<? super T> predicate) {
        Objects.requireNonNull(predicate);
        if (!isPresent())
            return this;
        else
            return predicate.test(value) ? this : empty();
    }

    /**
     * 如果存在值,则将提供的映射函数应用于该值,如果结果为非null,则返回描述结果的值。 否则返回空
     */
    public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent())
            return empty();
        else {
            return Optional.ofNullable(mapper.apply(value));
        }
    }

    /**
     * 如果存在则返回值,否则返回其他给定的值
     */
    public T orElse(T other) {
        return value != null ? value : other;
    }

    /**
     *如果值存在,则返回该值,否则返回指定的被调用函数的返回值。
     */
    public T orElseGet(Supplier<? extends T> other) {
        return value != null ? value : other.get();
    }

    /**
     * 如果值存在,则返回该值,否则抛出创建者提供的异常信息
     */
    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
        if (value != null) {
            return value;
        } else {
            throw exceptionSupplier.get();
        }
    }

    // other code....
}

empty()方法

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

如果明确表示一个持有NULL值的Optional实例,则使用Optional.empty()方法,例如:

Optional nullOpt = Optional.empty();

of()方法

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

如果能够明确判断一个对象的值不为NULL时,应该使用Optional.of()方法,例如:

User user = userService.findById(userId);
if(user != null){
    Optional userOptional = Optional.of(user);
}

ofNullable()方法

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

如果无法确认一个对象的值是否为空的时候,应该使用Optional.ofNullable()方法,例如:

User user = userService.findByEmail(userEmail);
Optional userOptional = Optional.ofNullable(user);

get()方法

@NotNull
public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

一般地,get()方法需要先确认value的值不为空后才使用,get()方法需要先校验value存在与否。例如:

User user = userService.findByUsername(username);
if(user != null){
    Optional userOptional = Optional.of(user);
    User value = userOptional.get();
}

isPresent()方法

public boolean isPresent() {
    return value != null;
}

isPresent()方法用于判断value是否存在,如果存在,返回true;如果不存在,返回false。例如:

User user = userService.findById(userId);
boolean exist = Optional.ofNullable(user).isPresent();

Optional<User> uop = userService.findOne(userId);
if(uop.isPresent()){
    return uop.get();
}else{
    return new User();
}

ifPresent()方法

public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

顾名思义,如果value的值不为空,则使用value调用消费者函数式接口的消费方法Consumer.accept(),例如:

User user = userService.findById(userId);
Optional op = Optional.ofNullable(user);
op.ifPresent(u->System.out::println);

filter()方法

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent())
        return this;
    else
        return predicate.test(value) ? this : empty();
}

filter()方法用于实现简单的过滤功能,如果Optional容器包含的值不为空,则返回满足过滤条件的值,否则返回empty()函数的返回值。例如:

List<User> list = userService.findAll();
list.forEach(item->{
   Optional.ofNullable(item)
           .filter(user-> user.getStatus == 1)
           .isPresent(user->System.out::println); 
});

map()方法

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

map()方法用于类型转换操作,通过功能函数mapper.apply()对value进行类型映射,重新封装为可空的Optional对象实例,例如:

List<String> list = Arrays.asList("java","jvm","jre","jdk");
Optional<List<String>> listOptional = Optional.of(list);
int size = listOptional.map(List::size).orElse(0);
System.out.println("size = " + size);

output:

size = 4

flatMap()方法

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Objects.requireNonNull(mapper.apply(value));
    }
}

与map()方法一样,flatMap()方法也是进行类型映射操作,唯一不同于map()方法的是flatMap()方法中Optional类型返回值直接由外部决定,不需要通过值重新封装为Optional实例。例如:

public class User{
    private String username;

    public Optional<String> getUsername(){
        return Optional.ofNullable(username);
    }

    public User(String username){
        this.username = username;
    }
}
public class TestFlatMapMethod{
    public static void main(String[] args){
        User user = new User("ramostear");
        Optional<User> userOptional = Optional.of(user);

        Optional<Optional<String>> usernameOptional = userOptional.map(User::getUsername);
        //map()方法需要再次封装Optional类型
        Optioanl<String> nameOptional = usernameOptional.orElseThrow(IllegalArgumentException::new);
        String name = nameOptional.orElse("");
        System.out.println(name);

        //flatMap()方法不需要再次封装Optional类型
        String username = userOptional.flatMap(User::getUsername).orElse("");
        System.out.println(username);
    }
}

orElse()方法

public T orElse(T other) {
    return value != null ? value : other;
}

orElse()方法的作用是:当value的值不存在时,提供一个默认的值,例如:

String val1 = null;
String val2 = "default value";
String value = Optional.ofNullable(val1).orElse(val2);

String value2 = Optional.ofNullable(null).orElse("default value");

orElseGet()方法

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

orElseGet()方法是orElse()方法的升级版本,其好处是可以防止orElse()方法传入NULL值,例如:

String val1 = null;
Supplier<String> fun = ()->"default value";
String value = Optional.ofNullable(val1).orElseGet(fun);

orElseThrow()方法

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

orElseThrow()方法相当于是orElse()方法和orElseGet()方法的增强版,提供了异常处理能力。需要注意的是,orElseThrow()方法处理异常时是抛出而不是捕获。例如:

String username = null;
String name = Optional.ofNullable(username).orElseThrow(IllegalArgumentException::new);

orElse()与orElseGet()的区别

对于不太熟悉Optional的程序员来说,orElse()方法和orElseGet()方法之间的区别并不明显,甚至会觉得这两个方法在功能上是重叠的。然而,这两个方法还是有着本质上的区别,如果不能很好的理解其中的差异,使用这两个方法会严重影响到代码的运行性能。接下来,我们先看一个简单的例子:

public class OptionalExample {

    public static void main(String[] args) {

        String text = null;//"Hello Optional";

        System.out.println("Using Optional.orElseGet() method...");
        String value = Optional.ofNullable(text).orElseGet(OptionalExample::defaultValue);
        System.out.println("orElseGet() method return value = " + value);


        System.out.println("Using Optional.orElse() method...");
        value = Optional.ofNullable(text).orElse(defaultValue());
        System.out.println("orElse() method return value = " + value);

    }


    public static String defaultValue(){
        System.out.println("Getting Default Value...");
        return "Default Value";
    }
}

defaultValue()方法用于返回默认的值,该方法不带任何请求参数。变量text一开始的值为NULL,运行main方法并观察控制台输出:

"C:\Program Files\Java\jdk1.8.0_66\bin\java.exe"...

Using Optional.orElseGet() method...
Getting Default Value...
orElseGet() method return value = Default Value

Using Optional.orElse() method...
Getting Default Value...
orElse() method return value = Default Value

在变量text为NULL的情况下,orElse()方法和orElseGet()方法具有同等的作用,没有任何区别。接下来,将变量text的值修改为“Hello Optional”,运行main方法并观察控制台输出:

"C:\Program Files\Java\jdk1.8.0_66\bin\java.exe"...

Using Optional.orElseGet() method...
orElseGet() method return value = Hello Optional

Using Optional.orElse() method...
Getting Default Value...
orElse() method return value = Hello Optional

当变量text有值的情况下,结果发生了改变。使用orElseGet()方法来判断text的值时,defaultValue()方法不会被执行,因为text值不为空,但使用orElse()方法来判断text值时,无论text的值是否为空,defaultValue()方法都会被执行,在这种情况下,defaultValue()方法就显得非常的冗余且影响代码性能。

为什么为产生这样的区别?原因在于orElse()方法需要提供一个类型变量,在orElse()方法工作之前,就需要确定类型变量的值,这也就是为什么在变量text有值的情况下defaultValue()方法依然被执行的原因;而orElseGet()方法的入参是对象的提供者(Supplier),只有当变量text为空的时候,才会调用对象提供者所提供的具体方法。

总结

本文详细介绍了Java 8 Optional类的基本用法、注意事项以及区别。其中值得注意的是of(),ofNullable()之间的区别,get()的使用条件,map()和flatMap()的区别,orElse和orElseGet()的使用场景。

JSON Web Token绝非银弹,“蹲坑”需谨慎小心!

JSON Web Token绝非银弹,“蹲坑”需谨慎小心!

越来越多的开发者开始学习JWT技术并在实际项目中运用JWT来保护应用安全。一时间,JWT技术风光无限,很多公司的应用程序也开始使用JWT(Json Web Token)来管理用户会话信息。本文将从JWT的基本原理出发,分析在使用JWT构建基于Token的身份验证系统时需要谨慎对待的细节。

​ 任何技术框架都有自身的局限性,不可能一劳永逸,JWT也不例外。接下来,将从JWT的概念,基本原理和适用范围来剖析为什么说JWT不是银弹,需要谨慎处理。

​ 众所周知,如果我们的账户信息(用户名和密码)泄露,存储在服务器上的隐私数据将受到毁灭性的打击,如果是管理员的账户信息泄露,系统还有被攻击的危险。那么,JWT的信息发生泄露,会带来什么样的影响?该如何防范?这将是本文重点阐述的内容。

1、什么是Token?

​ Token(令牌)通常是指Security Token(安全令牌),可以分为Hardware Token(硬件令牌),Authentication Token(授权令牌),USB Token(USB令牌),Cryptographic Token(加密令牌),Virtual Token(虚拟令牌)和Key Fob(钥匙卡)。其主要作用是验证身份的合法性,以允许计算机系统的用户可以操作系统资源。生活中常见的令牌如:登录密码,指纹,声纹,门禁卡,银行电子卡等。Token的主要目的是为计算机系统提供一个可以识别用户的任意数值,例如“token123”这样的明文字符串,或者像“41ea873f-3a4d-57c8-1e38-ef74f31015af”之类的加密字符。

​ 由于篇幅关系,Token就了解到这里。接下来将聊聊有关JWT(JSON Web Token)的原理。

2、什么是JSON Web Token?

​ JSON Web Token(JWT)是一个基于RFC 7519的开放数据标准,它定义了一种宽松且紧凑的数据组合方式,使用JSON对象在各应用之间传输加密信息。该JSON对象可以通过数字签名进行鉴签和校验,一般地,JWT可以采用HMAC算法,RSA或者ECDSA的公钥/私钥对数据进行签名操作。

​ 一个JWT通常有HEADER(头),PAYLOAD(有效载荷)和SIGNATURE(签名)三个部分组成,三者之间使用“.”链接,格式如下:

下面是的字符串是一个JWT的实际案例:

注意三者之间有一个点号(“.”)相连

​ 为了更直观的了解JWT的创建过程和使用方式,我们通过一个简单的例子来演示这两个过程。

3、如何创建JWT?

​ JWT通常由“标头.有效载荷.签名”的格式组成。其中,标头用于存储有关如何计算JWT签名的信息,如对象类型,签名算法等。下面是JWT中Header部分的JSON对象实例:

在此JSON对象中,type表示该对象为JWT,alg表示创建JWT时使用HMAC-SHA256散列算法计算签名。有效载荷主要用于存储用户信息,如用户ID,Email,角色和权限信息等。下面是有效载荷的一个简单示例:

而签名则需要使用Base64URL编码技术对标头(Header)和有效载荷(Payload)进行编码,并作为参数和秘钥一同传递给签名算法,生成最终的签名(Signature)。以HMAC-SHA256算法为例,下面是生成签名的一个伪代码:

​ 现在,我们已经了解了JWT的基本原理,接下来将使用Java来演示生成JWT的完整过程。

4、基于Java实现的JWT(JJWT)案例

4-1、依赖

以Maven工程为例,需要在pom.xml文件中添加入下的配置信息:

如果是非Maven工程,你也可以到Maven中央仓库搜索jjwt,然后选择相应的版本(0.9.0)下载到本地,并将jar包添加到工程的类路径(classpath)中。

4-2、生成JWT

​ 在工程中新建JJWTUitls.java工具类,使用jjwt提供的方法实现JWT的生成,实现细节如下:

在此方法中,JJWT已经处理好JWT标头(Header)的信息,我们只需要提供签名所使用的算法(如SignatureAlgorithm.HS256),有效载荷,主题(包含了用户信息),过期时间(exp-time)和秘钥即可,最后使用jjwt的builder()方法组装JWT。下面是生成秘钥方法key()的源代码:

4-3、解析JWT

​ 使用JJWT解析JWT相对简单,首先获取秘钥,然后通过Jwts.parse()方法设置秘钥并JWT进行解析,实现细节如下:

4-4、测试JJWT

​ 最后,在工程中新建一个JavaJWT.java类,并在main方法中检验JJWTUtils工具类中生成和解析JWT两个方法是否有效。实现细节如下:

如上图所示,“jwt”将作为JWT标头(Header)“type”的值,有效载荷(payload)中的主题信息如下:

且JWT签名的有效时间为60,000毫秒。执行main方法,输出信息如下所示:

​ 从测试结果可以看出,我们成功的使用JJWT创建并解析了JWT。接下来,我们将了解到在实际的应用中,JWT对用户信息进行验证的基本流程。

5、 JSON Web Token的工作流程

​ 在身份验证中,当用户成功登录系统时,授权服务器将会把JSON Web Token返回给客户端,用户需要将此凭证信息存储在本地(cookie或浏览器缓存)。当用户发起新的请求时,需要在请求头中附带此凭证信息,当服务器接收到用户请求时,会先检查请求头中有无凭证,是否过期,是否有效。如果凭证有效,将放行请求;若凭证非法或者过期,服务器将回跳到认证中心,重新对用户身份进行验证,直至用户身份验证成功。以访问API资源为例,下图显示了获取并使用JWT的基本流程:

​ 现在,我们已经完全了解了JWT是什么,怎么实现以及用来干什么这三个问题。在上述的案例中,我们使用HS256算法对JWT进行签名,在这个过程中,只有身份验证服务器和应用服务器知道秘钥是什么。如果身份验证服务器和应用服务器完全独立,则应用服务器的JWT校验工作也可以交由认证服务器完成。当客户端对应用服务器发起调用时,应用服务器会使用秘钥对签名进行校验,如果签名有效且未过期,则允许客户端的请求,反之则拒绝请求。

6、使用JSON Web Token的利弊

​ 优势与劣势是相对而言的,这里主要以传统的Session模式作为参考,总结使用JWT可以获得优势以及带来的弊端。

6-1、 使用JWT的优势

​ 使用JSON Web Token保护应用安全,你至少可以获得以下几个优势:

  1. 更少的数据库连接:因其基于算法来实现身份认证,在使用JWT时查询数据的次数更少(更少的数据连接不等于不连接数据库),可以获得更快的系统响应时间。
  2. 构建更简单:如果你的应用程序本身是无状态的,那么选择JWT可以加快系统构建过程。
  3. 跨服务调用:你可以构建一个认证中心来处理用户身份认证和发放签名的工作,其他应用服务在后续的用户请求中不需要(理论上)在询问认证中心,可使用自有的公钥对用户签名进行验证。
  4. 无状态:你不需要向传统的Web应用那样将用户状态保存于Session中。

6-2、使用JWT的弊端

​ JWT不是万能的,使用JWT也会带来诸多问题。就个人使用情况,使用JWT时可能会面临以下几个麻烦:

  1. 严重依赖于秘钥:JWT的生成与解析过程都需要依赖于秘钥(Secret),且都以硬编码的方式存在于系统中(也有放在外部配置文件中的)。如果秘钥不小心泄露,系统的安全性将收到威胁。
  2. 服务端无法管理客户端的信息:如果用户身份发生异常(信息泄露,或者被攻击),服务端很难向操作Session那样主动将异常用户进行隔离。
  3. 服务端无法主动推送消息:服务端由于是无状态的,他将无法使用像Session那样的方式推送消息到客户端,例如过期时间将至,服务端无法主动为用户续约,需要客户端向服务端发起续约请求。
  4. 冗余的数据开销:一个JWT签名的大小要远比一个Session ID长很多,如果你对有效载荷(payload)中的数据不做有效控制,其长度会成几何倍数增长,且在每一次请求时都需要负担额外的网络开销。

​ JSON Web Token 很流行,但是它相比于Session,OIDC(OpenId Connect)等技术还比较新,支持JSON Web Token的库还比较少,而且JWT也并非比传统Session更安全,他们都没有解决CSRF和XSS的问题。因此,在决定使用JWT前,你需要仔细考虑其利弊。

7、JSON Web Token并非银弹,“蹲坑”需谨慎

考虑这样一个问题:如果客户端的JWT令牌泄露或者被盗取,会发生什么严重的后果?有什么补救措施?

​ 如果单纯的依靠JSON Web Token解决用户认证的所有问题,那么系统的安全性将是脆弱的。由于JWT令牌存储于客户端中,一旦客户端存储的令牌发生泄露事件或者被攻击,攻击者就可以轻而易举的伪造用户身份去修改/删除系统资源,岁如按JWT自带过期时间,但在过期之前,攻击者可以肆无忌惮的操作系统数据。通过算法来校验用户身份合法性是JWT的优势,同时也是最大的弊端——它太过于依赖算法。

​ 反观传统的用户认证措施,通常会包含多种组合,如手机验证码,人脸识别,语音识别,指纹锁等。用户名和密码只做用户身份识别使用,当用户名和密码泄露后,在遇到敏感操作时(如新增,修改,删除,下载,上传),都会采用另外的方式对用户的合法性进行验证(发送验证码,邮箱验证码,指纹信息等)以确保数据安全。

​ 与传统的身份验证方式相比,JWT过多的依赖于算法,缺乏灵活性,而且服务端往往是被动执行用户身份验证操作,无法及时对异常用户进行隔离。那是否有补救措施呢?答案是坑定的。接下来,将介绍在发生令牌泄露事件后,如何保证系统的安全。

8、使用JSON Web Token 爬坑指南

​ 不管是基于Sessions还是基于JSON Web Token,一旦密令被盗取,都是一件棘手的事情。接下来,将讲述基于JSON Web Token的方式发生令牌泄露是该采取什么样的措施(解决方案包含但不局限与本文所涉及的内容)。

为了防止用户JWT令牌泄露而威胁系统安全,你可以在以下几个方面完善系统功能:

  1. 清除已泄露的令牌:此方案最直接,也容易实现,你需将JWT令牌在服务端也存储一份,若发现有异常的令牌存在,则从服务端令牌列表中将此异常令牌清除。当用户发起请求时,强制用户重新进行身份验证,直至验证成功。对于服务端的令牌存储,可以借助Redis等缓存服务器进行管理,也可以使用Ehcache将令牌信息存储在内存中。
  2. 敏感操作保护:在涉及到诸如新增,修改,删除,上传,下载等敏感性操作时,定期(30分钟,15分钟甚至更短)检查用户身份,如手机验证码,扫描二维码等手段,确认操作者是用户本人。如果身份验证不通过,则终止请求,并要求重新验证用户身份信息。
  3. 地域检查:通常用户会在一个相对固定的地理范围内访问应用程序,可以将地理位置信息作为一个辅助来甄别用户的JWT令牌是否存在问题。如果发现用户A由经常所在的地区1变到了相对较远的地区2,或者频繁在多个地区间切换,不管用户有没有可能在短时间内在多个地域活动(一般不可能),都应当终止当前请求,强制用户重新进行验证身份,颁发新的JWT令牌,并提醒(或要求)用户重置密码。
  4. 监控请求频率:如果JWT密令被盗取,攻击者或通过某些工具伪造用户身份,高频次的对系统发送请求,以套取用户数据。针对这种情况,可以监控用户在单位时间内的请求次数,当单位时间内的请求次数超出预定阈值值,则判定该用户密令是有问题的。例如1秒内连续超过5次请求,则视为用户身份非法,服务端终止请求并强制将该用户的JWT密令清除,然后回跳到认证中心对用户身份进行验证。
  5. 客户端环境检查:对于一些移动端应用来说,可以将用户信息与设备(手机,平板)的机器码进行绑定,并存储于服务端中,当客户端发起请求时,可以先校验客户端的机器码与服务端的是否匹配,如果不匹配,则视为非法请求,并终止用户的后续请求。

总结

​ 本文从Token的基本含义,JSON Web Token的原理和流程出发,并结合实际的案例分析了使用JSON Web Token的优势与劣势;与此同时,结合自己实际使用JSON Web Token过程中发现的问题给出了避免“踩坑”的解决方案。

​ 世上没有完美的解决方案,系统的安全性需要开发者积极主动地去提升,其过程是漫长且复杂的,也许一开始的MVP系统并不需要那么强大的安全性,但随着业务的增长系统需要升级,或者说最终将重写整个系统,提前了解技术背后可能会遇到的问题,不失为一种好的编程习惯。

​ JSON Web Token的出现,为解决Web应用安全性问题提供了一种新思路。但JSON Web Token也不是银弹,你任然需要做很多复杂的工作才能提升系统的安全性。

实现SaaS(软件及服务)架构所面临的三大技术挑战

实现SaaS(软件及服务)架构所面临的三大技术挑战

​ 在当下的世界中,软件应用无处不在,随处都可见电子产品的身影,一个没有任何电子产品的生活是什么样子让人很难想象。而支撑起这些电子产品的是无数个应用程序和其背后所隐藏的实现技术。

​ 无论你需要做什么,都可以找到大量的应用程序供你选择。软件的本质工作是简化复杂的业务流程,让生活更简单,更有趣。然而,在享受软件带来便捷的同时,也伴随着一定的副作用,比如你需要安装大量的应用软件,且需要为这些应用软件支付一大笔费用,同时还需要关心软件升级维护的问题。为了最大程度的降低使用软件所带来的副作用,SaaS(软件及服务)应势而生。

​ 在开始探讨本文的主题之前,先花一分钟了解什么是SaaS应用程序以及使用SaaS软件的优势。

1. 什么是SaaS应用软件?

​ SaaS是Software as a Service(软件即服务)的缩写,它是云计算的主要体现形式之一,其他的还有诸如平台即服务(PaaS),基础设施即服务(IaaS)以及数据即服务(DaaS)等。简而言之,SaaS是一种软件交付模式,旨在以“即用即付”服务的方式为客户提供软件服务,客户不需要安装软件便可获得应用程序提供的所有功能。

​ SaaS是一种基于云计算的软件交付系统,客户只需要通过浏览器便可轻松订阅应用程序所提供的功能。这种“按需使用”或“软件+服务”的软件交付系统,被广泛应用于各大中小企业,组织中,以帮助他们降低软件的使用成本。当下,SaaS软件正被用于各种行业的业务领域中,例如:客户关系管理软件(CRM),企业资源规划软件(ERP),信息管理软件(MIS),人力资源管理软件(HRM),地理信息管理软件(GIS)以及学习管理软件(SIS)等等。

2. 使用SaaS应用软件的优势

​ SaaS软件交付模式的概念最早于20世纪60年代被提出,到20世纪90年代,随着互联网技术的不断发展,SaaS的发展速度也随之加快,许多软件提供商开始通过互联网托管和提供软件服务,而SaaS(软件即服务)这个术语也是在这一时期确定下来的。使用SaaS软件在以下几个方面可以获得较大优势:

  • 1 在软件使用早期,可以大幅度降低硬件,带宽,安装和运营成本。
  • 2 可以以更小的人员和资金的开销,准确的预测业务数据,更快的完成软件的升级和维护工作。
  • 3 与传统软件相比,用户通过互联网获的软件服务,从而降低了管理软件所带来的安全风险。
  • 4 快速部署,快速接入。由于不需要自行安装和部署软件,只需要向服务提供商注册信息,并为订阅的服务付费,便可获得软件服务。从而减少了获得软件的等待时间。
  • 5 跨平台操作。借助互联网,客户可以在任何时间,任何地点,通过浏览器便可接入服务。

​ 任何有价值的东西,在其出现之前都会历经各种困难与挑战,没有什么事物是一蹴而就的。接下来,将介绍构建一个SaaS软件在系统测试,数据安全和升级维护三个方面面临的巨大挑战。

3.实现SaaS软件挑战一#测试

​ 任何有价值的软件在交付之前,都需要进行大量的测试。这和我们买衣服一样,在确定付款前,都需要在试衣间试穿各种样式的衣服。SaaS软件的测试与传统的软件测试基本相同,通过执行各种逻辑验证,确保作为服务提供的软件在质量上是否符合客户的需求。SaaS软件的测试通常会包含数据安全测试,业务逻辑测试,数据集成测试,接口兼容测试,可伸缩测试和高并发测试。但SaaS软件的测试也有有别于传统软件测试的地方。

​ 与传统软件开发相比,SaaS软件的测试在测试周期和实施等方面都面临着诸多的挑战。尽管实现SaaS软件的技术框架会有所不同,但在测试环节都将面临一些常见的技术挑战。

3.1. 安全性测试

​ SaaS软件可以为使用者带来巨大的好处,但仍然有很多用户在质疑SaaS应用软件的数据安全。基于云计算应用的数据安全一直以来都是一个居高不下的热点话题。因此,针对SaaS软件的安全性测试必须慎重对待,需要有专门的测试策略和工具。

​ 与其他任何的云计算平台一样,在云计算环境下维护数据的安全性和完整性具有很大的风险与挑战,和传统的软件相比,SaaS应用软件的安全性测试更为复杂。在SaaS应用软件测试中,需要模拟多个租户下,不同安全级别的隐私要求,权限分配粒度,资源隔离等级和用户行为模式。传统的测试手段很难测试并发现SaaS软件中存在的安全漏洞,软件测试场景也很难完全发现漏洞并消除这些安全威胁。

3.2. 可伸缩和高可用测试

​ 可伸缩(也称可扩展)性是SaaS软件服务提供商重要的商业模型指标之一,要求SaaS应用软件可以根据客户量的大小进行水平方向的伸缩。简单来说,SaaS平台可以根据当前用户量的多少,动态地增加或者减少运行实例的数量;而高可用(性能)是客户衡量SaaS软件好坏的一个重要指标。

​ 如何成功的测试SaaS软件的可伸缩和高可用性,需要有专门的测试策略,才能组织出可用的测试场景,并且需要比传统软件测试更多的测试样本数据和测试标准。这些数据和标准需要仔细的考虑SaaS软件的应用场景,才可能被量化和设计。另外,还需要考虑如何在不同租户类型,不同的用户数量组合,不同使用环境(移动端,PC端)的复杂条件下,对系统的性能,峰值和负载能力进行测试。

3.3. 集成和开放API接口测试

​ SaaS应用软件在一定程度上需要集成第三方的业务系统,同时还有可能需要开放一定的API接口,以支持从其他平台集成或迁移数据。在何种情况下,保护数据的安全性和完整性将给测试带来巨大的压力。在SaaS应用软件的集成测试和API测试中,需要对入站和出站数据进行验证,以及对所有API的功能,安全性,性能以及文档的完整性进行测试。即便是这样,你也很难提前组织起所有的测试场景,而且这个过程非常的耗时。也许第一版的API还没有测试完,新版本的API已经添加到测试列表。实现SaaS软件是一个不断迭代的过程,因此很难在短时间内一次测试就涵盖所有的API。

4. 实现SaaS软件挑战二#数据安全

​ 对于SaaS软件而言,更为复杂和艰难的工作是如何保障SaaS平台中用户数据的安全。不管是大型企业还是小公司,他们对于数据的安全性要求都是一样严苛的。例如常见的CRM系统,HR系统,ERP系统和财务管理系统等,它们都存储了大量高度敏感的用户信息,如果SaaS软件的数据安全无法得到保障,不仅是客户的数据会遭受破坏,服务提供商的信誉也会收到严重的影响,甚至会伴随着相关法律的处理。因此,相比于传统的软件,SaaS软件的安全性要求更高,系统结构更为复杂,实现难度也更大。

​ 构建一个SaaS平台,在满足高性能和可伸缩的条件下,还需要着力保障用户数据的高度安全,这主要体现在以下三个方面:

  • 1 多租户数据隔离:多租户架构是SaaS软件的一个重要评定标准,如何对各个租户的数据进行识别,分割和存储需要在效费比,安全性和性能上取得一个平衡。
  • 2 数据备份与恢复:由于各租户订阅服务的组合不同,使用软件的时间段也不尽相同,对不同租户的数据进行备份和恢复的难度也相当大。另外,在操作其中一个租户数据时,需要保障其他所有租户的数据不会受到影响。在发生系统故障时,还需要及时恢复租户数据,面对庞大的数据量,数据恢复的复杂度和难度都比单体应用高很多。
  • 3 数据入站和出站校验:SaaS软件允许用户在任何地点通过浏览器获取服务,这就要求SaaS软件在数据传输,用户输入,系统输出等环节有着更高的安全性要求。相比于单体架构的应用程序,需要更高级别的安全传输加密/解密手段,更细粒度的用户认证和鉴权措施。另外,对用户的日志的收集,追踪和审计工作比单体软件更难。

​ 保护存储在SaaS平台中的数据安全,需要仔细的分析平台中每一个业务流程,细化权限下放的粒度,严格把控访问接入设置以及数据的存储规则。即便如此,由于SaaS系统通常需要面对比单体架构软件更为庞大的数据量,且业务流程更为复杂,更新周期相对较短,因此如何确保存储在SaaS平台中的数据不易被破坏或者泄露是一个永久存在的工作。

5.实现SaaS软件挑战三#升级维护

​ SaaS应用软件免去了客户使用软件时的安装,维护,升级等工作,但这些繁杂的工作并未因为SaaS而消亡,而是转移到了SaaS软件服务商的手中,且工作变得更为复杂和艰难。

​ SaaS软件需要全天候为客户提供可用的服务,因为你完全不知道客户会在什么时候登入系统开展工作。这就要求SaaS服务提供商在升级和维护软件的工程中不能影响当前用户使用软件。简单来说,SaaS软件的升级维护不能采取“冷启动”的方式来完成,需要采用“热部署”的方式,让客户基本上感觉不到升级工作正在进行。反观传统软件上线或更新过程,往往会出现各种问题:不一致的运行环境,过多的人为干预系统的构建和部署,代码改动引起不可控的质量,向下不兼容,服务中断,更长的更新时间,数据丢失等问题。

​ 对于SaaS系统,系统的升级维护工作不能暂停当前客户正在执行的业务,避免业务数据丢失,因此需要一种全新的软件发布机制,通过可视化,自动化的操作,实现持续,无缝,零重启的软件交付过程。在升级维护时,SaaS软件主要面临以下几个挑战:

  • 1 版本可回退:如果新上线的功能模块遇到重大问题,可以回退到之前的版本而不影响用户的正常业务。
  • 2 系统向下兼容:新版本的系统需要尽可能的向下兼容旧系统的数据。在最坏的情况下,当升级过程发生时,用户正在使用旧版本提交数据,如果适配旧版本提交的数据,需要慎重考虑。
  • 3 灰度发布:灰度发布包含两个方面:前后端灰度发布和移动端的灰度发布。
  • 4 零重启:零重启要求在不终止服务的情况下完成系统的升级工作,这就要求SaaS平台具备热部署的特性,确保SaaS平台能够保持7x24小时的持续服务能力。

​ SaaS软件易于使用是相对于软件使用者而言的,对于SaaS软件的提供者来说,软件的复杂性,安全性和可用性都面临者全新的技术难题,克服这些问题并不容易。作为SaaS软件的实现者,不能看着油漆是干燥的,就想当然的坐上去,在油漆真正干燥前,一切都是脆弱的,需要耗费大量的时间去处理诸多问题,才能让用户放心的坐上去。

​ 市场对于SaaS软件的需求逐年递增,各种类型的SaaS软件产品也层出不穷,但真正成功的SaaS软件却凤毛麟角。究其原因,主要还是SaaS化的软件并不是简单的将传统软件改造成多租户架构那么简单,用户对于易用性,集成性,安全性,灵活性和定制性的要求越来越高,作为SaaS软件的实现者和提供者,需要付出比对待传统软件更多的精力,更严苛的要求,更谨慎的考虑,才能正确认识到实现SaaS软件将要面临的技术难题与挑战。

​ 基于云计算的SaaS软件交互模式,其多租户,多平台环境,高并发等特点给SaaS软件的实现带来了诸多的技术难点,作为SaaS软件的缔造者和提供者,需要全面和谨慎的考虑和处理这些技术问题,方可缩短软件承诺与用户期望之间的差距。提前识别并跟踪不断变化的需求与技术走向,谨慎的考虑架构与实现之间存在的问题,才能在打磨出一款有商业价值的SaaS产品。

宠物与牛-全栈工程师的神话与谎言

宠物与牛-全栈工程师的神话与谎言

宠物与牛-全栈工程师的神话与谎言

最近几年,在软件开发(尤其是Web开发)领域,“全栈工程师”成为了开发者津津乐道的热词。你或许会听到这样一些话语:“我正在学习全站开发”,“我是全栈工程师”,”我们正在招聘全栈工程师“。那么,”全栈“到底意味着什么?出自什么地方?具体含义是什么?带着这样三个问题,我们将一步步去揭露全栈工程师的神话与谎言。

​ 也许你不止一次的看到过且认为下面的这个等式是正确的,而且高大上:

如果这个等式在软件开发中是一个真命题,那么我们可以得出这样一个推导式:

那么,如果一个软件开发人员不会前端或者后端,亦或是不会基础架构开发,那么他就不是软件工程师。显然,这样的推论是不正确的,也就是说上述关于全栈工程师的定义在软件开发中是荒谬的,是一个被精心包装过的神话。

”full stack“(全栈)一词只是一个商业上的流行语,而不是指代具体的工程或者某一种技术。真正的软件开发者不会用”全栈“一词来描述自己的技术,”全栈开发“是一个没有任何意义的定义,它就好比”五彩斑斓的黑“一样荒谬。也许使用这一词语的开发者是想在应聘时取悦面试官,留下比较好的印象,因为谁都不想在面试上表现平平,对于招聘者而言,当他们提及”全栈“一词时,更多的是想寻找一个技术领袖,而非真正的”全栈“开发。

​ 那么“全栈开发”这个技术神话,是如何被成功塑造出来的呢?

​ “全栈开发”杜撰于商业解决方案中对技术问题的描述,全栈是指一个包含完整技术解决方案的全部技术框架。以Web开发为例,全栈就意味着从前端(用户交互界面)到后端(业务系统,数据库,操作系统等)的全套技术套件。而这种端到端(客户端-服务器端)的解决方案需要依赖于诸多技术,如TCP/IP协议,硬件设施,防火墙,路由,负载均衡,网络代理,Web容器,数据存储,操作系统等。这里的全栈,更为明确的是指代技术的完整性,即全栈开发等同于完整的技术解决方案:

从表面上看,全栈开发工程师很合乎逻辑,它也成了技术大咖一种能力的象征。前面说过,全栈开发意味着完整的技术解决方案,如果全栈开发工程师这一命题成立,那么相当于一名全栈工程师就代表这一个完整的技术解决方案,很明显,这是一个伪命题。即便是一个天才或者通才,他也不能说自己所掌握的开发技术代表着一个完整的解决方案,这就是为什么说全栈工程师只是一个神话的原因。

​ 接下来,让我们看看一些生活中常见的例子,以揭示全栈开发工程师中所隐藏的谎言。

​ 对于开发者,当被问及“你是全栈开发人员吗?”,其实他正真的意思是想知道你是否会使用数据库,操作系统,使用一种或更多的编程语言编写业务代码,以及是否会操作HTML页面。如果你是一名软件工程师,你给出的答案是肯定的,但这并不意味着你是一个全栈开发人员,因为全栈开发人员这个术语本身没有任何意义。实际的情况是,任何技术都对于其专门的岗位,例如UI工程师,Java工程师,PHP工程师,运维工程师,DBA,网络工程师,系统架构师等等,他们都有着自身专注的领域,如果按照全栈工程师的定义,一个UI工程师不具备 系统架构的能力,那么是否就可以说他不是一名软件工程师呢?这是一种错误的认识。我们不能将描述技术解决方案的术语转嫁到具体的开发岗位上,因为人不能代表一个完整的技术解决方案。

​ 对于企业,当其想要招聘一个“全栈工程师”时,就像在找“五彩斑斓的黑”一样没有意义,因为它根本就不存在。只有不同色度的黑色,而不存在如彩虹般绚烂的黑色。但奇怪的是,当涉及此问题是,面试者往往会列举很多案例来证明他是全栈开发人员,这是因为他要取悦面试官,没有任何人会说自己是“半吊子”工程师,更不会说自己不是软件工程师,所以才导致一个谎言只能换来另外一个谎言的结局。当你有招聘“全栈工程师”的想法时,不妨将其更换为“优秀的软件工程师”,“高级Java工程师”,“经验丰富的UI工程师”或者“五年以上工作经验的软件工程师”这一类术语更为有效。明确需求,才能找到合适的人选,细化问题,才能得到真实的答案。

​ 如果公司正处于初创期或者只有一两名开发人员,在资源(人力,财力)相对缺乏的情况下,“全栈开发人员”确实很诱人,也很适合你。因为在这种情况下你别无选择,你只能相信“全栈开发人员”能够为你提供一套完整的技术解决方案,并成功实施该它。但也不能因为开发人员数量少而将人和方案等同起来,在这种情况下,你所说的“全栈开发人员”应该是指公司的技术领袖,他们对软件开发有着强大的知识储备,同时具备较好的编程能力,能在创业初期为你提供最小化的,可用的技术解决方案以及MVP(Minimum Viable Product,即最小化可用产品)。当你的公司步入成长期时,需要摒弃寻找“全栈工程师”这种无意义的想法。因为在此时期,需要完善最小化的技术解决方案,将解决方案进行拆分,投入更多的人力(开发人员)到各子方案中,才有可能使一个完整的技术解决方案得以顺利实施。这是一个技术组件的综合体,并不是单靠个人能完成的任务。在实际的开发工作中,为了解决方案种涉及的技术问题,公司工作岗位上会出现如系统架构师,项目经理,数据分析师,UI/UX工程师,DBA,Java工程师,IOS工程师,Android工程师,测试工程师等一系列配套的角色,各角色之间分工明确,各司其职。一个人无论其能力再强,也无法在同一时间内,完成多个角色的多项工作,除非公司能排除时间因素,在软件开发的总体拥有成本中(TCO)将时间范围(Time Scope)剔除。任何以盈利为最终目的的商业公司都不会允许这种愚蠢的事情发生。因此,“全栈工程师”对于企业而言,也是一个神话臆想和天大的谎言。

​ 下面,我们再从技能组成上来分析为什么说全栈开发者是一个谎言。以Java Web开发为例,如果“全栈开发人员”不是一个神话,那么他需要在有限的时间内,高质量的完成下表所列举的工作。表中列举的内容并非完整内容,只是列举了一些相对重要的知识点。在这其中,一些技能可能只需数个小时即可掌握,而有一些技能需要数月甚至数年才能融会贯通。人们对于“全栈开发人员”的期望远远超出了普通人的适应能力,甚至有些“丧心病狂”。

​ 如果你是一名“全栈开发人员”,那么就意味着需要对每一种技术组件都有足够的理解,以便在方案实施的过程中快速作出合理的决策,并能够利用该项技术完成系统的开发。与此同时,还需要有较强的沟通能力,能够以简单明了的语言向上层决策者阐明使用该技术的必要性和合理性。但众所周知的是,各种技术组件的迭代更新周期远比我们掌握的速度快,每隔几个月,或者几年,就会有新的技术组件发布,作为“全栈开发人员”就需要在解决方案,方案实施,学习新组件这三者之间疲于奔命,如果有一种或几种技术组件不能及时掌握,抑或是无法掌握,那是否可以做出这样的判断:你将不再是一名软件工程师呢?显然,这是一种错误的,不合理的定义。

​ 既然说“全栈开发人员”是一个神话,那现在该如何正确的理解这个词呢?

​ 就我的理解而言,如果被问及“是否是全栈开发人员”,正确的回答是:“我是一名优秀的软件工程师”。如果对方不理解其中的含义,那么他可能是一个对软件工程缺乏认知的人,对软件开发的整套流程知之甚少,对于样的人,可以忽视其问题的存在。上述的回答有两层的含义,第一是具有完整的软件工程专业知识,这些知识体系能帮助开发者在提出解决方案时合理的选择相应的技术组件,其次是具备较强的学习能力和编码能力,能够在有限的时间范围内专注于完成一件事情并协同其他人员完成开发任务。因此,应该将“全栈开发”一分为二,分为“全栈”和“开发”,前者表示完整的计算机理论知识,后者则表示丰富的编码经验。此时再将二者结合则有一个合理的定义:利用完整的计算机知识,完成产品的研发工作。

​ 企业需要技术领袖而非技术神话,但不是每一个开发人员都是技术领袖。如果将企业比作一个“养殖场”,那么开发人员就好比被“饲养”的“动物”,技术大咖或者技术领袖好比是牛,牛体型大,输出的产品值高,但其“饲养”周期长,成本高,在短时间内很难有明显的产出,且适应市场需求变化的能力较差;作为“养殖场”的“饲养员”,不能一味的只“养殖”牛,还应根据消费市场的变化,养殖鸡,鸭,鹅,兔等宠物(小体型)级别的“经济动物”。这些宠物的特点是“生长”周期短,“饲养”成本低,且在有限时间范围内能够灵活的适应市场需求,单位时间内输出的经济产值远比牛的高。软件开发者需要完整的知识储备而不是自欺欺人的技术谎言,完整的,成体系的计算机知识,以及精益求精的专注精神,才能让自己称为一名优秀的软件工程师,架构师或者技术专家。

​ 如果你现在是一名软件开发的求职者,是时候对“全栈开发人员”这种神话坚决说不,粉碎“全栈工程师”的谎言,勇于的对自己的技能做出明确定义,如Web工程师,Android工程师,IOS工程师或者系统架构师;如果你是一名面试主官,请不要再使用“全栈工程师”,“全栈开发人员”这类毫无意义的词汇,可以更换为更具体的”UI工程师”,“系统架构师”或“技术专家”,无需要使用“我们的技术人员都是全栈开发者”这类谎言来安慰决策者和员工,请将“全栈”一词用于描述完整的技术解决方案中。软件开发作为一种讲究科学性,严谨性,系统性的工作,不需要神话和谎言来粉饰,脚踏实地才是王道。作为一名优秀的软件工程师(软件开发人员),需要使自身的计算机知识体系化,完整化,专业化,专注于某一领域而博览众长,才是对“全栈”最好的诠释。对于企业,需要“核常”兼备,全面发展,维系好宠物与牛之间的平衡关系,才能为用户提供正真意义上的全栈解决方案。

开端-Java设计模式导读

开端-Java设计模式导读

开端-Java设计模式导读

和往常一样,本篇文章依旧采用“3W”顺序(即What,Why和Where)来回答软件工程中的设计模式是什么,为什么需要设计模式以及在什么地方使用设计模式这三个问题。

​ 本篇文章时Java设计模式系列技术文章的开篇,作为导读文章,将快速地对设计模式的基本概念、模式分类和适用范围进行解读。在后续的章节中,将对每一种类别的设计模式进行详细的讲解,讲解的内容包括每种设计模式的基本原理、适用范围和实战案例剖析三个部分。

1 模式的基本概念

​ 模式是指解决某个特定领域问题,实现既定目标的方法或思想。具体来说,模式是那些身处于某个行业的从业人员根据实际的工作经验总结出的,具有通用性的且被行业公认的解决问题的方法或流程。模式并非只在软件工程中被应用,其在日常的生产活动中被广泛地使用,如制造业,餐饮业,建筑设计、医疗卫生、教育培训以及软件工程等都有模式的身影。

2 什么是设计模式?

​ 首先,设计模式是一种模式。在软件工程中,设计模式是一种通用的、可重复使用的用于解决既定范围内普遍发生的重复性问题的软件设计方法。使用成熟可靠的设计模式,可以提高代码复用性,节省开发时间,从而实现功能更强大、高度可维护的代码。这有助于降低软件产品的总体拥有成本,即TCO(Total Cost of Ownership)。另一方面,由于采用了统一的标准设计方法(思想或理论知识),可以显著提升开发团队的生产效率和协作能力。

3 Java设计模式的分类

​ 在Java编程语言中,常用的设计模式可分为三种类型:

  • 建造类设计模式:主要用于定义和约束如何创建一个新的对象
  • 结构类设计模式:主要用于定义如何使用多个对象组合出一个或多个复合对象
  • 行为类设计模式:主要用于定义和描述对象之间的交互规则和限定对象的职责边界线

图3-1 设计模式分类

3.1 建造类设计模式

​ 建造类共包括五(5)种基本设计模式:单例模式,工厂模式,抽象工厂模式,建造器模式和原型模式,如图3-2所示:

图3-2 建造类设计模式

3.2 结构类设计模式

​ 结构类共包括八(8)种基本设计模式:适配器模式,组合模式,代理模式,享元模式,过滤器模式,桥接模式,修饰模式和外观模式,如图3-3所示:

图3-3 结构类设计模式

3.3 行为类设计模式

​ 行为类共包括十一(11)种基本设计模式:模板方法模式,解释器模式,责任链模式,观察者模式,战略模式,命令模式,状态模式,访客模式,转义模式,迭代器模式和备忘录模式,如图3-4所示:

图3-4 行为类设计模式

​ 设计模式不仅仅只有上述描述的这三大类,除此之外还有许多的设计模式。现已知的设计模式还有100多种,如DAO模式,依赖注入模式和MVC模式等。

4 快速理解设计模式

​ 在接下来的内容中,将快速对Java中常见的24中设计模式的基本概念进行梳理,以求对各种设计模式的原理和适用范围有一个大致的认识。

4.1 建造类

​ 建造类设计模式提供了对创建对象的基本定义和约束条件,以寻求最佳的实例化Java对象解决方案。

4.1.1 单例模式-Singleton

​ 单例模式限制类的实例化过程,以确保在Java虚拟机(JVM)中有且只有一个类的实例化对象。单例模式是Java中最常用,也是最简单的设计模式之一。单例模式通常需具备如下的几个特征:

  • 单例模式限制类的实例化,且Java虚拟机中只能存在一个该类的示例化对象
  • 单例模式必须提供一个全局可用的访问入口来获取该类的实例化对象
  • 单例模式常被用于日志记录,驱动程序对象设计,缓存以及线程池
  • 单例模式也会被用于其他的设计模式当中,如抽象工厂模式,建造者模式,原型模式等

单例模式的Java类的内部结构如图4-1所示:

图4-1 单例模式类图

下面是单例模式的一份示例代码清单:

4.1.2 工厂模式-Factory

​ 在Java程序设计过程中,当一个超类(super class)具有多个子类(sub class),且需要频繁的创建子类对象时,我们可以采用工厂模式。工厂模式的作用是将子类的实例化工作统一交由工厂类来完成,通过对输入参数的判断,工厂类自动实例化具体的子类。实现工厂模式需要满足三个条件:

  • 超类(super class):超类是一个抽象类
  • 子类(sub class): 子类需继承超类
  • 工厂类(factory class):工厂类根据输入参数实例化子类

图4-2为Java工厂模式的类图:

图4-2 工厂模式UML类图

下面是工厂模式的一份示例代码清单:

4.1.3 抽象工厂模式-Abstract Factory

​ 抽象工厂模式与工厂模式很类似,抽象工厂模式可以简单的理解为“工厂的工厂”。在工厂模式中,根据提供的输入参数返回产品类的实例化对象,这个过程需要通过if-else或者switch这样的逻辑判断语句来完成具体子类的判定。而在抽象工厂模式中,每种产品都有具体的工厂类与之对应,从而避免在编码过程中使用大量的逻辑判断代码。抽象工厂模式会根据输入的工厂类型以返回具体的工厂子类。抽象工厂类只负责实例化工厂子类,不参与商品子类的实例化工作。图4-3是抽象工厂模式的UML类图:

图4-3 抽象工厂模式

4.1.4 建造器模式-Builder

​ 建造者模式通常被用于需要多个步骤创建对象的场景中。建造者模式的主要意图是将类的构建逻辑转移到类的实例化之外,当一个类有许多的属性,当在实例化该类的对象时,并不一定拥有该实例化对象的全部属性信息,便可使用建造者模式通过逐步获取实例化对象的属性信息,来完成该类的实例化过程。而工厂模式和抽象工厂模式需要在实例化时获取该类实例化对象的全部属性信息。图4-4展示了建造器模式的基本逻辑关系:

图 4-4 建造器模式UML类图

4.1.5 原型模式-Prototype

​ 原型模式的主要作用是可以利用现有的类通过复制(克隆)的方式创建一个新的对象。当示例化一个类的对象需要耗费大量的时间和系统资源时,可是采用原型模式,将原始已存在的对象通过复制(克隆)机制创建新的对象,然后根据需要,对新对象进行修改。原型模式要求被复制的对象自身具备拷贝功能,此功能不能由外界完成。图4-5展示了原型模式的基本逻辑:

图4-5 原型模式UML类图

4.2 结构类

​ 结构类设计模式主要解决如何通过多个小对象组合出一个大对象的问题,如使用继承和接口实现将多个类组合在一起。

4.2.1 适配器模式-Adapter

​ 适配器模式的主要作用是使现有的多个可用接口能够在一起为客服端提供新的接口服务。在适配器模式中,负责连接不同接口的对象成为适配器。在现实生活中,我们也能够找到很多实际的案例来理解适配器的工作原理,例如常用的手机充电头,在手机和电源插座之间,手机充电头就扮演一个适配器的角色,它能够同时适配220V,200V,120V等不同的电压,最终将电转换成手机可用的5V电压为手机进行充电。图4-6展示了适配器的基本原理:

图 4-6 适配器模式UML类图

4.2.2 组合模式-Composite

​ 组合模式的主要作用是让整体与局部之前具有相同的行为。例如我们需要绘制一个图形(正方形,三角形,圆形或其他多边形),首先需要准备一张空白的纸,然后是选择一种绘制图案的颜色,再次是确定绘制图案的大小,最后是绘制图案。不管是绘制正方形还是三角形,都需要按照这个步骤进行。在软件设计过程中,组合模式的最大意义在于保证了客户端在调用单个对象与组合对象时,在其操作流程上是保持一致的。图4-7展示了组合模式的基本原理:

图 4-7 组合模式UML类图

4.2.3 代理模式-Proxy

​ 代理模式的主要作用是通过提供一个代理对象或者一个占位符来控制对实际对象的访问行为。代理模式通常用于需要频繁操作一些复杂对象的地方,通过使用代理模式,可以借由代理类来操作目标对象,简化操作流程。图4-8展示了代理模式的基本原理:

图 4-8 代理模式UML类图

4.2.4 享元模式-Flywight

​ 享元模式的主要作用是通过共享来有效地支持大量细粒度的对象。例如当需要创建一个类的很多对象时,可以使用享元模式,通过共享对象信息来减轻内存负载。如果在软件设计过程中采用享元模式,需要考虑以下三个问题:

  • 应用程序需要创建的对象数量是否很大?
  • 对象的创建对内存消耗和时间消耗是否有严格的要求?
  • 对象的属性是否可以分为内在属性和外在属性?对象的外在属性是否支持有客户端定义?

图4-9展示了享元模式的基本原理:

图 4-9 享元模式UML类图

4.2.5 外观模式-Facade

​ 外观模式的主要作用是为子系统中的一组接口提供一个统一的接口,以便客户端更容易去使用子系统中的接口。简单的理解是外观模式为众多复杂接口定义了一个更高级别的接口。外观模式的目的是让接口更容易被使用,图4-10展示了外观模式的基本原理:

图 4-10 外观模式UML类图

4.2.6 桥接模式-Bridge

​ 桥接模式的主要用途是将抽象类与抽象类的具体实现相分离,以实现结构上的解耦,使抽象和实现可以独立的进行变化。桥接模式的实现优先遵循组合而不是继承,当使用桥接模式时,在一定程度上可以在客户端中因此接口的内部实现。图4-11展示了桥接模式的基本原理:

图 4-11 桥接模式UML类图

4.2.7 修饰模式-Decorator

​ 修饰模式的主要作用是在运行时动态的组合类的行为。通常,你会添加一些新的类或者新的方法来扩展已有的代码库,然而,在某些情况下你需要在程序运行时为某个对象组合新的行为,此时你可以采用修饰模式。图4-12展示了修饰模式的基本原理:

图 4-12 修饰模式UML类图

4.2.8 过滤器模式-Filter

​ 过滤器模式是使用不同的标准来过滤一组对象,通过逻辑运算以解耦的方式将对象组合起来。图 4-13展示了过滤器模式的基本原理:

图 4-13 过滤器模式

4.3 行为类

​ 行为类设计模式主要用于定义和描述对象之间的交互规则和职责边界,为对象之间更好的交互提供解决方案。

4.3.1 模板方法模式-Template Method

​ 模板方法模式的主要作用是在一个方法里实现一个算法,可以将算法中的的一些步骤抽象为方法,并将这些方法的实现推迟到子类中去实现。例如建造一栋房子,我们需要设计图纸,打地基,构筑墙体,安装门窗和内部装修。我们可以设计不同的房屋样式(别墅,高楼,板房等),不同的门窗和不同的装修材料和风格,但是其顺序不能颠倒。在这种情况下,我们可以定义一个模板方法,规定方法的执行顺序,而将方法的实现推迟到子类中完成。图4-14展示了模板方法模式的基本原理:

图 4-14 模板方法模式UML类图

4.3.2 解释器模式-Mediator

​ 解释器(中介)模式的主要设计意图是定义一个中间对象,封装一组对象的交互,从而降低对象的耦合度,避免了对象间的显示引用,并可以独立地改变对象的行为。解释器(中介)模式可以在系统中的不同对象之间提供集中式的交互介质,降低系统中各组件的耦合度。图 4-15展示了解释器(中介)模式的基本原理:

图 4-15 解释器(中介)模式UML类图

4.3.3 责任链模式-Chain of Responsibility

​ 责任链模式主要作用是让多个对象具有对同一任务(请求)的处理机会,以解除请求发送者与接收者之间的耦合度。try-catch就是一个典型的责任链模式的应用案例。在try-catch语句中,可以同时存在多个catch语句块,每个catch语句块都是处理该特定异常的处理器。当try语句块中发生异常是,异常将被发送到第一个catch语句块进行处理,如果第一个语句块无法处理它,它将会被请求转发到链中的下一个catch语句块。如果最后一个catch语句块仍然不能处理该异常,则该异常将会被向上抛出。图4-16展示了责任链模式的基本原理:

图 4-16 责任链模式UML类图

4.3.4 观察者模式-Observer

​ 观察者模式的目的是在多个对象之间定义一对多的依赖关系,当一个对象的状态发生改变时,观察者会通知依赖它的对象,并根据新状态做出相应的反应。简单来说,如果你需要在对象状态发生改变时及时收到通知,你可以定义一个监听器,对该对象的状态进行监听,此时的监听器即为观察者(Observer),被监听对象称为主题(Subject)。Java消息服务(JMS)即采用了观察者设计模式(同时还使用了中介模式),允许应用程序订阅数据并将数据发布到其他应用程序中。图4-17展示了观察者模式的基本原理:

图 4-17 观察者模式UML类图

4.3.5 策略模式-Strategy

​ 策略模式的主要目的是将可互换的方法封装在各自独立的类中,并且让每个方法都实现一个公共的操作。策略模式定义了策略的输入与输出,实现则由各个独立的类完成。策略模式可以让一组策略共存,代码互不干扰,它不仅将选择策略的逻辑从策略本身中分离出来,还帮助我们组织和简化了代码。一个典型的例子是Collections.sort()方法,采用Comparator作为方法参数,根据Comparator接口实现类的不同,对象将以不同的方式进行排序。图 4-18 展示了策略模式的基本原理:

图 4-18 策略模式UML类图

4.3.6 命令模式-Command

​ 命令模式的设计意图是将请求封装在对象的内部。直接调用是执行方法的通常做法,然而,在有些时候我们无法控制方法被执行的时机和上下文信息。在这种情况下,可以将方法封装到对象的内部,通过在对象内部存储调用方所需要的信息,就可以让客户端或者服务自由决定何时调用方法。图 4-19 展示了命令模式的基本原理:

图 4-19 命令模式UML类图

4.37 状态模式-State

​ 状态模式的设计意图是更具对象的状态改变其行为。如果我们必须根据对象的状态改变对象的行为,可以在对象中定义一个状态变量,并使用逻辑判断语句块(如if-else)根据状态执行不同的操作。图4-20展示了状态模式的基本原理:

图 4-20 状态模式UML类图

4.3.8 访客模式-Visitor

​ 访客模式的设计意图是在不改变现有类层次结构的前提下,对该层次结构进行扩展。例如在购物网站中,我们将不同的商品添加进购物车,然后支付按钮时,它会计算出需要支付的总金额数。我们可以在购物车类中完成金额的计算,也可以使用访客模式,将购物应付金额逻辑转移到新的类中。图 4-21展示了访客模式的基本原理:

图 4-21 访客模式UML类图

4.3.9 转义(翻译)模式-Interpreter

​ 转义(翻译)模式的设计意图是让你根据事先定义好的一系列组合规则,组合可执行的对象。实现转义(翻译)模式的一个基本步骤如下:

  • 创建执行解释工作的上下文引擎
  • 根据不同的表达式实现类,实现上下文中的解释工作
  • 创建一个客户端,客户端从用户那里获取输入,并决定使用哪一种表达式来输出转义后的内容

图4-22展示了转义(翻译)模式的基本原理:

图 4-22 转义(翻译)模式UML类图

4.3.10 迭代器模式-Iterator

​ 迭代器模式为迭代一组对象提供了一个标准的方法。迭代器模式被广泛的应用于Java Collection框架中,Iterator接口提供了遍历集合元素的方法。迭代器模式不仅仅是遍历集合,我们还可以根据不同的要求提供不同类型的迭代器。迭代器模式通过集合隐藏内部的遍历细节,客户端只需要使用对应的迭代方法即可完成元素的遍历操作。图4-23 展示了迭代器的基本原理:

图 4-23 迭代器模式UML类图

4.3.11 备忘录模式-Memento

​ 备忘录模式的设计意图是为对象的状态提供存储和恢复功能。备忘录模式由两个对象来实现-Originator和Caretaker。Originator需要具有保存和恢复对象状态的能力,它使用内部类来保存对象的状态。内部内则称为备忘录,因为它是对象私有的,因此外部类不能直接访问它。图4-24展示了备忘录模式的基本原理:

图 4-24 备忘录模式UML类图

小节

​ 在本篇文章中,说明了模式是指解决某个特定领域问题,实现既定目标的方法或思想;设计模式是一种通用的、可重复使用的用于解决既定范围内普遍发生的重复性问题的软件设计方法。同时,对Java中常见的设计模式进行了分类,设计模式分为建造、结构和行为三种类型,并对每种类型的设计模式的基本概念和原理进行了介绍,在后续的章节中,将详细的介绍每种设计模式的原理、使用方式和适用范围,并给出相应的实战源码。

附件

​ 在文章最后,为了便于大家阅读,提供了本文的电子版文件

​ 百度网盘下载地址:https://pan.baidu.com/s/1OREFj5QmLsHcKNbaJGLTmw

​ 文件提取码:jqgf

​ 文件提取二维码:

技术精讲:Java 8 Stream API

技术精讲:Java 8 Stream API

技术精讲:Java 8 Stream API

Stream(流)是在Java 8中新增的新特性,首先需要为Java 8 Stream正名:Java 8中的Stream跟Java I/O Stream(例如:InputStream,OutputStream等)没有任何的关系。Stream是Java中数据源的包装器,通过Stream我们可以快速的对数据源进行操作(例如:过滤,排序、求和等等),且Stream不对任何数据进行存储,所以Stream也不是数据结构。

​ 在Java 8中,Stream增强了Array,List等对象操作数据的能力,Stream提供了一些列的方法使的这些原有的集合类可以轻松创建Stream对象实例。通过Stream API提供的静态方法,可以快速的生成有限/无限的数据流。特别指出,Stream只是增强了原有集合类的能力,并未对底层的源码做任何的修改。

1.Stream的工作流程

​ 接下来,将介绍使用Java Stream的基本步骤。在Java 8中,Stream的生命周期一共有三个阶段:

  • 1.获取数据源并创建Stream实例。数据源可以是数组、列表、对象或者I/O流
  • 2.执行中间操作(Intermediate Operations)。中间操作可以是过滤、排序、类型转换等操作
  • 3.执行终端操作(Terminal Operation)。终端操作主要是对最终结果进行计数、求和、创建新集合等操作

图1-1展示了Java 8 Stream的工作工作流程。

Stream工作流程

图 1-1 Stream工作流程

2.Stream 操作管道是什么?

​ 所谓的Stream操作管道是指“中间操作”和“终端操作”的组合。在Java 8 Stream API中,许多的操作方法都将当前的Stream作为最终结果返回,这就允许开发人员以连式编程的方式,组合出更大的Stream操作管道。Java 8 Stream API位于java.util.stream包下,其组织结构如图2-1所示:

java.util.stream包

图2-1 java.util.stream包

接下来,将详细介绍“中间操作”和“终端操作”各自的方法和特性。

3.Stream中间操作(Intermediate Operations)是什么?

​ Stream的中间操作将返回一个Stream,它允许开发者以查询的方式调用多个其他的操作。特别注意的是,在“终端操作”方法未被调用之前,“中间操作”的方法不会被执行。Stream的“中间操作”一共包括七个:Stream.filter,Stream.map,Stream.flatmap,Stream.peek,Stream.sorted和Stream.limit。

3.1.Stream.filter

​ Stream.filter将返回一个包含与之谓词相匹配的元素的新的Stream。下面将通过一个示例,演示Stream.filter的使用方法。

Stream.filter

执行结果:

此示例使用“中间操作”Stream.filter过滤以“j”开头的字符串,并使用“终端操作”Stream.count统计以“j”开头的字符串数量。

3.2.Stream.map

​ Stream.map将使用java.util.function.Function提供的方法转换Stream中的元素。Stream.map操作通常用于转换集合对象。下面是使用Stream.map的示例代码:

Stream.map

执行结果:

3.3.Stream.flatmap

​ Stream.flatmap通常将Stream中的每一个元素转换成0个或多个元素。下面将通过flatmap来统计一个文本中单词的出现次数(不重复)。示例代码如下:

Stream.flatmap

执行结果:

source.txt内容:

java 8 stream example by ramostear

3.4.Stream.peek

​ Stream.peek在调试期间非常有用,它允许您在操作Stream之前查看Stream内部的数据。下面是示例代码:

执行结果:

Java
C#
C++
GO
result size = 4

3.5.Stream.distinct

​ Stream.distinct是根据其内部的equals方法在Stream中对元素进行去重操作。下面是使用示例:

执行结果:

6
1
8
11

3.6.Stream.sorted

​ Stream.sorted方法用于将Stream中的数据元素进行排序。下面是使用示例:

执行结果:

1
6
8
11

3.7.Stream.limit

​ Stream.limit方法用于限定Stream中元素的个数。下面是使用示例:

执行结果:

0
1
2
3
4
5
11
618

4.Stream 终端操作(Terminal Operations)是什么?

​ 终端操作用于生产最终的数据结果,如对象,数组或者列表。在终端操作方法被执行前,中间操作方法将不会被执行。终端操作方法一共有十二个,它们是:forEach,toArray,reduce,collect,min,max,count,anymatch,allMatch,noneMatch,findFirst和findAny。表4-1列举了这十二个终端操作的使用方法:

Stream终端操作方法

图 4-1 Stream终端操作方法

5.如何创建Stream实例?

​ Stream支持多种数据源创建Stream实例,如Array,List,Object和I/O流。接下来,通过简单的示例来演示如何创建Stream。

5.1 使用数组创建Stream

​ 首先,我们定义一个静态的数组users,并初始几条用户数据,代码如下:

private static User[] users = {new User("user1"),new User("user2"),new User("user3")};

接下来,将利用users来创建一个Stream对象实例,示例代码如下:

public static void fromArray() {
    Stream<User> uStream = Stream.of(users);
    System.out.println("create stream from array.");
    uStream.forEach(u->System.out.println(u.getUsername()));
}

5.2 使用对象创建Stream

​ 沿用上面定义的users数组,通过数组下标获得用户对象,并利用这些对象创建一个Stream实例,代码如下:

public static void fromObjects() {
    Stream<User> uStream = Stream.of(users[0],users[1],users[2]);
    System.out.println("create stream from objects.");
    uStream.forEach(u->System.out.println(u.getUsername()));
}

5.3 使用List创建Stream

​ 接下来,我们将users数组转换成List,并使用此List来创建Stream实例。代码如下:

public static void fromList() {
    List<User> list = Arrays.asList(users);
    Stream<User> uStream = list.stream();
    System.out.println("create stream from list.");
    uStream.forEach(u->System.out.println(u.getUsername()));
}

5.4 使用builder方法创建Stream

​ 最后,我们将演示使用Stream内置的builder()静态方法创建Stream实例。代码如下:

public static void byBuilder() {
    Stream.Builder<User> userStream = Stream.builder();
    userStream.accept(users[0]);
    userStream.accept(users[1]);
    userStream.accept(users[2]);

    System.out.println("create stream by builder.");
    userStream.build().forEach(u->System.out.println(u.getUsername()));

}

​ 以上就是通过数据源创建Stream实例的几种不同方式。

6.为什么要使用Stream?

​ 在命令式编程中,我们必须逐行编写代码,才能完成相关的计算。例如,计算1~10000的数的总和,我们需要使用for(int i=1;i<=10000;i++){….}循环语句来迭代求值。在这个语句中,我们需要花费额外的精力来维护loop变量的值。而在声明式编程中,我们只需要关注想要实现的目标,而不是其内部重复的循环逻辑。Stream API通过使用内部迭代器和lambda表达式,帮助我们完成了那些重复性的逻辑。Stream API的作用不仅仅用于数据迭代,它还能通过中间操作和终端操作实现数据的排序、类型转换、求和、计数、求最大值、最小值以及模式匹配等操作。

​ 接下来,表6-1展示了命令式编程和声明式编程的优劣:

命令式编程与声明式编程

总结

​ 在本文中,介绍了Java Stream的基本工作流程,并详细列举了中间操作和终端操作的细节。通过使用Stream,我们可以将传统的命令式编程代码进行简化,以提高编程效率。

Ehcache:Another CacheManager with same name exception

Ehcache:Another CacheManager with same name exception

Ehcache:Another CacheManager with same name异常

​ 在Spring Boot中开启热部署功能后,整合Shiro和Ehcache缓存时发生如下的异常信息:

Caused by: net.sf.ehcache.CacheException: Another CacheManager with same name 'shiro-cache' already exists in the same VM. Please provide unique names for each CacheManager in the config or do one of following:
1. Use one of the CacheManager.create() static factory methods to reuse same CacheManager with same name or create one if necessary
2. Shutdown the earlier cacheManager before creating new one with same name.

导致此错误的原因是针对新版本(2.5以后)的Ehcache,CacheManager使用了单例模式,当使用Spring Boot devtools开启热加载功能时,在加载SecurityManager Bean 对象实例时,会创建多个CacheManager实例,从而导致上述的异常。

​ 在此之前,网上已给出很多的解决方案,如降低Ehcache版本,设置缓存shared属性等。那么,如何在不改变当前框架版本的情况下修复上述异常呢?下面是笔者的完整解决方案。

1.改造前的代码

​ 下面的代码片段是改造前的示例:

在此示例中,只是简单的创建了ehcacheManager对象实例,并设置了缓存文件的存放地址。而新版本中,CacheManager要求是单例模式,因此需要使用EhCachManagerFactoryBean对象来限定cacheManager为共享模式。EhCachManagerFactoryBean是Spring提供的ehcache实现类。接下来我们看看如何改造。

2. 改造后的代码

​ 修改的地方一共有三处,它们是:配置EhcacheManagerFactoryBean并设置其为共享模式,配置DefaultAdvisorAutoProxyCreator,让Spring 来管理Shiro的Bean生命周期,配置lifecycleBeanPostProcessor,并让DefaultAdvisorAutoProxyCreator依赖于此对象。

2.1 添加EhcacheManagerFactoryBean配置项

​ 在原有的配置文件中添加EhcacheManagerFactoryBean配置项,并将shared属性设置为true,示例代码如下:

2.2 添加LifecycleBeanPostProcessor配置项

​ 添加LifecycleBeanPostProcessor配置项,LifecycleBeanPostProcessor将统一管理Initializable和Destroyable的实现类,从而达到统一管理Shiro Bean的生命周期的目的。示例代码如下:

2.3 添加DefaultAdvisorAutoProxyCreator配置项

DefaultAdvisorAutoProxyCreator的作用是动态代理Shiro的事务,最终将事务交由Spring进行统一管理。此配置项需要依赖于LifecycleBeanPostProcessor。示例代码如下:

通过添加上述的三个配置项,在Spring Boot中开启热加载功能就可以避免Another CacheManager with same name的异常出现。

首席技术官(CTO)在IT创业公司的角色转变

首席技术官(CTO)在IT创业公司的角色转变

在大多数的IT创业公司中,大多是技术驱动型的公司。面对不断涌现的行业新技术,企业需要快速的掌握并采用这些创新型技术,以确保企业在激烈的竞争中获得相对的竞争优势。以技术为驱动力的公司,为了能快速的处理公司所有的技术问题,从而催生了一个新的职位——首席技术官,英文简称CTO。如果你的公司是一家以技术为驱动力的创业公司,或者你是一位准备转型做CTO的开发人员,那你应该先充分了解首席技术官在IT创业公司中所扮演的角色以及该角色需要承担的责任。

​ 本文以首席技术官的职责和技能为主题,分析首席技术官在IT创业公司的各个发展阶段所扮演的角色,帮助你快速地对首席技术官(CTO)这个职位有一个全面的了解。

一、IT创业公司的CTO在扮演什么角色?

​ 首席技术官,从字面意思上看,主要负责与公司内部技术相关的工作,但这个定义太过于模糊和笼统。在国内外的IT创业公司中,对于CTO该干什么,不该干什么没有明确的定义和统一的标准,不同的IT创业公司因其业务的不同,对CTO的职能要求也不尽相同。虽然不同IT创业公司对CTO有着不同的职能划分,但是从CTO的工作本质上看,可以将CTO划分成技术领导型和技术管理型两类。

1-1、技术领导型

​ 技术领导型的首席技术官都拥有这超高的编码水平,在软件编码、架构设计、基础设施配置等领域都拥有丰富的专业知识和实践经验。他们自身具备的丰富的专业知识和开发经验,帮助其有能力对技术团队中其他的开发人员的代码进行评审,同时能为公司的工程项目作出风险评估。

1-2、技术管理型

​ 这一类型的首席技术官主要的工作任务是监督和管理工程项目的各个项目经理。技术管理型的首席技术官也具备较高的编码水平,但他们通常不参与具体的编码工作,而是将主要的精力放在公司技术战略走向并管理技术团队内部的工程。

​ 随着IT创业公司的不断发展,首席技术官的工作重心将从技术领导过渡到技术管理。在下面的内容中,将逐步分析IT创业公司在各个阶段中,首席技术官角色的演变过程。

二、IT创业公司的CTO角色会发生什么变化?

​ 所有IT企业的首席技术官都有一个贯穿公司始末的伟大使命:确保公司的技术战略完全服务于公司的业务战略。下面是一个IT创业公司从零(0)到一(1)的过程中CTO角色发生的转变。

2-1、创业早期

​ 在创业早期,所有的工作都是围绕着一个商业创意开始的。在这个阶段,公司可能还没有成立,所有的创始人在一起思想碰撞,一场又一场的头脑风暴,目的是完善创业,起草商业计划书。此时的CTO(通常是创始人)往往是一个拥有强大技术专长的人,他们的主要工作职责是验证商业方案的技术可行性,并提出实施软件产品开发的技术解决方案。

2-2、创业初期

​ 如果创业团队的商业方案得到了投资人和消费者的关注,创业团队会注册公司并开始着手构建第一个版本的软件产品。此时的CTO是技术团队的第一个(大多数情况下也是唯一一个)开发人员,他们需要在有限的时间内开发出一个最小化的可使用的产品——MVP(MVP是Minimum Viable Product的缩写)。

​ 在这个阶段,CTO的主要工作如下:

  1. 选择并确定公司的技术栈
  2. 设计软件架构
  3. 配置可扩展的基础架构(硬件和网络)
  4. 软件源代码编写
  5. 测试应用软件

​ 这些初期的工作需要CTO具备强大编码能力(有时是全栈开发),经验和相关的开发流程知识。可能一些创业初始的公司会聘用技术人员,CTO不需要单挑所有的工作,但他们依旧需要在早期为产品原型编写大量的源代码。

2-3、创业成长期

​ 公司进入快速成长期时,软件产品开始投放市场,CTO的工作重点转向软件的升级工作。软件升级让初始版本的的产品更具功能性和可用性,以便吸引更多的用户使用软件。为了加快软件开发速度,企业通常在这个阶段加快扩充技术团队人数的速度,CTO的角色也相应发生了转变,技术团队的管理事务开始进入CTO的工作日程表:

  1. 优化应用程序的部署流程,以提高开发人员的工作效率
  2. 确保应用程序能安全、可靠的运行
  3. 提出更经济、可扩展的基础设施配置方案

​ 在这个阶段,CTO除了参与编码工作外,还要负责管理不断扩大的技术队伍,其管理工作主要体现在以下两个方向:

  1. 负责监督招聘新职员的工作,需要对招聘技术人员的工作做最后的把关
  2. 建立公司的技术文化,如开发工具集、SDK版本、编码风格、书写规范、命名规范、注释规范、文档规范等等,良好的技术文化可以确保所有的开发人员能够遵循同一个标准和最佳实践开展编码工作,从而提高生产效率。

2-4、创业成熟期

​ 公司进入创业成熟期,技术团队的人数将由原来的几人增加到几十人甚至几千人,在这个时期,CTO必须要带领大批的开发人员按照既定的开发规范和工作流程开展编码工作。此时CTO的工作重点将完全转向管理,CTO可能会同时协调并监督多个开发团队,完成不同的软件工程任务。

​ 在此阶段,CTO需要花时间和精力去构建软件产品的愿景,跟踪并分析行业中最新的技术动向,并给出在下一个软件版本中实施相关技术的可行性方案。为了做出合理的、正确的技术选择,CTO在这个时期需要进一步了解新技术的详细流程和行业竞争者的技术状态,需要采用相关的创新技术,确保相比于竞争对手有一定的技术竞争优势。

​ 在本小节中,分析了在IT创业公司中,随着公司的发展,CTO从技术领导型向技术管理型转变的原因。在下面的章节中,将介绍IT创业公司的CTO都需要具备什么样的技能。

三、IT创业公司的CTO需要哪些必杀技?

​ 前面我们分析了IT创业公司在不同阶段中CTO工作职责的转变,CTO需要在软件开发和团队管理上都有着优于常人的能力。接下来,将介绍IT创业公司在各个发展阶段CTO需要具备的一些技能。

3-1、软件架构师

​ 在创业初期,CTO是公司唯一的技术权威和技术实现者,这就要求CTO需要拥有以下的技战水平:

  1. 设计并实现软件架构
  2. 选择并确定技术栈
  3. 设计并配置基础架构(硬件和网络)
  4. 选择并调试开发工具
  5. 设计并实现数据建模
  6. 优化并升级应用程序
  7. 确保应用程序的安全性和可用性
  8. 确保基础架构的经济适用性
  9. 跟踪并分析行业新技术,并对是否采用新技术作出决策

3-2、系统管理员

​ 在创业初期,CTO大多数都兼任系统管理员一职,负责对基础架构和应用程序进行管理,需要具备以下的工作能力:

  1. 软硬件的安全防护管理
  2. 域名、SSL证书和第三方服务管理
  3. 内部开发沟通渠道管理
  4. 基础架构性能监控管理
  5. 数据备份和日志审查管理

随着公司的壮大,这一部分工作会交由首席信息官(CIO)来管理,但在早期,大多数工作还是由CTO来完成。

3-3、项目组长

​ 当技术团队开发人员数量增加时,CTO开始转向团队管理,编码工作的份额开始减少,此时的CTO开始承担一个项目组长的所有职责:

  1. 根据以往的最佳实践和新的编码方法定义和实施开发标准
  2. 制定开发流程
  3. 审查软件源代码
  4. 分配开发任务
  5. 监督招聘工作
  6. 培训新职工
  7. 与客户展开沟通
  8. 与决策层沟通
  9. 评估开发人员的KPI(Key Performance Indicatro,即关键绩效指标)
  10. 监督开发人员的工作进度
  11. 评估技术风险

​ 成为一个优秀的技术管理者不仅需要强大的专业技术知识,还需要有良好的沟通能力,需要随时与开发人员沟通,并解决开发中遇到的问题。

3-4、运维工程师

​ 除了推动公司的开发工作向前发展,管理好技术团队之外,CTO在公司的发展期,还需要成为一名不错的运维工程师,在公司进入创业发展期,CTO必须要负责建立安全、可靠、易用的软件部署通道,为开发人员提供良好的软件升级环境,此时的CTO需要建立起高效的运维流程:

  1. 构建易用、安全、稳定的软件部署通道
  2. 规划软件产品的版本
  3. 选择版本管理工具并管理软件产品版本
  4. 制定并实施软件产品版本的发布周期

3-5、企业高管

​ 在本文一开始提到,IT创业公司的CTO通常是公司的创始人之一,是公司软件工程的最高执行官。除了编码、运维等技术类工作,他们还需要参与公司的上层管理:

  1. 参与公司战略规范和重大决策工作
  2. 参与公司的高层管理工作
  3. 跟踪和分析竞争对手,提出技术创新建议
  4. 为公司的业务提供可行性的技术方案
  5. 监督并协调外部劳动力(外包团队)
  6. 跟踪行业内的技术趋势并识别出公司的技术竞争优势
  7. 评估并监督公司在技术领域的资金预算
  8. 优化基础设施配置方案,降低产品运营成本

​ 技术类的企业高管,不仅要具备强大的专业知识,还需要对企业的管理流程有深入的了解。单纯的技术达人,不是一个合格的IT企业CTO。

四、总结

​ 随着IT创业公司的发展,首席技术官所扮演的角色也在发生改变,总的来说,CTO需要处理公司内部所有和技术有关的问题,可能是微观上的,也可能是宏观上的,但其核心使命不变:确保公司的技术战略完全贴合公司的业务战略。一个成功的IT创业公司(什么样子才算成功?所有的IT创业公司都想要成为行业的领头羊或者独角兽,也许这样才算成功!),一定有一个优秀的首席技术官伴随着企业的成长,他们了解企业在不同的发展背景下应该做什么事、需要具备什么样的技术能力和需要思考什么样的问题。

​ 最后,如果需要用一句话来概括一名出色的CTO所具备的能力和特点,那可能是这样的:出色的CTO都是嚼着嘴里的,夹着碗里的,看着锅里的,想着地里长着的

总结:实施SaaS模型的11个关键步骤

总结:实施SaaS模型的11个关键步骤

总结:实施SaaS模型的11个关键步骤

前言:在SaaS模型中,应用程序是通过网络向用户提供服务。用户只需要通过网络访问应用程序便可完成自身的业务活动,而不需要安装和维护软件。任何的SaaS模型的实施,都应具备以下三个特征:

  1. 多租户架构:SaaS模型设定了多个用户共享一套应用程序源码,此源码被服务提供商统一托管
  2. 自定义可配置:不同用户共享一套应用程序源码,这就要求SaaS模型须具备一定的自定义和可配置功能,以便可以管理和维护用户的自定义业务
  3. 可访问:SaaS模型依赖于网络为用户提供服务,所以必须确保在任何的时间点都能为用户提供数据访问服务。

为了能够打造一款成功的SaaS软件,在实施SaaS模型的过程中,应该遵循以下的步骤:

1. 提前了解业务

​ 在考虑技术和架构的问题之前,清楚的了解业务非常重要。没有了解业务,任何应用程序的设计和开发工作都无从谈起。为了得到更好的SaaS模型实施效果,必须在很早的阶段就开展需求调研工作,明确软件的业务和设计目标。调研的范围应该是充足的、广泛的,以便更好的识别出SaaS模型的业务边界和核心内容。在调研时,应该包括以下的内容:

  1. 采用什么样的架构进行设计?
  2. 用户类型和用户规模分别是什么?
  3. 如何处理应用程序的可扩展性、安全性和稳定性这三个问题?

​ 业务先于设计和开发,在非常早的阶段便开展调研和需求分析工作非常重要,它有助于评估项目的成本、周期和风险。除此之外,还应该重视现有系统在改造工程中所面临的挑战。

2.选择能胜任的团队

​ 物尽其用,人尽其才,无论技术和架构如何,选择适合SaaS模型的技术团队尤为重要。在实施SaaS模型之前,决策者应该选择一支由经验丰富的开发人员组成的团队,他们必须对SaaS模型的概念有深入的了解,且该团队应该拥有多种专业技术的人员,同时对SaaS行业中应该遵循的最佳实践有充分的了解。

3.设计可扩展的基础架构

一旦技术团队了解完业务需求,下一步的重点就是构建可扩展的基础架构组件:

  1. 数据中心
  2. 网络设施:网络安全(如HTTPS加密、认证和鉴权)
  3. 硬件设施:操作系统和存储
  4. 数据备份和系统监控工具

​ 除了上述之外,内部的评审工作也应该同步进行,以便在构建基础架构是评估应用系统的总体成本,在最终决定实施SaaS模型前,还需考虑一下因素(成本驱动):

  1. SaaS软件的成熟度模型
  2. 可伸缩性和可用性
  3. 客户端兼容性
  4. 数据备份和恢复
  5. 网络带宽和存储空间
  6. 安全管理(加密级别和安全证书)

4.网络带宽和应用服务器

​ 这点非常重要,为了达到良好的用户体验效果,用户规模和类型决定了带宽的大小和应用服务的数量。因此需要综合考虑SaaS模型的带宽大小和应用服务器的数量,合适的搭配能够消除数据中心的网络瓶颈问题。如果不自行构建数据中心,选择外包托管(如阿里云,腾讯云,亚马逊)方式,则应该考虑以下因素:

  1. 数据中心是否高可用(7x24x365)?
  2. 是否具备监控和灾备能力(容灾和数据恢复)?
  3. 是否具备数据安全防护能力(防攻击,防破解)?
  4. 是否具备可测试能力(完备的API测试接口,沙盒测试环境,SDK开发包)?

5.构建基础架构组件

​ 基础设施设计完成后,我们须准备可靠的、稳定的、经过实际验证的功能组件,此过程对于确保SaaS模型的高可用性能指标尤为重要。在评估这些硬件指标时,我们还需要确保其能够在项目交付日期范围内交付所有的硬件组件并确保调试正常。

6.向基础设施中持续部署SaaS应用

​ 一旦基础设施可用,开发团队就应该持续构建和部署SaaS应用。此时应对服务器进行统一的管理和配置,根据不同的业务需要安装对应的操作系统,分配指定的IP,设置用户访问策略以及配置防火墙等。

7.容灾和数据恢复

​ 现在,SaaS应用程序已经准备就绪,下一步应该考虑的问题是如何容灾和数据恢复的问题。面对容灾和数据恢复,需要考虑一下几点:

  1. 如何应对灾难的发生(不可抗拒因素不在考虑范围)
  2. 如何缩小受灾范围以及如何减小受灾带来的影响
  3. 如何在灾难发生时,应用程序还可以局部可用
  4. 如何在有限的时间范围内完成数据恢复

8.系统监控

​ 系统监控子系统对于SaaS模型特别重要,好的应用监控子系统有助于更早的发现问题,及时的避免灾难的发生。在SaaS模型中,监控子系统应该对一下的参数进行监控:

  1. 内存和CPU的使用率
  2. 磁盘的I/O读写速率
  3. 操作系统和应用程序日志
  4. 其他应用组件(数据库、Web容器、TCP等)性能参数

9.建立客服中心

​ 一旦SaaS应用成功投放市场,它就需要有一个客户支持服务中心(面向业务或者技术)。客户支持服务中心应该具备适当的票据处理和在线沟通能力,一个优秀的SaaS模型,客户支持服务是必不可少的,它有助于提升SaaS软件的服务质量。客户支持服中心应当适时地提供邮件、短信、电话、票据等服务能力,如果SaaS模型中有需要技术团队继续关注的问题,还应该提供工单处理服务,以便在发生技术故障时,能够以最短的时间将故障信息以邮件或者短信的形式发送给相关的技术人员。

10.SaaS成熟度模型

​ 在实施SaaS模型时,选择何种等级的成熟度模型,直接决定了你需要为此付出的资金成本,同时也影响着软件的交付周期和开发风险。因此,在面对此问题时,可以先考虑第二级或者第三级SaaS软件成熟度模型,然后根据业务量和用户规模,逐步满足高性能、可配置、自定义等更高级别成熟度模型的要求,以降低实施SaaS模型的风险。SaaS软件成熟度模型评估对照表如下图所示:

11.规范化的文档

​ 在完成上述10个关键步骤后,需对整个SaaS模型做出详细的记录。如需求文档,设计文档,组件文档以及接口文档等。这些文档将帮助运维人员在运维过程中快速定位系统故障。如果SaaS模型在后期有任何的修改和升级操作,这些文档也可以帮助工作人员了解SaaS模型。

正确甄别API & REST API & RESTful API & Web Service之间的差异与联系

正确甄别API & REST API & RESTful API & Web Service之间的差异与联系

​ 看到API你会想起什么?是接口、第三方调用、还是API文档?初看你可能会觉得这太熟悉了,这不是系统开发日常系列吗?但你仔细想一想,你会发现API的概念在你脑海里是如此的模糊。如何你通过搜索引擎检索API,你会看到类似这样的信息:API——Application Programming Interface(应用程序编程接口),这太抽象了。接下来,我将结合在开发中总结的一些经验,以通俗的方式聊聊API、REST API、RESTful API以及Web Service这四者之间的联系与区别。

1、API 与 REST API

​ 什么是API?这里引述维基百科给出的定义:应用程序接口(英语:Application Programming Interface,缩写:API;又称为应用编程接口)是软件系统不同组成部分衔接的约定。这个对API的定义太过于广泛和抽象,而通俗的讲,API是一段应用程序与另一段应用程序相互“交流”的方式(协议)。在Web应用程开发中,API是我们通过网络进行数据检索的一种主要方式,API文档将告知你检索数据的URL列表、查询参数、请求方式以及响应状态,其目的是降低Web应用程序开发难度,共享两个应用程序之间的数据(文本、音频、视频、图片等),而屏蔽其内部复杂的实现细节。

​ REST是Representational State Transfer的缩写,直译过来就是:表述状态的转移。REST API是一组关于如何构建Web应用程序API的架构规则、标准或指导,或者说REST API是遵循API原则的一种架构风格。REST是专门针对Web应用程序而设计的,其目的在于降低开发的复杂度,提高系统的可伸缩性。下面是设计REST风格的系统架构时需要满足或者遵循的一些基本条件和原则:

  • 1、在REST架构中,Web中所有的事物(文本、音频、视频、图片、链接)都可以被统一的抽象为资源(resource)
  • 2、在REST架构中,每一个资源都有与之对应的唯一资源标识符(resource identifier),当资源的状态发生改变时,资源标识符不会发生改变
  • 3、在REST架构中,所有的操作都是无状态的。REST架构遵循CRUD原则,所有的资源都可以通过GET、POST、PUT和DELETE这四种行为完成对应的操作。
  • 4、可缓存(可选项),在REST架构中需要缓存来有效的处理大批量的请求
  • 5、接口一致

​ 现在,了解了API和REST API的基本概念,那这两者之间有什么异同?如果按照数学上集合的概念来解释API与REST API之间的联系与区别,API是REST API的超集,REST API 是API的子集;所有的REST API都是API,但不是所有的API都是REST API。更通俗的解释是:所有的男人都是人,但不是所有的人都是男人。

2、REST API 与RESTful API

​ 在第一小节中,了解了什么是REST API,接下来聊聊REST API与RESTful API之间的异同。很多初学者很容易将这两者等同起来,认为RESTful API就是REST API,这可能是单纯的从字面上去理解了,当你深入的去了解两者的本质后,你会发现其实不然。REST API是Web API设计的一种规范或者指导原则,而RESTful API则是这中架构设计原则或者规范的一种具体实现方式。也就是说,RESTful API是REST API的非正式实现方式,因为实现REST API的方式有很多,RESTful API只是其中一种,且没有完全满足REST API的所有设计原则,每个开发者在实现REST 架构时的则重点都会有差别。

​ 很多初学者容易将REST API与RESTful API两者的概念搞混淆,我想可能只是看字面意思,而没有关注它们本身的含义(就像认识中文字一样,有边读边,无边读中间,断章取义了)。这就好比很多人会把变性人等同于女人,变性人可能五官的表象看起来和女人一样,但变性人不能生育,它只是满足了定义一个女性的大多数条件(实现),但本质上不是女人。

​ 接下来,通过一个简单的例子以加深对REST API和RESTful API的理解。下面将给出一个执行CURD操作的RESTful API设计案例:

说明:resource代指某种资源的名称,可以时student(学生)、teacher(老师)、book(书籍)等等,通常使用名词来表示;{id}则代指某种资源的唯一标识符(resource identifier)

下面是一个具体的小例子,以学生管理为例,设计学生管理的API。学生资源包括ID,姓名和所学课程信息,学生资源信息如下:

​ 现在,我们需要将学生数据保存到数据库,然后执行查询、修改和删除学生数据的操作。学生管理API的使用者调用的API如下:

​ 前面的内容说到,API共享数据资源,而屏蔽内部实现,API的使用者(客户端)关注的是资源(读懂数据),并不需要了解API内部构造;API的提供者(服务端)只关注自己的内部实现,而不关系API使用者(客户端)的状态。为了加深对这一概念的理解,下面给出学生管理API的内部实现示例:

说明:

示例代码是基于Spring MVC进行实现的。

除了上述的内容之外,你还可以通过提供键值对的方式对查询数据进行过滤,如获取所有的学生数据时,只想获取性别为女性的学生数据,则可以通过这样的方式来完成:

[GET] http://www.example.com/students?gender=female

Tip:如果API拥有对数据过滤的功能,对应服务端的API实现代码也需要做调整。

​ 在前面的内容中,我们提到RESTful API是REST API的非正式实现方式或规范。为什么这么说呢?因为在RESTful API的设计中,我们完全可以通过GET的方式完成CURD操作,也可以通过DELETE行为来创建资源,通过POST行为来修改资源,它的实现方式并不严谨或者说并没有严格按照REST API提出的约束条件来进行。所以说RESTful API是REST API的非正式实现方式

3、REST与Web Service

3-1、什么是Web Service?

​ 如万维网联盟(W3C)所述,Web Service提供了在各种平台和/或框架上运行的不同软件应用程序之间可以进行互操作的标准方法。Web Service的特点是具有良好的互操作性和可扩展性,以及由于使用XML而可以对程序处理过程进行描述。它们可以以松散耦合的方式组合不同的服务以实现复杂的操作。提供简单服务的程序可以通过相互交互,以提供复杂的增值服务。

​ 两个Web Service之间主要通过HTTP网络协议进行通信,如我们熟知的SOA(面向服务的体系架构),主要依赖于XML-RPC和SOAP(Simple Object Access Protocol,即简单对象访问协议)。

Tip:千万不要将SOA(面向服务体系架构)和SOAP(简单对象访问协议)搞混,前者是一种架构设计形式,后者是一种数据交换协议。

​ 简单的一个示例:假设一个Web Service A提供允许其他应用程序通过URL获取用户信息的功能:[GET] http://www.abc.com/{id}。id是用户的唯一标识符,请求此URL将获得用户信息。现在假设浏览器、手机、桌面应用程序的用户都要获取服务A提供的用户信息,这三者只需要请求服务A提供的URL地址,并输入用户id信息即可。至于者三个不同客户端的实现方式(编程语言)是什么与服务A 没有任何关系,只要能够解析出服务A返回的XML文档即可。这样,应用程序之间交换数据就可以不用依赖于具体的语言和环境。这就好比不同国家不同语言的人,只要能够知晓对方语言的语法结构,两个人就可以进行交流。

3-2、Web Service的优点

​ 使用Web Service有如下的几个优点:

  • 1、互操作性:Web Service允许应用程序之间进行通信,交换数据和共享服务。
  • 2、可用性:Web Service的功能可以从简单的信息查找到复杂的算法计算。
  • 3、可重用性:Web Service之间可以相互组合,以提供更为复杂的服务,由于其互操作性的特点,可以轻松的在其他的服务中重用Web Service组件,提高了服务的重用率。
  • 4、易于部署:Web Service可以部署在基于Internet标准的容器中,如Apache、Axis2等,以提供HTTP或者WSDL(网络服务定义语言)驱动的服务。
  • 5、成本低:Web Service是通过打包成Web服务组件进行部署,从而降低了使用的成本。

3-3、Web Service的类型

​ 目前,Web Service主要有两大流派:

  • 1、基于SOAP的Web Service : SOAP(简单对象访问协议)是一种基于XML的协议,用以访问Web Service。其接口以机器可处理的格式进行描述,称为WSDL(Web服务定义语言)文档。通过使用标准的的XML文档来描述Web Service,在XML文件中,会详细记录接口的信息,如消息的格式、传输协议以及交互的位置等信息。
  • 2、基于REST的Web Service :REST(Representational State Transfer)是一种软件架构,它使用JSON来描述数据格式,最重要的是HTTP传输协议对REST来说是非必须的。

3-4、REST与SOAP的区别和联系

​ 下面,通过一张表格来对比REST与SOAP之间的异同:

总结

如上所述,我们了解了什么是API,什么是REST API,什么是RESTful API以及Web Service的相关概念。API代表应用程序编程接口,是一种较为宽泛的定义或者说是一种协议,作为软件程序之间相互通信的接口而存在。REST API是API的一个子集,所有的REST API都是API;RESTful API是对REST API架构风格的一种非正式实现方式。API与Web Service都是服务提供者和服务消费者之间的通信手段。最后,为了能够快速的识别API与Web Service之间的差异,将这两种手段的不同之处整理成对照表如下: