使用统一转换服务来处理不同数据展现的思路和实现

本文示例代码:https://github.com/flym/train-propertytranslate

本文描述了这样一个场景:
针对于一个功能场景,第三方过来的数据格式均不相同,但需要通过一个统一的功能接口来进行调用,然后根据不同来源数据格式进行不同的数据展现。在后端实现时,尽量不要通过if else进行硬编码,而是通过配置的方式来完成数据的处理和呈现。

场景中提到了几个概念,如下:

  • 功能场景和数据来源:分组信息,针对同一个来源,其格式是相同的。不同来源的数据格式不一样
  • 统一功能接口:调用入口是统一的,即方法名,参数定义,以及返回格式都是相同的,仅参数内容不相同
  • 配置化:场景是通用的,可以通过配置来实现,而不是在业务代码中硬编码
  • 不同的数据展现:可以针对不同的分组和场景通过模板来进行渲染,而模板本身是可以配置的,这样即隔离了数据封装这一层。

举例如下, 如下的一个数据内容:

{
    "trade_fullinfo_get_response": {
        "trade": {
            "seller_nick": "我在测试",
            "pic_name": "T1jVXXXePbXXaoPB6a_091917.jpg",

            "receiver_name": "东方不败",
            "buyer_message": "要送的礼物的,不要忘记的哦",
            "receiver_city": "杭州市",
            "receiver_district": "西湖区",
            "orders": {
                "order": [
                    {
                        "title": "苹果",
                        "unit": "个",
                        "oid": 1,
                        "price": 1.1,
                        "sum": 1
                    }
                ]
            },
        }
    }
}

通过转换服务处理,可以展现为下面两种不同的数据内容(以html渲染为例)

渲染场景1
渲染场景2

本文从数据格式,转换器,转换过程,数据渲染几个方面来描述这一思路.

继续阅读“使用统一转换服务来处理不同数据展现的思路和实现”

一种使用Logback进行日志脱敏的方法和实现

本文描述了一种使用logback进行项目中打印的日志进行脱敏的一种处理方法,通过底层消息转换,字段过滤,以及数据加密,对象lazy化多种方法来完成整个实现体系。

参考文章:https://blog.csdn.net/lrcxl/article/details/78723756 关于实现log4j2日志脱敏的一种方案

整个实现分为以下4个步骤

  1. 底层消息转换 logback层提供扩展方法,允许注入额外的参数填充实现
  2. 字段过滤 参数toString化工具处理
  3. 数据加密 信息加/解密处理
  4. 对象lazy化, 多种信息转换实现

1 logback层提供扩展方法,允许注入额外的参数填充实现

以下实现中修改了logback-classic的源码, 修改点为 LoggingEvent#getFormattedMessage

我们在logback-classic版本中提供了用于处理消息和参数进行格式化的处理类接口如下

public interface LogbackMessageFormat {
    String format(String message, Object[] argumentArray);
}

其默认实现为 

public class Slf4jLogbackMessageFormat implements LogbackMessageFormat {
    @Override
    public String format(String message, Object[] argumentArray) {
        return MessageFormatter.arrayFormat(message, argumentArray).getMessage();
    }
}

此即为原logback-classic的默认实现。 为了让外部注入额外的实现,提供了一个工厂类,以让外部设置新的处理实现,如下参考所示:

public class LogbackMessageFormatFactory {
    @Getter
    @Setter
    private static LogbackMessageFormat INSTANCE = new Slf4jLogbackMessageFormat();
}

通过此工厂方法,即可设置自己的实现了,一个用于脱敏的参考实现如下

public class SelfImpl implements LogbackMessageFormat {
    @Override
    public String format(String message, Object[] argumentArray) {
        ......
        //以下代码为将参数对象toString化,相应的脱敏即隐藏在此方法中
        for(int i = 0; i < size; i++) {
            argumentArray[i] = ToStringUtils.toString(argumentArray[i]);
        }

        //调用原始格式化信息为字符串
        return StringUtils.format(message, argumentArray);
    }
}

继续阅读“一种使用Logback进行日志脱敏的方法和实现”

Jetty中如何实现servlet的异步Request调用

在servlet3.1规范当中,已经支持在servlet中通过调用request.startAsync来开启1个异步调用,然后在相应的业务线程里面进行一些业务操作,再通过asyncContext.complete即完成业务的整个操作。一个参考的demo如下所示:

    val context = req.startAsync();
    //重新设置业务超时时间
    context.setTimeout(40_000);

    Runnable runnable = () -> {
        try{
            //执行你的业务操作

            //输出数据
            context.getResponse().getWriter().println(totalMoney);

            //完成业务
            context.complete();
        } catch(Exception e) {
            e.printStackTrace();
        }
    };
    
    new Thread(runnable).start();

在上面的参考中,原来的servlet在调用完 thread.start之后,相应的逻辑即完成。相应的容器线程则已经还给线程池,此线程即可以接收其它客户端的请求并处理了. 异步servlet的目的即在于将接收请求的io线程与实际的业务执行相分开,避免过慢的业务阻塞了整个容器,而不能再接收更多的请求了.甚至可以在后端的业务池中定义一个队列,将要进行执行的业务逻辑放入队列里面慢慢执行。

由于异步Servlet的目的在于将web容器的io线程与业务线程分离,那么关键的部分即在于当servlet方法执行完之后,当context.complete时,如何重新触发相应io流的操作。本文尝试以jetty为参考,从源码角度查看其实现的原理和机制。

本文参考的jetty版本为 9.4.11.v20180605

继续阅读“Jetty中如何实现servlet的异步Request调用”

如何正确地获取一个有效的数据库连接

这几天在研究各个数据库连接池,比如ali druid, dbcp2 以及最近很火的连接池 HikariCP, 除了常规的池化连接对象管理外。关键区别就是如何创建连接,防止连接泄漏,如何获取连接这些细节的区分点.在hikariCP的wiki中,提到一篇文章 https://github.com/brettwooldridge/HikariCP/wiki/Bad-Behavior:-Handling-Database-Down, 这里面提到由于网络的问题导致获取连接会比预期的时间长的这一问题。这也是我们为什么在选择一些组件和框架时,均会优先使用偏新的版本的原因。一些老的,旧的连接池,因为api的限制,对一些极端情况的处理并不能如意。比如在测试中的c3p0,dbcp这些常规连接池,由于历史原因,在处理一些新的需求时都不能满足需求。

我们来看对于一个标准的数据库连接,我们会有以下的要求:

  1. 在规定的时间内拿到数据库连接
  2. 使用一个alive的connection进行数据访问

整个要求其实就是判断connection存活,处理超时的问题。在常规的数据访问中,因为不同sql查询的原因,我们不能够期望数据库查询在一个较小的时间内就一定能返回(比如5秒)。但是在程序中,当获取一个连接时,又期望在较小的时间内返回给应用,以方便应用能够快速响应业务。如果5s之内(或者更小)不能拿到连接,能认为当前业务失败,避免业务长时间不能响应。甚至避免整个系统所有线程均blocking在获取数据库连接这一步,而导致系统不可用.

那么整个问题变为了以下3步:

  1. 业务在较小的时间内拿到连接,如果超时则立刻返回
  2. 连接池在较小的时间内创建数据库连接,如果超时则立即返回,进行下一步尝试
  3. 连接池在较小的时间内验证已经创建好的连接当前可用,如果超时则立即返回,并标记当前连接不可用,选择其它的连接

继续阅读“如何正确地获取一个有效的数据库连接”

spring-retry的简单模型整理

在业务系统中,经常会遇到进行业务重试的场景,当一个执行方法失败时,可能需要进行几次重试,如果重试时成功了,仍然认为业务是正确的。否则则throw相应的异常。这种通用的重试场景则可以使用标准的第三方重试库来完成,以避免自己在业务代码中写类似while try 的代码。同时,因为重试的条件以及处理的逻辑均不太一样,通过一个标准的重试库来完成此操作,也是很有必要的。

本文描述了spring提供的重试库spring-retry,通过提取相应的概念模型,了解其工作原理以及执行机制.
版本:1.2.2.RELEASE

会使用到的注释

Retryable
用于标识一个可以进行重试的方法,即一个方法如果需要在throw异常之后进行重试,则可以使用此注解进行标记.同时,此注解上标识了可以进行重试的一些条件以及次数信息。

Backoff
此标识一个方法在进行下一次重试时,需要暂停的一些手法以及参数信息,比如sleep多少秒,或者是是在一个区间范围内sleep。

Recover
此标识当一个方法执行多次都失败之后,进行的一个failback处理。

主要的业务接口

RetryContext
重试上下文,即维护了整个重试周期中相应调用对象,调用次数,以及在调用过程中的异常对象等,其它接口均从此对象中获取相应的数据.

RetryOperations
封装了一个主要可重试操作,可以理解为进行主要逻辑操作的template对象。

RetryPolicy
与@Backoff相对应的一个接口,用于在整个重试逻辑中,处理何时该重试,以及控制相应的重试上下文信息。

RetryListener
监听器,用于在重试过程中,对开始,重试出错,结束时进行拦截操作。默认情况下,此类为null,即无作用。

RetryCallback
简单的重试回调对象,用于封装原来的可重试方法,将整个操作统一使用callback来表示,以隔离相应的实现。

RecoveryCallback
与@Recover相对应,即最终进行failback的回调对象.

继续阅读“spring-retry的简单模型整理”

ZooKeeper Watcher 机制源码解释(转)

本文转自 https://www.ibm.com/developerworks/cn/opensource/os-cn-apache-zookeeper-watcher/index.html

ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。

ZooKeeper 的 Watcher 机制主要包括客户端线程、客户端 WatchManager 和 ZooKeeper 服务器三部分。在具体工作流程上,简单地讲,客户端在向 ZooKeeper 服务器注册 Watcher 的同时,会将 Watcher 对象存储在客户端的 WatchManager 中。当 ZooKeeper 服务器端触发 Watcher 事件后,会向客户端发送通知,客户端线程从 WatchManager 中取出对应的 Watcher 对象来执行回调逻辑。

    private final HashMap<String, HashSet<Watcher>> watchTable =new HashMap<String, HashSet<Watcher>>();

    public synchronized void addWatch(String path, Watcher watcher) {
        HashSet<Watcher> list = watchTable.get(path);
        if(list == null) {
            list = new HashSet<Watcher>(4);
            watchTable.put(path, list);
        }
        list.add(watcher);

        HashSet<String> paths = watch2Paths.get(watcher);
        if(paths == null) {
            // cnxns typically have many watches, so use default cap here
            paths = new HashSet<String>();
            watch2Paths.put(watcher, paths);
        }
        paths.add(path);
    }

继续阅读“ZooKeeper Watcher 机制源码解释(转)”