程序猿DD

程序猿DD 查看完整档案

上海编辑东华大学  |  计算机软件与理论 编辑永辉云创  |  架构 编辑 blog.didispace.com 编辑
编辑

翟永超,《Spring Cloud微服务实战》作者,Spring Cloud中文社区创始人,SpringForAll社区发起人之一。

关注我的公众号:程序猿DD

每日技术干货推送,还有一月多次的赠书、赠票等活动免费领取!

个人动态

程序猿DD 发布了文章 · 1月20日

Spring Boot 2.x基础教程:配置元数据的应用

在使用Spring Boot开发应用的时候,你是否有发现这样的情况:自定义属性是有高量背景的,鼠标放上去,有一个Cannot resolve configuration property的配置警告。

如果不对于这个警告觉得烦,想要去掉,那么可以通过设置来去除:

但是,我的建议是不要去掉,因为这个警告正好可以通过高亮来区分你的自定义配置以及框架配置,可以让你快速的分辨哪些是自定义的。

如果你实在想去掉,那么也不建议用上面说的方法,而是建议通过完善配置元数据的方式来完成。所以,今天就来具体说说配置元数据的应用!

啥是配置元数据?

我们不妨打开一个已经创建好的Spring Boot项目,查看一下它的Spring Boot依赖包,可以找到如下图的一个json文件:

这里报错的就是配置的元数据信息。有没有发现这些name的值都很熟悉?其中description是不是也很熟悉?对,这些就是我们常用的Spring Boot原生配置的元数据信息。

这下知道配置元数据可以用来做啥了吧?它可以帮助IDE来完成配置联想和配置提示的展示。

而我们自定义配置之所以会报警告,同时也没有提示信息,就是因为没有这个元数据的配置文件!

配置元数据的自动生成

既然知道了原理,那么接下来我们尝试用一下配置元数据试试!

第一步:创建一个配置类,定义一个自定义配置

@Data
@Configuration
@ConfigurationProperties(prefix = "com.didispace")
public class DidiProperties {
    
    /**
     * 这是一个测试配置
     */
    private String from;

}

第二步:在pom.xml中添加自动生成配置元数据的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

第三步mvn install下这个项目。

此时我们可以在工程target目录下找到元数据文件:

同时,我们在配置文件中尝试编写这个自定义的配置项时,可以看到编译器给出了联想和提示:

并且,编写完配置之后,也没有高亮警告了!

代码示例

本文的相关例子可以查看下面仓库中的chapter1-4目录:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 0 收藏 0 评论 0

程序猿DD 发布了文章 · 1月19日

目前用下来最溜的MacOS微信多开工具!

一个生活微信,一个工作微信是很多上班族的基本配置。

但由于微信客户端在PC端上只能打开一个,这使得在上班时候就非常不便,一个号在PC端上登录,一个在手机上使用,但是上班时候又不能一直看手机,不然老板还以为你在玩呢。

所以,对于打开多个微信客户端的需求就来了!

查了一下百度,有几个基本的多开方法,简单总结下大致有以下三类:

第一类:创建微信应用的副本,通过复刻多个微信应用来实现。

第二类:使用下面命令实现

open /Applications/WeChat.app/Contents/MacOS/WeChat

优化下命令,这样可以关闭终端

nohup /Applications/WeChat.app/Contents/MacOS/WeChat > /dev/null 2>&1 &

上面的两类虽然都能实现,但都不那么方便,昨天小编发现了一个更好用的工具:WeChatTweak-macOS

先上一波效果:

是不是很简洁?只需要在微信图标上右键,就能快速开启新的微信客户端!

有咩有觉得很好用呀?赶紧上车,开启微信多开之路吧

仓库地址:https://github.com/Sunnyyoung...

安装步骤

  1. git clone https://github.com/Sunnyyoung...
  2. cd WeChatTweak-macOS
  3. sudo make install
  4. 重启微信,右键微信图标,看看效果吧!
欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 0 收藏 0 评论 0

程序猿DD 发布了文章 · 1月18日

在IDEA中通过Module管理多个项目

你身边有没有这种顽固的Eclipse忠实用户:IDEA不能一个窗口管理多个项目!太不方便了!

对于一个窗口同时管理多个项目的需求,在我们日常开发时候是经常需要的。尤其当我们在分布式环境下,在一个窗口中调试起来就能方便很多。

如此强大的IDEA真的不支持吗?!当然不是!是你不会用!

下面我们就来说说如何在一个工作空间中管理多个项目的配置方式:

第一步:先创建一个新的空白工程

在弹出的项目名称和路径输入框中根据你的喜好输入即可。

第二步:添加模块

添加模块的方式有两种:

New Module:如果你要管理的是一个新项目,那么可以通过这个选项创建一个新项目,并纳入当前的项目管理界面中。

对于我们这些Spring开发者来说,可以继续用Spring Initializr来初始化你的项目,这样创建出来的项目会成为当前这个项目的模块来管理。

Import Module:如果你要管理的项目已经从git上拉下来了,可以直接用这个选项进行导入即可:

如果没有特殊配置,那就各种next完成导入。

第三步:在后续要继续添加一起管理的项目的时候,只需要在菜单中找到这两种方式。

  1. File -> New -> Module...:这个是新建项目
  2. File -> New -> Module from Existing Sources...:这个是导入项目

不断重复上面的动作,我们就可以把很多我们要一起调试的项目放到一起来使用了:

本文首发:http://blog.didispace.com/idea-multiple-projects-in-one-project/ ,更多技术干货欢迎关注与收藏

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~

查看原文

赞 0 收藏 0 评论 0

程序猿DD 发布了文章 · 1月18日

百度网盘再次回收免费空间!21日前赶紧登录下!网友评:想钱想疯了?

file

周末在家闲来无事,拿出一本三国又品了品,读到其中有关鸡肋的那一段。

某夜行军打仗,杨修在曹营帐中,恰遇将领请示曹操行军令,曹操此时正食一鸡肋,烦躁的他扬了下手重的鸡肋说,“就拿这个做行军令吧”。杨修闻言,回帐中,打点行头准备撤退,帐中余人纳闷问之,杨修曰“所谓鸡肋,食之而无味,弃之尚可惜。今主公以此为令,不喻此行?!退之为上策,故如是。”曹操得知后大怒,以扰乱军心之罪将杨修斩首示众。

想到现实生活中也有不少物件,虽不至于鸡肋那样夸张,但也类似。一般这类东西呢,不用的时候呢完全想不到他的存在,但需要用的时候呢,还总有这个那个的缺点,让人用的不够尽兴,但不用却又不方便。

就像百度网盘,很多人在网盘上面存放各种文件或者下载别人的网盘资源。平时不用的时候几乎没人想起来去登陆看看,可能大半年都不会去登陆;等到要存放东西啦,要找东西下载啦,却又觉得各种不爽,要么就是想找的资源没啦,要么就是使用时说你的账号违规莫名被封印了,但你说不去用的话嘛的确有很多不方便,骂了几句还是要用。

不过最近呢,百度网盘发布了一则新规,着实让网友们不敢再长时间不登陆,这是怎么一回事呢?

file

简单地说,就是对于那些长时间不登陆的账号,百度准备将其2T的免费空间调整为100G。暂时呢,在这个新规施行初期,会给予用户30天的缓刑期,超过缓刑期还不使用,就真的变成100G了!当然,尊贵的会员是不受影响的!

有些人会疑问,那如果我已经用了2T的空间,会不会把内容都清空了呢?这倒不会,百度没那么心狠手辣的直接删除,就是你只能看,无法存入新的内容了!

很多用户可能非常不爽百度这样的严苛要求,但其实早在18年底,百度就已经开始推广类似的管理措施。

file

file

很多用户可能当时看到过这个通知,但想着那时间要一年呢,估计都淡忘了这件事。

时间再往前回到2016年,国内免费网盘相继停止自己的免费服务时,百度网盘还是保留了免费和大容量。

但就像一样免费的东西用久了,使用的人就会觉得免费是理所应当的,当无法再免费使用同样的内容时,就会觉得对方不讲仁义道德。所以此次申明一出,很多网友暴跳如雷,觉得百度不够厚道。

从另外一点看,百度为了免费网盘,每年都付出了巨额的支出成本,例如百度阳泉数据中心曾经揭露该数据中心的硬件成本平均每TB 319.5元,电费和带宽费每年也达到了每TB 106元!

当然羊毛始终出在羊身上,百度也不是什么慈善家,对于免费空间来说,百度对于免费用户实施了限制下载速度,昨天小编还在用网盘下载资源,没记错的下载速度好像是70K/s...

如果想要好的下载速度,就必须充值百度的VIP会员。

file

如上图所示,目前百度网盘超级会员一个月费用为30元,哪怕是连续包月、连续包年也便宜不了多少,比起其他互联网厂商的会员价格,的确高出不少.

或许这次的新规之后,又能带动一批新会员的加入也不得而知。

事实上,在百度网盘如此操作之后,业内其他厂商竞相模仿,例如:

微软的OneDrive,宣布免费空间从15GB降至了5GB
谷歌的Google Photos云相册服务,从2021年6月1日开始,只提供15G的免费空间,想要更多云空间的用户需进行付费解锁
所以小编还是奉劝各位看官,如果没打算充值VIP,在1月21日之前,赶紧登陆自己的网盘账号,进行一次状态“激活”,毕竟2TB的存储空间,如果没有了再想开通,可是需要248元/年!

对于百度的这次新规,你是怎么看待的?欢迎留言讨论!

文末彩蛋

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 0 收藏 0 评论 0

程序猿DD 发布了文章 · 1月18日

JAR冲突问题的解决以及运行状态下如何查看加载的类

今天碰到群里小伙伴问,线上程序好像有多个不同版本的Netty包,怎么去看到底加载了哪一个?

在说如何看之前,先来说说,当你开始意识到项目里有多个不同版本的Jar包,都是因为遇到了这几个异常:

  1. java.lang.NoSuchMethodException:自己代码中调用了某个方法,因为加载了其他版本的jar,这个版本正好没这个方法。
  2. java.lang.NoClassDefFoundError:编译时候是好的,但是运行的时候,因为加载的jar版本问题,没有这个类。
  3. java.lang.ClassNotFoundException:在动态加载某个Class的时候,因为要加载的jar不是正确的版本,而导致找不到这个类。

当你在本地运行ok,但到服务器上发现出现这些错误的时候,就要意识到很可能是jar冲突了(有相同依赖存在多个版本)。这个问题往往也会有这样的表现:多实例部署的时候,有的实例是好的,有的实例则不行。

查看加载的类和方法

根据之前分析的异常种类,我们可以去运行中的现场确认当前加载的问题。

这里我们可以使用阿里开源的Arthas工具,如果第一次用,那么按下面操作先安装再运行:

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

运行好之后,会打印出当前运行着的java应用,比如:

[INFO] arthas-boot version: 3.4.6
[INFO] Process 40611 already using port 3658
[INFO] Process 40611 already using port 8563
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 40611 chapter4-3-0.0.1-SNAPSHOT.jar
  [2]: 37786

通过输入编号选择要查看的java应用,比如这里选择:1,进入到chapter4-3-0.0.1-SNAPSHOT.jar中去。

下面介绍两个重要命令:

第一个:sc命令,我们确认一下可能冲突的jar包下面,是否有对应的class。有些不同版本包下class就不一样,马上就可以分辨出来。

比如,通过下面的命令,我们查看一下com.didispace包下有什么类:

[arthas@40611]$ sc com.didispace.*
com.didispace.chapter43.Chapter43Application
com.didispace.chapter43.Chapter43Application$$EnhancerBySpringCGLIB$$8b82b194
com.didispace.chapter43.UploadController
Affect(row-cnt:3) cost in 6 ms.

第二个:sm命令,查看具体某个类有哪些方法。有的版本差异就是去掉了某个方法,这个时候我们就可以通过这个命令来查看。

比如,通过下面的命令,我们查看一下com.didispace.chapter43.UploadController类下有些什么方法:

[arthas@40611]$ sm com.didispace.chapter43.UploadController
com.didispace.chapter43.UploadController <init>()V
com.didispace.chapter43.UploadController create(Lorg/springframework/web/multipart/MultipartFile;)Ljava/lang/String;
com.didispace.chapter43.UploadController uploadPage()Ljava/lang/String;
Affect(row-cnt:3) cost in 5 ms.
本文首发:https://blog.didispace.com/ar...

找到冲突并解决冲突

在确认完是加载错误的情况下,我们要去解决冲突。那么解决冲突要做的就是找到到底哪里冲突了以及我们要去除或者强制

找出版本冲突的方法:使用Maven命令:mvn -U dependency:tree -Dverbose

命令执行之后,会在控制台以树状形式列出所有依赖内容,然后通过搜索的方式查找冲突的包,看看都是从哪个依赖中带进来的(在IDEA中搜索会高亮,更容易找到)。

[INFO] com.didispace:chapter4-3:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.4.1:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:2.4.1:compile
[INFO] |  |  +- org.springframework.boot:spring-boot:jar:2.4.1:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-autoconfigure:jar:2.4.1:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:2.4.1:compile
[INFO] |  |  |  +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] |  |  |  |  \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.13.3:compile
[INFO] |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.13.3:compile
[INFO] |  |  |  \- org.slf4j:jul-to-slf4j:jar:1.7.30:compile
[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] |  |  \- org.yaml:snakeyaml:jar:1.27:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-json:jar:2.4.1:compile
[INFO] |  |  +- com.fasterxml.jackson.core:jackson-databind:jar:2.11.3:compile
[INFO] |  |  |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.11.3:compile
[INFO] |  |  |  \- com.fasterxml.jackson.core:jackson-core:jar:2.11.3:compile
[INFO] |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.11.3:compile
[INFO] |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.11.3:compile
[INFO] |  |  \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.11.3:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.4.1:compile
[INFO] |  |  +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.41:compile
[INFO] |  |  +- org.glassfish:jakarta.el:jar:3.0.3:compile
[INFO] |  |  \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.41:compile
[INFO] |  +- org.springframework:spring-web:jar:5.3.2:compile
[INFO] |  |  \- org.springframework:spring-beans:jar:5.3.2:compile
[INFO] |  \- org.springframework:spring-webmvc:jar:5.3.2:compile
[INFO] |     +- org.springframework:spring-aop:jar:5.3.2:compile
[INFO] |     +- org.springframework:spring-context:jar:5.3.2:compile
[INFO] |     \- org.springframework:spring-expression:jar:5.3.2:compile
[INFO] +- org.springframework.boot:spring-boot-starter-thymeleaf:jar:2.4.1:compile
[INFO] |  +- org.thymeleaf:thymeleaf-spring5:jar:3.0.11.RELEASE:compile
[INFO] |  |  +- org.thymeleaf:thymeleaf:jar:3.0.11.RELEASE:compile
[INFO] |  |  |  +- org.attoparser:attoparser:jar:2.0.5.RELEASE:compile

解决版本冲突的方式主要两种:

  1. 通过上面的命令找到不需要的版本之后,在引入的依赖中,使用exclusions将其排除,比如下面这样:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    <exclusions>
        <exclusion>
            <groupId>xxx</groupId>
            <artifactId>yyy</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  1. pom.xml中强制指定要使用的版本,这样这个优先级最高,就不会引入其他版本要带进来的版本了。

好了,今天的分享到这里结束了,希望对你有所帮助。如果您觉得本文有用,欢迎转发扩散!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 2 收藏 1 评论 0

程序猿DD 发布了文章 · 1月15日

聊一聊:MyBatis和Spring Data JPA的选择问题

从个人开发角度来说,Spring Data JPA更好用,是因为开发起来更快。

但从团队角度,我们希望更好的维护性,spring data jpa就差一些,或者说对后期人的要求更高。

很容易出现这种情况:

监控系统发现某个慢查询了,运维把SQL发到开发群里,大家自查一下。

此时很可能发现根本没人回应,都说没有这句SQL。

然后运维定位到某个库,找到这个库的使用人,让他去看。

他可能也就拿着SQL全局去搜,发现还是搜不到。

如果这个人责任心不强,可能就说 没找到这个SQL,责任心强调的,对Spring Data JPA熟悉点的,就要开始去分析这个SQL可能在哪里,然后找到对应的实现地方去修改。

这就是Spring Data JPA在团队作战时候,容易引发维护成本高的真实场景。

P.S. 我开发自己独立产品的时候,还是喜欢用它的,因为自己再熟悉不过,不会有这样的场景。所以果断选择,但如果团队作战,我还是会选在MyBatis。

那么你怎么看呢?留言区见!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 3 收藏 1 评论 1

程序猿DD 发布了文章 · 1月14日

Spring Boot 2.x基础教程:多个文件的上传

昨天,我们介绍了如何在Spring Boot中实现文件的上传。有读者问:那么如果有多个文件要同时上传呢?这就马上奉上,当碰到多个文件要同时上传的处理方法。

动手试试

本文的动手环节将基于Spring Boot中实现文件的上传一文的例子之上,所以读者可以拿上一篇的例子作为基础来进行改造,以体会这之间的区别,下面也主要讲解核心区别的地方。

第一步:修改文件上传页面的上传表单

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8" />
    <title>文件上传页面 - didispace.com</title>
</head>
<body>
<h1>文件上传页面</h1>
<form method="post" action="/upload" enctype="multipart/form-data">
    文件1:<input type="file" name="files"><br>
    文件2:<input type="file" name="files"><br>
    <hr>
    <input type="submit" value="提交">
</form>
</body>
</html>

可以看到这里多增加一个input文件输入框,同时文件输入框的名称修改为了files,因为是多个文件,所以用了复数。注意:这几个输入框的name是一样的,这样才能在后端处理文件的时候组织到一个数组中。

第二步:修改后端处理接口

@PostMapping("/upload")
@ResponseBody
public String create(@RequestPart MultipartFile[] files) throws IOException {
    StringBuffer message = new StringBuffer();

    for (MultipartFile file : files) {
        String fileName = file.getOriginalFilename();
        String filePath = path + fileName;

        File dest = new File(filePath);
        Files.copy(file.getInputStream(), dest.toPath());
        message.append("Upload file success : " + dest.getAbsolutePath()).append("<br>");
    }
    return message.toString();
}

几个重要改动:

  1. MultipartFile使用数组,参数名称files对应html页面中input的name,一定要对应。
  2. 后续处理文件的主体(for循环内)跟之前的一样,就是对MultipartFile数组通过循环遍历的方式对每个文件进行存储,然后拼接结果返回信息。

更多本系列免费教程连载「点击进入汇总目录」

测试验证

第一步:启动Spring Boot应用,访问http://localhost:8080,可以看到如下的文件上传页面。

第二步:选择2个不大于2MB的文件,点击“提交”按钮,完成上传。

如果上传成功,将显示类似下面的页面:

你可以根据打印的文件路径去查看文件是否真的上传了。

代码示例

本文的相关例子可以查看下面仓库中的chapter4-4目录:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 4 收藏 4 评论 0

程序猿DD 发布了文章 · 1月13日

你一定需要知道的高阶JAVA枚举特性!

JAVA枚举,比你想象中还要有用!

我经常发现自己在Java中使用枚举来表示某个对象的一组潜在值。

在编译时确定类型可以具有什么值的能力是一种强大的能力,它为代码提供了结构和意义。

当我第一次了解枚举时,当时我认为它们只是一个为常量命名的工具,可以很容易地被静态常量字符串ENUM_VAL_NAME所取代。

后来我发现我错了。事实证明,Java枚举具有相当高级的特性,可以使代码干净、不易出错,功能强大。

让我们一起来看看Java中的一些高级枚举特性,以及如何利用这些特性使代码更简单、更可读。

枚举是类!

在Java中,枚举是Object的一个子类。让我们看看所有枚举的基类,Enum<E>(为简洁起见进行了修改)。


public abstract class Enum<E extends Enum<E>>
    implements Constable, Comparable<E>, Serializable {
  private final String name;
  
  public final String name() {
      return name;
  }
  
  private final int ordinal;
  
  public final int ordinal() {
      return ordinal;
  }
  
  protected Enum(String name, int ordinal) {
      this.name = name;
      this.ordinal = ordinal;
  }
  
  public String toString() {
      return name;
  }
  
  public final boolean equals(Object other) {
      return this==other;
  }
  
  public final int hashCode() {
      return super.hashCode();
  }
  
  public final int compareTo(E o) {
      Enum<?> other = (Enum<?>)o;
      Enum<E> self = this;
      if (self.getClass() != other.getClass() && // optimization
          self.getDeclaringClass() != other.getDeclaringClass())
          throw new ClassCastException();
      return self.ordinal - other.ordinal;
  }
}
  

我们可以看到,这基本上只是一个常规的抽象类,有两个字段,name和ordinal。

所以说枚举都是类,所以它们具有常规类的许多特性。

我们能够为枚举提供实例方法、构造函数和字段。我们可以重写toString(),但不能重写hashCode()或equals(Object other)。

接下来我们看下我们的枚举示例,Operation

  enum Operation {
    ADD,
    SUBTRACT,
    MULTIPLY
  }

这个枚举表示一个Operation可以对两个值执行,并将生成一个结果。关于如何实现此功能,您最初的想法可能是使用switch语句,如下所示:

  public int apply(Operation operation, int arg1, int arg2) {
    switch(operation) {
      case ADD:
        return arg1 + arg2;
      case SUBTRACT:
        return arg1 - arg2;
      case MULTIPLY:
        return arg1 * arg2;
      default:
        throw new UnsupportedOperationException();
  }
}
  

当然,这样子会有一些问题。

第一个问题是,如果我们将一个新操作添加到我们的枚举Operation中,编译器不会通知我们这个开关不能正确处理新操作。

更糟糕的是,如果一个懒惰的开发人员在另一个类中复制或重新编写这些代码,我们可能无法更新它。

第二个问题是默认情况default,每段程序里面都是必需的,尽管我们知道在正确的代码里它永远不会发生。

这是因为Java编译器知道上面的第一个问题,并且希望确保我们能够处理在不知情的情况下向Operation中添加了新枚举。

还好,Java8用函数式编程为我们提供了一个干净的解决方案。

函数枚举实现

因为枚举是类,所以我们可以创建一个枚举字段来保存执行操作的函数。

但是在我们找到解决方案之前,让我们先来看看一些重构。

首先,让我们把开关放在enum类中。

  
enum Operation {
  ADD,
  SUBTRACT,
  MULTIPLY;
  
  public static int apply(Operation operation, int arg1, int arg2) {
    switch(operation) {
      case ADD:
        return arg1 + arg2;
      case SUBTRACT:
        return arg1 - arg2;
      case MULTIPLY:
        return arg1 * arg2;
      default:
        throw new UnsupportedOperationException();
    }
  }
}
  

我们可以这样做:Operation.apply(Operation.ADD, 2, 3);

因为我们现在从Operation中调用方法,所以我们可以将其更改为实例方法并使用this,而不是用Operation.apply()来实现,如下所示:

public int apply(int arg1, int arg2) {
  switch(this) {
    case ADD:
      return arg1 + arg2;
    case SUBTRACT:
      return arg1 - arg2;
    case MULTIPLY:
      return arg1 * arg2;
    default:
      throw new UnsupportedOperationException();
  }
}
  

像这样使用:Operation.ADD.apply(2, 3);

看起来变好了。现在让我们更进一步,通过使用函数式编程完全消除switch语句。

enum Operation {
              ADD((x, y) -> x + y),
              SUBTRACT((x, y) -> x - y),
              MULTIPLY((x, y) -> x * y);
  
              Operation(BiFunction<Integer, Integer, Integer> operation) {
                      this.operation = operation;
              }
  
              private final BiFunction<Integer, Integer, Integer> operation;
  
              public int apply(int x, int y) {
                      return operation.apply(x, y);
              }
  
  }
  
  

这里我做的是:

  • 添加了一个字段 BiFunction<Integer, Integer, Integer> operation
  • 用BiFunction创建了用于Operation的构造函数。
  • 调用枚举定义中的构造函数,并用lambda指定BiFunction<Integer, Integer, Integer>。

这个java.util.function.BiFunction operation字段是对采用两个参数的函数(方法)的引用。

在我们的例子中,两个参数都是int型,返回值也是int型。不幸的是,Java参数化类型不支持原语,所以我们必须使用Integer。

因为BiFunction是用@functioninterface注释的,所以我们可以使用Lambda表示法定义一个。

因为我们的函数接受两个参数,所以我们可以使用(x,y)来指定它们。

然后我们定义了一个单行方法,它使用 ->x+y 返回一个值。这相当于下面的方法,只是更简洁而已。

  class Adder implements BiFunction<Integer, Integer, Integer> {
          @Override
          public Integer apply(Integer x, Integer y) {
                  return x + y;
    }
  }

我们的新Operation实现采用相同的方式:Operation.ADD.apply(2, 3);.

但是,这种实现更好,因为编译器会告诉我们何时添加了新Operation,这要求我们更新新函数。如果没有这一点,如果我们在添加新Operation时还不记得更新switch语句,就有可能得到UnsupportedOperationException()。

关键要点

  • Enum枚举是Enum<T>的扩展类。
  • Enum枚举可以有字段、构造函数和实例方法。
  • Enum枚举字段可以存储函数。与lambdas配合使用,可以创建干净、安全的特定于枚举的函数实现,并在编译时强制执行它们(而不是使用switch)。

下面是这个示例的GitHub地址。(https://github.com/alex-power...

本文参考:https://medium.com/javarevisi...

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 1 收藏 1 评论 0

程序猿DD 发布了文章 · 1月13日

Java微服务 vs Go微服务,究竟谁更强!?

前言

Java微服务能像Go微服务一样快吗?

这是我最近一直在思索地一个问题。

去年8月份的the Oracle Groundbreakers Tour 2020 LATAM大会上,Mark Nelson和Peter Nagy就对此做过一系列基础的的测试用以比较。接下来就给大家介绍下。

在程序员圈子里,普遍的看法是Java老、慢、无聊 ,而Go是快、新、酷

为了尽可能的进行一个相对公平的测试,他们使用了一个非常简单的微服务,没有外部依赖关系(比如数据库),代码路径非常短(只是操纵字符串),使用了小型的、轻量级的框架(Helidon for Java和Go工具包for Go),试验了不同版本的Java和不同的jvm。

对决双雄

我们先来看下擂台两边的选手:

  • 身穿深色战服的选手是JAVA
Java是由被甲骨文收购的Sun Microsystems开发的。它的1.0版本是1996年发布的,最新的版本是2020年的Java15。主要的设计目标是Java虚拟机和字节码的可移植性,以及带有垃圾收集的内存管理。它是全世界最流行的语言之一,在开源环境下开发。

我们先看下JAVA的问题,大家普遍认为它最大的问题就是速度慢,已经慢到让人觉得不再是合理的,而是更具历史意义的。不过这么多年来,Java诞生了很多不同的垃圾收集算法用来加快它运行的速度。

Oracle实验室最近已经开发了一个新的Java虚拟机GraalVM,它有一个新的编译器和一些令人兴奋的新特性,比如能够将Java字节码转换成一个本机映像,可以在没有javavm的情况下运行等。

  • 而它的对手就是年轻充满活力的GO
GO是由谷歌的罗伯特·格里默、罗伯·派克和肯·汤姆森创建的。他们对UNIX、B、C、Plan9、UNIX窗口系统等做出了重大贡献。GO是开源的,在2012年发布了1.0版本(比JAVA晚了16年),在2020年发布了1.15版本。无论是在采用方面,还是在语言和工具生态系统本身方面,它都在快速增长。

GO受C、Python、JavaScript和C++等多种语言的影响。被设计成高性能网络和多处理的最佳语言。

StackOverflow有27872个带“Go”的问题,而Java只有1702730个。足见长江后浪推前浪。

Go是一种静态类型的编译语言。它有称为goroutines的轻量级进程(这些不是OS线程),它们之间有独特的通信通道(类型化的,FIFO)。Go是许多CNCF项目的首选语言,例如Kubernetes、Istio、Prometheus和Grafana

赛前对比

从个人感觉来说,Go相比JAVA来说,优点在于:

  • Go更容易实现复合、纯函数、不变状态等功能模式。
  • Go处于生命周期的早期,因此它没有向后兼容性的沉重负担—Go仍然可以轻易打破某些限制来改进。
  • Go编译成一个本机静态链接的二进制文件-没有虚拟机层-二进制文件拥有运行程序所需的一切,这对于“从头开始”的容器来说非常好。
  • Go体积小、启动快、执行快(目前是的)
  • Go没有OOP,继承,泛型,断言,指针算法
  • Go写法上较少的括号
  • Go没有循环依赖、没有未使用的变量或导入、没有隐式类型转换的强制
  • Go样板代码少得多

缺点是:

  • Go工具生态系统还不成熟,尤其是依赖关系管理——有几个选项,没有一个是完美的,特别是对于非开源开发;仍然存在兼容性挑战。
  • 构建具有新的/更新的依赖项的代码非常慢(比如Maven著名的“下载Internet”问题)
  • 导入将代码绑定到存储库,这使得在存储库中移动代码成为一场噩梦。
  • 调试、评测等仍然是一个挑战
  • 用到了指针
  • 需要实现一些基本的算法
  • 没有动态链接
  • 没有太多旋钮来调优执行或垃圾收集、概要文件执行或优化算法。

比赛开始

使用JMeter来运行负载测试。这些测试多次调用这些服务,并收集有关响应时间、吞吐量(每秒事务数)和内存使用情况的数据。对于Go,收集驻留集大小;对于Java,跟踪本机内存。

在测量之前,使用1000次服务调用对应用程序进行预热。

应用程序本身的源代码以及负载测试的定义都在这个GitHub存储库中:https://github.com/markxnelso...

第一回合

在第一轮测试中,在一台“小型”机器上进行了测试,是一台2.5GHz双核Intel core i7笔记本电脑,16GB内存运行macOS。测试运行了100个线程,每个线程有10000个循环,上升时间为10秒。Java应用程序运行在JDK11和Helidon2.0.1上。使用Go 1.13.3编译的Go应用程序。

结果如下:

file

file

可以看出,第一回合是Go赢了!

JAVA占的内存太多了;预热对JVM有很大的影响—我们知道JVM在运行时会进行优化,所以这是有意义的

在第一回合的基础上,意犹未尽的又引入GraalVM映像以使 Java 应用程序的执行环境更接近于 Go 应用程序的环境,添加了 GraalVM 映像测试(用 GraalVM EE 20.1.1ー JDK 11构建的本机映像)的结果是:

file

file

通过使用 GraalVM 映像在 JVM 上运行应用程序,我们没有看到吞吐量或响应时间方面的任何实质性改进,但是内存占用的确变小了。

下面是一些测试的响应时间图:

file

第二回合

在第二轮测试中,使用一台更大的机器上运行测试。36核(每个核两个线程)、256GB内存、运行oraclelinux7.8的机器。

和第一轮类似,使用了100个线程,每个线程使用了10,000个循环,10秒的加速时间,以及相同版本的 Go,Java,Helidon 和 GraalVM。

结果如下:

file

这一回合是GraalVM 映像赢了!

下面是一些测试的响应时间图:

file

file

file

在这个测试中,Java变体的表现要好得多,并且在没有使用Java日志记录的情况下,它的性能大大超过了Go。Java似乎更能使用硬件提供的多核和执行线程(与Go相比)。

这一轮的最佳表现来自GraalVM native image,平均响应时间为0.25毫秒,每秒事务数为82426个,而Go的最佳结果为1.59毫秒和39227个tps,然而这是以多占用两个数量级的内存为代价的!

GraalVM映像比在jvm上运行的同一应用程序快大约30–40%!

第三回合

这次,比赛在Kubernetes集群中运行这些应用程序,这是一个更自然的微服务运行时环境。

这次使用了一个Kubernetes 1.16.8集群,它有三个工作节点,每个节点有两个内核(每个内核有两个执行线程)、14GB的RAM和oraclelinux7.8。

应用程序访问是通过Traefik入口控制器进行的,JMeter在Kubernetes集群外运行,用于一些测试,而对于其他测试,使用ClusterIP并在集群中运行JMeter。

与前面的测试一样,我们使用了100个线程,每个线程使用了10,000个循环,以及10秒的加速时间。

下面是各种不同容器的大小:

  • Go 11.6MB 11.6 MB
  • Java/Helidon 1.41GB 1.41 GB
  • Java/Helidon JLinked 150MB 150mb
  • Native image 25.2MB 25.2 MB

结果如下:

file

下面是一些测试的响应时间图:

file

在这一轮中,我们观察到 Go 有时更快,GraalVM 映像有时更快,但这两者之间的差别很小(通常小于5%)。

Java似乎比Go更善于使用所有可用的内核/线程—我们在Java测试中看到了更好的CPU利用率。Java性能在拥有更多内核和内存的机器上更好,Go性能在较小/功能较弱的机器上更好。在一台“生产规模”的机器上,Java很容易就和Go一样快,或者更快

最后

接下来会做更多的测试比赛,来看一看究竟谁更好!有兴趣的你也可以自己试一试,记得告诉我们结果哦!

本文参考:https://medium.com/helidon/ca...

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 1 收藏 1 评论 0

程序猿DD 发布了文章 · 1月12日

文件上传的单元测试怎么写?

早上有个群友问了一个不错的问题:文件上传的单元测试怎么写?后面也针对后端开发要不要学一下单元测试的话题聊了聊,个人是非常建议后端开发能够学一下单元测试的。所以,今天特地拿出来写一篇说说,并不是因为这有多难写,而是作为出色的后端开发人员,单元测试如果你能考虑周到,那么从代码结构,程序质量上都会有很大的提升。而实际开发过程中,很少有开发人员会特别关注这个方面。

言归正传,下面我们具体说说当碰到需要上传文件的接口,我们要如何写单元测试!

先来回忆一下,普通接口的单元测试我们是如何写的?看看我们入门例子中的单元测试:

@SpringBootTest
public class Chapter11ApplicationTests {

    private MockMvc mvc;

    @Before
    public void setUp() {
        mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
    }

    @Test
    public void getHello() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("Hello World")));
    }

}

这里我们所用到的核心是MockMvc工具,通过模拟http请求的提交并指定相关的期望返回来完成。

对于文件上传接口,本质上还是http请求的处理,所以MockMvc依然逃不掉,就是上传内容发生了改变,我们只需要去找一下文件上传的模拟对象是哪个,就可以轻松完成这个任务。

具体写法如下:

@SpringBootTest(classes = Chapter43Application.class)
public class FileTest {

    @Autowired
    protected WebApplicationContext context;
    protected MockMvc mvc;

    @BeforeEach
    public void setUp() {
        mvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    public void uploadFile() throws Exception {
        MockMultipartFile file = new MockMultipartFile(
                "file",
                "hello.txt",
                MediaType.TEXT_PLAIN_VALUE,
                "Hello, World!".getBytes()
        );

        final MvcResult result = mvc.perform(
                MockMvcRequestBuilders
                        .multipart("/upload")
                        .file(file))
                .andDo(print())
                .andExpect(status().isOk())
                .andReturn();
    }

}

可以看到MockMvc的测试主体是不变的,无非就是请求类型和请求内容发生了改变。

今天的这篇很水,但是否会编写单元测试以及能否写好单元测试,是很难看出一个后端开发水平的。所以,我是非常推荐大家能够在编写业务实现的时候,先考虑一下自己的单元测试是否方便写,甚至先定义好接口,并写好单元测试,再去写实现(传说中的测试驱动开发)。

最近的分享到这里结束,有更多想法可来公众号、星球或社群交流!

更多本系列免费教程连载「点击进入汇总目录」

代码示例

本文的相关例子可以查看下面仓库中的chapter4-3目录:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 3 收藏 1 评论 0

程序猿DD 发布了文章 · 1月12日

Spring Boot 2.x基础教程:使用Flyway管理数据库版本

之前已经介绍了很多在Spring Boot中使用MySQL的案例,包含了Spring Boot最原始的JdbcTemplateSpring Data JPA以及我们国内最常用的MyBatis。同时,对于一些复杂场景比如:更换Druid数据源,或是多数据源的情况也都做了介绍。

不论我们使用哪一个具体实现框架,都离不开对数据库表结构的管理。而这一类管理一直都存在一个问题:由于数据库表元数据存储于数据库中,而我们的访问逻辑都存在于Git或其他代码仓库中。Git已经帮助我们完成了代码的多版本管理,那么数据库中的表该如何做好版本控制呢?

今天我们就来介绍在Spring Boot中使用Flyway来管理数据库版本的方法。

Flyway简介

Flyway是一个简单开源数据库版本控制器(约定大于配置),主要提供migrate、clean、info、validate、baseline、repair等命令。它支持SQL(PL/SQL、T-SQL)方式和Java方式,支持命令行客户端等,还提供一系列的插件支持(Maven、Gradle、SBT、ANT等)。

官方网站:https://flywaydb.org/

本文对于Flyway的自身功能不做过多的介绍,读者可以通过阅读官方文档或利用搜索引擎获得更多资料。下面我们具体说说在Spring Boot应用中的应用,如何使用Flyway来创建数据库以及结构不一致的检查。

动手试试

下面我们先预设一个开发目标:

  1. 假设我们需要开发一个用户管理系统,那么我们势必要设计一张用户表,并实现对用户表的增删改查操作。
  2. 在任务1的功能完成之后,我们又有一个新需求,需要对用户表增加了一个字段,看看如何实现对数据库表结构的更改。

目标 1 的实现

第一步:创建一个基础的Spring Boot项目,并在pom.xml中加入Flyway、MySQL连接和数据访问相关的必要依赖(这里选用spring-boot-starter-jdbc作为例子)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-core</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

第二步:按Flyway的规范创建版本化的SQL脚本。

  • 在工程的src/main/resources目录下创建db目录,在db目录下再创建migration目录
  • migration目录下创建版本化的SQL脚本V1__Base_version.sql
DROP TABLE IF EXISTS user ;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(20) NOT NULL COMMENT '姓名',
  `age` int(5) DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:如果你不想将SQL脚本放到其他目录,可以用spring.flyway.locations参数来配置。这里不同于1.x版本的配置项flyway.locations

第三步:根据User表的结构,编写对应的实体定义

@Data
@NoArgsConstructor
public class User {

    private Long id;
    private String name;
    private Integer age;

}

第四步:编写用户操作接口和实现

public interface UserService {

    /**
     * 新增一个用户
     *
     * @param name
     * @param age
     */
    int create(String name, Integer age);

    /**
     * 根据name查询用户
     *
     * @param name
     * @return
     */
    List<User> getByName(String name);

    /**
     * 根据name删除用户
     *
     * @param name
     */
    int deleteByName(String name);

    /**
     * 获取用户总量
     */
    int getAllUsers();

    /**
     * 删除所有用户
     */
    int deleteAllUsers();

}

@Service
public class UserServiceImpl implements UserService {

    private JdbcTemplate jdbcTemplate;

    UserServiceImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public int create(String name, Integer age) {
        return jdbcTemplate.update("insert into USER(NAME, AGE) values(?, ?)", name, age);
    }

    @Override
    public List<User> getByName(String name) {
        List<User> users = jdbcTemplate.query("select * from USER where NAME = ?", (resultSet, i) -> {
            User user = new User();
            user.setId(resultSet.getLong("ID"));
            user.setName(resultSet.getString("NAME"));
            user.setAge(resultSet.getInt("AGE"));
            return user;
        }, name);
        return users;
    }

    @Override
    public int deleteByName(String name) {
        return jdbcTemplate.update("delete from USER where NAME = ?", name);
    }

    @Override
    public int getAllUsers() {
        return jdbcTemplate.queryForObject("select count(1) from USER", Integer.class);
    }

    @Override
    public int deleteAllUsers() {
        return jdbcTemplate.update("delete from USER");
    }

}
这里主要介绍Flyway的应用,所以采用这种比较简单的编写方式,实际项目应用中,还是推荐MyBatis的具体操作实现。

第五步:编写测试用例

@Slf4j
@SpringBootTest
public class Chapter311ApplicationTests {

    @Autowired
    private UserService userSerivce;

    @Test
    public void test() throws Exception {
        userSerivce.deleteAllUsers();

        // 插入5个用户
        userSerivce.create("Tom", 10);
        userSerivce.create("Mike", 11);
        userSerivce.create("Didispace", 30);
        userSerivce.create("Oscar", 21);
        userSerivce.create("Linda", 17);

        // 查询名为Oscar的用户,判断年龄是否匹配
        List<User> userList = userSerivce.getByName("Oscar");
        Assertions.assertEquals(21, userList.get(0).getAge().intValue());

        // 查数据库,应该有5个用户
        Assertions.assertEquals(5, userSerivce.getAllUsers());

        // 删除两个用户
        userSerivce.deleteByName("Tom");
        userSerivce.deleteByName("Mike");

        // 查数据库,应该有5个用户
        Assertions.assertEquals(3, userSerivce.getAllUsers());
    }

}
注意由于Spring Boot 2.4应用的junit版本与之前Spring Boot 1.x版本中的不同,因此单元测试的编写略有区别,有兴趣的读者可以分别查看之前介绍文章和这篇文章中的单元测试的区别,这里就不细说了。

第六步:运行上面编写的单元测试,验证一下效果。

不出意外,单元测试运行ok的话

连上数据库看看。此时应该多出了这两张表:

  • user表就是我们维护在SQL脚本中要创建的表
  • flyway_schema_history表是flyway的管理表,用来记录在这个数据库上跑过的脚本,以及每个脚本的检查依据。这样每次应用启动的时候,就可以知道哪个脚本需要运行,或者哪个脚本发生了变动,运行基础可能不对,造成数据结构的混乱而阻止运行。

目标 2 的实现

有了上面的基础之后,我们来说说后续要做表结构的表变动该怎么操作,这也是之前读者出现问题最多的情况,所以在2.x版本教程中特地讲一讲。

首先,大家在开始使用Flyway之后,对于数据库表接口的变更就要关闭这几个途径:

  1. 直接通过工具登录数据去修改表结构
  2. 已经发布的sql脚本不允许修改

正确的表结构调整途径:在flyway脚本配置路径下编写新的脚本,启动程序来执行变更。这样可以获得几个很大的好处:

  1. 脚本受Git版本管理控制,可以方便的找到过去的历史
  2. 脚本在程序启动的时候先加载,再提供接口服务,一起完成部署步骤
  3. 所有表结构的历史变迁,在管理目录中根据版本号就能很好的追溯

下面根据一个实际需求来具体操作下。假设我们现在想对User表增加一个字段:address,用来存储用户的通讯地址,那么我们就需要这样操作实现。

第一步:创建脚本文件V1_1__alter_table_user.sql,并写入增加address列的语句

ALTER TABLE `user` ADD COLUMN `address` VARCHAR(20) DEFAULT NULL;
对于脚本文件名的基本规则是:版本号__描述.sql。当然如果你有更细致的要求,那么可以做更细致的文件名规划,具体细节读者可以查阅文末参考资料中的官方文档获取。

第二步:再次执行单元测试,在控制台中可以看到如下日志:

2021-01-11 16:58:12.025  INFO 37330 --- [           main] o.f.c.i.database.base.DatabaseType       : Database: jdbc:mysql://localhost:3306/test (MySQL 8.0)
2021-01-11 16:58:12.063  INFO 37330 --- [           main] o.f.core.internal.command.DbValidate     : Successfully validated 2 migrations (execution time 00:00.020s)
2021-01-11 16:58:12.075  INFO 37330 --- [           main] o.f.core.internal.command.DbMigrate      : Current version of schema `test`: 1
2021-01-11 16:58:12.082  INFO 37330 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema `test` to version "1.1 - alter table user"
2021-01-11 16:58:12.113  INFO 37330 --- [           main] o.f.core.internal.command.DbMigrate      : Successfully applied 1 migration to schema `test` (execution time 00:00.045s)

再查看一下数据中国的内容:

User表中已经有了Address列

Flyway管理表中已经有新脚本的加载记录

如果你还没有体会到引入Flyway对给我们的表结构带来的好处的话,不妨也留言分享下你们的管理方式吧!

更多本系列免费教程连载「点击进入汇总目录」

代码示例

本文的相关例子可以查看下面仓库中的chapter3-11目录:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

参考资料

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 4 收藏 2 评论 0

程序猿DD 发布了文章 · 1月12日

当音乐学博士搞起编程,用一本书改变了Java世界!

前言

说到Spring,也许现在的开发者们最先想到的是 Josh Long

file

超快的语速与现场代码能力,让很多Java开发者折服。

然后Spring的历史上,最传奇的还是要数其创始人:Rod Johnson!

先不说别的,看到他的学历,你就震惊的了,悉尼大学的音乐学博士!不要惊讶,不是计算机!

也许就是因为这样一颗理性思维与艺术细胞结合的秃顶大佬,才能造就Spring这样的产物吧。

file

Rod Johnson

Rod Johnson,就是上图这位头顶略微地中海的男子,但是秃脑袋瓜并没有让Rod Johnson变得难看,好几个同学一致认为这家伙长得很酷。

按照他身边的密友所描述的,Rod Johnson平日里看上去就像是一个典型的英国绅士,虽然他好像出生在澳大利亚,但是现在住在伦敦。说起话来也是一板一眼,有条有理。字正腔圆而略有点尖的口音让人听起来特别清楚。

大多数人都认为Rod Johnson似乎天生缺少一样东西:幽默感。但实际上看看下面这端开场白:

回到2001年,当我写下第一行代码时,那些代码并没有放到GitHub,没放在上面是因为GitHub当时不存在,所以我想Spring应该比Git老三四岁...

让人不禁宛然一笑,Rod Johnson也跟其他程序员一样是个挺可爱的人吧。

file

轮子理论

提到Rod Johnson,大家还总会想起轮子理论
所谓轮子理论,就是指:不要重复发明轮子,这是西方国家的一句谚语,原话是:Don't Reinvent the Wheel。意思是企业中任何一项工作实际上都有人做过,我们所需要做的就是找到做过这件事情的人。拿到软件领域中就是指有的项目或功能,别人已经做过,我们需要用的时候,直接拿来用即可,而不要重新制造。

Rod Johnson想告诉我们,Spring就是在不重复发明轮子的理念及指导原则上做起来。

于是一夜之间,随着Spring在全世界的风风火火,特别是吹到我们祖国的时候,也许是爱屋及乌的原因吧,轮子理论也被众多的Spring粉丝当成做人做事做程序的信条及原则。

Spring与Expert One on one J2EEDevelopment without EJB

Spring,可以说就像是Rod Johnson的代名词一样,大家对Rod Johnson印象最深的成就自然是SpringFramework和Expert One on one J2EEDevelopment without EJB。

Java从诞生之日到如今经历了风风雨雨数十年,从低谷到高峰,JAVA之所以是世界上最受欢迎的开发语言之一,Spring框架起到了非常重要的作用。

当时间还停在21世纪初,Java EE的整个系统框架处在臃肿、低效、脱离现实的种种现状之中,将其进行轻量化成为业内的一致的呼声,此时Rod Johnson就像一颗璀璨的明珠一样横空出世,积极寻求探索革新之道。

Rod Johnson最开始在2000年为伦敦金融界提供独立咨询业务时曾经写了一个简单的框架,以此为基础他编写了interface21框架,这是一个力图冲破Java EE传统开发的困境,从实际需求出发,着眼于轻便、灵巧,易于开发、测试和部署的轻量级开发框架。可以说这就是Spring的前身,Rod Johnson当时的观点就是 :如何让应用程序能以超出当时大众所惯于接受的易用性和稳定性与J2EE平台上的不同组件合作。

Spring框架即以interface21框架为基础,经过重新设计,并不断丰富其内涵。最终于2004年3月24日,发布了1.0正式版。

配合Spring的诞生,Rod Johnson在同年编著了Expert one on one J2EE design and development一书,堪称经典,直至今日,还有不少莘莘学子将其视为Spring必读宝典。

file

这本书甫一面世,就在Java世界掀起了轩然大波,不断改变着Java开发者程序设计和开发的思考方式,影响至今。Rod Johnson根据自己多年丰富的实践经验,对EJB的各种笨重臃肿的结构进行了逐一的分析和否定,并分别以简洁实用的方式替换之。

Spring1.0版本发布之后,Spring框架在Java社区里变得异常流行,当然这也要部分的归功于它不错的文档功能和详尽的参考文献,特别是对于一个开源项目而言尤其如此。

Spring框架的一个重要设计目标就是更容易地与已有的J2EE(现在称之为JavaEE或JEE)标准和商用工具整合。

也正因此,Rod Johnson奠定了自己的江湖地位,成为一个改变Java世界的大师级人物。

本文首发:https://blog.didispace.com/he...

版本变迁

Spring 几乎已经成为现在每一位 Java 开发人员都耳熟能详的开发框架,不论你是一名初出茅庐的程序员还是经验丰富的老司机,都会对其有一定的了解或使用经验。在现代企业级应用架构中,Spring 技术栈几乎成为了 Java 语言的代名词。我们不妨从最初的 Spring 开始,看看它为什么能够横扫千军,一统江湖!

Spring版本变迁:

  • 自2004年Spring1.0发布之后,Spring 框架迅速发展,不断进化。1.0的出现彻底改变了开发企业级Java应用程序的方式。 Spring的依赖注入与声明式事务意味着组件之间再也不存在紧耦合,再也不用重量级的EJB了。
  • 2006 年 10 月,发布Spring 2.0 ,具有可扩展的 XML 配置功能,用于简化 XML 配置,支持 Java 5,额外的 IoC 容器扩展点,支持动态语言。更小、更简单易懂的配置文件让Spring本身更便于使用
  • 2007 年 11 月 ,Interface21 项目更名SpringSource,同时发布了 Spring 2.5,支持 Java 6 / Java EE 5,支持注释配置,classpath 中的组件自动检测和兼容 OSGi 的 bundle。让我们有了更优雅的面向注解的依赖注入模型(即@Component和@Autowired注解),以及面向注解的Spring MVC编程模型。不用再去显式地声明应用程序组件了,也不再需要去继承某个基础的控制器类了。
  • 2009 年 12 月,Spring 3.0 发布,具有许多重要特性,如重组模块系统,支持 Spring 表达式语言,基于 Java 的 bean 配置(JavaConfig),支持嵌入式数据库(如 HSQL,H2 和 Derby),模型验证/ REST 支持和对 Java EE 的支持。XML被取代,终于可以写出一个没有任何XML配置的Spring应用程序。
  • 2013 年 12 月,Pivotal(2013 年 4月,VMware 和 EMC 通过 GE 投资创建了一家名为 Pivotal 的合资企业。所有的 Spring 应用项目都转移到了 Pivotal) 宣布发布 Spring 框架 4.0。包含了对Java 8 的全面支持,更高的第三方库依赖性(groovy 1.8+,ehcache 2.1+,hibernate 3.6+等),Java EE 7 支持,groovy DSL for bean 定义,对 websockets 的支持以及对泛型类型的支持作为注入 bean 的限定符。
  • 2017年9月,Spring 5.0 GA版本发布,开始支持JDK 8和Java EE 7,同时兼容JDK9。全面支持Servlet 3.1,还引入了一个全新的模块Spring WebFlux用于替代老话的 spring-webmvc;对Kotlin也有了更好的支持。
  • 而目前,最新的是5.3.2 GA版本。

具体版本可见:https://spring.io/projects/sp...

Spring在不同的领域不断发展:移动开发,社交API集成、安全管理、NoSQL数据库、云计算和大数据等等都是它正在涉足和创新的领域,使其前景更加广阔,甚至已经形成与传统的JavaEE平台分庭抗礼之势。

file

离开

江湖,有聚必有散。

2007 年,SpringSource 从基准资本获得了 A 轮融资(1000万美元)。在此期间SpringSource也收购了多家公司,如Hyperic,G2One 等。

但是等到了2009年8月,SpringSource反倒是以 4.2 亿美元被 VMWare 收购。

而在3年后的2012年7月,Rod Johnson就离开了他一手创建的Spring团队。

或许我们可以这样想,如果当初SpringSource 没有被VMWare 收购,是不是Rod Johnson 就不会离开团队,是不是现在的Spring会更好?答案我们自然不得而知。

Rod Johnson当年在SpringSource官方博客上公布这一消息时声称SpringSource将成为VMware下属的一个部门,而他将仍是SpringSource的领导者。他当时对未来的展望是:

此次决定是很自然而符合逻辑的:这将带来更多的新技术,并且对Spring框架以及Spring社区都有好处。

我很兴奋。希望你也是。这将极其有趣。

Spring框架将继续提供优质的企业级Java支持。我们从关注开发者如何创建和使用应用,到关注他们如何部署和运行企业级应用;为此我们创建了dm Server和tc Server。我们收购Hyperic也是为了改善开发者管理企业级应用的方法。

与VMware的合作中,我们计划创建一个简单,集成,创建-运行-管理合一的数据中心、私有云和公共云的解决方案。这个方案将融合应用架构的知识,连带中间件以及管理控件,确保一个虚拟环境在部署过程中以及运行时的最大效率及弹性。这是一个PaaS,建立在你已知的技术之上,从而最大的减少花费与复杂度。这是一个围绕开源、可移植的中间件技术的解决方案,既可以在传统数据中心的Java EE应用服务器上运行,又可以在如Amazon EC2之类的弹性云上运行,也可以在VMware平台上运行。

与VMware的vSphere以及其他云技术一起,我们将在框架和基础设施上带来一个全新的体验。SpringSource的应用框架、服务器及管理软件将成为VMware平台的眼睛和耳朵。

SpringSource的下一步工作将是这些新的挑战:基于我们的Build/Run/Manage(创建运行管理)系统,提供从桌面端到云端的最佳解决方案。让百万Java开发者都能享受到云计算带来的好处。

可能他当时心里想的是作为Java领域的重要厂商,在加入VMware后,其Java开发经验将与VMware的虚拟化平台相结合,增强其在企业服务市场中的竞争力。另外,SpringSource在全球大型企业中的广泛客户资源也能够让VMware受益。不管怎么说,似乎从现在的结果来看,Spring也做得不错,没有辜负Rod Johnson当初的一番期望。

传奇现今

现在的Rod Johnson成为了一个天使投资人,同时也是多个公司的董事(例如Neo Technology,elastic,Meteor和Hazelcast等著名的开源公司),早已走上人生巅峰。同时他依然还是经常会在技术大会上做演讲,游走在世界各地,为后来者传授各种技术经验及想法。

在去年10月份的SpringOne Platform大会上,Rod Johnson还特地做了一个关于Spring18岁的演讲,也许他的样子变了,但是讲起Spring,他还是像讲起自己的孩子一般滔滔不绝,Rod Johnson分享了Spring的起源、历史,总结了一些Spring框架发展过程的经验教训:

  • Spring的历史起源
  • Lesson 1: Fairy tales can offer useful lessons 寓言故事能提供有用的教训
  • Lesson 2: Need Clear, Shared Values 需要清晰,共有的价值
  • Lesson 3: Know where you're going 明确方向
  • Lesson 4: Quality beats quantity in a team 打造团队宁缺毋滥
  • Lesson 5: Market and sell your technical solution 营销你的技术方案
  • Lesson 6: Other people have great ideas. Borrow them but acknowledge their work 认可借鉴别人的好点子
  • Lesson 7: The developers you want need autonomy 开发者需要自治
  • Lesson 8: Question the "enterprise" mindset 质疑“企业级”观念
  • Lesson 9: Some Spring Advice 一些Spring的建议
  • Next For Me: Atomist - A Framework for Development and Delivery 我的下一站:Atomist——一个关于开发与交付的框架
  • Lesson 10: Raising software is like raising a child 开发软件像养育一个孩子

有兴趣的读者可以自行查看,视频地址如下:https://www.bilibili.com/vide...

你知不知道Rod Johnson?

你有没有看过Expert One on one J2EEDevelopment without EJB?

欢迎留言分享你的感受!

查看原文

赞 3 收藏 1 评论 0

程序猿DD 发布了文章 · 1月7日

Spring Boot 2.x基础教程:实现文件上传

文件上传的功能实现是我们做Web应用时候最为常见的应用场景,比如:实现头像的上传,Excel文件数据的导入等功能,都需要我们先实现文件的上传,然后再做图片的裁剪,excel数据的解析入库等后续操作。

今天通过这篇文章,我们就来一起学习一下如何在Spring Boot中实现文件的上传。

动手试试

第一步:创建一个基础的Spring Boot项目,如果还不会的话就先看看这篇《快速入门》

第二步:在pom.xml中引入模版引擎依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

你也可以选择其他你熟悉的模版引擎,比如:Freemarker。

第三步:在resources目录下,创建新目录templates;在templates目录下再创建一个文件上传的页面upload.html,内容如下:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8" />
    <title>文件上传页面</title>
</head>
<body>
<h1>文件上传页面</h1>
<form method="post" action="/upload" enctype="multipart/form-data">
    选择要上传的文件:<input type="file" name="file"><br>
    <hr>
    <input type="submit" value="提交">
</form>
</body>
</html>

第四步:创建文件上传的处理控制器,命名为UploadController

@Controller
@Slf4j
public class UploadController {

    @Value("${file.upload.path}")
    private String path;

    @GetMapping("/")
    public String uploadPage() {
        return "upload";
    }

    @PostMapping("/upload")
    @ResponseBody
    public String create(@RequestPart MultipartFile file) throws IOException {
        String fileName = file.getOriginalFilename();
        String filePath = path + fileName;

        File dest = new File(filePath);
        Files.copy(file.getInputStream(), dest.toPath());
        return "Upload file success : " + dest.getAbsolutePath();
    }

}

其中包含这几个重要元素:

  1. 成员变量path,通过@Value注入配置文件中的file.upload.path属性。这个配置用来定义文件上传后要保存的目录位置。
  2. GET请求,路径/,用于显示upload.html这个文件上传页面。
  3. POST请求。路径/upload,用于处理上传的文件,即:保存到file.upload.path配置的路径下面。
注意:这里主要演示文件上传的主要流程,真实应用还有更多内容要考虑,比如:文件上传后的文件名处理(防止重名)、分布式情况下文件上传后如何共享访问等。更高级的最后,我们后续文章继续讲。

第五步:编辑application.properties配置文件

spring.servlet.multipart.max-file-size=2MB
spring.servlet.multipart.max-request-size=2MB

file.upload.path=/Users/didi/

前两个参数用于限制了上传请求和上传文件的大小,而file.upload.path是上面我们自己定义的用来保存上传文件的路径。

更多本系列免费教程连载「点击进入汇总目录」

测试验证

第一步:启动Spring Boot应用,访问http://localhost:8080,可以看到如下的文件上传页面。

第二步:选择一个不大于2MB的文件,点击“提交”按钮,完成上传。

如果上传成功,将显示类似下面的页面:

你可以根据打印的文件路径去查看文件是否真的上传了。

代码示例

本文的相关例子可以查看下面仓库中的chapter4-3目录:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 4 收藏 3 评论 0

程序猿DD 发布了文章 · 1月6日

GitHub上的开源复刻:暗黑破坏神2

现在的00后少年可能已经不太了解《暗黑破坏神2》这款由暴雪打造的经典游戏。

该游戏上市的时候,国内个人家用电脑还没有那么普及,网络游戏也没有那么风靡,现在的孩子可能无法想象,那时候网吧里的很多人都是在玩这款单机游戏!

元旦逛GitHub的时候,居然发现有大神复刻了一个暗黑破坏神2,该项目名称叫:OpenDiablo2,它的Logo有没有让你回忆起以前的图标呢?

该项目目前已经收获了8.9k的Star,非常受欢迎!

仔细看看,这款复刻游戏居然是用golang编写的,对于掌握Go语言,还只是用于网络与运维应用的开发者来说,或许也是一个学习用它来做游戏的一个优秀案例。

先不说如何学了,我们一起来看看它的效果吧:

有没有找回童年的赶脚?

关注公众号“TJ君”,回复“OpenDiablo”,获取仓库地址。

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 2 收藏 2 评论 2

程序猿DD 发布了文章 · 1月5日

IDEA中无法import自己工程中类的问题解决方法

今天开个很久没搞的工程,刚开的时候一片红,很自然的想到,要去配置一下项目的JDK,但是配置好之后,又出了个诡异问题:项目可以运行,但是import项目内部自己写的类的时候,都出现了红色错误。虽然import显示错误,但是实际类是存在的!!

就像下面这样:

那么碰到这类问题之后要如何解决呢?下面说说解决步骤:

第一步:菜单中选择File - Invalidate Caches/Restart...

第二步:在弹出框中,选择Invalidate and Restart

本文首发:https://blog.didispace.com/idea-import-project-class-failed/,转载注明出处。

静静等待IDEA重启,此时就可以看到红色import错误没有啦!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 2 收藏 2 评论 1

程序猿DD 发布了文章 · 1月4日

当会打王者荣耀的AI学会踢足球,一不小心拿下世界冠军!

难得的元旦小假期,没有什么比得上在慵懒的冬日艳阳下放松自己,拿起手机,叫上了许久未一起作战的小伙伴,到王者荣耀中激战了一番,仿佛又回到了当年那个年轻的自己。

file

厉害不,毕竟当年DD也是王者五十星的水平,哈哈。

file

不过哪怕是我这样的王者,在去年也被腾讯上线的超强电脑AI绝悟虐的不轻。记得当时有朋友来找我,说是腾讯上线了一个绝悟挑战模式,里面的电脑AI和以往被我们拿来练熟练度的电脑相比,简直就是云泥之别,一关比一关难,据说连一些职业选手组成的车队都翻车了。

听的那么玄乎,头硬的我自然不会轻信,然后随后的几天中,果不其然被绝悟虐的生活不可自理,最后还是灰溜溜的靠着网上流传的“大乔-米莱蒂”传送偷家套路才勉强过关。

file

一边回忆着被绝悟惨虐的经历一边浏览着最近的新闻,突然眼中爆射出一道惊芒,中国足球世界杯夺冠了!

file

你没有看错,的确是中国足球世界杯夺冠了,但是夺冠的并非传统意义上的男足女足,而是由我们曾经熟悉的绝悟进化之后重生的足球AI-WeKick

WeKick夺冠的是首届谷歌足球Kaggle竞赛,参赛队伍多达1138支,可以说是代表了目前地球上最顶尖的足球AI比赛,称之为足球AI世界杯也不为过。

而在所有参赛队伍中,WeKick的得分高达1785.8分,占据绝对优势地位,就像96年的公牛、02年的巴西一样,势不可挡!

file

file

不可置信?再给你看下精彩集锦!

快、准、直!一记完美的长传后,直射球门!

img

连续突破重围,轻松传球 4 次。

img

有些人可能不以为然,觉得之前绝悟在王者荣耀的表现,用去踢足球,也是很简单的。

其实不然,首先王者荣耀是一个5V5的游戏,而足球是一个11v11的运动,就是说AI需要控制的智能体(球员)个数多了一倍以上,其次足球赛虽也属于即时策略型游戏,但也需要AI具备长线思考、快速决策、处理复杂环境的能力。AI需要考虑到每个球员的速度、加速度、射门、头球、传球、防守等各种指数,同时还需要操控球员之间进行频繁的相互配合,也需要时刻观察对手球员的行为,防范于未然,做出最好的选择!

针对这些不同的情况,WeKick团队发挥想象,主要运用了以下三个创新进行针对性的模型训练。

Self-Play强化学习框架

WeKick团队采用Self-Play(自博弈)强化学习来从零开始训练模型,并以此部署到异步分布式的强化学习框架中。异步架构牺牲了一部分训练的实时性能,但是相应的,得到了更高的灵活性,同时可以支持在训练过程中按实际需要调整整个计算资源,使其能快速完美的适应智能体人数更多的足球游戏训练环境。

file

GAIL生成对抗模拟学习

王者荣耀是一款对抗类的MOBA游戏,其最终目的和足球游戏迥然不同,WeKick团队采用了GAIL(生成对抗模拟学习)与人工设计的奖励结合的方式,在特征与奖励设计上进行了扩展和创新。

运用这个方案,WeKick可以从其它球队学习,拟合专家行为的状态和动作分布,再将GAIL训练的模型作为固定对手进行进一步Self-Play训练,进一步提升策略的稳健性。

file

League多风格强化学习

上述的Self-Play强化学习方案,有一个尚未解决的缺憾,就是通过这个方案得到的模型很容易形成单一的风格。用足球比赛的说法就是打法一成不变,很容易被针对或遇上天生克制的阵型就不知所措。为了解决这个问题,WeKick团队采用了针对多智能体学习任务的 League(若干策略池)多风格强化学习训练方案,提升策略的多样性。

file

这种League多风格强化学习训练方案的主要流程,用一句话解释就是 由简入繁

  • 首先训练某一方面的基础模型,例如过人、盘带、传球、射门等。
  • 根据基础模型训练出多个风格化模型,每个模型专注一种风格打法,训练过程中加入主模型作为训练对手,避免训练效果死板不变通。
  • 再基于多个基础模型训练一个主模型,主模型可以将自己的历史版本作为训练对手,还可以加入所有风格化的模型作为不同的训练对商铺,使主模型遇上任何对手都有解决方案。

根据其内部能力评分系统显示,这种算法下的主模型,可以在基础模型的基础上提高200分,比最强的风格化打法高80分!

file

最后介绍下谷歌足球Kaggle竞赛

Kaggle创立于2010年,是全球最大的数据科学社区和数据科学竞赛平台。本届比赛是Kaggle首次针对足球AI领域发布的赛题。

由于足球运动团队策略要求在瞬息万变的赛场上,做出最正确的团队协作、实时决策和竞争策略,其中的难点,一直是困扰世界顶尖AI研究团队的难题。就像前文提到的,从绝悟进化到WeKick,控制的智能体各数从5v5提高到11v11,这中间强化学习的难度将随着智能体个数的增长呈现指数级的爆炸增长。

其实早在参加这个比赛之前,绝悟的开发团队早已经从足球比赛中的单个智能体控制转向多智能体同时控制、协同作战深入的研究方向。在先前参加的5v5形式的谷歌天梯比赛 Google Research Football League 中,绝悟已经赢得过冠军,这次可以说是再度升级版的夺冠。

从最早的围棋AI绝艺,到王者荣耀的MOBA游戏AI绝悟,再到如今的足球AI-WeKick,腾讯在人工智能的深度强化学习程度正在步步进化,未来很有可能运用于其他更广泛的行业中,真正做到人工智能为人类服务。

而此刻的我,只想什么时候能有机会和这个WeKick踢上(被虐)几场比赛,你也想和他过过招吗?

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 1 收藏 1 评论 0

程序猿DD 发布了文章 · 2020-12-31

为什么 StackOverflow 上的代码片段会摧毁你的项目?

昨天公司里碰到一件令人哑然失笑的事情。帮朋友公司做的一个项目,做SIT测试的时候发现一些bug,仔细查了下原因,原来是因为当初觉得这个项目比较简单,交给了几个新入职的新丁,也算是给他们练练手,结果其中一位写了一段代码出现了问题结果导致bug的出现。

虽然问题不大很快就修复了,但是正所谓初生牛犊不怕虎,这位出问题的新丁不太服气,一直嚷嚷着他这段代码是从CSDN上找到的,别人都说好用他才用的。问了问他是否理解原文中那么写的原因,结果才支支吾吾了半天讲不清楚,最后也承认他只是看到那段代码的结果是他要的,并不是理解中间一些方法的运用到底是什么逻辑。

真是又好气又好笑,想想现在的年轻人真是有个性,自己当年遇到这种情况只敢虚心求教,哪敢都没明白就去用一段代码,别人指出问题还忿忿不平。

也许是自己老了吧。晚上和一个刚从国外归国的同学说起这个事情,他倒是深有体会,说到他们在国外留学时,遇到技术问题想到网上咨询用的最多的是StackOverflow,相比起国内的CSDN,他觉得StackOverflow上提问题的人和回答的人都更加严谨,不会有太多的废话和问题之外的讨论。按他的理解,CSDN上很多国内的技术新手纯粹是缺乏独立思考的能力,都只是来求代码完成目标,不去探究为什么这么写,本身CSDN上问题回答质量不高,这样很容易出事。

同学的看法,一方面可能是因为国内程序员需求极大,但整体浮躁、功利性的大环境导致很多培训机构没有培训到位,很多程序员都是一边开始工作一边学习;但另一方面,不排除他崇洋媚外的自满情绪。于是,我便去查阅了下StackOverflow上面的内容和相关资料。

仔细研究后发现,其实StackOverflow没有同学说的那么好,即使是该网站上的回答,也有很多有问题的代码。看来这还是一个世界性通用的难题~

对于国外的程序员来说,StackOverflow就是他们的CSDN,是他们遇到各种问题查询的第一选择,很多国外的程序员,会到StackOverflow上需求帮助,找到他们需要的解决问题的代码,然后不假思索的直接复制使用,和我公司出问题的那位年轻人一样,这个习惯很不好。

代码复制本身来说并不是什么坏事,我们学开发的时候,肯定老师都教过我们代码复用能有效提高软件的开发效率。已经被解决了的问题,实在没有必要再去重新写一遍代码。但是这个前提是,使用者,也就是开发人员,必须知道你要用的这段代码的来龙去脉真正含义,就是你要真正明白你用的代码。

简单举个例子:

  • 几年前,很多程序员发现,在windows上使用Docker老是会碰到无法启动的问题,原因一直不明,很多人都是重装了系统什么的才解决。后来有一个大神发文解释原因后我们才知道了为什么
当 Windows 后台启动了 Razer Synapse 时,再去启动 Docker 就会触发此问题。根本原因是,Razer Synapse 运行后,Docker 就会认为已经有一个 Docker 实例正在运行中,所以不会再启动一个实例,也就是真的Docker。

为什么会有这种情况呢?

因为有一段代码中,返回的 GUID 类型是 System.Reflection.RuntimeAssembly(系统层面的运行时程序集),而不是 Windows 中对应 Docker 程序集中定义的类型。Docker处理的逻辑就是在同一时间只允许运行一个实例,判断的方式就是通过判断 GUID 是否存在,但在获取GUID 时,用的是系统层面的而非自己独立程序集里面,从而导致了这个问题。问题代码如下:

var name = string.Format("Global\{0}", (object) 
Assembly.GetExecutingAssembly().GetType().GUID);

当然了,如果系统只有一个应用程序使用了上面的错误代码,那么这个问题还是不会出现,因为只有一个实例。但实际情况是很多应用程序都使用了这一段错误代码,结果就不允许两个实例同时运行。

那这些应用程序,想必你已经猜到,都用了这段代码,而且都是从StackOverflow上下载后不假思索的使用导致的。

  • Andreas Lundblad,国外一位大神,Palantir 的 Java 开发人员,同时也是 StackOverflow 上排名最高的贡献者之一。

他有一段代码,作用是将字节计数转换为更易于阅读的格式。打个比方,1024字节转换为1kB,1048576 字节转换为1MB。他最初在2010年将这一段代码放到了StackOverflow上,根据官方统计,他的这段代码是StackOverflow上被拷贝次数最多的Java代码,GitHub上使用这段代码的项目有6千多个。

当Andreas Lundblad知道此事后,出于负责,很小心谨慎的重新检查了下这段代码,随后发现这段代码里有一些问题,于是他马上更新了版本,并发博告诉大家:

  • StackOverflow 上的代码可能存在 bug,不管他的好评是多少。
  • 如果你拿了StackOverflow的代码,一定记住要测试所有的可能的情况。
  • 在复制代码时,一定要包括其来源和贡献者,当出问题时,能快速帮你定位。

同样的,在去年,国外另一位大神Morteza Verdi发表了一篇研究论文指出,StackOverflow上流传最广的一段c++代码存在着69个重大安全隐患漏洞,GitHub上使用这段C++代码的项目也有近3千多个。

所以,无论是StackOverflow还是CSDN,无论是国外和国内,都务必记住一点,在复用别人的代码时,一定要搞清楚别人代码的含义,并做好充分的测试工作,并不是说你运行了一次没问题,就是真的没问题,可能只是你运气好,没有触发问题场景罢了。

引用 Ryan Donovan 的一句话,就是:

If you borrow things and you don’t understand the content of what you’re borrowing, then you fall in this trap of reusing code that has potential vulnerabilities. Then you are just spreading those things around.”

If you’re going to reuse code, you need to understand that code.

复用代码,一定要理解代码!

你复用代码时遇到过什么问题?一起讨论下吧!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 3 收藏 2 评论 0

程序猿DD 发布了文章 · 2020-12-30

开源编解码项目FFmpeg迎来20周年生日 凭一己之力养活全球无数播放器!

近日,开源编解码库项目FFmpeg迎来20周年生日。

2000.12.20-2020.12.20

可能很多人对于FFmpeg不是特别了解,那么以下几个名字是否大家或多或少都用过呢?

暴风影音、PotPlayer、KMPlayer、WinxDvd

这些播放器是不是让大家想起年轻时候看小电影的情景呢?

FFmpeg是一个和视频处理相关的开源项目,包含了丰富的多媒体解码库,这些大家常用的熟识的播放器,有良好的功能体验,是源自几乎每款都使用了FFmpeg的源代码,所以,大家明白了吧,我们能看到那么多好看的小电影,其实最该感谢的就是FFmpeg。

FFmpeg不仅被大量免费软件使用,同时使用的,还有很多大型公司,例如YouTube、iTunes等。

20年的风风雨雨,FFmpeg一路走来也是经历过不少挫折,在2011年 FFmpeg 就因为核心成员意见不一导致分裂,差点最终导致整个项目消亡殆尽。最终的结果就是,项目创始人法布里斯贝拉选择了离开FFmpeg,但是随后他与其他一起出走的开发者创建另一知名开源编解码库项目Libav

FFmpeg和 Libav ,就像南慕容和北乔峰一样,是当下所有主流播放器必备的编解码库,并且这些编解码库全部开源可免费使用无需额外付费。

不过,虽然说是免费使用,但FFmpeg是基于LGPL/GPL开源的,这意味着如果某软件使用了FFmpeg的代码,那么这个软件涉及这些代码的部分,也必须开源,并且需要在使用其项目源代码和编解码库时注明来源。但是有些软件呢连这个都不想去遵守。于是FFmpeg诞生了另一个让众多圈内人士耳熟能详的内容:

耻辱柱

FFmpeg会将发现的那些不遵守开源协议的软件公诸于世,虽然没有物质上的惩罚,但是耻辱柱更多的是一种精神层面的降维打击,提醒着大家要奉公守法!

很不幸,我们熟知的暴风影音等软件也在耻辱柱内。

最后,我们一起来看下耻辱柱的内容吧

* alive, issue tracker entry
* Alloksoft, issue tracker entry
* AMR Player, issue tracker entry
* Aplus Video Converter, issue tracker entry
* Applian Replay Converter, issue tracker entry
* AVCWare, issue tracker entry
* AVS Video Converter, issue tracker entry
* Aya Media Techologies, issue tracker entry
* Baofeng Storm, issue tracker entry
* CinemaForge, issue tracker entry
* Conceiva Mezzmo, issue tracker entry
* Doremi Asset Manager, issue tracker entry
* DownloadHelper ConvertHelper, issue tracker entry
* DVDFab, issue tracker entry
* DVDxDV, issue tracker entry
* EffectMatrix Software, issue tracker entry
* Eztoo, issue tracker entry
* Format Factory, issue tracker entry
* FreeTime Soft, issue tracker entry
* GeoVid, issue tracker entry
* GetFLV, issue tracker entry
* GOM Player, issue tracker entry
* H264Encoder.com, issue tracker entry
* iSkysoft, issue tracker entry
* The KMPlayer, issue tracker entry
* Koyote Software, issue tracker entry
* Livestation, issue tracker entry
* MasterSoft Inc., issue tracker entry
* MediaCoder, issue tracker entry
* Moyea, issue tracker entry
* MP4Converter, issue tracker entry
* Netgem, issue tracker entry
* Opell Video Converter Pro, issue tracker entry
* PowerPoint DVD Converter, issue tracker entry
* PresenterSoft, issue tracker entry
* Red Kawa, issue tracker entry
* Rhozet Carbon Coder, issue tracker entry
* Senstic Air TV, issue tracker entry
* ShenZhen Hawell, issue tracker entry
* SkypeCap, issue tracker entry
* Soft Service, Ltd. FlashCam, issue tracker entry
* Video Convert Master, issue tracker entry
* ViO mobile video converter, issue tracker entry
* WisMencoder, issue tracker entry
* Xilisoft Video Converter, issue tracker entry
* XMedia Recode, issue tracker entry
* ZoIPer, issue tracker entry

有没有你平时常用的播放器上榜了呢?

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 2 收藏 2 评论 1

程序猿DD 发布了文章 · 2020-12-30

听说又有兄弟因为用YYYY-MM-dd被锤了...

还记得去年分享过一篇日期格式化使用 YYYY-MM-dd 的潜在问题的文章不?

历史又重演了...

事故现场

我们来写个单元测试,重现一下这个问题。

测试逻辑:

  1. 创建两个日期格式化,一个是出问题的YYYY-MM-dd,另一个是正确用法yyyy-MM-dd
  2. 分别去格式化两个不同的日期:2020年12月26日(周六),2020年12月27日(周日)

具体代码如下:

public class Tests {

    @Test
    public void test() throws Exception {
        SimpleDateFormat df1 = new SimpleDateFormat("YYYY-MM-dd");
        SimpleDateFormat df2 = new SimpleDateFormat("yyyy-MM-dd");

        Calendar c = Calendar.getInstance();

        // 2020年12月26日周六
        c.set(Calendar.DATE, 26);
        System.out.println("YYYY-MM-dd = " + df1.format(c.getTime()));
        System.out.println("yyyy-MM-dd = " + df2.format(c.getTime()));

        // 分割线
        System.out.println("========================");

        // 2020年12月27日 周日
        c.add(Calendar.DATE, 1);
        System.out.println("YYYY-MM-dd = " + df1.format(c.getTime()));
        System.out.println("yyyy-MM-dd = " + df2.format(c.getTime()));
    }

}

跑一下测试,可以看到输出结果如下:

YYYY-MM-dd = 2020-12-26
yyyy-MM-dd = 2020-12-26
========================
YYYY-MM-dd = 2021-12-27
yyyy-MM-dd = 2020-12-27
  • 2020年12月26日(周六),两种格式化都正确
  • 2020年12月27日(周日),YYYY-MM-dd出了问题,年份到了2021年
本文首发于独立博客:http://blog.didispace.com/YYYY-MM-dd-2020-again/ ,更多技术干货欢迎收藏关注。

问题原因

为什么YYYY-MM-dd格式化2020年12月27日的时候,会到2021年呢?

因为YYYY是week-based-year,表示:当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,那么这周就算入下一年。

所以2020年12月27日那天在这种表述方式下就已经到 2021 年了。

而当使用yyyy的时候,就还是 2020 年。


最后,自查一下你的程序是否有这样的问题吧!

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~
查看原文

赞 10 收藏 8 评论 3

程序猿DD 发布了文章 · 2020-12-27

如何写出安全的、基本功能完善的Bash脚本

每个人或多或少总会碰到要使用并且自己完成编写一个最基础的Bash脚本的情况。真实情况是,没有人会说“哇哦,我喜欢写这些脚本”。所以这也是为什么很少有人在写的时候专注在这些脚本上。

我本身也不是一个Bash脚本专家,但是我会在本文中跟你展示一个最基础最简单的安全脚本模板,会让你写的Bash脚本更加安全实用,你掌握了之后肯定会受益匪浅。

为什么要写Bash脚本

其实关于Bash脚本最好的解释如下:

The opposite of "it's like riding a bike" is "it's like programming in bash".

A phrase which means that no matter how many times you do something, you will have to re-learn it every single time.

— Jake Wharton (@JakeWharton)

December 2, 2020

意思就是,跟骑自行车相反,无论做了多少次,每次都感觉像重新学一样。

但是Bash脚本语言和其他一些广受欢迎的语言,例如JavaScript一样,他们不会轻易突然消失,虽然Bash脚本语言不太可能成为业界的主流语言,但实际他就在我们周围,无处不在。

Bash就像继承了shell的衣钵一样,在每台linux上都可以看到他的身影,这可是大多数后端程序运行的环境,因此当你需要编写服务器的应用程序启动、CI/CD步骤或集成测试用的脚本,Bash就在那里等着你。

将几个命令粘在一起,将输出从一个传递到另一个,然后只启动一些可执行文件,Bash是众多方案中最简单的一个。虽然用其他语言编写更大、更复杂的脚本更有效果,但你不能指望Python、Ruby、fish或其他任何你认为最好的程序,可以在任何地方编译使用。所以在将其添加到某个prod server、Docker image或CI环境之前,往往会让人三思而后行。

当然啦,Bash还远远不够完美两个字。他的语法对初学者就像一个噩梦。错误处理也很困难。到处都是我们必须处理掉的陷阱。

Bash script template(Bash脚本模板)

废话不多说,献上我的模板


#!/usr/bin/env bash

set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)

usage() {
  cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]

Script description here.

Available options:

-h, --help      Print this help and exit
-v, --verbose   Print script debug info
-f, --flag      Some flag description
-p, --param     Some param description
EOF
  exit
}

cleanup() {
  trap - SIGINT SIGTERM ERR EXIT
  # script cleanup here
}

setup_colors() {
  if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
    NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
  else
    NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
  fi
}

msg() {
  echo >&2 -e "${1-}"
}

die() {
  local msg=$1
  local code=${2-1} # default exit status 1
  msg "$msg"
  exit "$code"
}

parse_params() {
  # default values of variables set from params
  flag=0
  param=''

  while :; do
    case "${1-}" in
    -h | --help) usage ;;
    -v | --verbose) set -x ;;
    --no-color) NO_COLOR=1 ;;
    -f | --flag) flag=1 ;; # example flag
    -p | --param) # example named parameter
      param="${2-}"
      shift
      ;;
    -?*) die "Unknown option: $1" ;;
    *) break ;;
    esac
    shift
  done

  args=("$@")

  # check required params and arguments
  [[ -z "${param-}" ]] && die "Missing required parameter: param"
  [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"

  return 0
}

parse_params "$@"
setup_colors

# script logic here

msg "${RED}Read parameters:${NOFORMAT}"
msg "- flag: ${flag}"
msg "- param: ${param}"
msg "- arguments: ${args[*]-}"

Choose Bash

#!/usr/bin/env bash

脚本为了获得最佳兼容性,它引用/usr/bin/env,而不是直接引用/bin/bash。

Fail fast

set -Eeuo pipefail

set命令可以更改脚本执行选项。例如,通常Bash不关心某个命令是否失败,返回非零退出状态代码。它只是快速地跳到下一个。现在考虑一下这个小脚本:

#!/usr/bin/env bash
cp important_file ./backups/
rm important_file

如果备份目录不存在,会发生什么情况?确切地说,你将在控制台中收到一条错误消息,但是在你能够做出反应之前,该文件已经被第二个命令删除。

Get the location

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)

这行代码尽其所能定义脚本的位置目录,然后我们对其进行cd配置。为什么?

通常,我们的脚本在相对于脚本位置的路径上运行,复制文件并执行命令,假设脚本目录也是一个工作目录。是的,只要我们从它的目录执行脚本。

但是,假设我们的CI配置执行脚本如下所示呢:

/opt/ci/project/script.sh

那么我们的脚本不是在项目目录中操作的,而是在CI工具的一些完全不同的工作目录中操作的。我们可以通过在执行脚本之前转到目录来修复它:

cd /opt/ci/project && ./script.sh

但从脚本的角度解决这个问题要好得多。因此,如果脚本从同一目录中读取某个文件或执行另一个程序,请按如下方式调用:

cat "$script_dir/my_file"

同时,脚本不会更改工作目录的位置。如果脚本是从其他目录执行的,并且用户提供了指向某个文件的相对路径,我们仍然可以读取它。

Try to clean up

trap cleanup SIGINT SIGTERM ERR EXIT

cleanup() {
  trap - SIGINT SIGTERM ERR EXIT
  # script cleanup here
}

在脚本结束时,将执行cleanup()函数。你可以在这里尝试删除脚本创建的所有临时文件。

请记住,cleanup()不仅可以在最后调用,在任何时候都可以。

Display helpful help

usage() {
  cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]

Script description here.

...
EOF
  exit
}

尽量让usage()函数相对靠近脚本的顶部,有两种作用:

  • 要为不知道所有选项并且不想查看整个脚本来发现这些选项的人显示帮助。
  • 当有人修改脚本时,保存一个最小的文档(因为两周后,你甚至不记得当初是怎么写的)。

我不主张在这里记录每个函数。但是一个简短、漂亮的脚本使用这些消息是必需的。

Print nice messages

setup_colors() {
  if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
    NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
  else
    NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
  fi
}

msg() {
  echo >&2 -e "${1-}"
}

首先,如果你还不想在文本中使用颜色,那么先删除setup_colors()函数。我保留它是因为我知道如果我不必每次都用谷歌编码的话,我会更频繁地使用颜色。

其次,这些颜色只用于msg()函数,而不是echo命令。

msg()函数用于打印不是脚本输出的所有内容。这包括所有日志和消息,而不仅仅是错误。引用
12 Factor CLI Apps的文章说法:

In short: stdout is for output, stderr is for messaging.

— Jeff Dickey, who knows a little about building CLI apps

stdout用于输出,stderr用于消息传递。

这就是为什么在大多数情况下你不应该为stdout使用颜色。

用msg()打印的消息被发送到stderr流并支持特殊的序列,比如颜色。如果stderr输出不是交互式终端,或者传递了一个标准参数,那么颜色将被禁用。
用法如下:

msg "This is a ${RED}very important${NOFORMAT} message, but not a script output value!"

要检查stderr是不是交互式终端时的行为,请在脚本中添加类似于上面的一行。然后执行它,将stderr重定向到stdout并通过管道将其发送到cat。管道操作使输出不再直接发送到终端,而是发送到下一个命令,因此颜色会被禁用。

$ ./test.sh 2>&1 | cat
This is a very important message, but not a script output value!

Parse any parameters

parse_params() {
  # default values of variables set from params
  flag=0
  param=''

  while :; do
    case "${1-}" in
    -h | --help) usage ;;
    -v | --verbose) set -x ;;
    --no-color) NO_COLOR=1 ;;
    -f | --flag) flag=1 ;; # example flag
    -p | --param) # example named parameter
      param="${2-}"
      shift
      ;;
    -?*) die "Unknown option: $1" ;;
    *) break ;;
    esac
    shift
  done

  args=("$@")

  # check required params and arguments
  [[ -z "${param-}" ]] && die "Missing required parameter: param"
  [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"

  return 0
}

如果在脚本中参数化有意义的话,我就通常就会去做,即使整个脚本只在一个地方使用。它使复制和重用它变得更容易,而这通常是早晚发生的。而且,即使某些东西需要硬编码,通常在比Bash脚本更高的级别上有更好的位置。

CLI参数有三种主要类型:标志、命名参数和位置参数。parse_params()函数支持所有这些参数。

这里没有处理的唯一一个公共参数模式是连接多个单字母标志。为了能够传递两个标志作为-ab,而不是-a-b,需要一些额外的代码。

while循环是一种手动解析参数的方法。在其他语言中,您应该使用一个内置的解析器或可用的库,但是,好吧,这是Bash。

模板中有一个示例标志(-f)和命名参数(-p)。只需更改或复制它们以添加其他参数。之后不要忘记更新usage()。

这里最重要的一点是,当您使用第一个google结果进行Bash参数解析时,通常会丢失一个未知选项的错误。脚本收到未知选项的事实意味着用户希望它执行脚本无法完成的操作。所以用户的期望和脚本行为可能会有很大的不同。最好是在坏事发生之前完全阻止处决。

在Bash中解析参数有两种选择。是一个接一个的。有人赞成和反对使用它们。我发现这些工具不是最好的,因为默认情况下,macOS上的getopt行为完全不同,getopts不支持长参数(比如--help)。

Using the template

复制粘贴它,就像你在网上找到的大多数代码一样。

复制后,只需更改4件事:

  • 包含脚本说明的usage()文本
  • cleanup()内容
  • parse_params()中的参数–保留--help和--no color,但替换示例:-f和-p
  • 实际的脚本逻辑

Portability

我在MacOS上测试了这个模板(使用默认的bash3.2)和几个Docker映像:Debian、Ubuntu、CentOS、amazonlinux、Fedora。它的确起作用了。

显然,它不能在缺少Bash的环境中工作,比如alpinellinux。

Further reading

在用Bash或其他更好的语言创建CLI脚本时,有一些通用规则。这些资源将指导您如何使小型脚本和大型CLI应用程序可靠,参考如下:

Closing notes

我不会是第一个也不是最后一个创建Bash脚本模板的人。这个项目是一个很好的选择,虽然对我的日常需求来说有点太大了。毕竟,我尽量使Bash脚本尽可能小(而且很少使用)。

编写Bash脚本时,请使用支持ShellCheck linter的IDE,如JetBrains IDEs。它会阻止你做一堆适得其反的事情。

本文首发:http://blog.didispace.com/min...

欢迎关注我的公众号:程序猿DD,获得独家整理的免费学习资源助力你的Java学习之路!另每周赠书不停哦~

查看原文

赞 6 收藏 3 评论 0