如何对alpine设置时区为Asia/Shanghai

alpine是个非常简单的docker镜像,沿用以前的修改容器时间的方法都不能起效,因为它去掉了很多功能,其中就去掉了timezone这个功能,默认就是UTC时间。

我们进入alpine容器内看看里面的内容:
docker exec -it –rm alpine /bin/sh
> date
显示的是UTC时间,和北京时间相差8小时
> ls /usr/share/zoneinfo/
提示目录不存在

所以,我们需要对alpine补全这个timezone的功能,如此才能起效,Dockfile的内容如下:
FROM alpine
RUN apk add –no-cache tzdata
ENV TZ=Asia/Shanghai

为什么要安装tzdata这个包,见下面的参考资料《Linux – 时区和时间调整方法》,讲得很好。

参考资料

Docker Alpine 镜像设置东八区
https://github.com/gliderlabs/docker-alpine/issues/136
Linux – 时区和时间调整方法

简单事情复杂化和复杂事情简单化的两种极端

在工作中,经常能遇到这两种极端现象:
1,简单事情复杂化
2,复杂事情简单化
这2种极端都是不可取的,需要在工作中进行克服的。

可惜的是,这2种极端又屡次能在工作中遇到,尽管有句话是这么说的:条条道路通罗马。但是不同的道路会导致不同的效果,甚至可能有些道路太过于弯曲而到不了罗马,这种也是存在的。

简单事情复杂化

公司里开一个产品的讨论会,产品经理把该产品的需求、界面原型侃侃说完以后,然后询问下面的开发人员该如何实现。因为以前做个一个类似的产品,只不过那个产品的流程相对固化和简单,而这个产品就复杂多了,业务流程也是多变。然后下面的开发人员发言是相当积极,但是每个人的发言提问都提出了很难实现的说法,然后大家讨论,你说这个解决,那么那个就出现问题;那个解决,这个又出现问题。说来说去,最后都说相当复杂。我听了大家的踊跃发言,一直插不上话,因为太激烈了,各个都是专家啊。到了最后,气氛终于缓点下来,我终于能插上话了,不容易啊,我就这么说(简化了):“在10多年前,因为没有…技术,所以大家都用那种解决办法,那种解决办法在解决简单固化的流程时效果是不错的;但是此一时彼一时,你们不能拿着这么老掉牙的技术来应付这么复杂的流程和业务。其实,换个思路换个方式,用某种比较先进的技术来做这个产品,大家会发现原来是那么的简单轻松愉快。”,后来大家采纳了我的意见,最后做好的产品所花费时间相当短,相当简单,上线后效果也是非常不错。

这个事情能体现出以下几点:
1,有些技术人员,表现的像是个专家,其实短板很厉害的。
2,有些技术人员,对新的技术没有去学习,一直都用老式的技术,说白了就在单位里混。
3,有些技术人员,看上去好像对新兴技术都精通,好像上知天文下知地理,其实半瓶水在晃悠。
4,有些技术人员,自以为掌握了新技术,就开始滥用技术,不该用的场所用新技术。

上面这几种现象,每个单位都有这样的人可以对号入座,真心希望这样的人能少一点。

复杂事情简单化

有个需求很复杂,数据分析方面的需求,涉及到ETL的复杂度,ETL中还要加入很复杂的业务判断。于是开发人员为了完成这个需求,就千方百计把这个问题给简单化,为了简化,引入了很多约束和限制,最后做好了,用户用起来是相当的别扭,尽管结果能出来,但是整个过程受到了很多制约。

像这样的场景,在每个公司中都大量存在,我有一次去临危受命于一个产品,那个产品已经运转不起来了,我接手后,花了一段时间理顺里面的业务关系和技术,发现整个产品一开始的设计和实现都是还行的,只是在后面的需求开发上就出现了歪的地方,每次的实现都是把复杂事情简单化处理,如此越积越多,导致了到后面不能随便更改一个地方,只要更改了一个地方,N多地方因为简单化处理的弊端导致了都要随之改动。

复杂事情简单化,在某些场景下偶偶用用是没有问题的,为了赶时间赶效率,但是假如一开始不细心耐心的去构建整个流程,不用合适的架构去解决问题,到了后面就会病入膏肓,隐疾越积越多。


上面的这2个极端,希望大家在工作中都要去认真反思,尽力去克服,多想多做多学习,而不要一拍脑袋应付了事。

如何在springboot中禁用某些jar的自动配置功能

我们知道在springboot中有这么一个自动配置的功能:假如你在工程中加入某个jar,比如mysql的jar,或者mongodb的jar,或者redis的jar,那么就算不写配置文件,springbboot也会启动相关的功能来简化构建这些客户端的复杂度。

但是有那么一种场景,比如我只想在工程中使用某个jar的某个类的功能,所以我必须要引入某个jar,但是又不想让springboot自动配置,那该怎么办?

其实很简单,只需要在启动类上进行配置,如下用mongodb来举例:

@SpringBootApplication(exclude={MongoAutoConfiguration.class})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

这个时候来问题了,我假如想不包含 redis 的自动配置,那该如何写这个 XXXConfiguration.class 呢,这个到哪里找?
在回答这个问题之前,我们先来看一个错误提示,以redis的自动配置来举例,当设置不正确的时候,springboot启动的时候会进行报错,报错的内容大致如下:

Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled

with ‘debug’ enabled,这个是什么鬼?这个debug在哪里设置啊,难道在 logback 中?不是吧。带着这个问题,我查了资料,找到这篇文章:http://www.it1352.com/908199.html,这篇文章里讲了该如何在springboot中进行设置,哦,原来就是在 application.properties 中假如 debug=true 即可。顺便吐槽一下这篇文章的网站,里面广告太多了,感觉是一个。。。

好了,我们继续上面的那个redis那个例子,我们把 debug=true 设置好,好了,真相出来了:

    RedisAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'org.springframework.data.redis.connection.jedis.JedisConnection', 'org.springframework.data.redis.core.RedisOperations', 'redis.clients.jedis.Jedis'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)

   RedisAutoConfiguration.RedisConfiguration#redisTemplate matched:
      - @ConditionalOnMissingBean (names: redisTemplate; SearchStrategy: all) did not find any beans (OnBeanCondition)

   RedisAutoConfiguration.RedisConfiguration#stringRedisTemplate matched:
      - @ConditionalOnMissingBean (types: org.springframework.data.redis.core.StringRedisTemplate; SearchStrategy: all) did not find any beans (OnBeanCondition)

   RedisAutoConfiguration.RedisConnectionConfiguration matched:
      - @ConditionalOnClass found required class 'org.apache.commons.pool2.impl.GenericObjectPool'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)

   RedisAutoConfiguration.RedisConnectionConfiguration#redisConnectionFactory matched:
      - @ConditionalOnMissingBean (types: org.springframework.data.redis.connection.RedisConnectionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition)

   RedisCacheConfiguration matched:
      - Cache org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration automatic cache type (CacheCondition)

   RedisRepositoriesAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'redis.clients.jedis.Jedis', 'org.springframework.data.redis.repository.configuration.EnableRedisRepositories'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
      - @ConditionalOnProperty (spring.data.redis.repositories.enabled=true) matched (OnPropertyCondition)
      - @ConditionalOnMissingBean (types: org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean; SearchStrategy: all) did not find any beans (OnBeanCondition)

于是,我们就可以这么设置我们的启动类:

@SpringBootApplication(exclude={RedisRepositoriesAutoConfiguration.class,RedisAutoConfiguration.class})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

上面的RedisCacheConfiguration是个私有类,不能用,不用也达到了效果。

全文结束。

在dubbox的rest接口中使用表单提交对象

dubbox使用resteasy作为rest的实现方式,所以这篇文章的标题也可以称作:在resteasy的接口中使用表单来提交对象。

我们都知道,在resteasy中,假如接口参数是对象,那么参数定义的时候,什么注解都不需要写,直接就可以在前端用requestbody的形式提交json即可,非常方便。

但是需求来了,前端需要用 form 表单的形式来提交对象。

首先我参考了 resteasy 的官方文档 http://docs.jboss.org/resteasy/docs/3.0.7.Final/userguide/html/_Form.html

然后我查了资料,查到的是这样的一篇博文,RESTEasy数据自动装配之@FormParam,写得非常不错,我的解决就是参考这篇博文。但是在这篇文章中有2个疑问:
1,@Mapped这个是个什么鬼,不用这个也没有任何问题。
2,@Consumes(MediaType.APPLICATION_FORM_URLENCODED),这个我在postman中直接用 Content-Type:application/x-www-form-urlencoded 是没有任何问题的,也就是说 @Consumes(MediaType.APPLICATION_FORM_URLENCODED) 完全可以去掉。

在实现的时候,我们用得包和类如下:
@Form 是 org.jboss.resteasy.annotations.Form,属于resteasy-jaxrs-xxx.jar
@FormParam 是 javax.ws.rs.FormParam,属于javax.ws.rs-api-xx.jar

这个示例工程就不举了,因为还牵扯 springboot 和 dubbox 的细节,不举这个工程的例子了,大家参照上面的那篇博文,完全能写出来。

在junit中如何assert一个对象是null或者空字符串

在junit测试中,我们对某个对象需要判断是否为null,但是有时候在一些情况下,该对象可能是””(也就是空字符串),这个时候我们以前写得那个Assert.assertNull()就不行了。其实这种情况在现实中也是大量存在的,比如页面表单提交后的action获取表单中的字段,假如是字符串,那么往往是空字符串的存在。

那么,在junit中如何assert一个对象是null或者空字符串,我查了下资料,可以见这里:https://www.baeldung.com/java-assert-string-not-empty,我们可以用 Apache Commons Lang 来完成这个需求:
Assert.assertTrue(StringUtils.isEmpty(someObject));
不再用Assert.assertNull(),改用Assert.assertTrue()

这里有个小小的工程,可以看看 Assert.assertTrue(StringUtils.isEmpty(someObject)); 的使用:
https://github.com/champbay/springboot-test.git

SpringBootTest中启用不同的profiles

在springboot中,我们的工程常常有下面的配置文件:

application.properties
application-prod.properties
application-dev.properties

其中,application.properties是默认就加载的,prod和dev是需要在application.properties中指定spring.profiles.active=xxx来确定。

在测试的时候,需要测试环境,不管是prod还是dev都不适用的,所以在测试的时候,需要有个地方来设定配置文件。

查了资料,首先查到一篇文章:如何在@SpringBootTest中动态地启用不同的profiles,这篇文章讲了一大堆,我也没有全部看全,可能场景不适合我吧,总觉得这个需求应该非常普遍简单的,总不至于这么复杂的来解决吧。

继续查资料,看到这一篇文章:https://stackoverflow.com/questions/41985262/spring-boot-test-overriding-bootstrap-properties,这里有个人回答了几种办法,其中使用 Add @ActiveProfiles(‘test’),大家 test class 这种方式我觉得应该是最最正确的解决办法。

为此,我做了一个很小的测试工程放在 github 上,用来验证@ActiveProfiles(‘test’),大家可以去下来junit测试看看,看了后就能明白了。

工程地址:
https://github.com/champbay/springboot-test.git

JAVA字符串前补零和后补零的方法

字符串的前补零,往往用在排序上,比如:某个表id是字符串类型,a记录的id是“2”,b记录的id是“11”,从我们看来,因为是a排在b的前面,但是用字符串去排序的话,那么b将是在a的前面,所以这个时候需要进行前补零,其实入库之前就应该前补零就使得a的id就是“02”这样的格式。

字符串的后补零,往往用在财务统计报表上,需要对小数点进行统一,从而使得在界面的排版上数字的排列清晰。

前后补零的文章,我这里转载自一篇博文,JAVA字符串前补零和后补零的快速方法,例子举得很明白。

String fileName = "130181";
System.out.println("================  前补零方法一   =================");
DecimalFormat g1=new DecimalFormat("0000000");
String startZeroStr = g1.format(Integer.valueOf(fileName));
System.out.println("前补零方法一:"+startZeroStr);

System.out.println("================  前补零方法二   =================");
startZeroStr = String.format("%07d",Integer.valueOf(fileName));
System.out.println("前补零方法二:"+startZeroStr);

System.out.println("================  后补零方法一   =================");
DecimalFormat g2=new DecimalFormat("0.000000");
String endZeroStr = g2.format(Integer.valueOf(fileName));
System.out.println("后补零:"+endZeroStr);
System.out.println("虽然后补零出现这种情况,带有小数点");
System.out.println("比如你要长度要在7位以内,可以这么做");
System.out.println("后补零转变后:"+endZeroStr.replace(".","").substring(0,7));

输出结果如下:

================  前补零方法一   =================
前补零方法一:0130181
================  前补零方法二   =================
前补零方法二:0130181
================  后补零方法一   =================
后补零:130181.000000
虽然后补零出现这种情况,带有小数点
比如你要长度要在7位以内,可以这么做
后补零转变后:1301810

对于String.format()方法,可以查看jdk文档,里面有关于 formatter 的详细说明,:anguished:,太详细了以至于看得眼花缭乱,这里有一篇博文 JAVA字符串格式化-String.format()的使用,写得简单明了,大家可以上去看看前补零为什么是这样的格式。

用showdoc来编写网关接口文档

最近在工作上做了一个网关对外暴露接口,做好后要需要提供接口的文档,然后上网去查找有没有好用的编写接口文档的工具。

接口文档的编写分在线、自己部署2种。在线的网上大家可以搜一下,好多都是提供几个免费的文档,多了后需要付费。自己部署的分为:免费、开源、商业的。这里有一篇文章罗列了一些工具网站:接口文档工具,我就是从这里找到了 showdoc 这个比较满意的工具。

showdoc的官网地址:https://www.showdoc.cc/ 里面有一个示例工程,可以看到一些最终的效果;同时假如你不想搭建一套私有的,你可以进行注册后创建项目,同时拉人进来查看文档。

如何搭建一套showdoc的应用,showdoc的帮助文档写得比较仔细,我因为偷懒,采用了他的docker来进行安装,安装的时候,因为没有严格按照他的步骤,导致了出现一些文件写权限的问题,注意:必须要严格按照文档上的步骤来进行部署 docker 。

部署好首次登录,用户名和密码为:showdoc/123456,进去后别忘记修改密码。

我这里大致罗列一下功能点,这些功能点对于接口文档的编写挺有用的,说明作者是用心了:
1,左侧可以做成树形结构,可以用单独的页面来描述全局性的说明、流程等,用个目录来包含业务接口的页面。
2,有人员权限的划分,基本上对于我们来说,编写、查看这样简单的权限足矣。
3,页面采用markdown,基本上可以满足各种格式的书写;作者特别有心,考虑到一些人对markdown的语法不熟悉,就弄了一些常用的模板,很贴心。

对于其他的一些功能,比如接口测试这样的功能,我个人觉得倒没有这个必要了,术业有专攻,可以用postman来测试的,没有必要这里那么复杂的再实现一个。

最后,提醒一下用 showdoc 的人,别忘记备份了,docker 的备份是 /showdoc_data 这个目录。

mysql的时间字段如何设置为支持毫秒

mysql的时间字段类型,datetime,timestamp,我用的都是精确到秒,格式为 yyyy-MM-dd HH:mm:ss ,但是如何让这2个字段支持毫秒呢, 也就是格式为:yyyy-MM-dd HH:mm:ss.SSS。

这篇文章 https://blog.csdn.net/mchdba/article/details/75259947 写得挺好的,但是他给的那个官方文档没有链接,我这里给出:Fractional Seconds in Time Values,可以看到mysql从 5.6 开始支持毫秒,而在 mysql 5.5 是不支持毫秒的。可以从官方的文档上有个下拉框可以选择 5.5 的版本,看到是不支持毫秒的,那么5.5对于毫秒该怎么办?只能将时间转为long型后存储,存取的时候需要进行转换。

DATETIME(3),TIMESTAMP(3) 括号中的数字是表示小数位取几位,一般我们取得是 3位,所以写3 即可。

socketio中的关于acknowledgement的进一步解释

websocket中对于消息的确认(acknowledgement)是没有实现的,需要我们自己在收到消息后回复一遍,如此增加了程序的复杂度。

socketio是对websocket的一些封装,提供了很多有价值的方法,其中就是确认(acknowledgement)的实现,非常棒的封装。

首先让我们来描述一下场景,然后从场景中知道一些定义:
1,网页发出一个websocket请求,发给服务器,服务器收到后,对网页来个确认消息。我们称网页为client,服务端为server。这个非常好理解。
2,服务器发一个websocket请求给网页,网页收到后,对服务器来个确认消息。我们还是称网页为client,服务端为server。这个也是容易理解的。

但是websocket非常特殊,因为服务器端能主动给网页端发消息,假如我们从消息的发送方(sender)和消息的接收方(receiver)来看,我们可以这么称消息的发送方为client,消息的接收方为server。那么从这个方面来看,当服务器端能主动给网页端发消息,服务器端为client,而网页端为server。

为了避免混淆,我们统统称网页端为client,服务器端为server。

我们来看一下 socketio 的文档:Sometimes, you might want to get a callback when the client confirmed the message reception. #有时候,在客户端发送消息后,客户端对服务端收到消息后,客户端需要有个回调来进行一些处理(确认)

下面这个例子就举了客户端发送消息给服务端,然后服务端对该消息进行了确认,这个确认能在客户端得到响应:


Server (app.js)
var io = require('socket.io')(80);

io.on('connection', function (socket) {
  socket.on('ferret', function (name, word, fn) {
    fn(name + ' says ' + word);
  });
});
--------------------------------------------------
Client (index.html)
<script>
  var socket = io(); // TIP: io() with no args does auto-discovery
  socket.on('connect', function () { // TIP: you can avoid listening on `connect` and listen on events directly too!
    socket.emit('ferret', 'tobi', 'woot', function (data) { // args are sent in order to acknowledgement function
      console.log(data); // data will be 'tobi says woot'
    });
  });
</script>

socketio的文档关于acknowledgement只讲了上面这个例子,其实从websocket来看,还有服务端发消息给客户端,然后客户端进行确认,从而服务端能得到回调这样的流程。

所以,从上面2个流程的意义上来看,那么这句话改成如下的就更加能明白了:
Sometimes, you might want to get a callback when the sender confirmed the message reception.

嗯,最好的例子我找了个,写得非常好:
https://github.com/mrniko/netty-socketio-demo ,大家可以去看看
另外,http://www.cnblogs.com/lightsong/p/10226940.html, 这篇文章解释了确认(acknowledgement)的实现机理,感兴趣的同学可以去看看。

参考资料

socketio文档
netty-socketio例子
acknowledgement的实现机理