
大道至简! https://waylau.com/
JDK 17已经于2021年3月16日如期发布。本文介绍JDK 17新特性。JDK 17于2021年9月14日正式发布(General-Availability Release)。JDK 17将是大多数供应商的长期支持(LMS)版本。上一个LTS版本是JDK 11。本文总结了JDK 17发布的新特性。发布版本说明根据发布的规划,这次发布的 JDK 17 将是一个长期支持版(LTS 版)。LTS 版每 3 年发布一个,上一次长期支持版是 18 年 9 月发布的 JDK 11。 JDK 17是Java SE平台版本17的开源参考实现,由JSR 392在JCP(Java Community Process)指定。安装包下载主要分为OpenJDK版本和Oracle版本,下载地址如下:OpenJDK版本:https://jdk.java.net/16/Oracle版本:https://www.oracle.com/java/technologies/downloads/上述版本,如果是个人学习用途,则差异不大。但如果是用于商业用途,则需要仔细看好相关的授权。Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 更多有关Java的基本知识,可以参阅《Java核心编程》这本书,描述的非常详细。JDK 17 新特性说明JEP 406:switch的模式匹配(预览)(JDK-8213076)specification通过switch表达式和语句的模式匹配,以及模式语言的扩展,增强Java编程语言。将模式匹配扩展到switch允许对表达式进行测试,每个模式都有特定的操作,以便可以简洁而安全地表达复杂的面向数据的查询。有关更多详细信息,请参见JEP 406JEP 409:密封类(JDK-8260514)specification密封类(Sealed Class)已添加到Java语言中。密封类和接口限制了哪些其他类或接口可以扩展或实现它们。密封类由JEP 360并在JDK 15中作为预览功能交付。它们再次被提出,并进行了改进,由JEP 397并在JDK 16中作为预览功能提供。现在,在JDK 17中,密封类正在最终确定,与JDK 16没有任何更改。有关更多详细信息,请参见JEP 409JEP 382:新的macOS渲染管道(JDK-8238361)client-libs/2dSwing API用于渲染的Java 2D API现在可以使用新的Apple Metal加速渲染API 给macOS。目前默认情况下,这是禁用的,因此渲染仍然使用OpenGL API,这些API被Apple弃用,但仍然可用和支持。要启用金属,应用程序应通过设置系统属性指定其使用:-Dsun.java2d.metal=trueMetal或OpenGL的使用对应用程序是透明的,因为这是内部实现的区别,对Java API没有影响。Metal管道需要macOS 10.14.x或更高版本。在早期版本上设置它的尝试将被忽略。有关更多详细信息,请参见[JEP 382(https://openjdk.java.net/jeps/382)大图标访问新API(JDK-8182043)client-libs/javax.swingJDK 17中提供了一个新的方法javax.swing.filechooser.FileSystemView.getSystemIcon(File, int, int),该方法允许在可能的情况下访问更高质量的图标。它已为Windows平台完全实施;但是,其他平台上的结果可能会有所不同,稍后将增强。例如,通过使用以下代码:FileSystemView fsv = FileSystemView.getFileSystemView(); Icon icon = fsv.getSystemIcon(new File("application.exe"), 64, 64); JLabel label = new JLabel(icon);用户可以为“application.exe”文件获得更高质量的图标。此图标适用于创建可以在HighDPI环境中更好地扩展的标签。DatagramSocket可以直接加入多播组(JDK-8237352)core-libs/java.netjava.net.DatagramSocket在此版本中已更新,以添加对加入多播组(multicast group)的支持。它现在定义了加入和离开多播组的加入组和离开组方法。java.net.DatagramSocket的类级API文档已更新,以解释如何配置普通DatagramSocket并用于加入和离开多播组。此更改意味着DatagramSocket API可以用于组播应用程序,而无需使用旧的java.net.MulticastSocket API。MulticastSocket API的工作原理和以前一样,尽管它的大多数方法都被弃用了。有关此变更理由的更多信息,请查看CSRJDK-8260667JEP 356:增强型伪随机数生成器(JDK-8193209)core-libs/java.util为伪随机数生成器(PRNG)提供新的接口类型和实现,包括可跳转的PRNG和一类额外的可拆分PRNG算法(LXM)。有关更多详细信息,请参见JEP 356Ideal Graph Visualizer的现代化(JDK-8254145)hotspot/compilerIdeal Graph Visualizer(IGV)是一个可视化和交互式地探索HotSpot VM C2即时(JIT)编译器中使用的中间表示的工具,已经现代化。增强功能包括:支持在最多JDK 15上运行IGV(IGV底层NetBeans平台支持的最新版本)更快的、基于Maven的IGV构建系统块形成、组删除和节点跟踪的稳定默认过滤器中更直观的着色和节点分类具有更自然默认行为的排名快速节点搜索现代化的IGV部分兼容从早期JDK版本生成的图形。它支持基本功能,如图形加载和可视化,但辅助功能,如节点聚类和着色可能会受到影响。有关构建和运行IGV的详细信息,请参见https://github.com/openjdk/jdk17/tree/master/src/utils/IdealGraphVisualizer。“New API”的新页面和改进的“Deprecated”页(JDK-8263468)tools/javadoc(tool)JavaDoc现在可以生成一个页面,总结API中最近的更改。要包括的最近版本的列表是使用 --since命令行选项指定的。这些值用于查找具有匹配@since的声明,因为要包含在新页面上的标记。--since-label命令行选项提供了要在“New API”页面标题中使用的文本。在总结已弃用项目的页面上,您可以查看按已弃用项目的版本分组的项目。错误消息中的源详细信息(JDK-8267126)tools/javadoc(tool)当JavaDoc报告输入源文件中的问题时,它将以类似编译器(javac)诊断消息的方式显示问题的源行,以及包含指向该行位置的插入符号(^)的行。此外,日志记录和其他“信息”消息现在写入标准错误流,留下标准输出流用于命令行选项特别请求的输出,如命令行帮助。JEP 412:外部函数和内存API(孵化)(JDK-8265033)core-libs引入一个API,Java程序可以通过该API与Java运行时之外的代码和数据互操作。通过有效地调用外部函数(即JVM外部的代码),并通过安全地访问外部内存(即不由JVM管理的内存),该API使Java程序能够调用本机库并处理本机数据,而不会有JNI的脆弱性和危险。有关更多详细信息,请参阅JEP 412控制台字符集API(JDK-8264208)core-libsjava.io.Console已更新,以定义一个新方法,该方法返回控制台的Charset。返回的Charset可能与Charset.defaultCharset()方法返回的Charset不同。例如,它返回IBM437,而Charset.defaultCharset()在Windows (en-US)上返回windows-1252。请参阅https://bugs.openjdk.java.net/browse/JDK-8264209了解更多详细信息。用于反序列化的JDK Flight Recorder事件(JDK-8261160)core-libs/java.io:serialization现在可以使用JDK Flight Recorder (JFR)监控对象的反序列化。当启用JFR且JFR配置包括反序列化事件时,每当运行程序尝试反序列化对象时,JFR将发出事件。反序列化事件名为jfr.Derialization,默认情况下禁用。反序列化事件包含序列化筛选器机制使用的信息;请参阅对象输入筛选器规范。此外,如果启用了过滤器,JFR事件指示过滤器是接受还是拒绝对象的反序列化。有关如何使用JFR反序列化事件的更多信息,请参阅文章监控反序列化提高应用安全性。有关使用和配置JFR的参考信息,请参阅JFR运行时指南和JFR命令参考JDK任务控制文件的章节。JEP 415:实现特定于上下文的反序列化过滤器(JDK-8264859)core-libs/java.io:serializationJEP 415:特定于上下文的反序列化过滤器允许应用程序通过JVM范围的过滤器工厂配置特定于上下文的和动态选择的反序列化过滤器,该工厂被调用以为每个单独的反序列化操作选择过滤器。用于序列化过滤的Java核心库开发人员指南介绍了用例,并提供了示例。本机字符编码名称的系统属性(JDK-8265989)core-libs/java.lang引入了一个新的系统属性本机.encode。此系统属性提供基础主机环境的字符编码名称。例如,它通常在Linux和macOS平台中具有UTF-8,在Windows (en-US)中具有Cp1252。请参阅https://bugs.openjdk.java.net/browse/JDK-8266075了解更多详细信息。添加java.time.InstantSource (JDK-8266846)core-libs/java.time引入了一个新的接口java.time.InstantSource。此接口是java.time.Clock的抽象,只关注当前时刻,不引用时区。十六进制格式和解析实用程序(JDK-8251989)core-libs/java.utiljava.util.HexFormat为基元类型和字节数组提供十六进制和十六进制之间的转换。分隔符、前缀、后缀和大写或小写的选项由返回HexFormat实例的工厂方法提供。实验Compiler Blackholes支持(JDK-8259316)hotspot/compiler增加了对Compiler Blackholes的实验支持。这些对于低级基准测试非常有用,以避免关键路径上的死代码消除,而不影响基准性能。当前的支持以CompileCommand的形式实现,可访问为-XX:CompileCommand=blackhole,,并计划最终将其毕业到公共API。JMH已经能够在指示/可用时自动检测和使用此设施。有关后续步骤,请查阅JMH文档。HotSpot JVM中的新类层次结构分析实现(JDK-8266074)hotspot/compilerHotSpot JVM中引入了一个新的类层次结构分析实现。它的特点是对抽象和默认方法的增强处理,从而改进了JIT编译器所做的内联决策。新实现将取代原始实现,并在默认情况下打开。为了帮助诊断与新实现相关的可能问题,可以通过指定 -XX:+UnlockDiagnosticVMOptions -XX:-UseVtableBasedCHA命令行标志来打开原始实现。原始实现可能会在未来的版本中删除。JEP 391: macOS/AArch64端口(JDK-8251280)hotspot/compilermacOS 11.0现在支持AArch64体系结构。此JEP在JDK中实现了对macos-aarch64平台的支持。添加的功能之一是支持W^X(write xor execute)内存。它仅对macos-aarch64启用,并可以在某些时候扩展到其他平台。JDK可以在英特尔计算机上交叉编译,也可以在基于Apple M1的计算机上编译。有关更多详细信息,请参见JEP 391统一日志支持异步日志刷新(JDK-8229517)hotspot/runtime为了避免使用统一日志记录的线程中出现不希望的延迟,用户现在可以请求统一日志记录系统在异步模式下运行。这可以通过传递命令行选项-Xlog:async来完成。在异步日志记录模式下,日志站点将所有日志记录消息入队到缓冲区。独立线程负责将它们刷新到相应的输出。中间缓冲区是有界的。缓冲区耗尽时,入队消息将被丢弃。用户可以使用命令行选项-XX:AsyncLogBufferSize=.来控制中间缓冲区的大小。ARM上的macOS早期访问可用(JDK-8266858)infrastructure/build新的macOS现在可用于ARM系统。ARM端口的行为应与英特尔端口类似。没有已知的功能差异。在macOS上报告问题时,请指定是使用ARM还是x64。支持在Keytool -genkeypair命令中指定签名者(JDK-8260693)security-libs/java.security-signer和-signerkeypass选项已添加到keytool实用程序的-genkey对命令中。-signer选项指定签名者的私钥条目的密钥库别名,-signerkeypass选项指定用于保护签名者私钥的密码。这些选项允许keytool -genkey对使用签名者的私钥对证书进行签名。这对于生成具有密钥协商算法作为公钥算法的证书特别有用。SunJCE提供程序通过AES密码支持KW和KWP模式(JDK-8248268)security-libs/javax.cryptoSunJCE提供程序已得到增强,以支持AES密钥换行算法(RFC 3394)和带填充算法的AES密钥换行算法(RFC 5649)。在早期版本中,SunJCE提供程序在“AESWrap”密码算法下支持RFC 3394,该算法只能用于包装和解包装密钥。通过此增强,增加了两种分组密码模式,KW和KWP,支持使用AES进行数据加密/解密和密钥包装/解包装。有关更多详细信息,请查看“JDK提供程序文档”指南的“SunJCE提供程序”部分。新SunPKCS11配置属性(JDK-8240256)security-libs/javax.crypto:pkcs11SunPKCS11提供程序添加了新的提供程序配置属性,以更好地控制本机资源的使用。SunPKCS11提供程序使用本机资源以便与本机PKCS11库一起工作。为了管理和更好地控制本机资源,添加了额外的配置属性,以控制清除本机引用的频率,以及是否在注销后销毁基础PKCS11令牌。SunPKCS11提供程序配置文件的3个新属性是:destroyTokenAfterLogout (布尔值,默认值为false)如果设置为true,则在SunPKCS11提供程序实例上调用java.security.AuthProvider.logout() 时,基础令牌对象将被销毁,资源将被释放。这基本上会在logout() 调用后使SunPKCS11提供程序实例不可用。请注意,不应将此属性设置为true的PKCS11提供程序添加到系统提供程序列表中,因为提供程序对象在logout() 方法调用后不可用。cleaner.shortInterval(整数,默认值为2000,以毫秒为单位)这定义了在繁忙期间清除本机引用的频率,即cleaner线程应多久处理队列中不再需要的本机引用以释放本机内存。请注意,cleaner线程将在200次失败尝试后切换到“longInterval”频率,即在队列中找不到引用时。cleaner.longInterval(整数,默认值为60000,以毫秒为单位)这定义了在非繁忙期间检查本机引用的频率,即cleaner线程应检查队列中的本机引用的频率。请注意,如果检测到用于清理的本机PKCS11引用,cleaner线程将切换回“短间隔”值。具有系统属性的可配置扩展(JDK-8217633)security-libs/javax.net.ssl已添加两个新的系统属性。系统属性jdk.tls.client.disableExts用于禁用客户端中使用的TLS扩展。系统属性jdk.tls.server.disableExts用于禁用服务器中使用的TLS扩展。如果禁用了扩展,则在握手消息中既不会生成也不会处理扩展。属性字符串是在IANA文档中注册的逗号分隔的标准TLS扩展名称列表(例如,server_name、status_request和签名_algorithms_cert)。请注意,扩展名区分大小写。未知、不支持、拼写错误和重复的TLS扩展名称令牌将被忽略。请注意,阻止TLS扩展的影响是复杂的。例如,如果禁用了强制扩展,则可能无法建立TLS连接。请不要禁用强制扩展,除非您清楚地了解其影响,否则不要使用此功能。包摘要页面上的“Related Packages”(JDK-8260388)tools/javadoc(tool)软件包的摘要页面现在包括一个列出任何“Related Packages”的部分。Related Packages(相关软件包)是根据常见命名约定启发式确定的,可能包括以下内容:“parent”包(即,包是子包的包)同级包(即具有相同父包的其他包)任何子包相关软件包不一定都在同一个模块中。参考引用本文同步至: https://waylau.com/jdk-17-released/https://waylau.com/jdk-16-released/https://waylau.com/jdk-15-released/https://waylau.com/jdk-14-released/《Java核心编程》开源项目“现代Java案例大全” https://github.com/waylau/modern-java-demos
随着业务数据的增加,原有的数据库性能瓶颈凸显,以此就需要对数据库进行分库分表操作。为啥需要分库分表随着业务数据的增加,原有的数据库性能瓶颈凸显,主要体现在以下两个方面。IO瓶颈IO瓶颈主要有以下几种情况:第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度。这种情况适合采用分库和垂直分表。第二种:网络IO瓶颈,请求的数据太多,网络带宽不够。这种情况适合采用分库。CPU瓶颈CPU瓶颈主要有以下几种情况:第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作。这种情况适合采用SQL优化,建立合适的索引,或者把一些SQL操作移到在业务层中台代码中去做业务计算。第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈这种情况适合采用水平分表。综上,大多数情况下,需要使用数据库的分库分表方案来解决性能瓶颈。理解分库分表“分库分表”本质就是把数据分到不同的数据库或者分到不同的数据表上,以减轻单库或者单表的数据量,从而降低访问单库或者单表时的数据压力。在理解了分库分表的重要性之后,那么来理解下分库分表的实现原理。水平分库水平分库是指,以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。比如以下的例子。对用户表进行水平分库,分库的策略是对user_id字段进行取模。如果取模结果是0,则放入数据库01;如果取模结果是1,则放入数据库02。水平分库的结果是:每个库的结构都一样;每个库的数据都不一样,没有交集;所有库的并集是全量数据。水平分库适用的场景是,系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。水平分表水平分表是指,以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。比如以下的例子。对用户表user_t进行水平分表,分库的策略是对user_id字段进行取模。如果取模结果是0,则放入user_t_01表;如果取模结果是1,则放入user_t_02表。水平分表的结果是:每个表的结构都一样;每个表的数据都不一样,没有交集;所有表的并集是全量数据。水平分表适用的场景是,系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。垂直分库垂直分库是指,以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。比如以下的例子。用户业务相关的表放入到01库,订单业务相关的表放入到02库。垂直分库的结果是:每个库的结构都不一样;每个库的数据也不一样,没有交集;所有库的并集是全量数据。垂直分库适用的场景是,系统绝对并发量上来了,并且可以抽象出单独的业务模块。到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化或者微服务化。垂直分表垂直分表是指,以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。垂直分表的结果是:每个表的结构都不一样;每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;所有表的并集是全量数据。垂直分表适用的场景是,系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。比如以下“新闻头条”应用的例子,“新闻头条”分为了新闻列表页和新闻详情页。垂直分表的拆分原则是将热点数据(比如新闻的标题)放在一起作为主表(news_t),非热点数据(新闻的内容)放在一起作为扩展表(news_ext_t)。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。需要注意的是,垂直分表关联两个表查询的时候,避免使用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,尽量是放在业务层中台来做。分库分表的几种分配策略hash取模比如,对用户表user_t进行水平分表,分库的策略是对user_id字段进行取模。如果取模结果是0,则放入user_t_01表;如果取模结果是1,则放入user_t_02表。范围分片(range)比如,user_id从1到10000作为一个分片,从10001到20000作为另一个分片。地理位置分片华南区一个分片,华北一个分片。时间分片按月、季度、年分片等等,可以做到冷热数据。比如,今年内的数据一般就是热数据,而往年的数据就是冷数据。那么可以分为 user_t_2021、user_t_2020等表,user_t_2021是热数据,user_t_2020为冷数据。参考引用本文同步至: https://waylau.com/database-sharding/https://shardingsphere.apache.org/document/current/en/overview/
本文介绍了MyBatis的${}和#{}的用法区别,以及针对$可能带来的风险提供一种简易的SQL防注入的方法。#{}用法select语句是MyBatis中最常用的元素之一,例如:<select id="selectPerson" parameterType="int" resultType="hashmap"> SELECT * FROM PERSON WHERE ID = #{id} </select>此语句称为selectPerson,采用int(或Integer)类型的参数,并将查询结果封装为HashMap作为返回。语句中#{id}这告诉MyBatis创建一个PreparedStatement参数。对于JDBC而言,这样的参数类似于PreparedStatement语句中的“?”标识,如下:// JDBC代码 String selectPerson = "SELECT * FROM PERSON WHERE ID=?"; PreparedStatement ps = conn.prepareStatement(selectPerson); ps.setInt(1,id);${}用法默认情况下,使用#{}语法将导致MyBatis生成PreparedStatement属性,并根据PreparedStatement参数安全地设置值(例如“?”标识)。虽然这更安全、更快,而且几乎总是首选,但有时只是想直接将未修改的字符串注入SQL语句。例如,对于订单排序,可以使用类似的内容:ORDER BY ${columnName}在上面的用法中,MyBatis不会修改或转义字符串。但需要注意的是: ${}用法如果是直接接受用户的输入,将未经修改的语句注入到程序是不安全的。这将导致潜在的SQL注入攻击风险。针对${}的SQL防注入器${}用法存在SQL注入的风险,因此需要对用户的输入内容进行校验。这里提供一个简易的SQL防注入的方法。import java.util.regex.Matcher; import java.util.regex.Pattern; /** * SQL注入防护器 * * @author waylau.com * @since 2021-04-18 */ public class SqlInjectionProtector { private static final String SPECIAL_CHAR = "\\W"; /** * 校验SQL语句是否合法 * 如果非法,则抛出异常 * * @param statement 语句 * @return 是否合法 * @throws IllegalArgumentException 校验非法则抛出此异常 */ public static boolean verifySqLStatement(String statement) { if (!StringUtility.isEmpty(statement)) { if (isSpecialChar(statement)) { throw new IllegalArgumentException("illegal statement: " + statement); } } return Boolean.TRUE; } /** * 判断是否含有特殊字符 * * @param str 校验的字符串 * @return 是否特殊字符 */ public static boolean isSpecialChar(String str) { Pattern pattern = Pattern.compile(SPECIAL_CHAR); Matcher matcher = pattern.matcher(str); return matcher.find(); } }上述代码核心思想是,通过正则表达式的方式,来检测出特殊字符。有特殊字符,就抛出异常,中断程序继续往下运行。何为特殊字符?针对ORDER BY ${columnName} 这个例子而言,字段名的所使用的字符是有一定限制的,限制只能使用[a-z0-9A-Z_]这个范围内的字符。因此超出这个范围内的所有字符,即为特殊字符。在正则表达式里面,非[a-z0-9A-Z_]范围内的字符,可以用“\W”表示。以下是测试用例import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; /** * SqlInjectionProtector Test * * @author waylau.com * @since 2021-04-18 */ public class SqlInjectionProtectorTest { @Test public void testIsSpecialChar() { assertFalse(SqlInjectionProtector.isSpecialChar("user_name")); assertTrue(SqlInjectionProtector.isSpecialChar("user_name!")); assertTrue(SqlInjectionProtector.isSpecialChar("user_name@")); assertTrue(SqlInjectionProtector.isSpecialChar("user_name^")); assertTrue(SqlInjectionProtector.isSpecialChar("user_name ")); // 空格 assertTrue(SqlInjectionProtector.isSpecialChar("insert\ninto\nuser_t")); // 换行符 assertTrue(SqlInjectionProtector.isSpecialChar("user_name|user_t")); } @Test public void testVerifySqLStatement() { assertTrue(SqlInjectionProtector.verifySqLStatement("user_name")); } }参考引用本文同步至: https://waylau.com/mybatis-parameters-and-sql-inject-protector/《轻量级Java EE企业应用开发实战》(https://item.jd.com/12817685.html)《大型互联网应用轻量级架构实战》(https://item.jd.com/12629095.html)
JDK 16已经于2021年3月16日如期发布。本文介绍JDK 16新特性。发布版本说明根据发布的规划,这次发布的 JDK 16 将是一个短期的过度版。下一个长期支持版(LTS 版)会在今年的 9 月份候发布(Java 17),LTS 版每 3 年发布一个,上一次长期支持版是 18 年 9 月发布的 JDK 11。 JDK 16是Java SE平台版本16的开源参考实现,由JSR 390在JCP(Java Community Process)指定。安装包下载主要分为OpenJDK版本和Oracle版本,下载地址如下:OpenJDK版本:https://jdk.java.net/16/Oracle版本:https://www.oracle.com/java/technologies/javase-jdk16-downloads.html上述版本,如果是个人学习用途,则差异不大。但如果是用于商业用途,则需要仔细看好相关的授权。Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 更多有关Java的基本知识,可以参阅《Java核心编程》这本书,描述的非常详细。JDK 16 新特性说明JDK 16 为用户提供了17项主要的增强/更改,包括全新的 Java 语言改进,工具和内存管理,以及还有一些孵化和预览特性,有了这些新功能,Java 会进一步提高开发人员的生产力。值得关注的变化是,JDK 14 中提供的预览特性:模式匹配和记录(Records),经过一年的社区反馈和实际应用,终于在 JDK 16 中完成最终落地了。另外,Oracle 还为 Java SE 订阅服务中免费提供 GraalVM 企业版服务,GraalVM 可以帮助提高应用程序的性能并减少资源消耗,尤其是在微服务和云原生架构中。1. 338: Vector API (孵化)这个不是集合中的Vector,而是一个新的初始迭代孵化器模块jdk.incubator.vector,用于表示在运行时可靠地编译到支持的 CPU 架构上的最佳矢量硬件指令的矢量计算。2. 347: Enable C++14 Language Features允许在 JDK 底层的C++源代码中使用C++14的新语言特性,并且提供了在HotSpot虚拟机代码中,哪些代码使用了这些新特性的指南。3. 357: Migrate from Mercurial to Git将 OpenJDK 社区的源代码存储库从 Mercurial 迁移到 Git。4. 369: Migrate to GitHub在 GitHub 上托管 OpenJDK 社区的 Git 存储库。GitHub 是世界流行的Git代码托管平台。在国内,托管代码推荐Gitee哦。5. 376: ZGC: Concurrent Thread-Stack ProcessingZGC 最早是在 JDK 11 中集成进来的,在 JDK 15 中正式转正。这个版本则是为了让 ZGC 支持并发栈处理,解决了最后一个重大瓶颈,把 ZGC 中的线程栈处理从安全点移到了并发阶段。并且还提供了一种机制,使得其他 HotSpot 子系统可以通过该机制延迟处理线程栈。6. 380: Unix-Domain Socket ChannelsUNIX 域套接字通道,为 java.nio.channels 包中的套接字通道和服务端套接字通道 APIs 增加 Unix 域套接字通道所有特性支持。UNIX 域套接字主要用于同一主机上的进程间通信(IPC),大部分方面与 TCP/IP套接字类似,不同的是 UNIX 域套接字是通过文件系统路径名寻址,而不是通过 IP 地址和端口号。7. 386: Alpine Linux Port在 x64 和 AArch64 平台体系结构上,将 JDK 移植到 Alpine Linux 以及使用 musl 作为其主要 C 语言库的其他 Linux 发行版中。8. 387: Elastic Metaspace弹性的元空间,可以帮助 HotSpot 虚拟机,将元空间中未使用的 class 元数据内存更及时地返回给操作系统,以减少元空间的内存占用空间。另外,还简化了元空间的代码,以降低维护成本。9. 388: Windows/AArch64 Port将 JDK 移植到 Windows/ AArch64 平台系列。10. 389: Foreign Linker API (孵化)引入了一个新的 API,该 API 提供了对本地 native 代码的静态类型访问支持。11. 390: Warnings for Value-Based Classes基于值的类的警告,将基础类型包装类指定为基于值的类,废除其构造函数以进行删除,从而提示新的弃用警告。并且提供了在任何基于值的类的实例上不正常进行同步的警告。12. 392: Packaging Tool提供了 jpackage 打包工具,可用于打包独立的 Java 应用程序。jpackage 打包工具是在 JDK 14 中首次作为孵化工具引入的新特性,到了 JDK 15 它仍然还在孵化中,现在它终于转正了。13. 393: Foreign-Memory Access API (三次孵化)该 API 允许 Java 应用程序安全有效地访问 Java 堆之外的外部内存。这个最早在 JDK 14 中成为孵化特性,JDK 15/ JDK 16 中继续二、三次孵化并对其 API 有了一些更新,这个可以在 JDK 17 中好好期待一下转正。14. 394: Pattern Matching for instanceof模式匹配 for instanceof,相当于是增强的 instanceof,在 JDK 14 中首次成为预览特性,在 JDK 16 中正式转正。模式匹配的到来将使得 instanceof 变得更简洁、更安全,为什么这么说,请看下面的示例。Java 14 之前用法:if (obj instanceof String) { String s = (String) obj; // 使用s }Java 14之后的用法:if (obj instanceof String s) { // 使用s }15. 395: Records简单来说,Records 就是一种新的语法糖,目的还是为了简化代码,在 JDK 14 中首次成为预览特性,在 JDK 16 中正式转正。Records 可以在一定程度上避免低级冗余的代码,比如:constructors, getters, equals(), hashCode(), toString() 方法等,相当于 Lombok 的 @Data 注解,但又不能完全替代。下面来看一个示例:旧写法:class Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.y = y; } int x() { return x; } int y() { return y; } public boolean equals(Object o) { if (!(o instanceof Point)) return false; Point other = (Point) o; return other.x == x && other.y = y; } public int hashCode() { return Objects.hash(x, y); } public String toString() { return String.format("Point[x=%d, y=%d]", x, y); } }新的Records写法:record Point(int x, int y) { }16. 396: Strongly Encapsulate JDK Internals by DefaultJDK 内部默认强封装,JDK 16 开始对 JDK 内部大部分元素默认进行强封装,sun.misc.Unsafe 之类的关键内部 API 除外,从而限制对它们的访问。此外,用户仍然可以选择自 JDK 9 以来的默认的宽松的强封装,这样可以帮助用户毫不费力地升级到未来的 Java 版本。17. 397: Sealed Classes (二次预览)封闭类(二次预览),可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。参考引用本文同步至: https://waylau.com/jdk-16-released/https://waylau.com/jdk-15-released/https://waylau.com/jdk-14-released/《Java核心编程》https://github.com/waylau/modern-java-demos
ChannelHandler(管道处理器)其工作模式类似于Java Servlet过滤器,负责对I/O事件或者I/O操作进行拦截处理。采用事件的好处是,ChannelHandler可以选择自己感兴趣的事件进行处理,也可以对不感兴趣的事件进行透传或者终止。ChannelHandler接口基于ChannelHandler接口,用户可以方便实现自己的业务,比如记录日志、编解码、数据过滤等。ChannelHandler接口定义如下:package io.netty.channel; import io.netty.util.Attribute; import io.netty.util.AttributeKey; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; public interface ChannelHandler { void handlerAdded(ChannelHandlerContext ctx) throws Exception; void handlerRemoved(ChannelHandlerContext ctx) throws Exception; void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; @Inherited @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface Sharable { } }ChannelHandler接口定义比如简单,只有三个方法:handlerAdded方法在ChannelHandler被添加到实际上下文中并准备好处理事件后调用。handlerRemoved方法在ChannelHandler从实际上下文中移除后调用,表明它不再处理事件。exceptionCaught方法会在抛出Throwable类后调用。还有一个Sharable注解,该注解用于表示多个ChannelPipeline可以共享同一个ChannelHandler。正式因为ChannelHandler接口过于简单,我们在实际开发中,不会直接实现ChannelHandler接口,因此,Netty提供了ChannelHandlerAdapter抽象类。ChannelHandlerAdapter抽象类ChannelHandlerAdapter抽象类核心代码如下:package io.netty.channel; import io.netty.util.internal.InternalThreadLocalMap; import java.util.Map; import java.util.WeakHashMap; public abstract class ChannelHandlerAdapter implements ChannelHandler { boolean added; public boolean isSharable() { Class<?> clazz = getClass(); Map<Class<?>, Boolean> cache = InternalThreadLocalMap.get().handlerSharableCache(); Boolean sharable = cache.get(clazz); if (sharable == null) { sharable = clazz.isAnnotationPresent(Sharable.class); cache.put(clazz, sharable); } return sharable; } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // NOOP } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // NOOP } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.fireExceptionCaught(cause); } }ChannelHandlerAdapter对exceptionCaught方法做了实现,并提供了isSharable方法。需要注意的是,ChannelHandlerAdapter是抽象类,用户可以自由的选择是否要覆盖ChannelHandlerAdapter类的实现。如果对某个方法感兴趣,直接覆盖掉这个方法即可,这样代码就变得简单清晰。ChannelHandlerAdapter抽象类提供了两个子类ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter用于针对出站事件、入站事件的进行处理。其中ChannelInboundHandlerAdapter实现了ChannelInboundHandler接口,而ChannelOutboundHandlerAdapter实现了ChannelOutboundHandler接口。在实际开发过程中,我们的自定义的ChannelHandler多数是继承自ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter类或者是这两个类的子类。比如在前面章节中所涉及的编解码器ByteToMessageDecoder、MessageToMessageDecoder、MessageToByteEncoder、MessageToMessageEncoder等,就是这两个类的子类。参考引用原文同步至https://waylau.com/netty-channel-handler《Netty原理解析与开发实战》
IntelliJ IDEA一个吸引人的地方在于,他有比较好的反编译工具,这让Eclipse用户牙痒痒。但不要紧,本文介绍如何在Eclipse IDE中使用IntelliJ IDEA的反编译工具Fernflower。 为啥需要反编译 很多jar不提供源码,那么打开class是这个鸟样。 不具备人类可读性。因此需要反编译。 什么是Fernflower 那么我是怎么知道Fernflower的呢?你随便用IntelliJ IDEA打开一个jar中的class文件,可以看到下面的信息,这就是IEDA中的反编译工具Fernflower。 看官网介绍Fernflower(https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine) Fernflower is the first actually working analytical decompiler for Java and probably for a high-level programming language in general “Fernflower是第一个真正为Java工作分析反编译器,通常也适用于一般的高级编程语言” 看介绍是很牛批的样子,当然实际也是。 如何获取Fernflower 非常遗憾的是,Fernflower是IntelliJ IDEA独家所有,那我是怎么搞定的呢? 我先在eclipse市场找了下,没有找到Fernflower,却找到了Enhanced Class Decompiler 看官网介绍(https://marketplace.eclipse.org/content/enhanced-class-decompiler) Enhanced Class Decompiler integrates JD, Jad, FernFlower, CFR, Procyon seamlessly with Eclipse and allows Java developers to debug class files without source code directly. It also integrates with the eclipse class editor, m2e plugin, supports Javadoc, reference search, library source attaching, byte code view and the syntax of JDK8 lambda expression. 简言之,Enhanced Class Decompiler集JD、Jad、FernFlower、CFR、Procyon等各种反编译工具之大成。换言之,FernFlower就是我Enhanced Class Decompiler的一个子集呗。 呵呵,好一招曲线救国。用Enhanced Class Decompiler变相用了FernFlower。 如何在Eclipse IDE中安装Fernflower 1. 在线安装 这是最简单的方式。使用Eclipse的同学都懂。 安装地址是: https://ecd-plugin.github.io/update 2. 离线安装 获取离线安装包zip文件(见附件),在“Add Repository”中指定该zip文件即可。 3. 可选组件 一般就选Core就够用了,不嫌多就全选上。 装完重启Eclipse就能看到这个工具了。 怎么使用Fernflower 右键class文件,使用如何在Eclipse IDE中安装FernFlower打开即可 反编译成功!看到庐山真面目了。 参考引用 原文同步至 https://waylau.com/eclipse-install-fernflower/
正常情况下,在Java中入参是不建议用做返回值的。除了造成代码不易理解、语义不清等问题外,可能还埋下了陷阱等你入坑。 问题背景 比如有这么一段代码: @Named public class AService { private SupplyAssignment localSupply = new SupplyAssignment(); @Inject private BService bervice; public List<Supply> calcSupplyAssignment() List<Supply> supplyList = bService.getLocalSupplyList(this.localSupply); … return supplyList; } } 上面代码,服务A希望调用服务B,以获取supplyList,但同时,服务A又希望修改localSupply的状态值,未能避免修改calcSupplyAssignment接口的(不想改返回的类型),将localSupply作为了入参但同时也用作了返回值。 服务B代码如下: @Named public class BService { public List<Supply> getLocalSupplyList (SupplyAssignment localSupply) SupplyAssignment supplyAssignment = this.getSupplyAssignment(); // 希望localSupply被重新赋值后返回 localSupply = supplyAssignment; … return supplyList; } } 在服务B代码内部,服务A的入参localSupply被传入,希望重新被supplyAssignment赋值而后返回新值。然而,这样做是无效的。 问题原因 先来看下编程语言中关于参数传递的类型: 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。 因为Java程序设计语言是采用的值传递,因为Java没有指针的概念。也就是说方法得到的是所有参数值的一个拷贝,方法并不能修改传递给它的任何参数变量的内容。 因此,上述代码中,服务A调用服务B时,服务B的参数localSupply实际上是服务A的localSupply的一个拷贝,当然,这两个都是指向了同一个地址对象supplyAssignment1。 当在服务B内部对参数localSupply进行重新赋值是localSupply = supplyAssignment,实际上,只是对B的参数localSupply做了从新赋值,B的参数localSupply会指向一个新的地址对象supplyAssignment2。 从上图可以清晰看到,因此,服务A的localSupply和B的参数localSupply已经指向了不同的对象了,对B的参数localSupply做任何的修改,都不会影响服务A的localSupply的原值。这就是问题的原因,你希望服务B来修改服务A入参的状态,并将改后的值返回给服务A,但并不奏效。 解决方案 方案1:入参不要用作返回值 当然,这个是最清晰的且易于理解的,但这会导致有的接口的返回类型产生变化。 有时确实想要入参做返回值,那看方案2。 方案2:入参不要赋值新对象 这个方案就是直接在入参的对象上做状态的修改,而不要去赋值新对象。还是这个图: 在这个图中,只要我们是一直在B的参数localSupply修改的是supplyAssignment1的状态值,那结果就能反馈到服务A的localSupply上。如何实现?看下下面代码: @Named public class BService { public List<Supply> getLocalSupplyList (SupplyAssignment localSupply) SupplyAssignment supplyAssignment = this.getSupplyAssignment(); // 针对localSupply不能新建引用,只能重新赋值属性 BeanUtils.copyProperties(supplyAssignment, localSupply); … return supplyList; } } 在上面的方法中,我们用到了Spring的工具类BeanUtils,该类的copyProperties方法的实质是将supplyAssignment的属性值,赋值到了localSupply的属性上。这意味着我们是修改的B的参数localSupply上的属性,而并未新建对象。 参考引用 原文同步至 https://waylau.com/trap-in-java-use-param-for-return/ Java核心编程
今天遇到一个奇怪的Java三元表达式中的空指针异常。特此记录。 代码 代码示意如下: Integer itemVO = null; Integer globleLatenessToleranceUseAlternate = null; Integer latenessToleranceUseAlternate = (itemVO == null ? globleLatenessToleranceUseAlternate : itemVO.intValue()); 从上面代码可以看出:当itemVO不为空时,就取itemVO的值;否则,就取globleLatenessToleranceUseAlternate的值。 原因 但问题就在globleLatenessToleranceUseAlternate。当itemVO为空时,如果取globleLatenessToleranceUseAlternate,并不会得到值null,而是Java会把globleLatenessToleranceUseAlternate进行一个自动开箱拆箱处理。简言之,取得是 globleLatenessToleranceUseAlternate.intValue(),此时,因为globleLatenessToleranceUseAlternate 本身是 null,因此 globleLatenessToleranceUseAlternate.intValue() 导致了空指针因此。 解法 修改如下解决: Integer itemVO = null; Integer globleLatenessToleranceUseAlternate = null; Integer latenessToleranceUseAlternate; if (itemVO != null) { latenessToleranceUseAlternate = itemVO.intValue(); } else { latenessToleranceUseAlternate = globleLatenessToleranceUseAlternate; } 值得注意的是,在新版的JDK和Eclipse中,会做出友好的提示,从而能够有效规避上述问题。提示如下: Null pointer access: This expression of type Integer is null but requires auto-unboxing 参考引用 原文同步至 https://waylau.com/trap-in-java-ternary-expressions/ Java核心编程
JDK 15已经于2020年9月15日如期发布。本文介绍JDK 15新特性。 发布版本说明 根据发布的规划,这次发布的 JDK 15 将是一个短期的过度版,只会被 Oracle 支持(维护)6 个月,直到明年 3 月的 JDK 16 发布此版本将停止维护。而 Oracle 下一个长期支持版(LTS 版)会在明年的 9 月份候发布(Java 17),LTS 版每 3 年发布一个,上一次长期支持版是 18 年 9 月发布的 JDK 11。 下图展示了各个版本的发布历史。 安装包下载 主要分为OpenJDK版本和Oracle版本,下载地址如下: OpenJDK版本:https://jdk.java.net/15/ Oracle版本:http://www.oracle.com/technetwork/java/javase/downloads/index.html 上述版本,如果是个人学习用途,则差异不大。但如果是用于商业用途,则需要仔细看好相关的授权。Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 安装、验证 本例子以OpenJDK版本为例。解压安装包openjdk-15_windows-x64_bin.zip到任意位置。 设置系统环境变量“JAVA_HOME”,如下图所示。 在用户变量“Path”中,增加“%JAVA_HOME%bin”。 安装完成后,执行下面命令进行验证: >java -version openjdk version "15" 2020-09-15 OpenJDK Runtime Environment (build 15+36-1562) OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing) 更多有关Java的基本知识,可以参阅《Java核心编程》这本书,描述的非常详细。 JDK 15 新特性说明 JDK 15 为用户提供了14项主要的增强/更改,包括一个孵化器模块,三个预览功能,两个不推荐使用的功能以及两个删除功能。 1. EdDSA 数字签名算法 新加入 Edwards-Curve 数字签名算法(EdDSA)实现加密签名。在许多其它加密库(如 OpenSSL 和 BoringSSL)中得到支持。与 JDK 中的现有签名方案相比,EdDSA 具有更高的安全性和性能。这是一个新的功能。 使用示例如下: // example: generate a key pair and sign KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519"); KeyPair kp = kpg.generateKeyPair(); // algorithm is pure Ed25519 Signature sig = Signature.getInstance("Ed25519"); sig.initSign(kp.getPrivate()); sig.update(msg); byte[] s = sig.sign(); // example: use KeyFactory to contruct a public key KeyFactory kf = KeyFactory.getInstance("EdDSA"); boolean xOdd = ... BigInteger y = ... NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519"); EdECPublicKeySpec pubSpec = new EdECPublicKeySpec(paramSpec, new EdPoint(xOdd, y)); PublicKey pubKey = kf.generatePublic(pubSpec); 有关EdDSA 数字签名算法的详细内容见RFC 8032规范。 2. 封闭类(预览特性) 可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。 有了这个特性,意味着以后不是你想继承就继承,想实现就实现了,你得经过允许才行。 示例如下: public abstract sealed class Student permits ZhangSan, LiSi, ZhaoLiu { ... } 类 Student 被 sealed 修饰,说明它是一个封闭类,并且只允许指定的 3 个子类继承。 3. 隐藏类 此功能可帮助需要在运行时生成类的框架。框架生成类需要动态扩展其行为,但是又希望限制对这些类的访问。隐藏类很有用,因为它们只能通过反射访问,而不能从普通字节码访问。此外,隐藏类可以独立于其他类加载,这可以减少框架的内存占用。这是一个新的功能。 4. 移除了 Nashorn JavaScript 脚本引擎 移除了 Nashorn JavaScript 脚本引擎、APIs,以及 jjs 工具。这些早在 JDK 11 中就已经被标记为 deprecated 了,JDK 15 被移除就很正常了。 Nashorn 是 JDK 1.8 引入的一个 JavaScript 脚本引擎,用来取代 Rhino 脚本引擎。Nashorn 是 ECMAScript-262 5.1 的完整实现,增强了 Java 和 JavaScript 的兼容性,并且大大提升了性能。 那么为什么要移除? 官方的解释是主要的:随着 ECMAScript 脚本语言的结构、API 的改编速度越来越快,维护 Nashorn 太有挑战性了,所以……。 5. 重新实现 DatagramSocket API 重新实现旧版 DatagramSocket API,更简单、更现代的实现来代替java.net.DatagramSocket和java.net.MulticastSocketAPI 的基础实现,提高了 JDK 的可维护性和稳定性。 新的底层实现将很容易使用虚拟线程,目前正在 Loom 项目中进行探索。这也是 JEP 353 的后续更新版本,JEP 353 已经重新实现了 Socket API。 6. 准备禁用和废除偏向锁 在 JDK 15 中,默认情况下禁用偏向锁(Biased Locking),并弃用所有相关的命令行选项。 后面再确定是否需要继续支持偏向锁,国为维护这种锁同步优化的成本太高了。 7. 模式匹配(第二次预览) 第一次预览是 JDK 14 中提出来的,点击这里查看我之前写的详细教程。 Java 14 之前用法: if (obj instanceof String) { String s = (String) obj; // 使用s } Java 14之后的用法: if (obj instanceof String s) { // 使用s } Java 15 并没有对此特性进行调整,继续预览特性,只是为了收集更多的用户反馈,可能还不成熟吧。 8. ZGC 功能转正 ZGC是一个可伸缩、低延迟的垃圾回收器。 ZGC 已由JEP 333集成到JDK 11 中,其目标是通过减少 GC 停顿时间来提高性能。借助 JEP 377,JDK 15 将 ZGC 垃圾收集器从预览特性变更为正式特性而已,没错,转正了。 这个 JEP 不会更改默认的 GC,默认仍然是 G1。 9. 文本块功能转正 文本块,是一个多行字符串,它可以避免使用大多数转义符号,自动以可预测的方式格式化字符串,并让开发人员在需要时可以控制格式。 文本块最早准备在 JDK 12 添加的,但最终撤消了,然后在 JDK 13 中作为预览特性进行了添加,然后又在 JDK 14 中再次预览,在 JDK 15 中,文本块终于转正,暂不再做进一步的更改。 Java 13 之前用法,使用one-dimensional的字符串语法: String html = "<html>\n" + " <body>\n" + " <p>Hello, world</p>\n" + " </body>\n" + "</html>\n"; Java 13 之后用法,使用two-dimensional文本块语法: String html = """ <html> <body> <p>Hello, world</p> </body> </html> """; 10. Shenandoah 垃圾回收算法转正 Shenandoah 垃圾回收从实验特性变为产品特性。这是一个从 JDK 12 引入的回收算法,该算法通过与正在运行的 Java 线程同时进行疏散工作来减少 GC 暂停时间。Shenandoah 的暂停时间与堆大小无关,无论堆栈是 200 MB 还是 200 GB,都具有相同的一致暂停时间。 JDK 15 Shenandoah垃圾收集器从预览特性变更为正式特性而已,没错,又是转正了。 11. 移除了 Solaris 和 SPARC 端口。 移除了 Solaris/SPARC、Solaris/x64 和 Linux/SPARC 端口的源代码及构建支持。这些端口在 JDK 14 中就已经被标记为 deprecated 了,JDK 15 被移除也不奇怪。 12. 外部存储器访问 API(二次孵化) 这个最早在 JDK 14 中成为孵化特性,JDK 15 继续二次孵化并对其 API 有了一些更新。 目的是引入一个 API,以允许 Java 程序安全有效地访问 Java 堆之外的外部内存。这同样是 Java 14 的一个预览特性。 13. Records Class(二次预览) Records Class 也是第二次出现的预览功能,它在 JDK 14 中也出现过一次了,使用 Record 可以更方便的创建一个常量类,使用的前后代码对比如下。 旧写法: class Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.y = y; } int x() { return x; } int y() { return y; } public boolean equals(Object o) { if (!(o instanceof Point)) return false; Point other = (Point) o; return other.x == x && other.y = y; } public int hashCode() { return Objects.hash(x, y); } public String toString() { return String.format("Point[x=%d, y=%d]", x, y); } } 新写法: record Point(int x, int y) { } 也就是说在使用了 record 之后,就可以用一行代码编写出一个常量类,并且这个常量类还包含了构造方法、toString()、equals() 和 hashCode() 等方法。 14. 废除 RMI 激活 废除 RMI 激活,以便在将来进行删除。需要说明的是,RMI 激活是 RMI 中一个过时的组件,自 Java 8 以来一直是可选的。 参考引用 本文同步至: https://waylau.com/jdk-15-released/ https://jdk.java.net/15/release-notes https://openjdk.java.net/projects/jdk/15/ https://openjdk.java.net/projects/jdk/15/spec/ 《Java核心编程》 https://github.com/waylau/modern-java-demos
曾几何时,业界流行使用LAMP架构(Linux、Apache、MySQL和PHP)来快速开发中小网站。LAMP是开放源代码的,而且使用简单、价格廉价,因此LAMP这个组合成为了当时开发中小网站的首选,号称“平民英雄”。而今,随着Node.js的流行,这使得JavaScript终于能够在服务器端拥有了一席之地。JavaScript成为了从前端到后端再到数据库层能够支持全栈开发的语言。而以MongoDB、Express、Angular和Node.js四种开源技术为基础的MEAN架构,除了具备LAMP架构的一切优点外,更能支撑高可用、高并发的大型互联网应用的开发。MEAN架构势必也会成为新的“平民英雄”。 本文介绍了MEAN架构的概念、发展趋势,并阐述了如何学习和使用MEAN架构。 什么是MEAN架构? MEAN架构,是指以MongoDB、Express、Angular和Node.js四种技术为核心的技术栈,广泛应用于全堆栈Web开发。 1. MongoDB MongoDB是强大的非关系型数据库(NoSQL)。与Redis或者HBase等不同,MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富、最像关系数据库的,旨在为Web应用提供可扩展的高性能数据存储解决方案。它支持的数据结构非常松散,是类似JSON的BSON格式,因此可以存储比较复杂的数据类型。MongoDB最大的特点是其支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。自MongoDB 4.0开始,MongoDB开始支持事务管理。 图1-1是最新的数据库排行结果。从图中可以看到,MongoDB是在NoSQL数据库中是排行第一的。该数据来自于DB-Engines(https://db-engines.com/en/ranking) [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FSWZ67e-1591539233123)(https://waylau.com/images/post/20200607-mongodb.png)] 在MEAN架构中,MongoDB承担着数据存储的角色。 2. Express Express是一个简洁而灵活的Node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用。同时,Express也是一款功能非常强大的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。其核心特性包括: 可以设置中间件来响应HTTP请求。 定义了路由表用于执行不同的HTTP请求动作。 可以通过向模板传递参数来动态渲染HTML页面。 在MEAN架构中,Express承担着构建Web服务的角色。 3. Angular 前端组件化开发是目前主流的开发方式,不管是Angular、React还是Vue.js都如此。相比较而言,Angular不管是其开发功能,还是编程思想,在所有前端框架中都是首屈一指的,特别适合大型企业级应用的开发。 Angular不仅仅是一个前端的框架,而更像是一个前端开发平台,试图解决现代Web应用开发各个方面的问题。Angular有着诸多特性,核心功能包括MVC模式、模块化、自动化双向数据绑定、语义化标签、服务、依赖注入等。而这些概念即便对于后端开发人员来说也不陌生。比如,Java开发人员肯定知道MVC模式、模块化、服务、依赖注入等。 在MEAN架构中,Angular承担着UI客户端开发的角色。 4. Node.js Node.js是整个MEAN架构的基石。Node.js采用事件驱动和非阻塞I/O模型,使其变得轻微和高效,非常适合构建运行在分布式设备的数据密集型实时应用。自从有了Node.js,JavaScript不再只是前端开发的小脚色,而是拥有了从前后台到数据数据库完整开发能力的全栈能手。JavaScript和Node.js是相辅相成的,配合流行的JavaScript语言,使得Node.js拥有更广泛的受众。 Node.js能够火爆的另外一个原因是npm。npm可以轻松管理项目依赖,同时也促进了Node.js生态圈的繁荣,因为npm让开发人员分享开源技术变得不再困难。 MEAN架构的优势 MEAN架构的在企业级应用中被广泛采用,总结起来具备以下优势。 1. 开源 正如前两节所述,无论是MongoDB、Express、Angular、Node.js四种核心技术,还是NG-ZORRO、ngx-markdown、NGINX、basic-auth等周边技术,MEAN架构所有的技术栈都是开源的。 开源技术相对与闭源技术而言,有其优势。一方面,开源技术源码是公开的,互联网公司在考察某项技术是否符合自身开发需求时,可以对源码进分析;另一方面,开源技术相对闭源技术而言,商用的成本相对比较低,这对于很多初创的互联网公司而言,可以节省一大笔技术投入。以此,MEAN架构也被称为开发下一代大型互联网应用的“平民英雄”。 当然,开源技术是把双刃剑,你能够看到源码,并不意味着你可以解决所有问题。开源技术在技术支持上不能与闭源技术相提并论,毕竟闭源技术都有成熟的商业模式,会提供完善的商业支持。而开源技术,更多依赖于社区对于开源技术的支持。如果在使用开源技术过程中发现了问题,可以反馈给开源社区,但开源社区不会给你保证什么时候、什么版本能够修复发现的问题。所以,使用开源技术,需要开发团队对开源技术要有深刻的了解。最好能够吃透源码,这样在发现问题时,能够及时解决源码上的问题。 比如,在关系型数据库方面,同属于Oracle公司的MySQL数据库和Oracle数据库,就是开源与闭源技术的两大代表,两者占据了全球数据库的占有率的前两名。MySQL数据库主要是在中小企业或者是云计算供应商中广泛采用,而Oracle数据库则由于其稳定、高性能的特性,深受政府和银行等客户的信赖。 2. 跨平台 跨平台,意味着开发和部署的应用的成本的降低。 试想一下,当今操作系统三足鼎立,分别是Linux、macOS、Windows。如果开发者需要针对不同的操作系统平台,而要开发不同的软件,那么开发成本势必会非常高。而且每个操作系统平台,都有不同的版本、分支,仅仅做不同的版本的适配都需要耗费极大的人力,更别提要针对不同的平台开发软件了。以此,跨平台可以节省开发成本。 同理,由于MEAN架构开发的软件是具有跨平台的,无需担心在部署应用过程中的兼容性问题。开发者在本地开发环境所开发的软件,理论上是可以通过CICD平台直接一键部署到测试环境,甚至是生产环境中,因而可以节省部署的成本。 MEAN架构的跨平台特性,使其非常适合构建Cloud Native应用,特别是在当今容器技术常常作为微服务的宿主,而MEAN架构的应用是支持Docker部署的。 有关Cloud Native方面的内容,可以参阅笔者所著的《Cloud Native 分布式架构原理与实践》。 3. 全栈开发 类似与系统架构师,全栈开发者应该是比一般的软件工程师具有更广的知识面,是拥有全端软件设计思想并掌握多种开发技能的复合型人才,能狗独当一面。相比于Node.js工程师、Angular工程师偏重于某项技能而言,全栈开发意味着必须掌握整个架构的全部细节,要求全栈开发者能够从零开始构建全套完整的企业级应用。 作为一名全栈开发者,在开发时往往会做如下风险的预测,并做好防御。 当前所开发的应用会部署到什么样的服务器、网络环境中? 服务哪里可能会崩?为什么会崩? 是否应该适当的使用云存储? 程序有无具备数据冗余? 是否具备可用性? 界面是否友好? 性能是否能够满足当前的要求? 哪些位置需要加日志,方便日志排查问题? 除上述的思考外,全栈开发者要能够建立合理的、标准的关系模型,包括外键、索引、视图、查找表等。 全栈开发者要熟悉非关系型数据存储,并且知道它们相对关系型存储优势所在。 当然,人的精力毕竟有限,所以想要成为全栈开发者并非易事。所幸MEAN架构让这一切成为了可能。MEAN架构以Node.js为整个技术栈的核心,而Node.js的编程语言是JavaScript,这意味着,开发者只需要掌握JavaScript这一种编程语言,即可以打通所有MEAN架构的技术,这不得不说是全栈开发者的福音。 4. 支持企业级应用 无论是Node.js、Angular还是MongoDB,这些技术在大型互联网公司都被广泛采用。无数应用也证明了MEAN架构是非常适合构建企业级应用的。企业级应用是指那些为商业组织、大型企业而创建并部署的解决方案及应用。这些大型企业级应用的结构复杂,涉及的外部资源众多、事务密集、数据量大、用户数多,有较强的安全性考虑。 MEAN架构用来开发企业级应用,不但具有强大的功能,还能够满足未来业务需求的变化,且易于升级和维护。 更多有关企业级应用开发方面的内容,可以参阅笔者所著的《Spring Boot 企业级应用开发实战》《Angular企业级应用开发实战》《Node.js企业级应用开发实战》等。 5. 支持构建微服务 微服务(Microservices)架构风格就像是把小的服务开发成单一应用的形式,运行在其自己的进程中,并采用轻量级的机制进行通信(一般是HTTP资源API)。这些服务都是围绕业务能力来构建,通过全自动部署工具来实现独立部署。这些服务,其可以使用不同的编程语言和不同的数据存储技术,并保持最小化集中管理。 MEAN架构非常适合构建微服务: Node.js本身提供了跨平台的能力,可以运行在自己的进程中。 Express易于构建Web服务,并支持HTTP的通信。 Node.js+MongoDB支持从前端到后端再到数据库全栈开发能力。 开发人员可以轻易地通过MEAN架构来构建并快速启动一个微服务应用。业界也提供了成熟的微服务解决方案来打造大型微服务架构系统,比如Tars.js、Seneca等。 读者欲了解更多微服务方面的内容,可以参阅笔者所著的《Spring Cloud 微服务架构开发实战》。 6. 业界主流 MEAN架构所涉及的技术都是业界主流,主要体现在以下几方面。 MongoDB是在NoSQL数据库中是排行第一的,而且用户量还在递增。 只要知道JavaScript就必然知道Node.js,而JavaScript是在开源界最流行的开发语言。 前端组件化开发是目前主流的开发方式,不管是Angular、React还是Vue.js都如此。相比较而言,Angular不管是其开发功能,还是编程思想,在所有前端框架中都是首屈一指的,特别适合大型企业级应用的开发。而且,从市场占有率来看,Angular都是首屈一指的。 在大型互联网应用中,经常使用NGINX作为Web服务器。NGINX也是目前使用最广泛的代理服务器。 如何学习MEAN架构? MEAN架构知识点繁多,涉及面广,不是一时可以掌握。关于MEAN架构,笔者撰写了多本开源书籍,方便网友学习。包括: REST 案例大全 REST 实战 CSS3 教程 跟老卫学Ionic Node.js 案例大全 跟老卫学Angular 这些开源书都免费的,可以随时学习哦,附带案例和源码。有任何问题都可以在线在相关的主页留言,有问必答。 当然,笔者也出版了一些的专著,网友们可以按需选择。包括: Angular企业级应用开发实战 大型互联网应用轻量级架构实战 MongoDB+Express+Angular+Node.js全栈开发实战派 上面的案例和源码也都是公开免费的哦。 参考引用 本文同步至: https://waylau.com/mean-architecture-in-action/ Angular企业级应用开发实战(2019年06月出版) 大型互联网应用轻量级架构实战(2019年12月出版) MongoDB+Express+Angular+Node.js全栈开发实战派(2020年06月出版) Spring Boot 企业级应用开发实战(2018年03月出版) Spring Cloud 微服务架构开发实战(2018年06月出版)
本文介绍了Java的发展趋势,并阐述了如何学习Java技术。 Java为啥火爆? 随着互联网应用的发展,各种编程语言层出不穷,比如C#、Golang、TypeScript、ActionScript等,但不管是哪种语言,都无法撼动Java的“霸主”地位。Java语言始终占据着各类编程语言排行榜的榜首,开发者对于Java的热情也是与日俱增。Java已然成为了企业级应用、云计算和Cloud Native应用的首选语言。 图1-1展示的是1985年至2020年TIOBE编程语言排行榜情况(https://www.tiobe.com/tiobe-index/)。从图中可以看出,自Java诞生以来,一直占据排行版前三的位置。 那么为什么Java一致能保持这么火爆呢?究其原因,笔者认为Java能够长盛不衰的最大的秘诀就是能够与时俱进,不断推陈出新。 笔者从事Java开发已经有十几年了,可以说是Java技术发展的见证者和实践者。为了推广Java技术,笔者撰写了包括《分布式系统常用技术及案例分析》、《Spring Boot 企业级应用开发实战》、《Spring Cloud 微服务架构开发实战》、《Spring 5开发大全》、《Cloud Native 分布式架构原理与实践》等在内了几十本Java领域的专著和开源书,期望以个人微薄之力对Java语言有所贡献。由于目前企业所使用的Java,大多是Java 8之前的版本,市面上也缺乏最新Java 12的学习资料,因此笔者才撰写了这本《Java核心编程》一书以补空白。 Java应该怎么学? 那么,Java应该怎么学?参考《Java核心编程》,学习Java分为以下几个层次: 1. 零基础的读者 如果你是没有任何编程经验的技术爱好者,本书可以帮助你打开编程之门。本书案例丰富、思路清晰,可以由浅及深地帮助读者掌握Java。 同时,本书可以帮助读者从一开始就建立正确的编程习惯,逐步树立良好的面向对象设计思维,这对于学习其他语言都是非常有帮助的。 针对这类读者,建议读者在学习过程中,从头至尾详细跟随笔者来理解Java的概念,并编写《Java核心编程》书中的示例。该书附赠从Java 8到Java 14全套案例:https://github.com/waylau/modern-java 2. 有后端开发经验的读者 对于有后端或者是其他面向对象编程的经验的开发而言,理解并掌握Java并非难事。 针对这类读者,适当理解下Java的语法即可,把精力放在动手编写Java示例上面。 3. 有Java开发经验的读者 大多数Java开发人员肯定熟悉Java的语法,那么需要并把精力放在Java新特性上面,根据自身的实际情况,可以选学本书中的知识点,做到查漏补缺。 让我们一起踏上Java的学习之旅吧。 参考引用 本文同步至: https://waylau.com/java-development-is-still-mainstream/ Eclipse IDE支持Java 14: https://waylau.com/eclipse-ide-support-java14/ 现代Java案例大全,从Java 8到Java 14:https://github.com/waylau/modern-java
本文演示了如何如何编写JUnit 5测试用例,在Maven项目中运行JUnit 5测试用例。 编写JUnit 5测试用例 如果你是Java开发者,那么对于JUnit应该就不陌生。JUnit是Java单元测试的基础工具。 JUnit目前最新的版本是JUnit 5.x,但广大的Java开发者估计还停留在JUnit 4.x,因此有必要演示下如何编写JUnit 5测试用例。 引入JUnit 5依赖 相比较JUnit 4而言,JUnit 5一个比较大的改变是JUnit 5拥有与JUnit 4不同的全新的API。JUnit 5分成了三部分: JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage JUnit Platform是在JVM上启动测试框架的基础。 它还定义了TestEngine API,用于开发在平台上运行的测试框架。 此外,该JUnit Platform还提供了一个控制台启动器(用于从命令行启动该平台)和一个基于JUnit 4的运行器,用于在基于JUnit 4的环境中在该平台上运行任何TestEngine。 流行的IDE(IntelliJ IDEA,Eclipse,NetBeans和Visual Studio Code等)和构建工具(Gradle,Maven和Ant等)中也存在对JUnit平台的一流支持。 JUnit Jupiter是新编程模型和扩展模型的组合,用于在JUnit 5中编写测试和扩展。Jupiter子项目提供了一个TestEngine,用于在平台上运行基于Jupiter的测试。 JUnit Vintage提供了一个TestEngine,用于在平台上运行基于JUnit 3和基于JUnit 4的测试。 因此,在Maven中,JUnit 5分模块的,意味着你可以按需引入上面定义的任意模块。这使得引入JUnit 5依赖就有了多个选择。 一般而言,力求省事,就可以通过引入junit-jupiter依赖。junit-jupiter就是常用JUnit 5模块的聚合包。 <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit-jupiter.version}</version> <scope>test</scope> </dependency> 编写测试用例 下面是一段简单的Java程序: /** * Welcome to https://waylau.com */ package com.waylau.java.demo; /** * Hello World. * * @since 1.0.0 2020年4月12日 * @author <a href="https://waylau.com">Way Lau</a> */ public class HelloWorld { private String words; public HelloWorld(String words) { this.words = words; } public String getWords() { return words; } } 按照管理,我们会在Maven工程的test目录,创建一个与之对应的单元测试用例: /** * Welcome to https://waylau.com */ package com.waylau.java.demo; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; /** * HelloWorld Test. * * @since 1.0.0 2020年4月12日 * @author <a href="https://waylau.com">Way Lau</a> */ class HelloWorldTests { @Test void testGetWords() { var words = "Hello World"; var hello = new HelloWorld(words); assertEquals(words, hello.getWords()); } } 上述用例非常简单,就是想测试下,HelloWorld的getWords方法,是否与预期的一致。这里需要强调的是JUnit 5和JUnit 4的不同点: JUnit 5使用的API是org.junit.jupiter.api.*包下 测试方法(比如上例testGetWords),可以不加public。 运行JUnit 5测试用例 上如上文所讲,在大多数主流的IDE中,都提供了对JUnit 5的支持。因此可以选择在IDE中运行,也可以通过Maven执行测试。 在IDE中运行 以Eclipse IDE为例,右键类或者方法,选择“Run As -> JUnit Test”即可。如下图所示。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tONJQN3t-1587044607054)(../images/post/20200412-ide.jpg)] 通过Maven执行测试 在Maven中执行测试用例的命令如下: mvn test 如果你执行了上述命令,会得到下面的测试结果 ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.waylau.java.demo.HelloWorldTests Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec Results : Tests run: 0, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.983 s [INFO] Finished at: 2020-04-12T11:22:16+08:00 [INFO] ------------------------------------------------------------------------ 上面结果没有失败的用例,但同时你也发现了没有成功的用例。因为根本没有执行测试用例。 这是因为,在Maven中并不能直接识别JUnit 5测试用例。如何解决?此时,还需要额外加多Maven Surefire或Maven Failsafe两个插件。 <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>${maven-surefire-plugin.version}</version> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>${maven-failsafe-plugin.version}</version> </plugin> </plugins> </build> 在Maven中再次执行测试用例,会得到下面的测试结果: [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.waylau.java.demo.HelloWorldTests [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.045 s - in com.waylau.java.demo.HelloWorldTests [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.116 s [INFO] Finished at: 2020-04-12T11:30:29+08:00 [INFO] ------------------------------------------------------------------------ 可以看到,HelloWorldTests类已经得到了测试执行。 参考引用 本文同步至: https://waylau.com/running-junit5-tests-with-maven/ Eclipse IDE支持Java 14: https://waylau.com/eclipse-ide-support-java14/ 本文示例源码:https://github.com/waylau/java-data-structures-and-algorithms-in-action
随着JDK 14的发布(https://waylau.com/jdk-14-released/),各大Java IDE也开始支持JDK 14。最新版本的Eclipse IDE 2020-03也于2020年3月18日发布,本文介绍如何通过Eclipse IDE来开发Java 14应用。 下载 下载地址https://www.eclipse.org/downloads/packages/ 根据个人的需要,下载Java Developers或者Enterprise Java Developers版本。 设置JDK 下载最新的JDK 14,并在Eclipse IDE中指向该JDK。 设置Maven 可选。如果项目是使用Maven管理,则建议下载最新的Maven,并在Eclipse IDE中指向该Maven。 设置网络代理 为了更快的下载插件,需要设置代理。 设置代码样式 可选。设置符合自己需求的代码样式。 设置字符 建议使用UTF-8。 安装支持Java 14的插件 由于当前的Eclipse还未正式支持Java 14,需要额外安装Java 14 Support for Eclipse 2020-03 (4.15)插件,用以支持Java 14。插件地址: https://marketplace.eclipse.org/content/java-14-support-eclipse-2020-03-415 安装完成之后,就可以选择使用JDK 14的编译器,同时启用预览功能。 编程 终于可以愉快的玩耍了Java 14了。 本文所有源码可见https://github.com/waylau/modern-java-demos。 参考引用 本文同步至: https://waylau.com/eclipse-ide-support-java14/ 现代Java案例大全:https://github.com/waylau/modern-java-demos
初次使用TortorliseGit的小伙伴,怕是很难找到删除分支的菜单。本文介绍如何使用TortorliseGit删除分支。 右键项目,点击“Switch/Checkout”菜单 在点击右侧的三个小点 选中要删除的分支,右键分支,可以看到“Delete branch”按钮,点击该按钮就能删除分支。 点击左侧“remotes”,可以用同样的手法,来删除远端的分支。 参考引用 本文同步至: https://waylau.com/tortorlise-git-delete-branch/
JDK 14已经于2020年3月17日如期发布。本文介绍JDK 14特性。 JEP 305: instanceof的模式匹配(预览) 通过对instanceof运算符进行模式匹配来增强Java编程语言。 模式匹配允许程序中的通用逻辑,即从对象中有条件地提取组件,可以更简洁,更安全地表示。 这是JDK 14中的预览语言功能。 动机 几乎每个程序都包含某种逻辑,这些逻辑结合了对表达式是否具有某种类型或结构的测试,然后有条件地提取其状态的组件以进行进一步处理。例如,以下是在Java程序中常见的instanceof-and-cast用法: if (obj instanceof String) { String s = (String) obj; // 使用s } 上述示例中,为了能够安全地将obj转为我们期望的String类型,需要通过instanceof运算符对obj进行类型判断。这里发生了三件事: 测试obj是否是一个String 将obj转换为String 声明新的局部变量s,以便我们可以使用字符串值。 这种模式很简单,并且所有Java程序员都可以理解,但是由于一些原因,它不是最优的。 语法乏味 同时执行类型检测和类型转换并不是必要的 String类型在程序中出现了3次,这混淆了后面更重要的逻辑 重复的代码容易滋生错误 在JDK 14中,上述代码可以改为下面的方式: if (obj instanceof String s) { // 使用s } 这样整个代码看上去更加简洁。 描述 类型测试模式由指定类型的谓词和单个绑定变量组成。在下面的代码中,短语String是类型测试模式: if (obj instanceof String s) { // 使用s } else { // 不能使用s } 如果obj是String的实例,则将其强制转换为String并分配给绑定变量s。绑定变量在if语句的true块中,而不在if语句的false块中。 与局部变量的范围不同,绑定变量的范围由包含的表达式和语句的语义确定。例如,在此代码中: if (!(obj instanceof String s)) { .. s.contains(..) .. } else { .. s.contains(..) .. } true块中的s表示封闭类中的字段,false块中的s表示由instanceof运算符引入的绑定变量。 当if语句的条件变得比单个instanceof更复杂时,绑定变量的范围也会相应地增长。 例如,在此代码中: if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..} 绑定变量s在&&运算符右侧以及true块中。仅当instanceof成功并分配给s时,才评估右侧。 另一方面,在此代码中: if (obj instanceof String s || s.length() > 5) {.. s.contains(..) ..} 绑定变量s不在||右侧的范围内运算符,也不在true块的范围内。s指的是封闭类中的一个字段。 Joshua Bloch的经典著作Effective Java中有一段代码示例: @Override public boolean equals(Object o) { return (o instanceof CaseInsensitiveString) && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); } 这段代码可以使用新的语法写成: @Override public boolean equals(Object o) { return (o instanceof CaseInsensitiveString cis) && cis.s.equalsIgnoreCase(s); } 这个特性很有意思,因为它为更为通用的模式匹配打开了大门。模式匹配通过更为简便的语法基于一定的条件来抽取对象的组件,而instanceof刚好是这种情况,它先检查对象类型,然后再调用对象的方法或访问对象的字段。 JEP 343: 打包工具(孵化) 该特性旨在创建一个用于打包独立Java应用程序的工具。 动机 许多Java应用程序需要以一流的方式安装在本机平台上,而不是简单地放置在类路径或模块路径上。对于应用程序开发人员来说,交付简单的JAR文件是不够的。他们必须提供适合本机平台的可安装软件包。这允许以用户熟悉的方式分发,安装和卸载Java应用程序。例如,在Windows上,用户希望能够双击一个软件包来安装他们的软件,然后使用控制面板删除该软件。在macOS上,用户希望能够双击DMG文件并将其应用程序拖到Application文件夹中。 打包工具还可以帮助填补其他技术的空白,例如Java Web Start(已从JDK 11中删除)和pack200(已在JDK 11中弃用,可能在以后的版本中删除)。开发人员可以使用jlink将JDK分解为所需的最小模块集,然后使用打包工具生成一个压缩的、可安装的映像,该映像可以部署到目标计算机。 为了以前满足这些要求,JDK 8分发了一个名为javapackager的打包工具。但是,作为删除JavaFX的一部分,该工具已从JDK 11中删除。 描述 jpackage工具将Java应用程序打包到特定于平台的程序包中,该程序包包含所有必需的依赖项。该应用程序可以作为普通JAR文件的集合或作为模块的集合提供。受支持的特定于平台的软件包格式为: Linux:deb和rpm macOS:pkg和dmg Windows:MSI和EXE 默认情况下,jpackage会以最适合其运行系统的格式生成一个软件包。 以下是基本用法: $ jpackage --name myapp --input lib --main-jar main.jar 用法 1. 基本用法:非模块化应用 假设你有一个由JAR文件组成的应用程序,所有应用程序都位于lib目录下,并且主类在lib/main.jar中。下列命令 $ jpackage --name myapp --input lib --main-jar main.jar 将以本地系统的默认格式打包应用程序,并将生成的打包文件保留在当前目录中。如果main.jar中的MANIFEST.MF文件没有Main-Class属性,我们必须显式地指定主类: $ jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main 打包的名称是myapp。要启动该应用程序,启动器将从输入目录复制的每个JAR文件都放在JVM的类路径上。 如果希望生成默认格式以外的软件安装包,可以使用--type选项。例如要在macOS上生成pkg文件(而不是dmg文件),我们可以使用下面的命令: $ jpackage --name myapp --input lib --main-jar main.jar --type pkg 2. 基本用法:模块化应用 如果你有一个模块化应用程序,该应用程序由lib目录中的模块化JAR文件和/或JMOD文件组成,并且主类位于myapp模块中,则下面的命令 $ jpackage --name myapp --module-path lib -m myapp 能够将其打包。如果myapp模块无法识别主类,则必须明确指定: $ jpackage --name myapp --module-path lib -m myapp/myapp.Main JEP 345: G1的NUMA内存分配优化 通过实现可识别NUMA的内存分配,提高大型计算机上的G1性能。 动机 现代的多插槽计算机越来越多地具有非统一的内存访问(non-uniform memory access,NUMA),即内存与每个插槽或内核之间的距离并不相等。插槽之间的内存访问具有不同的性能特征,对更远的插槽的访问通常具有更大的延迟。 并行收集器中通过启动-XX:+UseParallelGC能够感知NUMA,这个功能已经实现了多年了,这有助于提高跨多插槽运行单个JVM的配置的性能。其他HotSpot收集器没有此功能,这意味着他们无法利用这种垂直多路NUMA缩放功能。大型企业应用程序尤其倾向于在多个多插槽上以大堆配置运行,但是它们希望在单个JVM中运行具有可管理性优势。 使用G1收集器的用户越来越多地遇到这种扩展瓶颈。 描述 G1的堆组织为固定大小区域的集合。一个区域通常是一组物理页面,尽管使用大页面(通过 -XX:+UseLargePages)时,多个区域可能组成一个物理页面。 如果指定了+XX:+UseNUMA选项,则在初始化JVM时,区域将平均分布在可用NUMA节点的总数上。 在开始时固定每个区域的NUMA节点有些不灵活,但是可以通过以下增强来缓解。为了为mutator线程分配新的对象,G1可能需要分配一个新的区域。它将通过从NUMA节点中优先选择一个与当前线程绑定的空闲区域来执行此操作,以便将对象保留在新生代的同一NUMA节点上。如果在为变量分配区域的过程中,同一NUMA节点上没有空闲区域,则G1将触发垃圾回收。要评估的另一种想法是,从距离最近的NUMA节点开始,按距离顺序在其他NUMA节点中搜索自由区域。 该特性不会尝试将对象保留在老年代的同一NUMA节点上。 此分配政策中不包括Humongous区。对于这些区,将不做任何特别的事情。 JEP 349: JFR事件流 公开JDK Flight Recorder数据以进行连续监视。 动机 HotSpot VM通过JFR产生的数据点超过500个,但是使用者只能通过解析日志文件的方法使用它们。 用户要想消费这些数据,必须开始一个记录并停止,将内容转储到磁盘上,然后解析记录文件。这对于应用程序分析非常有效,但是监控数据却十分不方便(例如显示动态更新数据的仪表盘)。 与创建记录相关的开销包括: 发出在创建新记录时必须发生的事件 写入事件元数据(例如字段布局) 写入检查点数据(例如堆栈跟踪) 将数据从磁盘存储复制到单独的记录文件 如果有一种方法,可以在不创建新记录文件的情况下,从磁盘存储库中读取正在记录的数据,就可以避免上述开销。 描述 jdk.jfr模块里的jdk.jfr.consumer包,提供了异步订阅事件的功能。用户可以直接从磁盘存储库读取记录数据,也可以直接从磁盘存储流中读取数据,而无需转储记录文件。可以通过注册处理器(例如lambda函数)与流交互,从而对事件的到达进行响应。 下面的例子打印CPU的总体使用率,并持有锁10毫秒。 try (var rs = new RecordingStream()) { rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1)); rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10)); rs.onEvent("jdk.CPULoad", event -> { System.out.println(event.getFloat("machineTotal")); }); rs.onEvent("jdk.JavaMonitorEnter", event -> { System.out.println(event.getClass("monitorClass")); }); rs.start(); } RecordingStream类实现了接口jdk.jfr.consumer.EventStream,该接口提供了一种统一的方式来过滤和使用事件,无论源是实时流还是磁盘上的文件。 public interface EventStream extends AutoCloseable { public static EventStream openRepository(); public static EventStream openRepository(Path directory); public static EventStream openFile(Path file); void setStartTime(Instant startTime); void setEndTime(Instant endTime); void setOrdered(boolean ordered); void setReuse(boolean reuse); void onEvent(Consumer<RecordedEvent> handler); void onEvent(String eventName, Consumer<RecordedEvent handler); void onFlush(Runnable handler); void onClose(Runnable handler); void onError(Runnable handler); void remove(Object handler); void start(); void startAsync(); void awaitTermination(); void awaitTermination(Duration duration); void close(); } 创建流的方法有3种: EventStream::openRepository(Path)从磁盘存储库中构造一个流。这是一种可以直接通过文件系统监视其他进程的方法。磁盘存储库的位置存储在系统属性jdk.jfr.repository中,可以使用API读取到。 EventStream::openRepository()方法执行进程内监控。与RecordingStream不同,它不会开始录制。相反,仅当通过外部方式(例如,使用JCMD或JMX)启动记录时,流才接收事件。 EventStream::openFile(Path)从记录文件中创建流,扩充了已经存在的RecordingFile类。 该接口还可用于设置缓冲的数据量,以及是否应按时间顺序对事件进行排序。为了最大程度地降低分配压力,还可以选择控制是否应为每个事件分配新的事件对象,或者是否可以重用以前的对象。我们可以在当前线程中启动流,也可以异步启动流。 JVM每秒一次将线程本地缓冲区中存储的事件定期刷新到磁盘存储库。 一个单独的线程解析最近的文件,直到写入数据为止,然后将事件推送给订阅者。 为了保持较低的开销,仅从文件中读取活动订阅的事件。 要在刷新完成后收到通知,可以使用EventStream::onFlush(Runnable)方法注册处理程序。 这是在JVM准备下一组事件时将数据聚合或推送到外部系统的机会。 JEP 352: 非易失性映射字节缓冲区 添加新的特定于JDK的文件映射模式,以便可以使用FileChannel API创建引用非易失性内存(non-volatile memory,NVM)的MappedByteBuffer实例。 动机 NVM为应用程序程序员提供了在程序运行过程中创建和更新程序状态的机会,而减少了输出到持久性介质或从持久性介质输入时的成本。这对于事务程序特别重要,在事务程序中,需要定期保持不确定状态以启用崩溃恢复。 现有的C库(例如Intel的libpmem)为C程序提供了对基层NVM的高效访问。他们还以此为基础来支持对各种持久性数据类型的简单管理。当前,由于频繁需要进行系统调用或JNI调用来调用原始操作,从而确保内存更改是持久的,因此即使仅使用Java中的基础库也很昂贵。同样的问题限制了高级库的使用,并且由于C中提供的持久数据类型分配在无法从Java直接访问的内存中这一事实而加剧了这一问题。与C或可以低成本链接到C库的语言相比,这使Java应用程序和中间件(例如Java事务管理器)处于严重的劣势。 该特性试图通过允许映射到ByteBuffer的NVM的有效写回来解决第一个问题。由于Java可以直接访问ByteBuffer映射的内存,因此这可以通过实现与C语言中提供的客户端库等效的客户端库来解决第二个问题,以管理不同持久数据类型的存储。 描述 1. 初步变更 该JEP使用了Java SE API的两个增强功能: 支持implementation-defined的映射模式 MppedByteBuffer::force方法以指定范围 2. 特定于JDK的API更改 通过新模块中的公共API公开新的MapMode枚举值 一个公共扩展枚举ExtendedMapMode将添加到jdk.nio.mapmode程序包: package jdk.nio.mapmode; . . . public class ExtendedMapMode { private ExtendedMapMode() { } public static final MapMode READ_ONLY_SYNC = . . . public static final MapMode READ_WRITE_SYNC = . . . } 在调用FileChannel::map方法创建映射到NVM设备文件上的只读或读写MappedByteBuffer时,可以使用上述的枚举值。如果这些标志在不支持NVM设备文件的平台上传递,程序会抛出UnsupportedOperationException异常。在受支持的平台上,仅当目标FileChannel实例是从通过NVM设备打开的派生文件时,才能传递这些参数。在任何其他情况下,都会抛出IOException异常。 发布BufferPoolMXBean,用于跟踪MappedByteBuffer统计信息 JEP 358: 友好的空指针异常 精确描述哪个变量为null,提高JVM生成的NullPointerException的可用性。 动机 每个Java开发人员都遇到过NullPointerException(NPE)问题。NPE几乎可以出现在程序的任意位置,因此尝试捕获和修复它们是不可能的。下面的代码: a.i = 99; JVM会打印出方法名、文件名和NPE异常的行数: Exception in thread "main" java.lang.NullPointerException at Prog.main(Prog.java:5) 使用这个错误报告,开发人员可以定位到a.i = 99;并推断对象a是null。但是对于更复杂的代码,不使用调试器就无法确定哪个变量为空。假设下面的代码中出现了一个NPE: a.b.c.i = 99; 仅仅使用文件名和行数,并不能精确定位到哪个变量为null,是a、b还是c? 访问数组也会发生类似的问题。假设此代码中出现一个NPE: a[i][j][k] = 99; 文件名和行号不能精确指出哪个数组组件为空。是a还是a[i]或a[i][j]? 一行代码可能包含多个访问路径,每个访问路径都可能是NPE的来源。假设此代码中出现一个NPE: a.i = b.j; 文件名和行号并不能确定哪个对象为空,是a还是b? NPE也可能在方法调用中传递,看下面的代码: x().y().i = 99; 文件名和行号不能指出哪个方法调用返回null。是x()还是y()? 描述 JVM在程序调用空引用的位置抛出NPE异常,通过分析程序的字节码指令,JVM可以精确判断哪个变量为空,并在NPE中描述详细信息(根据源代码)。包含方法名、文件名和行号的null-detail消息将显示在JVM的消息中。 例如a.i = 99;的NPE异常可能是如下格式: Exception in thread "main" java.lang.NullPointerException: Cannot assign field "i" because "a" is null at Prog.main(Prog.java:5) 在更复杂的a.b.c.i = 99;语句中,NPE消息会包含导致空值的完整访问路径: Exception in thread "main" java.lang.NullPointerException: Cannot read field "c" because "a.b" is null at Prog.main(Prog.java:5) 同样,如果数组访问和赋值语句a[i][j][k] = 99;引发NPE: Exception in thread "main" java.lang.NullPointerException: Cannot load from object array because "a[i][j]" is null at Prog.main(Prog.java:5) 类似地,a.i = b.j;会引发NPE: Exception in thread "main" java.lang.NullPointerException: Cannot read field "j" because "b" is null at Prog.main(Prog.java:5) JEP 359: record(预览) 通过record增强Java编程语言。record提供了一种紧凑的语法来声明类,这些类是浅层不可变数据的透明持有者。 动机 我们经常听到这样的抱怨:“Java太冗长”、“Java规则过多”。首当其冲的就是充当简单集合的“数据载体”的类。为了写一个数据类,开发人员必须编写许多低价值、重复且容易出错的代码:构造函数、访问器、equals()、hashCode()和toString()等等。 尽管IDE可以帮助开发人员编写数据载体类的绝大多数编码,但是这些代码仍然冗长。 从表面上看,将Record是为了简化模板编码而生的,但是它还有“远大”的目标:modeling data as data(将数据建模为数据)。record应该更简单、简洁、数据不可变。 描述 record是Java的一种新的类型。同枚举一样,record也是对类的一种限制。record放弃了类通常享有的特性:将API和表示解耦。但是作为回报,record使数据类变得非常简洁。 一个record具有名称和状态描述。状态描述声明了record的组成部分。例如: record Point(int x, int y) { } 因为record在语义上是数据的简单透明持有者,所以记录会自动获取很多标准成员: 状态声明中的每个成员,都有一个 private final的字段; 状态声明中的每个组件的公共读取访问方法,该方法和组件具有相同的名字; 一个公共的构造函数,其签名与状态声明相同; equals和hashCode的实现; toString的实现。 限制 records不能扩展任何类,并且不能声明私有字段以外的实例字段。声明的任何其他字段都必须是静态的。 records类都是隐含的final类,并且不能是抽象类。这些限制使得records的API仅由其状态描述定义,并且以后不能被其他类实现或继承。 在record中额外声明变量 也可以显式声明从状态描述自动派生的任何成员。可以在没有正式参数列表的情况下声明构造函数(这种情况下,假定与状态描述相同),并且在正常构造函数主体正常完成时调用隐式初始化(this.x=x)。这样就可以在显式构造函数中仅执行其参数的验证等逻辑,并省略字段的初始化,例如: record Range(int lo, int hi) { public Range { if (lo > hi) /* referring here to the implicit constructor parameters */ throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi)); } } JEP 361: Switch Expressions (标准) 扩展switch可以使其应用于语句或表达式。扩展switch使其可以用作语句或表达式,以便两种形式都可以使用传统的“case ... :”标签(带有贯穿)或“... ->”标签(不带有贯穿),还有另一个新语句,用于从switch表达式产生值。这些更改将简化日常编码,并为在交换机中使用模式匹配提供了方法。这些功能在JDK 12和JDK 13中是属于预览语言功能,在JDK 14中正式称为标准。 动机 当我们准备增强Java编程语言以支持模式匹配(JEP 305)时,现有switch语句的一些不规则性(长期以来一直困扰着用户)成为了障碍。下面的代码中,众多的break语句使代码变得冗长,这种“视觉噪声”通常掩盖了更多的错误。 switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; } 我们建议引入一种新形式的switch标签“case L ->”,以表示如果匹配标签,则只执行标签右边的代码。switch标签允许在每种情况下使用逗号分隔多个常量。现在可以这样编写以前的代码: switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); } switch标签“case L ->”右侧的代码被限制为表达式、代码块或throw语句。这样局部变量的范围在本块之内,而传统的switch语句局部变量的作用域是整个模块! switch (day) { case MONDAY: case TUESDAY: int temp = ... // The scope of 'temp' continues to the } break; case WEDNESDAY: case THURSDAY: int temp2 = ... // Can't call this variable 'temp' break; default: int temp3 = ... // Can't call this variable 'temp' } 许多现有的switch语句实质上是对switch表达式的模拟,其中每个分支要么分配给一个公共目标变量,要么返回一个值: int numLetters; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: numLetters = 6; break; case TUESDAY: numLetters = 7; break; case THURSDAY: case SATURDAY: numLetters = 8; break; case WEDNESDAY: numLetters = 9; break; default: throw new IllegalStateException("Wat: " + day); } 上面的表述是复杂、重复且容易出错的。代码设计者的意图是为每天计算numLetters。这段代码可以改写成下面这段形式,更加清晰和安全: int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; }; 描述 1. “case L ->”标签 除了传统的“case L :”标签外,还定义了一种更简洁的形式:“case L ->”标签。如果表达式匹配了某个标签,则仅执行箭头右侧的表达式或语句;否则将不执行任何操作。 static void howMany(int k) { switch (k) { case 1 -> System.out.println("one"); case 2 -> System.out.println("two"); default -> System.out.println("many"); } } 下面的代码: howMany(1); howMany(2); howMany(3); 将会打印: one two many 2. Switch表达式 JDK 14扩展了switch语句,使其可以应用于表达式中。例如上述的howMany方法可以重写为如下形式: static void howMany(int k) { System.out.println( switch (k) { case 1 -> "one"; case 2 -> "two"; default -> "many"; } ); } 在通常情况下,switch表达式如下所示: T result = switch (arg) { case L1 -> e1; case L2 -> e2; default -> e3; }; 3. 通过yield产生值 大多数switch表达式在“case L->”标签的右侧都有一个表达式。如果需要一个完整的块,JDK 14引入了一个新的yield语句来产生一个值,该值成为封闭的switch表达式的值。 int j = switch (day) { case MONDAY -> 0; case TUESDAY -> 1; default -> { int k = day.toString().length(); int result = f(k); yield result; } }; JEP 362: 弃用Solaris和SPARC端口 不建议使用Solaris/SPARC,Solaris/x64和Linux/SPARC端口,以在将来的发行版中删除它们。 动机 放弃对这些端口的支持将使OpenJDK社区中的贡献者能够加速新功能的开发,这些新功能将推动平台向前发展。 JEP 363: 移除CMS垃圾收集器 移除CMS(Concurrent Mark Sweep)垃圾收集器。 动机 在两年多以前的JEP 291中,就已经弃用了CMS收集器,并说明会在以后的发行版中删除,以加快其他垃圾收集器的发展。在这段时间里,我们看到了2个新的垃圾收集器ZGC和Shenandoah的诞生,同时对G1的进一步改进。G1自JDK 6开始便成为CMS的继任者。我们希望以后现有的收集器进一步减少对CMS的需求。 描述 此更改将禁用CMS的编译,删除源代码中gc/cms目录的内容,并删除仅与CMS有关的选项。尝试使用命令-XX:+UseConcMarkSweepGC开启CMS会收到以下警告: Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; \ support was removed in <version> VM将使用默认收集器继续执行。 JEP 364: macOS系统上的ZGC(实验) 将ZGC垃圾收集器移植到macOS。 动机 尽管我们希望需要ZGC可伸缩性的用户使用基于Linux的环境,但是在部署应用程序之前,开发人员通常会使用Mac进行本地开发和测试。 还有一些用户希望运行桌面应用程序,例如带有ZGC的IDE。 描述 ZGC的macOS实现由两部分组成: 支持macOS上的多映射内存。 ZGC设计大量使用彩色指针,因此在macOS上我们需要一种将多个虚拟地址(在算法中包含不同颜色)映射到同一物理内存的方法。我们将为此使用mach microkernel mach_vm_remap API。堆的物理内存在单独的地址视图中维护,在概念上类似于文件描述符,但位于(主要是)连续的虚拟地址中。该内存被重新映射到内存的各种ZGC视图中,代表了算法的不同指针颜色。 ZGC支持不连续的内存保留。在Linux上,我们在初始化期间保留16TB的虚拟地址空间。我们假设没有共享库将映射到所需的地址空间。在默认的Linux配置上,这是一个安全的假设。但是在macOS上,ASLR机制会侵入我们的地址空间,因此ZGC必须允许堆保留不连续。假设VM实现使用单个连续的内存预留,则共享的VM代码也必须停止。如此一来,is_in_reserved(),reserved_region()和base()之类的GC API将从CollectedHeap中删除。 JEP 365: Windows系统上的ZGC(实验) 将ZGC垃圾收集器移植到Windows系统上。 描述 ZGC的大多数代码库都是平台无关的,不需要Windows特定的更改。现有的x64负载屏障支持与操作系统无关,也可以在Windows上使用。需要移植的特定于平台的代码与如何保留地址空间以及如何将物理内存映射到保留的地址空间有关。用于内存管理的Windows API与POSIX API不同,并且在某些方面不太灵活。 Windows实现的ZGC需要进行以下工作: 支持多映射内存。 ZGC使用彩色指针需要支持堆多重映射,以便可以从进程地址空间中的多个不同位置访问同一物理内存。在Windows上,分页文件支持的内存为物理内存提供了一个标识(句柄),该标识与映射它的虚拟地址无关。使用此标识,ZGC可以将同一物理内存映射到多个位置。 支持将分页文件支持的内存映射到保留的地址空间。 Windows内存管理API不如POSIX的mmap/munmap灵活,尤其是在将文件支持的内存映射到以前保留的地址空间区域中时。为此,ZGC将使用Windows概念的地址空间占位符。 Windows 10和Windows Server版本1803中引入了占位符概念。不会实现对Windows较早版本的ZGC支持。 支持映射和取消映射堆的任意部分。 ZGC的堆布局与其动态调整堆页面大小(以及重新调整大小)相结合,需要支持映射和取消映射任意堆粒子。此要求与Windows地址空间占位符结合使用时,需要特别注意,因为占位符必须由程序显式拆分/合并,而不是由操作系统自动拆分/合并(如在Linux上)。 支持提交和取消提交堆的任意部分。 ZGC可以在Java程序运行时动态地提交和取消提交物理内存。为了支持这些操作,物理内存将被划分为多个分页文件段并由其支持。每个分页文件段都对应一个ZGC堆粒度,并且可以独立于其他段进行提交和取消提交。 JEP 366: 弃用Parallel Scavenge和Serial Old垃圾收集算法的组合 弃用Parallel Scavenge和Serial Old垃圾收集算法的组合。 动机 有一组GC算法的组合很少使用,但是维护起来却需要巨大的工作量:并行年轻代GC(ParallelScavenge)和串行老年代GC(SerialOld)的组合。用户必须使用-XX:+UseParallelGC -XX:-UseParallelOldGC来启用此组合。 这种组合是畸形的,因为它将并行的年轻代GC算法和串行的老年代GC算法组合在一起使用。我们认为这种组合仅在年轻代很多、老年代很少时才有效果。在这种情况下,由于老年代的体积较小,因此完整的收集暂停时间是可以接受的。但是在生产环境中,这种方式是非常冒险的:年轻代的对象容易导致OutOfMemoryException。此组合的唯一优势是总内存使用量略低。我们认为,这种较小的内存占用优势(最多是Java堆大小的约3%)不足以超过维护此GC组合的成本。 描述 除了弃用选项组合-XX:+UseParallelGC -XX:-UseParallelOldGC外,我们还将弃用选项-XX:UseParallelOldGC,因为它唯一的用途是取消选择并行的旧版GC,从而启用串行旧版GC。 因此,任何对UseParallelOldGC选项的明确使用都会显示弃用警告。 JEP 367: 移除Pack200工具和API 删除java.util.jar软件包中的pack200和unpack200工具以及Pack200 API。这些工具和API在Java SE 11中已经被注明为不推荐,并明确打算在将来的版本中删除它们。 动机 Pack200是JSR 200在Java SE 5.0中引入的一种JAR文件压缩方案。其目标是“减少Java应用程序打包,传输和交付的磁盘和带宽需求”。开发人员使用一对工具pack200和unpack200压缩和解压缩其JAR文件。在java.util.jar包中提供了一个API。 删除Pack200的三个原因: 从历史上看,通过56k调制解调器缓慢下载JDK阻碍了Java的采用。 JDK功能的不断增长导致下载量膨胀,进一步阻碍了采用。使用Pack200压缩JDK是缓解此问题的一种方法。但是,时间已经过去了:下载速度得到了提高,并且JDK 9为Java运行时(JEP 220)和用于构建运行时的模块(JMOD)引入了新的压缩方案。因此,JDK 9和更高版本不依赖Pack200。 JDK 8是在构建时用pack200压缩的最新版本,在安装时用unpack200压缩的最新版本。总之,Pack200的主要使用者(JDK本身)不再需要它。 除了JDK,Pack200还可以压缩客户端应用程序,尤其是applet。某些部署技术(例如Oracle的浏览器插件)会自动解压缩applet JAR。但是,客户端应用程序的格局已经改变,并且大多数浏览器都放弃了对插件的支持。因此,Pack200的主要消费者类别(在浏览器中运行的小程序)不再是将Pack200包含在JDK中的驱动程序。 Pack200是一项复杂而精致的技术。它的文件格式与类文件格式和JAR文件格式紧密相关,二者均以JSR 200所无法预料的方式发展。(例如,JEP 309向类文件格式添加了一种新的常量池条目,并且JEP 238在JAR文件格式中添加了版本控制元数据。)JDK中的实现是在Java和本机代码之间划分的,这使得维护变得很困难。 java.util.jar.Pack200中的API不利于Java SE平台的模块化,从而导致在Java SE 9中删除了其四种方法。总的来说,维护Pack200的成本是巨大的,并且超过了其收益。包括在Java SE和JDK中。 描述 JDK最终针对的JDK功能发行版中将删除以前用@Deprecated(forRemoval = true)注解的java.base模块中的三种类型: java.util.jar.Pack200 java.util.jar.Pack200.Packer java.util.jar.Pack200.Unpacker 包含pack200和unpack200工具的jdk.pack模块先前已使用@Deprecated(forRemoval = true)进行了注解,并且还将在此JEP最终针对的JDK功能版本中将其删除。 JEP 368: Text Blocks(二次预览) Java语言增加文本块功能。文本块是多行字符串文字,能避免大多数转义。 动机 在Java中,HTML, XML, SQL, JSON等字符串对象都很难阅读和维护。 1. HTML 使用one-dimensional的字符串语法: String html = "<html>\n" + " <body>\n" + " <p>Hello, world</p>\n" + " </body>\n" + "</html>\n"; 使用two-dimensional文本块语法: String html = """ <html> <body> <p>Hello, world</p> </body> </html> """; 2. SQL 使用one-dimensional的字符串语法: String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" + "WHERE `CITY` = 'INDIANAPOLIS'\n" + "ORDER BY `EMP_ID`, `LAST_NAME`;\n"; 使用two-dimensional文本块语法: String query = """ SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB` WHERE `CITY` = 'INDIANAPOLIS' ORDER BY `EMP_ID`, `LAST_NAME`; """; 3. 多语言示例 使用one-dimensional的字符串语法: ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); Object obj = engine.eval("function hello() {\n" + " print('\"Hello, world\"');\n" + "}\n" + "\n" + "hello();\n"); 使用two-dimensional文本块语法: ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); Object obj = engine.eval(""" function hello() { print('"Hello, world"'); } hello(); """); 描述 文本块是Java语言的新语法,可以用来表示任何字符串,具有更高的表达能力和更少的复杂度。 文本块的开头定界符是由三个双引号字符(""")组成的序列,后面跟0个或多个空格,最后跟一个行终止符。内容从开头定界符的行终止符之后的第一个字符开始。 结束定界符是三个双引号字符的序列。内容在结束定界符的第一个双引号之前的最后一个字符处结束。 与字符串文字中的字符不同,文本块的内容中可以直接包含双引号字符。允许在文本块中使用\",但不是必需的或不建议使用。 与字符串文字中的字符不同,内容可以直接包含行终止符。允许在文本块中使用\n,但不是必需或不建议使用。例如,文本块: """ line 1 line 2 line 3 """ 等效于字符串文字: "line 1\nline 2\nline 3\n" 或字符串文字的串联: "line 1\n" + "line 2\n" + "line 3\n" JEP 370: 外部存储器API(孵化) 引入一个API,以允许Java程序安全有效地访问Java堆之外的外部内存。 动机 许多Java的库都能访问外部存储,例如Ignite、mapDB、memcached及Netty的ByteBuf API。这样可以: 避免垃圾回收相关成本和不可预测性 跨多个进程共享内存 通过将文件映射到内存中来序列化、反序列化内存内容。 但是Java API却没有提供一个令人满意的访问外部内存的解决方案。 Java 1.4中引入的ByteBuffer API允许创建直接字节缓冲区,这些缓冲区是按堆外分配的,并允许用户直接从Java操作堆外内存。但是,直接缓冲区是有限的。 开发人员可以从Java代码访问外部内存的另一种常见途径是使用sun.misc.Unsafe API。Unsafe有许多公开的内存访问操作(例如Unsafe::getInt和putInt)。使用Unsafe访问内存非常高效:所有内存访问操作都定义为JVM内在函数,因此JIT会定期优化内存访问操作。然而根据定义,Unsafe API是不安全的——它允许访问任何内存位置(例如,Unsafe::getInt需要很长的地址)。如果Java程序了访问某些已释放的内存位置,可能会使JVM崩溃。最重要的是,Unsafe API不是受支持的Java API,并且强烈建议不要使用它。 虽然也可以使用JNI访问内存,但是与该解决方案相关的固有成本使其在实践中很少适用。整个开发流程很复杂,因为JNI要求开发人员编写和维护C代码段。 JNI本质上也很慢,因为每次访问都需要Java到native的转换。 在访问外部内存时,开发人员面临一个难题:应该使用安全但受限(可能效率较低)的方法(例如ByteBuffer),还是应该放弃安全保证并接受不受支持和危险的Unsafe API? 该JEP引入了受支持的,安全且有效的外部内存访问API。并且设计时就充分考虑了JIT优化。 描述 外部存储器访问API引入了三个主要的抽象:MemorySegment,MemoryAddress和MemoryLayout。 MemorySegment用于对具有给定空间和时间范围的连续内存区域进行建模。可以将MemoryAddress视为段内的偏移量,MemoryLayout是内存段内容的程序描述。 可以从多种来源创建内存段,例如本机内存缓冲区,Java数组和字节缓冲区(直接或基于堆)。例如,可以如下创建本机内存段: try (MemorySegment segment = MemorySegment.allocateNative(100)) { ... } 上述代码将创建大小为100字节的,与本机内存缓冲区关联的内存段。 内存段在空间上受限制;任何试图使用该段来访问这些界限之外的内存的尝试都会导致异常。正如使用try-with-resource构造所证明的那样,片段在时间上也是有界的。也就是说,它们已创建,使用并在不再使用时关闭。关闭段始终是一个显式操作,并且可能导致其他副作用,例如与该段关联的内存的重新分配。任何访问已关闭的内存段的尝试都将导致异常。空间和时间安全性检查对于确保内存访问API的安全性至关重要。 通过获取内存访问var句柄可以取消引用与段关联的内存。这些特殊的var句柄具有至少一个强制访问坐标,类型为MemoryAddress,即发生取消引用的地址。它们是使用MemoryHandles类中的工厂方法获得的。要设置本机段的元素,我们可以使用如下所示的内存访问var句柄: VarHandle intHandle = MemoryHandles.varHandle(int.class); try (MemorySegment segment = MemorySegment.allocateNative(100)) { MemoryAddress base = segment.baseAddress(); for (int i = 0 ; i < 25 ; i++) { intHandle.set(base.offset(i * 4), i); } } 参考引用 本文同步至: https://waylau.com/jdk-14-released/ https://openjdk.java.net/projects/jdk/14/
在前文,我们介绍来了分布式事务,以及分布式事务的解决方案之一的二阶段提交。本文介绍分布式事务处理方案之一的三阶段提交协议。 分布式事务 分布式事务是指发生在多个数据节点之间的事务,分布式事务比单机事务要复杂的多。在分布式系统中,各个节点之间在是相互独立的,需要通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确地知道其他节点的事务执行情况。所以从理论上来讲,两个节点的数据是无法达到一致的状态。如果想让分布式部署的多个节点中的数据保持一致性,那么就要保证在所有节点数据的写操作,要么全部都执行,要么全部都不执行。但是,一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果,所以它也就不知道本次事务到底应该commit还是rollback。所以,常规的解决办法就是引入一个"协调者"的组件来统一调度所有分布式节点的执行。 为了解决这种分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two Phase Commitment Protocol)、三阶提交协议(Three Phase Commitment Protocol)和Paxos算法。针对分布式事务,是X/Open 这个组织定义的一套分布式事务的标准X/Open DTP(X/Open Distributed Transaction Processing ReferenceModel),定义了规范和API接口,可以由各个厂商进行具体的实现。 大部分的关系型数据库通过两阶段提交(Two Phase Commit,2PC)算法来完成分布式事务,比如Oracle中通过dblink方式进行事务处理。下面重点介绍下3PC算法。 下面重点介绍下三阶提交协议算法。 三阶段提交概述 三阶段提交协议可以理解为两阶段提交协议的改良版,是在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段分成了两步: 询问,然后再锁资源,最后真正提交。 两阶段提交协议最早是分布式事务的专家Jim Gray在1978年的一篇文章Notes on Database Operating Systems中提及。两阶段提交协议可以保证数据的强一致性,即保证了分布式事务的原子性:所有结点要么全做要么全不做。许多分布式关系型数据管理系统采用此协议来完成分布式事务。它是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。同时也是解决一致性问题的算法。该算法能够解决很多的临时性系统故障(包括进程、网络节点、通信等故障),被广泛地使用。但是,它并不能够通过配置来解决所有的故障,在某些情况下它还需要人为的参与才能解决问题。两阶段提交协议存在的问题是,协调者在某些时刻如果失败了, 整个事务就会阻塞。于是Skeen发布了"NonBlocking Commit Protocols" (1981)这篇论文,论文指出在一个分布式的事务里面, 需要一个三阶段的提交协议来避免在两阶段提交中存在的阻塞问题。 顾名思义,三阶段提交分为以下三个阶段: CanCommit PreCommit DoCommit 在三阶段提交协议中,系统一般包含两类角色: 协调者(Coordinator),通常一个系统中只有一个; 参与者(Participant),一般包含多个,在数据存储系统中可以理解为数据副本的个数。 CanCommit 在CanCommit阶段,协调者协议流程如下: 写本地日志“BEGIN_COMMIT”,并进入WAIT状态; 向所有参与者发送“VOTE_REQUEST”消息; 等待并接收参与者发送的对“VOTE_REQUEST”的响应。参与者响应“VOTE_ABORT”或“VOTE_COMMIT”消息给协调者。 该流程与两阶段提交协议类似。 PreCommit 在PreCommit阶段,,协调者将通知事务参与者准备提交或取消事务,写本地的redo和undo日志,但不提交。 协调者协议流程如下: 若收到任何一个参与者发送的“VOTE_ABORT”消息; 写本地“GLOBAL_ABORT”日志,进入ABORT状态; 向所有的参与者发送“GLOBAL_ABORT”消息; 若收到所有参与者发送的“VOTE_COMMIT”消息; 写本地“PREPARE_COMMIT”日志,进入PRECOMMIT状态; 向所有的参与者发送“PREPARE _COMMIT”消息; 等待并接收参与者发送的对“GLOBAL_ABORT”消息或“PREPARE_COMMIT”消息的确认响应消息。一旦收到所有参与者的“GLOBAL_ABORT”确认消息或者超时没有收到,写本地“END_TRANSACTION”日志流程结束,则不再进入DoCommit阶段。如果收到所有参与者的“PREPARE_COMMIT”确认消息,则进入DoCommit阶段。 该流程与两阶段提交协议相比,多了一个PRECOMMIT状态。 DoCommit 在该阶段, 协调者协议流程如下: 向所有参与者发送的“GLOBAL _COMMIT”消息; 等待并接收参与者发送的对 “GLOBAL_COMMIT”消息的确认响应消息,一旦收到所有参与者的确认消息,写本地“END_TRANSACTION”日志流程结束。 在DoCommit阶段,如果参与者无法及时接收到来自协调者的GLOBAL_COMMIT请求时,会在等待超时之后,会继续进行事务的提交。 三阶段提交状态机 下图为三阶段提交协议中的协调者及参与者的状态机。左侧a为协调者状态机;右侧b为参与者状态机。 三阶段提交的缺陷 相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。 参考引用 本文同步至: https://waylau.com/three-phase-commitment-protocol/ 分布式事务——两阶段提交: https://waylau.com/two-phase-commitment-protocol/ Distributed systems: principles and paradigms Notes on Database Operating Systems NonBlocking Commit Protocols 分布式系统常用技术及案例分析(第二版):https://github.com/waylau/distributed-systems-technologies-and-cases-analysis
在分布式系统中,为了保证数据的高可用,通常会将数据保留多个副本(replica), 这些副本会放置在不同的节点上。这些数据节点可能是物理机器,也可能是虚拟机。为了对用户提供正确的CURD等语意,我们需要保证这些放置在不同节点上的副本是一致的,这就涉及分布式事务的问题。 本文介绍分布式事务处理方案之一的两阶段提交协议。 分布式事务 分布式事务是指发生在多个数据节点之间的事务,分布式事务比单机事务要复杂的多。在分布式系统中,各个节点之间在是相互独立的,需要通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确地知道其他节点的事务执行情况。所以从理论上来讲,两个节点的数据是无法达到一致的状态。如果想让分布式部署的多个节点中的数据保持一致性,那么就要保证在所有节点数据的写操作,要么全部都执行,要么全部都不执行。但是,一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果,所以它也就不知道本次事务到底应该commit还是rollback。所以,常规的解决办法就是引入一个"协调者"的组件来统一调度所有分布式节点的执行。 为了解决这种分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two Phase Commitment Protocol)、三阶提交协议(Three Phase Commitment Protocol)和Paxos算法。针对分布式事务,是X/Open 这个组织定义的一套分布式事务的标准X/Open DTP(X/Open Distributed Transaction Processing ReferenceModel),定义了规范和API接口,可以由各个厂商进行具体的实现。 大部分的关系型数据库通过两阶段提交(Two Phase Commit,2PC)算法来完成分布式事务,比如Oracle中通过dblink方式进行事务处理。下面重点介绍下2PC算法。 两阶段提交概述 两阶段提交协议最早是分布式事务的专家Jim Gray在1978年的一篇文章Notes on Database Operating Systems中提及。两阶段提交协议可以保证数据的强一致性,即保证了分布式事务的原子性:所有结点要么全做要么全不做。许多分布式关系型数据管理系统采用此协议来完成分布式事务。它是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。同时也是解决一致性问题的算法。该算法能够解决很多的临时性系统故障(包括进程、网络节点、通信等故障),被广泛地使用。但是,它并不能够通过配置来解决所有的故障,在某些情况下它还需要人为的参与才能解决问题。 顾名思义,两阶段提交分为以下两个阶段: 准备阶段(Prepare Phase) 提交阶段(Commit Phase) 在两阶段提交协议中,系统一般包含两类角色: 协调者(Coordinator),通常一个系统中只有一个; 参与者(Participant),一般包含多个,在数据存储系统中可以理解为数据副本的个数。 准备阶段 在准备阶段,协调者将通知事务参与者准备提交或取消事务,写本地的redo和undo日志,但不提交,然后进入表决过程。在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。 协调者协议流程如下: 写本地日志“BEGIN_COMMIT”,并进入WAIT状态; 向所有参与者发送“VOTE_REQUEST”消息; 等待并接收参与者发送的对“VOTE_REQUEST”的响应。参与者响应“VOTE_ABORT”或“VOTE_COMMIT”消息给协调者。 提交阶段 在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。参与者在接收到协调者发来的消息后将执行响应的操作。 协调者协议流程如下: 若收到任何一个参与者发送的“VOTE_ABORT”消息; 写本地“GLOBAL_ABORT”日志,进入ABORT状态; 向所有的参与者发送“GLOBAL_ABORT”消息; 若收到所有参与者发送的“VOTE_COMMIT”消息; 写本地“GLOBAL_COMMIT”日志,进入COMMIT状态; 向所有的参与者发送“GLOBAL_COMMIT”消息; 等待并接收参与者发送的对“GLOBAL_ABORT”消息或“GLOBAL_COMMIT”消息的确认响应消息,一旦收到所有参与者的确认消息,写本地“END_TRANSACTION”日志流程结束。 两阶段提交状态机 下图为两阶段提交协议中的协调者及参与者的状态机。左侧a为协调者状态机;右侧b为参与者状态机。 实际案例 MySQL从5.5版本开始支持,SQL Server 2005开始支持,Oracle 7开始支持。 两阶段提交的缺陷 一般情况下,两阶段提交机制都能较好的运行,当在事务进行过程中,有参与者宕机时,重启以后,可以通过询问其他参与者或者协调者,从而知道这个事务到底提交了没有。当然,这一切的前提都是各个参与者在进行每一步操作时,都会事先写入日志。 两阶段提交不能解决的困境如下: 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。 单点故障。由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题) 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。 参考引用 本文同步至: https://waylau.com/two-phase-commitment-protocol/ Distributed systems: principles and paradigms Notes on Database Operating Systems 分布式系统常用技术及案例分析:https://github.com/waylau/distributed-systems-technologies-and-cases-analysis
Angular CLI 是 Angular 客户端命令行工具,提供非常多的命令来简化 Angular 的开发。今天执行“ng serve”命令时,竟然报找不到模块"@angular-devkit/build-angular"的错误。 问题背景 执行“ng serve”命令时,竟然报找不到模块"@angular-devkit/build-angular"的错误。信息如下: >ng serve An unhandled exception occurred: Could not find module "@angular-devkit/build-angular" from "D:\\workspaceGithub\\mean-news-ui\\mean-news-ui". See "C:\Users\LYF\AppData\Local\Temp\ng-FStMRr\angular-errors.log" for further details. 解决 怀疑是 Angular CLI 与 Angular 应用版本不匹配或者是本地环境有错误引起的。 解决的方案就是卸载 Angular CLI再重新安装,错误就没有了。 1. 卸载老版本 Angular CLI 卸载老版本 Angular CLI,命令如下: >npm uninstall -g @angular/cli removed 244 packages in 20.263s 2. 验证卸载 执行 Angular CLI验证是否已经卸载成功,命令如下: >ng 'ng' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 3. 清理缓存(可选) 建议清理下缓存,该步骤是可选的: >npm cache clean --force npm WARN using --force I sure hope you know what you are doing. 4. 安装新版本 Angular CLI 安装新版本 Angular CLI,命令如下: >npm install -g @angular/cli C:\Users\LYF\AppData\Roaming\npm\ng -> C:\Users\LYF\AppData\Roaming\npm\node_modules\@angular\cli\bin\ng > @angular/cli@8.3.12 postinstall C:\Users\LYF\AppData\Roaming\npm\node_modules\@angular\cli > node ./bin/postinstall/script.js + @angular/cli@8.3.12 added 244 packages from 185 contributors in 63.738s 参考引用 本文同步至: https://waylau.com/angular-could-not-find-module-build-angular/ 完整源码:https://github.com/waylau/angular-enterprise-application-development-samples Angular CLI 常用命令:https://waylau.com/angular-cli-commands/
近期 Java 界好消息频传。先是 Java 13 发布,接着 Eclipse 也发布了新版本表示支持新版本的Java 特性。本文介绍了 Java 13 的新特性并展示了相关的示例。 2019年9月17日,Java 13 正式发布。特性如下。 Java 13 新特性 此版本带来了以下几大新特性: JEP 350,Dynamic CDS Archives:扩展应用程序类-数据共享,以允许在 Java 应用程序执行结束时动态归档类。归档类将包括默认的基础层 CDS(class data-sharing)存档中不存在的所有已加载的应用程序类和库类。 JEP 351,ZGC: Uncommit Unused Memory:增强 ZGC 以将未使用的堆内存返回给操作系统。 JEP 353,Reimplement the Legacy Socket API:使用易于维护和调试的更简单、更现代的实现替换 java.net.Socket 和 java.net.ServerSocket API 使用的底层实现。 JEP 354,Switch Expressions (Preview):可在生产环境中使用的 switch 表达式,JDK 13 中将带来一个 beta 版本实现。switch 表达式扩展了 switch 语句,使其不仅可以作为语句(statement),还可以作为表达式(expression),并且两种写法都可以使用传统的 switch 语法,或者使用简化的“case L ->”模式匹配语法作用于不同范围并控制执行流。这些更改将简化日常编码工作,并为 switch 中的模式匹配(JEP 305)做好准备。 JEP 355,Text Blocks (Preview):将文本块添加到 Java 语言。文本块是一个多行字符串文字,它避免了对大多数转义序列的需要,以可预测的方式自动格式化字符串,并在需要时让开发人员控制格式。 安装 JDK 13 JDK 13下载地址为 https://www.oracle.com/technetwork/java/javase/downloads/index.html。 以Windows环境为例,可通过jdk-13_windows-x64_bin.exe或jdk-13_windows-x64_bin.zip来进行安装。 .exe文件的安装方式较为简单,按照界面提示点击“下一步”即可。 下面演示.zip安装方式。 1. 解压.zip文件到指定位置 将jdk-13_windows-x64_bin.zip文件解压到指定的目录下即可。比如,本例子放置在了D:\Program Files\jdk-13位置。 2. 设置环境变量 创建系统变量“JAVA_HOME”,其值指向了JDK的安装目录。 在用户变量“Path”中,增加“%JAVA_HOME%bin”。 注:JDK13已经无需再安装JRE,设置环境变量时也不用设置CLASSPATH了。 3. 验证安装 执行“java -version”命令进行安装的验证: $ java -version java version "13" 2019-09-17 Java(TM) SE Runtime Environment (build 13+33) Java HotSpot(TM) 64-Bit Server VM (build 13+33, mixed mode, sharing) 如果现实上述信息,则说明JDK已经安装完成。 如果显示的内容还是安装前的老JDK版本,则可按照如下步骤解决。 首先,卸载老版本的JDK 其次,在命令行输入如下指令来设置JAVA_HOM和Path: >SET JAVA_HOME=D:\Program Files\jdk-13 >SET Path=%JAVA_HOME%\bin Eclipse IDE 2019-09 在 Java 13 发布两天后的2019年9月19日,Eclipse IDE 2019-09 发布。Eclipse IDE 2019-09 声称支持Java 13。接下里将演示如何使用Eclipse IDE 2019-09编写 Java 13 的示例。 Eclipse IDE 2019-09 下载地址为 https://www.eclipse.org/downloads/packages/。本例使用的是Eclipse 4.14版本。 编写 Java 13 示例 实战1:Switch表达式的例子 下面是原有的Switch表达式的写法: switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; } 在Java 12中,Switch表达式可以改为如下写法: switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); } 还能支持在表达式中返回值: int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; }; 在Java 13中,Switch表达式可以改为如下写法: int date = switch (day) { case MONDAY, FRIDAY, SUNDAY : yield 6; case TUESDAY : yield 7; case THURSDAY, SATURDAY : yield 8; case WEDNESDAY : yield 9; default : yield 1; // default条件是必须的 }; System.out.println(date); 需要注意的是,在使用yield时,必须要有default条件。 实战2:文本块 自Java 13开始,支持文本块(Text Blocks)。 以下是Java 13之前的文本块的处理方式的示例: String html = "<html>\n" + " <body>\n" + " <p>Hello, world</p>\n" + " </body>\n" + "</html>\n"; System.out.println(html); 在上述示例中,由于文本块需要换行,所以产生了很多本文的拼接和转义。 以下是Java 13中的文本块示例: String html2 = """ <html> <body> <p>Hello, world</p> </body> </html> """; System.out.println(html2); 在上述示例中,对于文本块的处理变得简洁、自然。 以上两个示例在控制台输出内容都是一样的,效果如下: <html> <body> <p>Hello, world</p> </body> </html> 更多Java示例,可见“现代Java案例大全”。 参考引用 本文同步至: https://waylau.com/java-13-new-features-and-samples/ 完整源码:https://github.com/waylau/modern-java-demos http://openjdk.java.net/projects/jdk/13
本文介绍了如何从 Angular 中的 URL 获取查询参数。 通过注入ActivatedRoute的实例,可以订阅各种可观察对象,包括queryParams和params observable。以下是范例: import { ActivatedRoute } from '@angular/router'; // 用于获取路由参数 import { Component, OnInit } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; // 用于HTML过滤 import { Location } from '@angular/common'; // 用于回退浏览记录 import { NewsDetailService } from '../news-detail.service'; @Component({ selector: 'app-news-detail', templateUrl: './news-detail.component.html', styleUrls: ['./news-detail.component.css'] }) export class NewsDetailComponent implements OnInit { newsDetailData = null; newsUrl = null; constructor(private newsDetailService: NewsDetailService, private domSanitizer: DomSanitizer, private route: ActivatedRoute, private location: Location) { } ngOnInit() { this.showNewsDetailData(); } // 展示新闻详情数据 showNewsDetailData() { this.route.queryParams.subscribe(p => { this.newsUrl = p.newsUrl // 获取参数 this.newsDetailService.getNewsData(this.newsUrl).subscribe( (newsApiData) => this.newsDetailData = this.domSanitizer.bypassSecurityTrustHtml(newsApiData.toString()) //HTML过滤 ); }); } // 返回 goback() { // 浏览器回退浏览记录 this.location.back(); } } 参考引用 本文同步至: https://waylau.com/get-query-params-from-url-in-angular/ 完整源码:https://github.com/waylau/angular-enterprise-application-development-samples 《Angular企业级应用开发实战》
当往MongoDB中插入一条数据时,会自动生成ObjectId作为数据的主键。那么如何通过ObjectId来做数据的唯一查询呢? 在MongoDB中插入一条数据 在MongoDB中插入一条如下结构的数据: { _id: 5d6a32389c825e24106624e4, title: 'GitHub 上有什么好玩的项目', content: '上个月有水友私信问我,GitHub 上有没有比较好玩的项目可以推荐?我跟他说:"有,过两天我整理一下"。\n' + '\n' + '然而,一个月过去了,我把这件事情忘了精光,直至他昨天提醒我才记起2_05.png。\n', creation: 2019-08-31T08:39:20.384Z } 其中,上述_id的值“5d6a32389c825e24106624e4”,是MongoDB自动分配的。 使用 MongoDB 的 ObjectId 作为查询条件 须知,_id的值“5d6a32389c825e24106624e4”并非是字符串,而是ObjectId对象类型。因此,如下查询是行不通的: // 查询指定文档 const findNews = function (db, newsId, callback) { // 获取集合 const news = db.collection('news'); // 查询指定文档 news.findOne({_id: newsId},function (err, result) { if (err) { console.error('error end: ' + err.stack); return; } console.log("查询指定文档,响应结果是:"); console.log(result); callback(result); }); } 需将上述newsId转为 ObjectId对象类型。怎么做呢?做法参考如下: const ObjectId = require('mongodb').ObjectId; // 查询指定文档 const findNews = function (db, newsId, callback) { // 获取集合 const news = db.collection('news'); // 查询指定文档 news.findOne({_id: ObjectId(newsId)},function (err, result) { if (err) { console.error('error end: ' + err.stack); return; } console.log("查询指定文档,响应结果是:"); console.log(result); callback(result); }); } 其中,require('mongodb').ObjectId用于获取ObjectId类,并将字符串newsId转为了 ObjectId 类型。 参考引用 本文同步至: https://waylau.com/node.js-mongodb-objectid/ 完整源码:https://github.com/waylau/mean-book-samples
mysql模块(项目地址为https://github.com/mysqljs/mysql)是一个开源的、JavaScript编写的MySQL驱动,可以在Node.js应用中来操作MySQL。但在使用过程中,出现了“ER_NOT_SUPPORTED_AUTH_MODE”问题。 本文介绍了出现该问题的原因及解决方案。 报错信息 当我试图使用mysql模块来连接MySQL 8时,出现了如下错误信息: D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\index.js:17 throw error; ^ Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client at Handshake.Sequence._packetToError (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\sequences\Sequence.js:47:14) at Handshake.ErrorPacket (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\sequences\Handshake.js:123:18) at Protocol._parsePacket (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Protocol.js:291:23) at Parser._parsePacket (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Parser.js:433:10) at Parser.write (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Parser.js:43:10) at Protocol.write (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Protocol.js:38:16) at Socket.<anonymous> (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\Connection.js:91:28) at Socket.<anonymous> (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\Connection.js:525:10) at Socket.emit (events.js:196:13) at addChunk (_stream_readable.js:290:12) -------------------- at Protocol._enqueue (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Protocol.js:144:48) at Protocol.handshake (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Protocol.js:51:23) at Connection.connect (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\Connection.js:119:18) at Object.<anonymous> (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\index.js:12:12) at Module._compile (internal/modules/cjs/loader.js:759:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10) at Module.load (internal/modules/cjs/loader.js:628:32) at Function.Module._load (internal/modules/cjs/loader.js:555:12) at Function.Module.runMain (internal/modules/cjs/loader.js:826:10) at internal/main/run_main_module.js:17:11 出错原因 导致这个错误的原因是,目前,最新的mysql模块并未完全支持MySQL 8的“caching_sha2_password”加密方式,而“caching_sha2_password”在MySQL 8中是默认的加密方式。因此,下面的方式命令是默认已经使用了“caching_sha2_password”加密方式,该账号、密码无法在mysql模块中使用。 mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '123456'; Query OK, 0 rows affected (0.12 sec) 解决方法 解决方法是从新修改用户root的密码,并指定mysql模块能够支持的加密方式: mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; Query OK, 0 rows affected (0.12 sec) 上述语句,显示指定了使用“mysql_native_password”的加密方式。这种方式是在mysql模块能够支持。 再此运行应用,可以看到如下的控制台输出信息: $ node index.js The result is: RowDataPacket { user_id: 1, username: '老卫' } 其中,“RowDataPacket { user_id: 1, username: '老卫' }”就是数据库查询的结果。 源码 本节例子可以在https://github.com/waylau/nodejs-book-samples的“mysql-demo”应用中找到。 参考引用 本文同步至: https://waylau.com/node.js-mysql-client-does-not-support-authentication-protocol/ 有关MySQL 8的“caching_sha2_password”加密方式,可见https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password
对于聊天室,大家应该都不陌生,笔者也写过很多关于聊天室的例子。 本节,我们将演示如何通过Node.js来实现一个WebSocket聊天服务器的例子。 使用ws创建WebSokcet服务器 Node.js原生API并未提供WebSocket的支持,因此,需要安装第三方包才能使用WebSocket功能。对于WebSocket的支持,在开源社区有非常多的选择,本例子采用的是“ws”框架(项目主页为https://github.com/websockets/ws)。 “ws”顾名思义是一个用于支持WebSocket客户端和服务器的框架。它易于使用,功能强大,且不依赖于其他环境。 想其他Node.js应用一样,使用ws的首选方式是使用npm来管理。以下命令行用于安装ws在应用里面: npm install ws 具备了ws包之后,就可以创建WebSocket服务器了。以下是创建服务器的j简单示例: const WebSocket = require('ws'); const server = new WebSocket.Server({ port: 8080 }); 上述例子服务器启动在8080端口。 聊天服务器的需求 聊天服务器的业务需求比较简单,是一个群聊聊天室。换言之,所有人发送的消息大家都可以见到。 当有新用户连接到服务器时,会以该用户的“IP+端口”作为用户的名称。 服务器的实现 根据前面知识的学习,实现一个聊天服务器比较简单,完整代码如下: const WebSocket = require('ws'); const server = new WebSocket.Server({ port: 8080 }); server.on('open', function open() { console.log('connected'); }); server.on('close', function close() { console.log('disconnected'); }); server.on('connection', function connection(ws, req) { const ip = req.connection.remoteAddress; const port = req.connection.remotePort; const clientName = ip + port; console.log('%s is connected', clientName) // 发送欢迎信息给客户端 ws.send("Welcome " + clientName); ws.on('message', function incoming(message) { console.log('received: %s from %s', message, clientName); // 广播消息给所有客户端 server.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { client.send( clientName + " -> " + message); } }); }); }); 当客户端给服务器发送消息时,服务器会将该客户端的消息转发给所有客户端。 客户端的实现 客户端是通HTML+JavaScript的方式实现的。由于浏览器原生提供了WebSocket的API,所以并不需要ws框架的支持。 客户端client.html文件代码如下: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>WebSocket Chat</title> </head> <body> <script type="text/javascript"> var socket; if (!window.WebSocket) { window.WebSocket = window.MozWebSocket; } if (window.WebSocket) { socket = new WebSocket("ws://localhost:8080/ws"); socket.onmessage = function (event) { var ta = document.getElementById('responseText'); ta.value = ta.value + '\n' + event.data }; socket.onopen = function (event) { var ta = document.getElementById('responseText'); ta.value = "连接开启!"; }; socket.onclose = function (event) { var ta = document.getElementById('responseText'); ta.value = ta.value + "连接被关闭"; }; } else { alert("你的浏览器不支持 WebSocket!"); } function send(message) { if (!window.WebSocket) { return; } if (socket.readyState == WebSocket.OPEN) { socket.send(message); } else { alert("连接没有开启."); } } </script> <form onsubmit="return false;"> <h3>WebSocket 聊天室:</h3> <textarea id="responseText" style="width: 500px; height: 300px;"></textarea> <br> <input type="text" name="message" style="width: 300px" value="Welcome to waylau.com"> <input type="button" value="发送消息" onclick="send(this.form.message.value)"> <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空聊天记录"> </form> <br> <br> <a href="https://waylau.com/">更多例子请访问 waylau.com</a> </body> </html> 运行应用 首先启动服务器。执行下面的命令: node index.js 接着用浏览器直接打开client.html文件,可以看到如下的聊天界面。 打开多个聊天窗口,就能模拟多个用户之间的群聊了。 源码 本节例子可以在https://github.com/waylau/nodejs-book-samples的“ws-demo”应用中找到。 本节例子可以在“ws-demo”应用中找到。 参考引用 本文同步至: https://waylau.com/node.js-websocket-chat/ MINA 实现聊天功能: https://waylau.com/mina-chat/ Netty 实现 WebSocket 聊天功能: https://waylau.com/netty-websocket-chat/ Netty 实现聊天功能: https://waylau.com/netty-chat/ WebSocket 和 Golang 实现聊天功能: https://waylau.com/go-websocket-chat/
12-Factor(twelve-factor),也称为“十二要素”,是一套流行的应用程序开发原则。Cloud Native架构中使用12-Factor作为设计准则。 12-Factor 的目标在于: 使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入项目中。 和底层操作系统之间尽可能的划清界限,在各个系统中提供最大的可移植性。 适合部署在现代的云计算平台,从而在服务器和系统管理方面节省资源。 将开发环境和生产环境的差异降至最低,并使用持续交付实施敏捷开发。 可以在工具、架构和开发流程不发生明显变化的前提下实现扩展。 12-Factor 可以适用于任意语言和后端服务(数据库、消息队列、缓存等)开发的应用程序,自然也适用于 Cloud Native。在构建 Cloud Native 应用时,也需要考虑这十二个方面的内容。 1 基准代码 代码是程序的根本,有什么样的代码最终会表现为怎么样的程序软件。从源码到产品发布中间会经历多个环节,比如开发、编译、测试、构建、部署等,这些环节可能都有自己的不同的部署环境,而不同的环境相应的责任人关注于产品的不同阶段。比如,测试人员主要关注于测试的结果,而业务人员可能关注于生产环境的最终的部署结果。但不管是哪个环节,部署到怎么的环境中,他们所依赖的代码是一致的,即所谓的“一份基准代码(Codebase),多份部署(Deploy)”。 现代的代码管理,往往需要进行版本的管理。即便是个人的工作,采用版本管理工具进行管理,对于方便查找特定版本的内容,或者是回溯历史的修改内容都是极其必要。版本控制系统就是帮助人们协调工作的工具,它能够帮助我们和其他小组成员监测同一组文件,比如说软件源代码,升级过程中所做的变更,也就是说,它可以帮助我们轻松地将工作进行融合。 版本控制工具发展到现在已经有几十年了,简单地可以将其分为四代: 文件式版本控制系统,比如 SCCS、RCS; 树状版本控制系统—服务器模式,比如 CVS; 树状版本控制系统—双服务器模式,比如 Subversion; 树状版本控制系统—分布式模式,比如 Bazaar、Mercurial、Git。 目前,在企业中广泛采用服务器模式的版本控制系统,但越来越多的企业开始倾向于采用分布式模式版本控制系统。 读者如果对版本控制系统感兴趣,可以参阅笔者所著的《分布式系统常用技术及案例分析》中的“第7章分布式版本控制系统”内容。本书“10.3 代码管理”章节部分,还会继续深入探讨 Git 的使用。 2 依赖 应该明确声明应用程序依赖关系(Dpendency),这样,所有的依赖关系都可以从工件的存储库中获得,并且可以使用依赖管理器(例如 Apache Maven、Gradle)进行下载。 显式声明依赖的优点之一是为新进开发者简化了环境配置流程。新进开发者可以检出应用程序的基准代码,安装编程语言环境和它对应的依赖管理工具,只需通过一个构建命令来安装所有的依赖项,即可开始工作。 比如,项目组统一采用 Gradle 来进行依赖管理。那么可以使用 Gradle Wrapper。Gradle Wrapper 免去了用户在使用 Gradle 进行项目构建时需要安装 Gradle 的繁琐步骤。每个 Gradle Wrapper 都绑定到一个特定版本的 Gradle,所以当你第一次在给定 Gradle 版本下运行上面的命令之一时,它将下载相应的 Gradle 发布包,并使用它来执行构建。默认,Gradle Wrapper 的发布包是指向的官网的 Web 服务地址,相关配置记录在了 gradle-wrapper.properties 文件中。我们查看下 Sring Boot 提供的这个 Gradle Wrapper 的配置,参数“distributionUrl”就是用于指定发布包的位置。 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip 而这个 gradle-wrapper.properties 文件是作为依赖项,而纳入代码存储库中的。 3 配置 相同的应用,在不同的部署环境(如预发布、生产环境、开发环境等等)下,可能有不同的配置内容。这其中包括: 数据库、Redis 以及其他后端服务的配置; 第三方服务的证书; 每份部署特有的配置,如域名等。 这些配置项不可能硬编码在代码中,因为我们必须要保证同一份基准代码(Codebase)能够多份部署。一种解决方法是使用配置文件,但不把它们纳入版本控制系统,就像 Rails 的 config/database.yml。这相对于在代码中硬编码常量已经是长足进步,但仍然有缺点: 不小心将配置文件签入了代码库; 配置文件的可能会分散在不同的目录,并有着不同的格式,不方便统一管理; 这些格式通常是语言或框架特定的,不具备通用性。 所以,推荐的做法是将应用的配置存储于环境变量中。好处在于: 环境变量可以非常方便地在不同的部署间做修改,却不动一行代码; 与配置文件不同,不小心把它们签入代码库的概率微乎其微; 与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,环境变量与语言和系统无关。 本书介绍了另外一种解决方案——集中化配置中心。通过配置中心来集中化管理各个环境的配置变量。配置中心的实现也是于具体语言和系统无关的。欲了解有关配置中心的内容,可以参阅本书“10.5 配置管理”章节的内容。 4 后端服务 后端服务(Backing Services)是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL,CouchDB),消息/队列系统(RabbitMQ,Beanstalkd),SMTP 邮件发送服务(Postfix),以及缓存系统(Memcached,Redis)。 这些后端服务,通常由部署应用程序的系统管理员一起管理。除了本地服务之外,应用程序有可能使用了第三方发布和管理的服务。示例包括 SMTP(例如 Postmark),数据收集服务(例如 New Relic 或 Loggly),数据存储服务(如 Amazon S3),以及使用 API 访问的服务(例如 Twitter、Google Maps 等等)。 12-Factor 应用不会区别对待本地或第三方服务。对应用程序而言,本地或第三方服务都是附加资源,都可以通过一个 URI 或是其他存储在配置中的服务定位或服务证书来获取数据。12-Factor 应用的任意部署,都应该可以在不进行任何代码改动的情况下,将本地 MySQL 数据库换成第三方服务(例如 Amazon RDS)。类似的,本地 SMTP 服务应该也可以和第三方 SMTP 服务(例如 Postmark )互换。比如,在上述两个例子中,仅需修改配置中的资源地址。 每个不同的后端服务都是一份资源 。例如,一个 MySQL 数据库是一个资源,两个 MySQL 数据库(用来数据分区)就被当作是两个不同的资源。12-Factor 应用将这些数据库都视作附加资源,这些资源和它们附属的部署保持松耦合。 使用后端服务的好处在于,部署可以按需加载或卸载资源。例如,如果应用的数据库服务由于硬件问题出现异常,管理员可以从最近的备份中恢复一个数据库,卸载当前的数据库,然后加载新的数据库,整个过程都不需要修改代码。 5 构建、发布、运行 基准代码进行部署需要以下三个阶段: 构建阶段:是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包依赖项,编译成二进制文件和资源文件。 发布阶段:会将构建的结果和当前部署所需配置相结合,并能够立刻在运行环境中投入使用。 运行阶段:是指针对选定的发布版本,在执行环境中启动一系列应用程序进程。 应用严格区分构建、发布、运行这三个步骤。举例来说,直接修改处于运行状态的代码是非常不可取的做法,因为这些修改很难再同步回构建步骤。 部署工具通常都提供了发布管理工具,在必要的时候还是可以退回至较旧的发布版本。 每一个发布版本必须对应一个唯一的发布 ID,例如可以使用发布时的时间戳(2011-04-06-20:32:17),亦或是一个增长的数字(v100)。发布的版本就像一本只能追加的账本,一旦发布就不可修改,任何的变动都应该产生一个新的发布版本。 新的代码在部署之前,需要开发人员触发构建操作。但是,运行阶段不一定需要人为触发,而是可以自动进行。如服务器重启,或是进程管理器重启了一个崩溃的进程。因此,运行阶段应该保持尽可能少的模块,这样假设半夜发生系统故障而开发人员又捉襟见肘也不会引起太大问题。构建阶段是可以相对复杂一些的,因为错误信息能够立刻展示在开发人员面前,从而得到妥善处理。 6 进程 12-Factor 应用推荐以一个或多个无状态进程运行应用。这里的“无状态”是与 REST 中的无状态是一个意思,即进程的执行不依赖于上一个进程的执行。 举例来说,内存区域或磁盘空间可以作为进程在做某种事务型操作时的缓存,例如下载一个很大的文件,对其操作并将结果写入数据库的过程。12-Factor 应用根本不用考虑这些缓存的内容是不是可以保留给之后的请求来使用,这是因为应用启动了多种类型的进程,将来的请求多半会由其他进程来服务。即使在只有一个进程的情形下,先前保存的数据(内存或文件系统中)也会因为重启(如代码部署、配置更改、或运行环境将进程调度至另一个物理区域执行)而丢失。 一些互联网应用依赖于“粘性 session”, 这是指将用户 session 中的数据缓存至某进程的内存中,并将同一用户的后续请求路由到同一个进程。粘性 session 是 12-Factor 极力反对的。Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。 相比于有状态的应用而言,无状态具有更好的可扩展性。 7 端口绑定 传统的互联网应用有时会运行于服务器的容器之中。例如 PHP 经常作为 Apache HTTPD 的一个模块来运行,而 Java 应用往往会运行于 Tomcat 中。 12-Factor 应用完全具备自我加载的能力,而不依赖于任何网络服务器就可以创建一个面向网络的服务。互联网应用通过端口绑定(Port binding)来提供服务,并监听发送至该端口的请求。 举例来说,Java 程序完全能够内嵌一个 Tomcat 在程序中,从而自己就能启动并提供服务,省去了将 Java 应用部署到 Tomcat 中的繁琐过程。在这方面,Spring Boot 框架的布道者 Josh Long 有句名言“Make JAR not WAR”,即 Java 应用程序应该被打包为可以独立运行的 JAR 文件,而不是传统的 WAR 包。 以 Spring Boot 为例,构建一个具有内嵌容器的 Java 应用是非常简单的,只需要引入以下依赖: // 依赖关系 dependencies { // 该依赖用于编译阶段 compile('org.springframework.boot:spring-boot-starter-web') } 这样,该 Spring Boot 应用就包含了内嵌 Tomcat 容器。 如果想使用其他容器,比如 Jetty、Undertow 等,只需要在依赖中加入相应 Servlet 容器的 Starter 就能实现默认容器的替换,比如: spring-boot-starter-jetty:使用 Jetty 作为内嵌容器,可以替换 spring-boot-starter-tomcat; spring-boot-starter-undertow:使用 Undertow 作为内嵌容器,可以替换 spring-boot-starter-tomcat。 可以使用 Spring Environment 属性配置常见的 Servlet 容器的相关设置。通常您将在 application.properties 文件中来定义属性。 常见的 Servlet 容器设置包括: 网络设置:监听 HTTP 请求的端口(server.port)、绑定到 server.address 的接口地址等; 会话设置:会话是否持久(server.session.persistence)、会话超时(server.session.timeout)、会话数据的位置(server.session.store-dir)和会话 cookie 配置(server.session.cookie.*); 错误管理:错误页面的位置(server.error.path)等; SSL; HTTP 压缩。 Spring Boot 尽可能地尝试公开这些常见公用设置,但也会有一些特殊的配置。对于这些例外的情况,Spring Boot 提供了专用命名空间来对应特定于服务器的配置(比如 server.tomcat 和 server.undertow)。 8 并发 在 12-factor 应用中,进程是一等公民。由于进程之间不会共享状态,这意味着应用可以通过进程的扩展来实现并发。 类似于 unix 守护进程模型,开发人员可以运用这个模型去设计应用架构,将不同的工作分配给不同的进程。例如,HTTP 请求可以交给 web 进程来处理,而常驻的后台工作则交由 worker 进程负责。 在 Java 语言中,往往通过多线程的方式来实现程序的并发。线程允许在同一个进程中同时存在多个线程控制流。线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器、栈以及局部变量。线程还提供了一种直观的分解模式来充分利用操作系统中的硬件并行性,而在同一个程序中的多个线程也可以被同时调度到多个CPU上运行。 毫无疑问,多线程编程使得程序任务并发成为了可能。而并发控制主要是为了解决多个线程之间资源争夺等问题。并发一般发生在数据聚合的地方,只要有聚合,就有争夺发生,传统解决争夺的方式采取线程锁机制,这是强行对CPU管理线程进行人为干预,线程唤醒成本高,新的无锁并发策略来源于异步编程、非阻塞I/O等编程模型。 并发的使用并非没有风险。多线程并发会带来如下的问题: 安全性问题。在没有充足同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果。线程间的通信主要是通过共享访问字段及其字段所引用的对象来实现的。这种形式的通信是非常有效的,但可能导致两种错误:线程干扰(thread interference)和内存一致性错误(memory consistency errors)。 活跃度问题。一个并行应用程序的及时执行能力被称为它的活跃度(liveness)。安全性的含义是“永远不发生糟糕的事情”,而活跃度则关注于另外一个目标,即“某件正确的事情最终会发生”。当某个操作无法继续执行下去,就会发生活跃度问题。在串行程序中,活跃度问题形式之一就是无意中造成的无限循环(死循环)。而在多线程程序中,常见的活跃度问题主要有死锁、饥饿以及活锁。 性能问题。在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总是带来某种程度的运行时开销。而这种开销主要是在线程调度器临时关起活跃线程并转而运行另外一个线程的上下文切换操作(Context Switch)上,因为执行上下文切换,需要保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加贡献内存总线的同步流量。所以这些因素都会带来额外的性能开销。 9 易处理 12-Factor 应用的进程是易处理(Disposable)的,意味着它们可以瞬间启动或停止。比如,Spring Boot 应用,它可以无需依赖容器,而采用内嵌容器的方式来实现自启动。这有利于迅速部署变化的代码或配置,保障系统的可用性,并在系统负荷到来前,快速实现扩展。 进程应当追求最小启动时间。 理想状态下,进程从敲下命令到真正启动并等待请求的时间应该只需很短的时间。更少的启动时间提供了更敏捷的发布以及扩展过程,此外还增加了健壮性,因为进程管理器可以在授权情形下容易的将进程搬到新的物理机器上。 进程一旦接收终止信号(SIGTERM)就会优雅的终止。就网络进程而言,优雅终止是指停止监听服务的端口,即拒绝所有新的请求,并继续执行当前已接收的请求,然后退出。 对于 worker 进程来说,优雅终止是指将当前任务退回队列。例如,RabbitMQ 中,worker 可以发送一个 NACK 信号。Beanstalkd 中,任务终止并退回队列会在 worker 断开时自动触发。有锁机制的系统诸如 Delayed Job 则需要确定释放了系统资源。 10 开发环境与线上环境等价 我们期望一份基准代码可以部署到多个环境,但如果环境不一致,最终也可能导致运行程序的结果不一致。 比如,在开发环境,我们是采用了 MySQL 作为测试数据库,而在线上生产环境,则是采用了 Oracle。虽然,MySQL 和 Oracle 都遵循相同的 SQL 标准,但两者在很多语法上还是存在细微的差异。这些差异非常有可能导致两者的执行结果不一致,甚至某些 SQL 语句在开发环境能够正常执行,而在线上环境根本无法执行。这都给调试增加了复杂性,同时,也无法保障最终的测试效果。 所以,一个好的指导意见是,不同的环境尽量保持一样。开发环境、测试环境与线上环境设置成一样,更早发现测试问题,而不至于在生产环境才暴露出问题。 11 日志 在应用程序中打日志是一个好习惯。日志使得应用程序运行的动作变得透明。日志是在系统出现故障时,排查问题的有力帮手。 日志应该是事件流的汇总,将所有运行中进程和后端服务的输出流按照时间顺序收集起来。尽管在回溯问题时可能需要看很多行,日志最原始的格式确实是一个事件一行。日志没有确定开始和结束,但随着应用在运行会持续的增加。对于传统的 Java EE 应用程序而言,有许多框架和库可用于日志记录。Java Logging (JUL) 是 Java 自身所提供的现成选项。除此之外 Log4j、Logback 和 SLF4J 是其他一些流行的日志框架。 对于传统的单块架构而言,日志管理本身并不存在难点,毕竟所有的日志文件,都存储在应用所部属的主机上,获取日志文件或者搜索日志内容都比较简单。但在 Cloud Native 应用中,情况则有非常大的不同。分布式系统,特别是微服务架构所带来的部署应用方式的重大转变,都使得微服务的日志管理面临很多新的挑战。一方面随着微服务实例的数量的增长,伴随而来的就是日志文件的递增。另一方面,日志被散落在各自的实例所部署的主机上,不方面整合和回溯。 在这种情况下,将日志进行集中化的管理变得意义重大。本书的“10.4 日志管理”章节内容,会对 Cloud Native 的日志集中化管理进行详细的探讨。 12 管理进程 开发人员经常希望执行一些管理或维护应用的一次性任务,例如: 运行数据移植(Django 中的 manage.py migrate, Rails 中的 rake db:migrate)。 运行一个控制台(也被称为 REPL shell),来执行一些代码或是针对线上数据库做一些检查。大多数语言都通过解释器提供了一个 REPL 工具(python 或 perl),或是其他命令(Ruby 使用 irb, Rails 使用 rails console)。 运行一些提交到代码仓库的一次性脚本。 一次性管理进程应该和正常的常驻进程使用同样的环境。这些管理进程和任何其他的进程一样使用相同的代码和配置,基于某个发布版本运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。 所有进程类型应该使用同样的依赖隔离技术。例如,如果 Rub y的 web 进程使用了命令 bundle exec thin start,那么数据库移植应使用 bundle exec rake db:migrate。同样的,如果一个 Python 程序使用了 Virtualenv,则需要在运行 Tornado Web 服务器和任何 manage.py 管理进程时引入 bin/python。
当今软件行业正发生着巨变。自上世纪50年代计算机诞生以来,软件从最初的手工作坊式的交付方式,逐渐演变成为了职业化开发、团队化开发,进而定制了软件件行业的相关规范,形成了软件产业。 今天,无论是大型企业还是个人开发者,都或多或少采用了云的方式来开发、部署应用。不管是私有云,还是公有云,都终将给整个软件产业带来的革命。个人计算机或者以手机为代表的智能设备已经走进寻常百姓家了。每个人几乎都拥有手机,手机不仅仅是通信工具,还能发语音、看视频、玩游戏,让人与人之间的联系变得更加紧密。智能手环随时监控你的身体状况,并根据你每天的运动量、身体指标来给你提供合理的饮食运动建议。出门逛街甚至不需要带钱包了,吃饭购物搭车时使用手机就可以支付费用,多么方便快捷。智能家居系统更是你生活上的“管家”,什么时候该睡觉了,智能家居系统就自动拉上窗帘,关灯;早上起床了,智能家居系统会自动拉开窗帘,并播放动人的音乐,让你可以愉快地享受新的一天的来临;你再也不用担心家里的安全情况,智能家居系统会帮你监控一切,有异常情况时会及时发送通知到你的手机,让你第一时间掌握家里的状况。未来,每个人都能够拥有《Iron Man》(钢铁侠)中所描述的智能管家 Jarvis。而这一切,都离不开背后那个神秘的巨人——分布式系统。正是那些看不见的分布式系统,每天处理着数以亿计的计算,提供可靠而稳定的服务。这些系统往往是以 Cloud Native 方式来部署、运维的。 软件需求的发展 早期的软件,大多数是由使用该软件的个人或机构研制的,所以软件往往带有非常强烈的个人色彩。早期的软件开发也没有什么系统的方法论可以遵循,完全是“个人英雄主义”,也不存在所谓的软件设计,纯粹就是某个人的头脑中思想的表达。而且,当时的软件往往是围绕硬件的需求来定制化开发的,有什么样的硬件就有什么样的软件。所以,软件缺乏通用性。同时,由于软件开发过程不需要与他人协作,所以,软件除了源代码外,往往没有软件设计、使用说明书等文档。这样,就造成了软件行业缺乏经验的传承。 从60年代中期到70年代中期是计算机系统发展的第二个时期,在这一时期软件开始被当作一种产品被广泛使用。所谓产品,就是可以提供给不同的人使用,从而提高了软件的重用率,降低了软件开发的成本。比如,以前,一套软件,只能专门提供给某个人使用。现在,同一套软件可以批量的卖给不同的人,显然,分摊到相同软件上的开发成本而言,卖的越多,成本自然就越低。这个时期,出现了类似“软件作坊”的专职替别人的开发软件的团体。虽然是团体协作,但软件开发的方法基本上仍然沿用早期的个体化软件开发方式,这样导致的问题是,软件的数量急剧膨胀,软件需求日趋复杂,软件的维护难度也就越来越大,开发成本变得越来越高了,从而导致软件项目频频遭遇失败。这就演变成了“软件危机”。 “软件危机”迫使人们开始思考软件的开发方式,使得人们开始对软件及其特性进行了更加深入的研究,人们对待软件的观念也在发生悄然的改变。在早期,由于计算机的数量很少,只有少数军方或者科研机构才有机会接触到计算机,这就让大多数人认为,软件开发人员都是稀少且优秀的(一开始确实也是如此,毕竟计算机最初制作者都是数学界的天才)。由于,软件开发的技能,只能被少数人所掌握,所以大多数人对于“什么是好的软件”缺乏共识。实际上,早期那些被认为是优秀的程序常常很难被别人看懂,里面充斥着各种程序技巧。加之,当时的硬件资源比较紧缺,迫使开发人员在编程时,往往需要考虑更少的占用机子资源,从而会采用不易阅读的“精简”方式来开发,这更加加重了软件的个性化。而现在人们普遍认为,优秀的程序除了功能正确,性能优良之外,还应该更加让人容易看懂、容易使用、容易修改和扩充。这就是软件可维护性的要求。 1968年 NATO 会议上首次提出“软件危机”(Software Crisis)这个名词,同时,提出了期望通过“软件工程(Sotfwrae Engineeirng)”来解决“软件危机”。“软件工程”的目的,就是要把软件开发从“艺术”和“个体行为”向“工程”和“群体协同工作”进行转化,从而解决“软件危机”包含两方面问题: 如何开发软件,以满足不断增长,日趋复杂的需求; 如何维护数量不断膨胀的软件产品。 事实证明,在软件的可行性分析方面,事先对软件的进行可行性分析,可以有效的规避软件失败的风险,提高软件的开发的成功率。 在需求方面,软件行业的规范是,需要制定相应的软件规格说明书、软件需求说明书,从而让开发工作有了依据,划清了开发边界,并在一定程度上减少了“需求蔓延”的情况的发生。 在架构设计方面,需制定软件架构说明书,划分了系统之间的界限,约定了系统间的通信接口,并将系统分为多个模块。这样,更容易将任务分解,从而降低系统的复杂性。 今天,制定软件需求的方式越来越多样化了。客户与系统分析师也许只是经过简单的口头讨论,制定了粗略的协议,就安排开发工程师进行原型设计了。开发工程师开发一个微服务,并部署到云容器中,从而可以实现软件的交付。甚至,可以不用编写任何写后台代码,直接使用云服务供应商所提供的 API,使应用快速推向市场。客户在使用完这个应用时,马上就能将自己体验反馈到开发团队,使开发团队能够快速的响应客户的需求变化,并促使软件进行升级。 通过敏捷的方式,最终软件形成了“开发——测试——部署——反馈”的良性循环,软件产品也得到了进化。而这整个过程,都比传统的需求获取的方式将更加的迅捷。 开发方式的巨变 早些年,瀑布模型还是标准的软件开发模型。瀑布模型将软件生命周期划分为制定计划、需求分析、软件设计、程序编写、软件测试和运行维护等六个基本活动,并且规定了它们自上而下、相互衔接的固定次序,如同瀑布流水,逐级下落。在瀑布模型中,软件开发的各项活动严格按照线性方式进行,当前活动接受上一项活动的工作结果,实施完成所需的工作内容。当前活动的工作结果需要进行验证,如验证通过,则该结果作为下一项活动的输入,继续进行下一项活动,否则返回修改。 瀑布模型优点是严格遵循预先计划的步骤顺序进行,一切按部就班,整个过程比较严谨。同时,瀑布模型强调文档的作用,并要求每个阶段都要仔细验证文档的内容。但是,这种模型的线性过程太理想化,主要存在以下几个方面的问题在: 各个阶段的划分完全固定,阶段之间产生大量的文档,极大地增加了工作量; 由于开发模型是线性的,用户只有等到整个过程的末期才能见到开发成果,从而增加了开发的风险; 早期的错误可能要等到开发后期的测试阶段才能发现,进而带来严重的后果; 各个软件生命周期衔接花费时间较长,团队人员交流成本大。 瀑布式方法在需求不明并且在项目进行过程中可能变化的情况下基本是不可行的,所以瀑布式方法非常适合需求明确的软件开发。但在如今,时间就是金钱,如何快速抢占市场,是每个互联网企业需要考虑的第一要素。所以,快速迭代、频繁发布的原型开发、敏捷开发方式,被越来越多的互联网企业所采用。甚至,很多传统企业,也在逐步向敏捷的“短平快”的开发方式靠拢。毕竟,谁愿意等待呢? 客户将需求告诉了你,当然是越快希望得到反馈越好,那么,最快的方式莫过于在原有系统的基础上,搭建一个原型提供给客户作为参考。客户拿到原型之后,肯定会反馈他的意见,是好或者坏的方面都会有。这样,开发人员就能根据客户的反馈,来对原型进行快速更改,快速发布新的版本,从而实现了良好的反馈闭环。 今天,Cloud Native 的开发方式正在流行。Cloud Native 是以云架构为优先的应用开发模式。Cloud Native 利于最大化整合现有的云计算所提供的资源,同时也最大化节约了项目启动的成本。 云是大势所趋 目前,越来越多的企业已经在大规模开始拥抱云,在云环境开发应用、部署应用、发布应用。未来,越来越多的开发者也将采用 Cloud Native 来开发应用。 那么,为什么我们需要使用 Cloud Native? 云计算的第一个浪潮是关于成本节约和业务敏捷性,尤其是云计算的基础设施更加廉价。随着云计算的不断发展,企业开始采用基础架构即服务(IaaS)和平台即服务(PaaS)服务,并利用它们构建利用云的弹性和可伸缩性的应用程序,同时也能够满足云环境下的容错性。 很多企业倾向于使用微服务架构来开发应用。微服务开发快速,职责单一,能够更快速的被客户所采纳。同时,这些应用能够通过快速迭代的方式,得到进化,赢得客户的认可。Cloud Native 可以打通微服务开发、测试、部署、发布的整个流程环节。 云供应商为迎合市场,提供了满足各种场景方案的 API,例如用于定位的 Google Maps,用于社交协作的认证平台等。将所有这些 API 与企业业务的特性和功能混合在一起,可以让他们为客户构建独特的方案。所有这些整合都在 API 层面进行。这意味着,不管是移动应用还是传统的桌面应用都能无缝集成。所以,采用 Cloud Native 所开发的应用都且具备极强的可扩展性。 软件不可能不出故障。传统的企业级开发方式,需要有专职人员来对企业应用进行监控与维护。而在 Cloud Native 架构下,底层的服务或者是 API 都由将部署到云中,等价于将繁重的运维工作转移给了云平台供应商。这意味着客户应用将得到更加专业的看护,同时,也节省了运维成本。 那么如何来实现 Cloud Native 呢?其实这是一个非常大的话题,比如,作为开发者,你需要了解目前市面上流行的云供应商,了解微服务、SOA,了解 HTTP 和 REST,了解领域驱动设计(DDD),了解CICD和TDD,了解两个披萨,了解分布式的常用架构和模式等等。这里每一样都是一个庞大的课题,还好目前市面上已经有了一些资料可供学习,比如《Cloud Native 分布式架构原理与实践》,可以非常全面的指导开发者轻松入门 Cloud Native。 参考引用 原文同步至:https://waylau.com/the-way-to-cloud-native/ Cloud Native 案例大全 简述 Microservices(微服务) Spring Boot 企业级应用开发实战 Spring Cloud 微服务架构开发实战 Cloud Native 分布式架构原理与实践 分布式系统常用技术及案例分析
目前,越来越多的企业已经在大规模开始拥抱云,在云环境开发应用、部署应用、发布应用。Cloud Native(云原生)是以云架构为优先的应用开发模式。那么,为什么说 Cloud Native 是未来开发应用的趋势呢?本文一一解答。 什么是 Cloud Native Cloud Native (国内译为“云原生”),最早是 Matt Stine 提出的一个概念。与微服务一样,Cloud Native 并不是一种具体的技术,而是一类思想的集合,包括DevOps、持续交付(Continuous Delivery)、微服务(MicroServices)、敏捷基础设施(Agile Infrastructure)、康威定律(Conways Law)等,以及根据商业能力对公司进行重组。Cloud Native 既包含技术(微服务,敏捷基础设施),也包含管理(DevOps,持续交付,康威定律,重组等)。所以,Cloud Native 也可以说是一系列Cloud技术、企业管理方法的集合。 有关Cloud Native的概述,可见“简述什么是 Cloud Native(云原生)”一文的论述。 为什么说 Cloud Native 是大势所趋 目前,越来越多的企业已经开始拥抱云,在云环境下开发应用、部署应用和发布应用。未来,越来越多的开发者也将采用 Cloud Native 来开发应用。 那么,为什么说 Cloud Native 是大势所趋 1. 云计算带来的是成本的节约和业务的敏捷性 特别是使用云计算所提供的基础设施,费用会更加低廉。随着云计算的不断发展,企业越来越倾向于使用 IaaS(基础设施即服务)和 PaaS(平 台即服务)来构建应用程序。这种应用可以利用云计算的弹性和可伸缩性,同时还能满足云环境下的容错性。 2. 很多企业倾向于使用微服务架构来开发应用 微服务开发快速、职责单一,能够更快速地被客户所采纳。同时,这些应用能够通过快速迭代的方式得到进化,赢得客户的认可。Cloud Native 可以打通微服务开发、测试、部署、发布的整个流程环节。 3. 云供应商为迎合市场,提供了满足各种场景方案的 API 例如,用于定位的 Google Maps,用于社交协作的认证平台等。将这些 API 与企业业务的特性和功能结合在一起,可以让它们为客户构建独特的方案。所有整合都在 API 层面进行。这意味着,无论是移动应用还是传统的桌面应用都能无缝集成。所以,采用 Cloud Native 所开发的应用都具备极强的可扩展性。 4. 软件不可能不出故障 传统的企业级开发方式需要有专职人员来对企业应用进行监控与维护。而在 Cloud Native 架构下,底层的服务或 API 都将部署到云中,相当于将繁重的运维工作转 移给了云平台供应商。这意味着客户应用将得到更加专业的看护,同时也节省了运维成本。 如何实现 Cloud Native 那么如何来实现 Cloud Native 呢?其实这是一个非常大的话题,比如,作为开发者,你需要了解目前市面上流行的云供应商,了解微服务、SOA,了解 HTTP 和 REST,了解领域驱动设计(DDD),了解CICD和TDD,了解两个披萨,了解分布式的常用架构和模式等等。这里每一样都是一个庞大的课题,还好目前市面上已经有了一些资料可供学习,比如《Cloud Native 分布式架构原理与实践》,可以非常全面的指导开发者轻松入门 Cloud Native。 参考引用 原文同步至:https://waylau.com/why-cloud-native/ Cloud Native 分布式架构原理与实践
Cloud Native(云原生)是以云架构为优先的应用开发模式。目前,越来越多的企业已经在大规模开始拥抱云,在云环境开发应用、部署应用、发布应用。未来,越来越多的开发者也将采用 Cloud Native 来开发应用。本书是国内第一本 Java 领域 Cloud Native 著作。 那么为什么Cloud Native模式会越来越流行?Cloud Native与微服务有什么区别?何时选择使用Cloud Native?等等,这些问题将在本文一一解答。 什么是 Cloud Native Cloud Native (国内译为“云原生”),最早是 Matt Stine 提出的一个概念。与微服务一样,Cloud Native 并不是一种具体的技术,而是一类思想的集合,包括DevOps、持续交付(Continuous Delivery)、微服务(MicroServices)、敏捷基础设施(Agile Infrastructure)、康威定律(Conways Law)等,以及根据商业能力对公司进行重组。Cloud Native 既包含技术(微服务,敏捷基础设施),也包含管理(DevOps,持续交付,康威定律,重组等)。所以,Cloud Native 也可以说是一系列Cloud技术、企业管理方法的集合。 Cloud Native 具备有以下特性: 以云为基础架构 云服务 无服务 可扩展 高可用 敏捷 云优先 等等 下图是《Cloud Native 分布式架构原理与实践》书中所罗列的 Cloud Native 云架构模式。可见 Cloud Native 体系是非常庞杂的。 随着云计算的不断发展,企业开始采用基础架构即服务(IaaS)和平台即服务(PaaS)服务,并利用它们构建利用云的弹性和可伸缩性的应用程序,同时也能够满足云环境下的容错性。同时,云环境更加便宜和经济,因此,未来云环境会被作为企业部署、个人开发的优先选择。Cloud Native 的出现恰逢其时, 其架构可以指导企业或者个人轻松实现云应用开发或者云部署。 Cloud Native 与微服务的关系 在“简述 Microservices(微服务)”一文中,已经对微服务的概念做了简单的论述。 微服务架构风格其本质是把大的应用拆分成为小的服务(微服务)。微服务是单一应用的形式, 因此可以独立部署和运行在其自己的进程中。微服务一般采用轻量级的机制进行通信(一般是 HTTP 资源 API),因此可以不限制技术栈。微服务是围绕业务能力来构建,因此更加聚焦业务能力,能够把握住领域边界,放置需求的蔓延。微服务其固有的特性,方便通过全自动部署工具来实现独立部署,因此非常适合在云环境中进行部署。 在 Cloud Native 中,倾向于使用微服务来构建应用。同时,Cloud Native因为是以云环境为优先的,非常适合微服务的部署和管理。 目前,业界针对微服务有非常多的成熟方案,比如Spring Boot、Spring Cloud等,都可以简化微服务的开发工作。这微服务方面,笔者也撰写了一些列的免费教程(https://waylau.com/books/),读者朋友可以作为参考。 为什么我们需要使用 Cloud Native? 云计算的第一个浪潮是关于成本节约和业务敏捷性,尤其是云计算的基础设施更加廉价。 很多企业倾向于使用微服务架构来开发应用。微服务开发快速,职责单一,能够更快速的被客户所采纳。同时,这些应用能够通过快速迭代的方式,得到进化,赢得客户的认可。Cloud Native 可以打通微服务开发、测试、部署、发布的整个流程环节。 云供应商为迎合市场,提供了满足各种场景方案的 API,例如用于定位的 Google Maps,用于社交协作的认证平台等。将所有这些 API 与企业业务的特性和功能混合在一起,可以让他们为客户构建独特的方案。所有这些整合都在 API 层面进行。这意味着,不管是移动应用还是传统的桌面应用都能无缝集成。所以,采用 Cloud Native 所开发的应用都且具备极强的可扩展性。 软件不可能不出故障。传统的企业级开发方式,需要有专职人员来对企业应用进行监控与维护。而在 Cloud Native 架构下,底层的服务或者是 API 都由将部署到云中,等价于将繁重的运维工作转移给了云平台供应商。这意味着客户应用将得到更加专业的看护,同时,也节省了运维成本。 因此,云是大势所趋。快来拥抱Cloud Native! 如何实现 Cloud Native 那么如何来实现 Cloud Native 呢?其实这是一个非常大的话题,比如,作为开发者,你需要了解目前市面上流行的云供应商,了解微服务、SOA,了解 HTTP 和 REST,了解领域驱动设计(DDD),了解CICD和TDD,了解两个披萨,了解分布式的常用架构和模式等等。这里每一样都是一个庞大的课题,还好目前市面上已经有了一些资料可供学习,比如《Cloud Native 分布式架构原理与实践》,可以非常全面的指导开发者轻松入门 Cloud Native。 在本文的最后也列出了一些学习资料,读者有兴趣的话,可以由点及面,慢慢扩展自己的知识体系。 参考引用 原文同步至:https://waylau.com/about-cloud-native/ 简述 Microservices(微服务) Spring Boot 企业级应用开发实战 Spring Cloud 微服务架构开发实战 Cloud Native 分布式架构原理与实践 分布式系统常用技术及案例分析
本文介绍Sidecar模式的特点,及其应用的场景。熟悉Native Cloud或者微服务的童鞋应该知道,在云环境下,技术栈可以是多种多样的。那么如何能够将这些异构的服务组件串联起来,成为了服务治理的一个重大课题。而Sidecar模式为服务治理,提供了一种解决方案。 将应用程序的组件部署到单独的进程或容器中,以提供隔离和封装。此模式还可以使应用程序由异构组件和技术组成。 这种模式被称为Sidecar,因为它类似于连接到摩托车的边车。在该模式中,边车附加到父应用程序并为应用程序提供支持功能。 sidecar还与父应用程序共享相同的生命周期,与父项一起创建和退役。边车图案有时被称为搭接图案并且是分解图案。 问题背景 应用程序和服务通常需要相关的功能,例如监控、日志、集中化配置和网络服务等。这些外围任务可以作为单独的组件或服务来实现。 如果它们紧密集成到应用程序中,它们可以在与应用程序相同的进程中运行,从而有效地使用共享资源。但是,这也意味着它们没有很好地隔离,并且其中一个组件的中断可能会影响其他组件或整个应用程序。此外,它们通常需要使用与父应用程序相同的语言或者技术栈来实现。因此,组件和应用程序彼此之间具有密切的相互依赖性。 如果将应用程序分解为服务,则可以使用不同的语言和技术构建每个服务。虽然这提供了更大的灵活性,但这意味着每个组件都有自己的依赖关系,并且需要特定于语言的库来访问底层平台以及与父应用程序共享的任何资源。此外,将这些功能部署为单独的服务可能会增加应用程序的延迟。管理这些特定于语言的接口的代码和依赖关系也会增加相当大的复杂性,尤其是对于托管、部署和管理服务。 解决方案 上述问题的解决方案是,将一组紧密结合的任务与主应用程序共同放在一台主机(Host)中,但会将它们部署在各自的进程或容器中。这种方式也被称为“Sidecar(边车)模式”。 下图展示了任务与主应用程序的部署关系图。 Sidecar模式 边车服务不一定是应用程序的一部分,而是与之相关联。它适用于父应用程序的任何位置。Sidecar支持与主应用程序一起部署的进程或服务。这就像是如下图所示的边三轮摩托车那样,将边车安装在一辆摩托车上,就变成了边三轮摩托车。每辆边三轮摩托车都有自己的边车。类似同样的方式,边车服务共享其父应用程序的主机。对于应用程序的每个实例,边车的实例被部署并与其一起托管。 使用边车模式的优点包括: 在运行时环境和编程语言方面,边车独立于其主要应用程序,因此不需要为每种语言开发一个边车。 边车可以访问与主应用程序相同的资源。例如,边车可以监视边车和主应用程序使用的系统资源。 由于它靠近主应用程序,因此在它们之间进行通信时没有明显的延迟。 即使对于不提供可扩展性机制的应用程序,也可以使用边车通过将其作为自己的进程附加到与主应用程序相同的主机或子容器中来扩展功能。 Sidecar模式通常与容器一起使用,并称为边车容器。有关容器方面的内容,可以参阅https://waylau.com/ahout-docker/。 Sidecar模式的实现 Spring Cloud Netflix Sidecar框架提供了Sidecar模式的现成解决方案。Spring Cloud Netflix Sidecar框架框架可以提供对其他非Spring Cloud技术栈的微服务的治理。比如,你可以使用Node或者Golang编写一个Web项目,这个服务同样可以以Sidecar模式,纳入到Spring Cloud管理中去。 下面是实现步骤。 1. 为Web项目添加健康检查接口 提供REST接口,返回JSON格式内容{"status" : "up"}。其中status用于描述微服务的状态,常见的取值有UP、DOWN、OUT_OF_SERVICE和UNKNOWN等。 2. 编写Sidecar微服务 创建项目,添加Eureka、Sidecar的依赖: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-sidecar</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> 启动类上加上@EnableSidecar注解。这是一个组合注解,它整合了三个注解,分别是@EnableCircuiBreaker和@EnableDiscoveryClient。 在配置文件中加入端口号、服务名称、Eureka地址以及Web项目的端口以及健康检查地址,如: server.port=8887 spring.application.name=sidecar-mylife-service eureka.client.serviceUrl.defaultZone=http://localhost:8881/eureka/ eureka.client.instance.prefer-ip-address=true sidecar.port=8080 sidecar.health-uri=http://localhost:8080/health eureka.instance.hostname=localhost 启动项目,并访问8887接口,就可以访问到Web项目中的接口。 3. Sidecar的一些端点 以下是Sidecar的常用端点: /hosts/{serviceId} 指定微服务在Eureka上的实例列表 /ping 返回OK字符串 /{serviceId} 请求对应的微服务 4. 部署应用 将Sidecar与Web服进行部署。一般是部署在相同的主机里面。 有关Spring Cloud的更多内容,可以参阅Spring Cloud 教程。 参考引用 原文同步至https://waylau.com/sidecar-pattern/ Spring Cloud 微服务架构开发实战:https://github.com/waylau/spring-cloud-microservices-development Spring Boot 企业级应用开发实战:https://github.com/waylau/spring-boot-enterprise-application-development https://docs.microsoft.com/en-us/azure/architecture/patterns/sidecar
2019年快来了,为了答谢各位关注老卫的读者、学员朋友,特送《Spring 5开发大全》三本,无套路,快上车。 时间的脚步总是匆匆,经历了一年的付出收获、欢乐忧伤,我们悄然间就要迈步到2019年了。 回顾这一年,最大的成就莫过于将Spring三剑客付梓出版。 何为Spring三剑客?即《Spring Boot 企业级应用开发实战》、《Spring Cloud 微服务架构开发实战》、《Spring 5 案例大全》,全方面覆盖从Java企业级到微服务的各个领域开发,是每个Java开发者的良师益友。 当然,书中的案例来自工资实践,来自于网友的讨论,更来自于这个开源的世界。不管是书里,还是老卫的博客里,每一篇教程的编写,我都满怀憧憬和希冀,每一个代码,都是一个相遇的音符。 2018年对我来说,是收获的一年。我收获了读者们的支持,给了我全新的人生体验。和你们的互动,让我感受到知识分享的快乐。你们对我的肯定,是我坚持的动力,所以谢谢大家。 2018年岁末,我的新书《Spring 5 案例大全》由北京大学出版社出版发行,在京东、当当等电商网站上陆续开始上市。这当然离不开你们的支持,为了感谢大家,特送出《Spring 5开发大全》三本,以回馈读者朋友们。 活动方式 愿意参加本次活动的朋友,请你们在该书的主页(https://book.douban.com/subject/30370024/),进行评价留言,讲述你我关于Spring的故事。届时将会挑选出三篇最佳留言,送上价值119元的新年礼物三份。绝对没有套路! Tips:打5星、长文字更容易中奖哦~ 活动时间 只要在2019年1月5日24时前的评价留言,都可以参与评选。 礼品介绍 礼品介绍可以见: 当当:http://product.dangdang.com/25581763.html 京东:https://item.jd.com/12474404.html
在前后台分离的应用中,Angular 与 Java 是一对好搭档。但是如果是分开部署应用,则势必会遇到跨域访问的问题。 什么是跨域 启动应用之后,有些浏览器会提示如下告警信息: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access. 这个是典型的跨域问题。浏览器为了安全考虑,不同的域之间是不能够互相访问的的。 比如,Angular 应用部署在本地的4200端口,而 Java 服务部署在8080端口,他们虽然是同台机子,但仍然是不同的域。Angular 应用视图通过HttpClient 去访问 Java 的 http://localhost:8080/hello 接口是不允许的。 如何解决跨域问题 在知道了什么是跨域之后,解决方案就有多种。 1. 避免跨域 既然,分开部署导致了跨域,那么最简单的方式莫过于避免分开部署,即Angular 与 Java 同时部署到同个 Web 服务器中。 这种方式部署在传统的 Java Web 中非常常见。比如,JSP 应用。 但带来的问题是,水平扩展和性能调优将变得困难,不适合大型互联网应用。 2. 安装支持跨域请求的插件 其实,很多浏览器都提供了允许跨域访问的插件,只需启用这种插件,就能实现在开发环境跨域请求第三方 API 了。 下图展示的是在 Firefox 浏览器中能够实现的跨域访问的插件。 这种方式是最简单,但使用的场景比较受限,一般用于开发环境。 3. 设置反向代理 这种方式是业界最为常用的方式,原理是设置反向代理服务器,让 Angular 应用都访问自己的服务器中的API,而这类API都会被反向代理服务器转发到 Java 服务API中,而这个过程对于 Angular 应用是无感知的。 业界经常是采用 NGINX 服务来承担反向代理的职责。而在 Angular 中,使用反向代理将变得更加简单。在 Angualr 应用的根目录下,添加配置文件proxy.config.json,并填写如下格式内容: { "/lite": { "target": "http://localhost:8080", "secure": "false" } } 使用 Angular CLI 启动应用时,只需要执行如下命令即可,非常方法: ng serve --proxy-config proxy.config.json 这样,当 Angular 要访问http://localhost:4200/lite> 时,会被转发到 Java 的 接口。 参考引用 原文同步至https://waylau.com/angular-proxy/ Angular CLI 常用命令:https://waylau.com/angular-cli-commands/ Spring 5 开发大全:https://github.com/waylau/spring-5-book 跟老卫学Angular:https://github.com/waylau/angular-tutorial NGINX 教程:https://github.com/waylau/nginx-tutorial
有时,应用中需要一些比较新的依赖,而这些依赖并没有正式发布,还是处于milestone或者是snapshot阶段,并不能从中央仓库或者镜像站上下载到。此时,就需要 自定义Maven的<repositories>。 自定义Maven的<repositories> 以Spring应用程序程序为例,需要添加一个Spring Security 5.2.0.BUILD-SNAPSHOT版本的依赖,可惜这是个snapshot,并不在镜像站中。 解决方法就是在pom.xml添加一个snapshot的地址: <!-- Spring Snapshots仓库 --> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> </repository> </repositories> 这样,就能从这个仓库中,获取到 Spring Security 5.2.0.BUILD-SNAPSHOT版本的依赖了。 检查Maven的settings.xml 但有时,跟着上面的设置方法并不奏效,仍然还是从之前的镜像站中去下载。此时,我们需要去检查下 Maven 安装目录下的settings.xml: <mirror> <id>nexus-aliyun</id> <mirrorOf>*</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> 可以看到,在设置镜像时,mirrorOf设置为了“*”,意味所有的依赖都是从这个镜像上下载。完全不给Spring Snapshots机会了。 解决方法也简单,改为下面的方式: <mirror> <id>nexus-aliyun</id> <mirrorOf>*,!spring-snapshots</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> mirrorOf设置为了“*,!spring-snapshots”,意味除了Spring Snapshots外,所有的依赖都是从这个镜像上下载。这样就能下载到 Spring Snapshots了。 以下是mirrorOf的详细解释。 mirrorOf的配置解释 mirrorOf的配置支持如下场景: *:所有依赖; external:*:所有不再本地和不是基于文件的依赖; repo,repo1:所有来自repo或者repo1仓库的依赖; *,!repo1:除了repo1外的所有依赖。即上面示例中的场景。 参考引用 原文同步至https://waylau.com/customize-maven-repositories/
Angular CLI 是 Angular 客户端命令行工具,提供非常多的命令来简化 Angular 的开发。本文总结了在实际项目中经常会用到的 Angular CLI 命令。 获取帮助(ng -h) ng -h等同于ng --help,跟所有的其他命令行一样,用于查看所有命令的一个帮助命令。执行该命令可以看到 Angular CLI 所有的命令: >ng -h Available Commands: add Adds support for an external library to your project. build (b) Compiles an Angular app into an output directory named dist/ at the given output path. Must be executed from within a workspace directory. config Retrieves or sets Angular configuration values. doc (d) Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword. e2e (e) Builds and serves an Angular app, then runs end-to-end tests using Protractor. generate (g) Generates and/or modifies files based on a schematic. help Lists available commands and their short descriptions. lint (l) Runs linting tools on Angular app code in a given project folder. new (n) Creates a new workspace and an initial Angular app. run Runs a custom target defined in your project. serve (s) Builds and serves your app, rebuilding on file changes. test (t) Runs unit tests in a project. update Updates your application and its dependencies. See https://update.angular.io/ version (v) Outputs Angular CLI version. xi18n Extracts i18n messages from source code. For more detailed help run "ng [command name] --help" 创建应用 以下示例,创建一个名为“user-management”的 Angular 应用: ng new user-management 创建组件 以下示例,创建一个名为 UsersComponent 的组件: ng generate component users 创建服务 以下示例,创建一个名为 UserService 的服务: ng generate service user 启动应用 执行: ng serve --open 此时,应用就会自动在浏览器中打开。访问地址为 http://localhost:4200/。 升级依赖 目前,Angular 社区非常活跃,版本会经常更新。对 Angular 的版本做升级,只需简单一步执行: ng update 如果是想把整个应用的依赖都升级,则执行: ng update --all 自动化测试 Angular 支持自动化测试。Angular的测试,主要是基于Jasmine和Karma库来实现的。只需简单一步执行: ng test 要生成覆盖率报告,运行下列命令: ng test --code-coverage 下载依赖 光有 Angular 源码是否不足以将 Angular 启动起来的,需要先安装 Angular 应用所需要的依赖到本地。 在应用目录下执行: npm install 参考引用 更多有关 Angular 的内容,可以参阅《跟老卫学Angular》:https://github.com/waylau/angular-tutorial 原文同步至:https://waylau.com/angular-cli-commands/
Java 开发者对于 Spring 应该不会陌生。Spring 可以说是 Java EE 开发事实上的标准。无论是 Web 开发,还是分布式应用,Spring 都致力于简化开发者创建应用的复杂性。本文讨论 Spring 在狭义上以及广义上,所承载的不同的概念。 Spring 有广义与狭义之说。 狭义上的 Spring——Spring Framework 狭义上的 Spring, 是特指 Spring 框架(Spring Framework)。Spring 框架是为了解决企业应用开发的复杂性而创建的。Spring 框架的主要优势之一就是其分层架构。分层架构允许使用者选择使用哪一个组件,同时为 Java EE 应用程序开发提供集成的框架。Spring 框架使用基本的 POJO 来完成以前只可能由 EJB 完成的事情。然而,Spring 框架的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 框架中受益。Spring 框架的核心是控制反转(IoC)和面向切面(AOP)。简单来说,Spring 框架是一个分层的面向与 Java 应用的一站式轻量级开源框架。 Spring 框架前身,是 Rod Johnson 发表在 Expert One-on-One J2EE Design and Development 一书中所包含的3万行代码的附件。在书中,他展示了如何在不使用 EJB 的情况下构建高质量、可扩展的在线座位预留应用程序。为了构建该应用程序,他写了超过万行的基础结构代码。这些代码包含了许多可重用的 Java 接口和类,如 ApplicationContext 和 BeanFactory 等。由于 Java 接口是依赖注入的基本构件,因此他将类的根包命名为com.interface21,意思是这是一个提供给21世纪的参考!根据书中描述,这些代码已经在一些真实的金融系统中使用。 由于该书影响甚广,当时有几个开发人员 Juergen Hoeller 以及 Yann Caroff 联系上了 Rod Johnson,希望将com.interface21代码开源。Yann Caroff 将这个新框架并命名为了“Spring”,意思是就像一缕春风扫平传统 J2EE 的恶冬。所以说,Rod Johnson、Juergen Hoeller 以及 Yann Caroff 是 Spring 框架的共同创立者。 2003年2月,Spring 0.9 发布,采用了 Apache 2.0 开源协议。2004年4月,Spring 1.0 发布。到如今,Spring 框架已经是第5个主要版本了。 广义上的 Spring——Spring 技术栈 广义上的 Spring 是指以 Spring 框架为核心的 Spring 技术栈。这些技术栈涵盖了从企业级应用到云计算等各个方面的内容。包括: Spring Data:Spring 框架中的数据访问模块对 JDBC 及 ORM 提供了很好的支持。随着 NoSQL 和大数据的兴起,出现了越来越多的新技术,比如非关系型数据库、MapReduce 框架。Spring Data 正是为了让 Spring 开发者能更方便地使用这些新技术而诞生的“大”项目——它由一系列小的项目组成——分别为不同的技术提供支持,例如 Spring Data JPA、Sprng Data Hadoop、Spring Data MongoDB、Spring Data Redis 等等。通过 Spring Data,开发者可以用 Spring 提供的相对一致的方式来访问位于不同类型的数据存储中的数据。 Spring Batch:一款专门针对企业级系统中的日常批处理任务的轻量级框架,能够帮助开发者方便地开发出强壮、高效的批处理应用程序。通过 Spring Batch 可以轻松构建出轻量级的、健壮的并⾏处理应用,并支持事务、并发、流程、监控、纵向和横向扩展,提供统⼀的接口管理和任务管理。Spring Batch 对批处理任务进行了一定的抽象,它的架构可以大致分为三层,自上而下分别是业务逻辑层、批处理执行环境层和基础设施层。Spring Batch 可以很好地利用 Spring 框架所带来的各种便利,同时也为开发者提供了相对熟悉的开发体验。 Spring Integration:在企业软件开发过程中,经常会遇到需要与外部系统集成的情况,这时可能会使用 EJB、RMI、JMS 等各种技术,也许你会引入ESB。如果你在开发时用了 Spring 框架,那么不妨考虑下 Spring Integration——它为 Spring 编程模型提供了一个支持企业集成模式的扩展,在应用程序中提供轻量级的消息机制,可以通过声明式的适配器与外部系统进行集成。Spring Integraton 中有几个基本的概念:Message(带有元数据的Java对象)、Channel(传递消息的管道)和Message Endpoint(消息的处理端)。在处理端可以对消息进行转换、路由、过滤、拆分、聚合等操作;更重要的是可以使用 Channel Adapter,这是应用程序与外界交互的地方,输入是 Inbound、输出则是 Outbound,可选的连接类型有很多,比如 AMQP、JDBC、Web Services、FTP、JMS、XMPP、多种 NoSQL 数据库等。只需通过简单的配置文件就能将所有这些东西串联在一起,实现复杂的集成工作。 Spring Security:前身是 Acegi,是较为成熟的子项目之一,是一款可定制化的身份验证和访问控制框架。读者朋友如果对该技术有兴趣,可以参阅笔者所著的开源书《Spring Security 教程》(https://github.com/waylau/spring-security-tutorial)以了解更多 Spring Security 方面的内容。 Spring Mobile:对 Spring MVC 的扩展,旨在简化移动 Web 应用的开发。 Spring for Android:用于简化 Android 原生应用程序开发的 Spring 扩展。 Spring Boot:是 Spring 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。Spring Boot 为 Spring 平台及第三方库提供开箱即用的设置,这样你就可以有条不紊地来进行应用的开发。多数 Spring Boot 应用只需要很少的 Spring 配置。通过这种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域成为领导者。读者朋友如果对该技术有兴趣,可以参阅笔者所著的开源书《Spring Boot 教程》(https://github.com/waylau/spring-boot-tutorial)是了解更多 Spring Boot 方面的内容。本书的“第25章 Spring Boot”也会对 Spring Boot 做深入的探讨。 Spring Cloud:使用 Spring Cloud,开发人员可以开箱即用的实现分布式系统中常用的服务。这些服务可以任何环境下运行,包括分布式环境,也包括开发人员自己的笔记本电脑、裸机数据中心,以及 Cloud Foundry 等托管平台。Spring Cloud 基于 Spring Boot 来进行构建服务,并可以轻松地集成第三方类库,来增强应用程序的行为。读者如果对该技术有兴趣,可以参阅笔者所著的开源书《Spring Cloud 教程》(https://github.com/waylau/spring-cloud-tutorial)是了解更多 Spring Cloud 方面的内容。本书的“第26章 Spring Cloud”也会对 Spring Cloud 做深入的探讨。 ...... Spring 的技术栈还有很多,读者朋友有兴趣的话,可以访问笔者所著的“Spring 5案例大全”项目(<https://github.com/waylau/spring-5-book)了解更多信息。 约定 由于 Spring 是早期 Spring 框架的总称,所以,有时候这个“Spring”这个命名会给读者产生困扰。一般地,我们约定“Spring 框架”特指是狭义上的 Spring,即 Spring Framework;而“Spring”特指是广义上的 Spring,泛指 Spring 技术栈。 参考引用 原文同步至:https://waylau.com/what-is-spring/ 《Spring 5 开发大全》:https://book.douban.com/subject/30370024/
本文介绍了Java虚拟机(Java SE 11版本)加载类和接口。 加载类和接口 加载是指查找具有特定名称的类或接口类型的二进制形式的过程。典型的做法是,查找事先由Java编译器从源代码计算而来二进制表示,但也可能是通过动态计算。二进制形式最终会构造成一个Class对象。 加载的精确语义在Java Java Machine Specification,Java SE 11 Edition的第5章中给出。在这里,我们从Java编程语言的角度概述了该过程。 类或接口的二进制格式通常是上面引用的Java虚拟机规范Java SE 11版中描述的类文件格式,但只要满足第13.1节中规定的要求,其他格式也是可能的。 类ClassLoader的方法defineClass可用于从类文件格式的二进制表示构造Class对象。 表现良好的类加载器维护这些性质: 给定相同的名称,一个好的类加载器应该总是返回相同的类对象。 如果类加载器L1将类C的加载委托给另一个加载器L2,那么对于作为直接超类或C的直接超接口出现的任何类型T,或作为C中的字段类型,或作为类型方法的正式参数或C中的构造函数,或者作为C,L1和L2中方法的返回类型应该返回相同的Class对象。 恶意类加载器可能违反这些性质。但是,它不能破坏类型系统的安全性,因为Java虚拟机可以防范这种情况。 有关这些问题的进一步讨论,请参阅Java虚拟机规范,Java SE 11版和Java虚拟机中的动态类加载,作者:Sheng Liang和Gilad Bracha,作为ACO SIGPLAN发布的OOPSLA '98会议录。通告,第33卷,第10期,1998年10月,第36-44页。Java编程语言设计的基本原则是运行时类型系统不能被用Java编程语言编写的代码破坏,即使是这样的实现也是如此。否则敏感的系统类如ClassLoader和SecurityManager。 加载过程 加载过程由类ClassLoader及其子类实现。 ClassLoader的不同子类可以实现不同的加载策略。特别地,类加载器可以缓存类和接口的二进制表示,基于预期的使用来预取它们,或者将一组相关的类加载在一起。例如,如果找不到新编译的类,因为旧版本由类加载器缓存,这些活动可能对正在运行的应用程序不完全透明。但是,类加载器的责任是仅在程序中可能出现的情况下反映加载错误,而无需预取或组加载。 如果在类加载期间发生错误,那么将在程序中(直接或间接)使用该类型的任何点抛出类LinkichError的以下子类之一的实例: ClassCircularityError:无法加载类或接口,因为它将是自己的超类或超接口(第8.1.4节,第9.1.3节,第13.4.4节)。 ClassFormatError:声称指定所请求的编译类或接口的二进制数据格式错误。 NoClassDefFoundError:相关类加载器无法找到所请求的类或接口的定义。 因为加载涉及新数据结构的分配,所以它可能会因OutOfMemoryError而失败。 参考引用 原文同步至:https://waylau.com/jvm-loading-of-classes-and-interfaces/ Java 虚拟机规范(第11版)》:https://github.com/waylau/java-virtual-machine-specification
MyBatis虽然有很好的SQL执行性能,但毕竟不是完整的ORM框架,不同的数据库之间SQL执行还是有差异。笔者最近在升级 Oracle 驱动至 ojdbc 7 ,就发现了处理DATE类型存在问题。还好MyBatis提供了使用自定义TypeHandler转换类型的功能。 本文介绍如下使用 TypeHandler 实现日期类型的转换。 问题背景 项目中有如下的字段,是采用的DATE类型: birthday = #{birthday, jdbcType=DATE}, 在更新 Oracle 驱动之前,DateOnlyTypeHandler会做出处理,将 jdbcType 是 DATE 的数据转为短日期格式(‘年月日’)插入数据库。毕竟是生日嘛,只需要精确到年月日即可。 但是,升级 Oracle 驱动至 ojdbc 7 ,就发现了处理DATE类型存在问题。插入的数据格式变成了长日期格式(‘年月日时分秒’),显然不符合需求了。 解决方案: MyBatis提供了使用自定义TypeHandler转换类型的功能。可以自己写个TypeHandler来对 DATE 类型做特殊处理: /** * Welcome to https://waylau.com */ package com.waylau.lite.mall.type; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.text.DateFormat; import java.util.Date; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; /** * 自定义TypeHandler,用于将日期转为'yyyy-MM-dd' * * @since 1.0.0 2018年10月10日 * @author <a href="https://waylau.com">Way Lau</a> */ @MappedJdbcTypes(JdbcType.DATE) @MappedTypes(Date.class) public class DateShortTypeHandler extends BaseTypeHandler<Date> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException { DateFormat df = DateFormat.getDateInstance(); String dateStr = df.format(parameter); ps.setDate(i, java.sql.Date.valueOf(dateStr)); } @Override public Date getNullableResult(ResultSet rs, String columnName) throws SQLException { java.sql.Date sqlDate = rs.getDate(columnName); if (sqlDate != null) { return new Date(sqlDate.getTime()); } return null; } @Override public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException { java.sql.Date sqlDate = rs.getDate(columnIndex); if (sqlDate != null) { return new Date(sqlDate.getTime()); } return null; } @Override public Date getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { java.sql.Date sqlDate = cs.getDate(columnIndex); if (sqlDate != null) { return new Date(sqlDate.getTime()); } return null; } } 如果是 Spring 项目,以下面方式进行 TypeHandler 的配置: <!-- 自定义 --> <!--声明TypeHandler bean--> <bean id="dateShortTypeHandler" class="com.waylau.lite.mall.type.DateShortTypeHandler"/> <!-- MyBatis 工厂 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <!--TypeHandler注入--> <property name="typeHandlers" ref="dateShortTypeHandler"/> </bean> 如何使用 TypeHandler 方式1 :指定 jdbcType 为 DATE 比如,目前,项目中有如下的字段,是采用的DATE类型: birthday = #{birthday, jdbcType=DATE}, 方式2 :指定 typeHandler 指定 typeHandler 为我们自定义的 TypeHandler: birthday = #{birthday, typeHandler=com.waylau.lite.mall.type.DateShortTypeHandler}, 源码 见https://github.com/waylau/lite-book-mall 参考引用 原文同步至https://waylau.com/mybatis-type-handler/
MySQL 8 带来了全新的体验,比如支持 NoSQL、JSON 等,拥有比 MySQL 5.7 两倍以上的性能提升。本文讲解如何在 Windows 下安装 MySQL 8,以及基本的 MySQL 用法。 下载 下载地址 https://dev.mysql.com/downloads/mysql/8.0.html。 本例为:MySQL Community Server 8.0.12。 解压 解压至安装目录,比如 D 盘根目录下。 本例为:D:\mysql-8.0.12-winx64。 创建 my.ini my.ini 是 MySQL 安装的配置文件: [mysqld] # 安装目录 basedir=D:\\mysql-8.0.12-winx64 # 数据存放目录 datadir=D:\\mysqlData\\data my.ini放置在 MySQL 安装目录的根目录下。需要注意的是,要先创建D:\mysqlData目录。data目录是由 MySQL 来创建。 初始化安装 执行: mysqld --defaults-file=D:\mysql-8.0.12-winx64\my.ini --initialize --console 控制台输出如下,说明安装成功: >mysqld --defaults-file=D:\mysql-8.0.12-winx64\my.ini --initialize --console 2018-08-20T16:14:45.287448Z 0 [System] [MY-013169] [Server] D:\mysql-8.0.12-winx64\bin\mysqld.exe (mysqld 8.0.12) initializing of server in progress as process 5012 2018-08-20T16:14:45.289628Z 0 [ERROR] [MY-010457] [Server] --initialize specified but the data directory has files in it. Aborting. 2018-08-20T16:14:45.299329Z 0 [ERROR] [MY-010119] [Server] Aborting 2018-08-20T16:14:45.301316Z 0 [System] [MY-010910] [Server] D:\mysql-8.0.12-winx64\bin\mysqld.exe: Shutdown complete (mysqld 8.0.12) MySQL Community Server - GPL. D:\mysql-8.0.12-winx64\bin>mysqld --defaults-file=D:\mysql-8.0.12-winx64\my.ini --initialize --console 2018-08-20T16:15:25.729771Z 0 [System] [MY-013169] [Server] D:\mysql-8.0.12-winx64\bin\mysqld.exe (mysqld 8.0.12) initializing of server in progress as process 18148 2018-08-20T16:15:43.569562Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: L-hk!rBuk9-. 2018-08-20T16:15:55.811470Z 0 [System] [MY-013170] [Server] D:\mysql-8.0.12-winx64\bin\mysqld.exe (mysqld 8.0.12) initializing of server has completed 其中,“L-hk!rBuk9-.”就是 root 用户的初始化密码。稍后可以做更改。 启动、关闭 MySQL server 执行mysqld就能启动 MySQL server,或者执行 mysqld --console可以看到完整的启动信息: >mysqld --console 2018-08-20T16:18:23.698153Z 0 [Warning] [MY-010915] [Server] 'NO_ZERO_DATE', 'NO_ZERO_IN_DATE' and 'ERROR_FOR_DIVISION_BY_ZERO' sql modes should be used with strict mode. They will be merged with strict mode in a future release. 2018-08-20T16:18:23.698248Z 0 [System] [MY-010116] [Server] D:\mysql-8.0.12-winx64\bin\mysqld.exe (mysqld 8.0.12) starting as process 16304 2018-08-20T16:18:27.624422Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed. 2018-08-20T16:18:27.793310Z 0 [System] [MY-010931] [Server] D:\mysql-8.0.12-winx64\bin\mysqld.exe: ready for connections. Version: '8.0.12' socket: '' port: 3306 MySQL Community Server - GPL. 关闭,可以执行 mysqladmin -u root shutdown。 使用 MySQL 客户端 使用 mysql 来登录,账号为 root,密码为“L-hk!rBuk9-.”: >mysql -u root -p Enter password: ************ Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 11 Server version: 8.0.12 Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 执行下面的语句来改密码。其中“123456”即为新密码。 mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '123456'; Query OK, 0 rows affected (0.13 sec) MySQL 常用指令 显示已有的数据库: mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 4 rows in set (0.08 sec) 创建新的数据库: mysql> CREATE DATABASE lite; Query OK, 1 row affected (0.19 sec) 使用数据库: mysql> USE lite; Database changed 建表: 建表执行: mysql> CREATE TABLE t_user (user_id BIGINT NOT NULL, username VARCHAR(20)); Query OK, 0 rows affected (0.82 sec) 查看表: 查看数据库中的所有表: mysql> SHOW TABLES; +----------------+ | Tables_in_lite | +----------------+ | t_user | +----------------+ 1 row in set (0.00 sec) 查看表的详情: mysql> DESCRIBE t_user; +----------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +----------+-------------+------+-----+---------+-------+ | user_id | bigint(20) | NO | | NULL | | | username | varchar(20) | YES | | NULL | | +----------+-------------+------+-----+---------+-------+ 2 rows in set (0.00 sec) 插入数据: mysql> INSERT INTO t_user(user_id, username) VALUES(1, '老卫'); Query OK, 1 row affected (0.08 sec) 参考引用 原文同步至https://waylau.com/installing-mysql-8-on-windows/
学员们在参与“基于Spring Boot的博客系统实战”课程的时候,可能没有太注意版本的问题。其实,版本是一个非常重要也是一个非常容易忽略的问题。 版本不一致会导致各种奇怪的问题,比如: 应用启动不了 启动过程中报错 提示找不到 class 功能不正常 等等 初级学员往往不重视软件版本,存在随意更改版本的现象,从而导致上述问题。同时,学员又缺乏调试程序的能力,一旦出错,将不知所措。认为代码都是跟老师的一样的啊,但是怎么就运行不成功呢? 所以,环境、版本需要纳入和代码同等重要的位置。从某种意义上来讲,环境配置、版本配置都是源码,都要纳入源码管理系统之中。 以下,是课程中学员经常犯的常见的由于环境、版本不一致所导致的问题: Spring RestTemplate 调用天气预报接口乱码的解决 Spring Data Elasticsearch与ES的关系 使用 Bootstrap 4 正式版重新定义网站的新Style! Elasticsearch 使用中文分词 Spring Data Elasticsearch与Elasticsearch的版本关系 Spring Boot自定义版本 学员们可以对照检查。 如何避免此类错误 简单一句话“莫装逼”。如果能力还不够的话,建议严格按照课程的所采用的版本来。因为课程中所有的软件版本、环境都是经过老师严格测试,并确保可用的。避免此类错误,有效节省学员试错的时间。时间就是money哦~ 以下是《基于Spring Boot的博客系统实战》课程所使用的版本: * JDK 8 * Gradle 3.5 * Eclipse Neon.2 Release (4.6.2):本书示例采用Eclipse编写,但示例源码与具体的IDE无关,读者朋友可以自行选择适合自己的IDE,比如IntelliJ IDEA、NetBeans等。 * Spring Boot 1.5.2.RELEASE * Thymeleaf 3.0.3.RELEASE * Thymeleaf Layout Dialec 2.2.0 * MySQL Community Server 5.7.17 * MySQL Workbench 6.3.9 * Spring Data JPA 1.11.1.RELEASE * Hibernate 5.2.8.Final * MySQL Connector/J 6.0.5 * H2 Database 1.4.193 * Elasticsearch 2.4.4 * Spring Data Elasticsearch 2.1.3.RELEASE * JNA 4.3.0 * Tether 1.4.0 :<http://tether.io/> * Bootstrap v4.0.0-alpha.6 : <https://v4-alpha.getbootstrap.com/> * jQuery 3.1.1 : <http://jquery.com/download/> * Font Awesome 4.7.0 :<http://fontawesome.io> * NProgress 0.2.0 :<http://ricostacruz.com/nprogress/> * Thinker-md :<http://git.oschina.net/benhail/thinker-md> * jQuery Tags Input 1.3.6 : <http://xoxco.com/projects/code/tagsinput/> * Bootstrap Chosen 1.0.3 :<https://github.com/haubek/bootstrap4c-chosen> * toastr 2.1.1 :<http://www.toastrjs.com/> * Spring Security 4.2.2.RELEASE * Thymeleaf Spring Security 3.0.2.RELEASE * Apache Commons Lang 3.5 * Markdown parser for the JVM 0.16 * MongoDB 3.4.4 * Embedded MongoDB 2.0.0 当对讲师的代码熟悉了之后,同时,掌握了调错的能力之后,学员就可以根据自己的情况来调整版本了。 比如,将 Spring Boot 升级到 Spring Boot 2 、使用最新的 ES 版本等。 想学 Spring Boot 2 目前,上述课程是基于 Spring Boot 1.5.2 来讲解的,新版本的 Spring Boot 大致上也是差不多的。学员可以举一反三。 如果想直接学 Spring Boot 2 ,可以关注老师出版的另外一门书《Spring Boot 企业级应用开发实战》(https://book.douban.com/subject/30192752/),基于最新的 Spring Boot 2 来展开。可以理解为是上述课程的升级。对课程的版本进行了升级,同时补充了课程中无法展开的知识点的梳理。 该书所使用的版本如下: * JDK 8 * Gradle 4.0 * Eclipse Oxygen Release (4.7.0):本书示例采用 Eclipse 编写,但示例源码与具体的 IDE 无关,读者朋友可以自行选择适合自己的 IDE,比如 IntelliJ IDEA、NetBeans 等。 * Spring Boot 2.0.0.M2 * Spring 5.0.0.RC2 * Thymeleaf 3.0.6.RELEASE * Thymeleaf Layout Dialect 2.2.2 * MySQL Community Server 5.7.17 * MySQL Workbench 6.3.9 * Spring Data JPA 2.0.0.M4 * Hibernate 5.2.10.Final * MySQL Connector/J 6.0.5 * H2 Database 1.4.196 * Elasticsearch 5.5.0 * Spring Data Elasticsearch 3.0.0.M4 * Tether 1.4.0 :<http://tether.io/> * Bootstrap v4.0.0-alpha.6 : <https://v4-alpha.getbootstrap.com/> * jQuery 3.1.1 : <http://jquery.com/download/> * Font Awesome 4.7.0 :<http://fontawesome.io> * NProgress 0.2.0 :<http://ricostacruz.com/nprogress/> * Thinker-md :<http://git.oschina.net/benhail/thinker-md> * jQuery Tags Input 1.3.6 : <http://xoxco.com/projects/code/tagsinput/> * Bootstrap Chosen 1.0.3 :<https://github.com/haubek/bootstrap4c-chosen> * toastr 2.1.1 :<http://www.toastrjs.com/> * Spring Security 5.0.0.M2 * Thymeleaf Spring Security 3.0.2.RELEASE * Apache Commons Lang 3.6 * Markdown parser for the JVM 0.16 * MongoDB 3.4.6 * Spring Data Mongodb 2.0.0.M4 * Embedded MongoDB 2.0.0 * IK Analysis for Elasticsearch 5.5.0 看视频还是看书 从我个人来看,看视频、看书是两种非常不同的学习方式。视频更加生动且易于理解。而书则更加全面且方便回顾。 如何选择看视频还是看书来学习,取决于个人学习习惯。当然,学习是不嫌多的,如果有充足的时间不妨书和视频一起结合起来看。 老卫课程的优点就是,所有的视频课程,都是会有相关配套的书籍提供的,以利于学员知识点的回顾。视频、看书两不误,双管齐下,老卫更懂你。 以下是老卫的课程,以及对应的书籍: 视频课程 对应书籍 基于Spring Boot的博客系统实战 Spring Boot 企业级应用开发实战 基于Spring Cloud的微服务实战 Spring Cloud 微服务架构开发实战
本文讲解 MyBatis 四大核心概念(SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、Mapper)。 MyBatis 作为互联网数据库映射工具界的“上古神器”,训有四大“神兽”,谓之:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、Mapper。可以说,了解了这四大核心,便可知 MyBatis 八九。 SqlSessionFactoryBuilder 从命名上可以看出,这个是一个 Builder 模式的,用于创建 SqlSessionFactory 的类。SqlSessionFactoryBuilder 根据配置来构造 SqlSessionFactory。 其中配置方式有两种 1. XML 文件方式 XML 文件方式是作为常用的一种方式: String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); mybatis-config.xml 就是我们的配置文件: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="org/mybatis/example/BlogMapper.xml"/> </mappers> </configuration> 2. Java Config 这是第二种配置方式,通过 Java 代码来配置: DataSource dataSource = BlogDataSourceFactory.getBlogDataSource(); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(BlogMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); Java Config 相比较 XML 文件的方式而言,会有一些限制。比如修改了配置文件需要重新编译,注解方式没有 XML 配置项多等。所以,业界大多数情况下是选择 XML 文件的方式。但到底选择哪种方式,这个要取决与自己团队的需要。比如,项目的 SQL 语句不复杂,也不需要一些高级的 SQL 特性,那么 Java Config 则会更加简洁一点;反之,则可以选择 XML 文件的方式。 SqlSessionFactory SqlSessionFactory 顾名思义,是用于生产 SqlSession 的工厂。 通过如下的方式来获取 SqlSession 实例: SqlSession session = sqlSessionFactory.openSession(); SqlSession SqlSession 包含了执行 SQL 的所有的方法。以下是示例: SqlSession session = sqlSessionFactory.openSession(); try { Blog blog = session.selectOne( "org.mybatis.example.BlogMapper.selectBlog", 101); } finally { session.close(); } 当然,下面的方式可以做到类型安全: SqlSession session = sqlSessionFactory.openSession(); try { BlogMapper mapper = session.getMapper(BlogMapper.class); Blog blog = mapper.selectBlog(101); } finally { session.close(); } Mapper Mapper 顾名思义,是用做 Java 与 SQL 之间的映射的。包括了 Java 映射为 SQL 语句,以及 SQL 返回结果映射为 Java。 比如,下面是一个常见的 Mapper 接口映射文件: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.mybatis.example.BlogMapper"> <select id="selectBlog" resultType="Blog"> select * from Blog where id = #{id} </select> </mapper> 其中 "org.mybatis.example.BlogMapper" 就是我们要射射的接口,selectBlog 就是BlogMapper上的方法。而这个 selectBlog 具体就是要执行“select * from Blog where id = #{id}”这个 SQL 语句。 这样,我们就能通过 Blog blog = session.selectOne( "org.mybatis.example.BlogMapper.selectBlog", 101); 或者是 BlogMapper mapper = session.getMapper(BlogMapper.class); Blog blog = mapper.selectBlog(101); 来获取到执行的结果。 当然,如果是采用注解的方式的话,可以省去 XML 文件: public interface BlogMapper { @Select("SELECT * FROM blog WHERE id = #{id}") Blog selectBlog(int id); } 参考引用 原文同步至https://waylau.com/mybatis-core-concepts/
Spring RestTemplate 调用天气预报接口可能遇到中文乱码的问题,解决思路如下。 问题出现 我们在网上找了一个免费的天气预报接口 http://wthrcdn.etouch.cn/weather_mini?citykey=101280601。我们希望调用该接口,并将返回的数据解析为 JSON 格式。 核心业务逻辑如下: private WeatherResponse doGetWeatherData(String uri) { ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class); String strBody = null; if (response.getStatusCodeValue() == 200) { strBody = response.getBody(); } ObjectMapper mapper = new ObjectMapper(); WeatherResponse weather = null; try { weather = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { e.printStackTrace(); } return weather; } 在浏览器里面访问该接口都挺正常。如下图所示: 但在纯 Spring 应用里面,尝试使用 RestTemplate 来调用,结果解析数据为 JSON 失败,因为数据有乱码。如下图所示: 尝试进行编码转换 一开始,我们认为这可能是对方转过来的数据不是 UTF-8 导致的,所以,尝试加入了消息转换器。 @Configuration public class RestConfiguration { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); // 支持中文编码 return restTemplate; } } StringHttpMessageConverter 默认是 ISO_8859_1,所以我们设置为了 UTF_8。 再次执行,发现仍然是乱码。 找到问题的根源 这一次我没有再瞎猜了,而是仔细观察了 HTTP 的请求协议。发现消息头里面的蛛丝马迹: 原来,数据是经过 GZIP 压缩过的。默认情况下, RestTemplate 使用的是 JDK 的 HTTP 调用器,并不支持 GZIP 解压,难怪解析不了。 解决方案 既然找到了问题所在,解决起来就简单了。主要考虑了以下几种方案。 1. 编写 GIZP 工具类 处理 Gizp 压缩的数据的工具类如下: /** * Welcome to https://waylau.com */ package com.waylau.spring.mvc.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.GZIPInputStream; /** * String Util. * * @since 1.0.0 2018年3月27日 * @author <a href="https://waylau.com">Way Lau</a> */ public class StringUtil { /** * 处理 Gizp 压缩的数据. * * @param str * @return * @throws IOException */ public static String conventFromGzip(String str) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in; GZIPInputStream gunzip = null; in = new ByteArrayInputStream(str.getBytes("ISO-8859-1")); gunzip = new GZIPInputStream(in); byte[] buffer = new byte[256]; int n; while ((n = gunzip.read(buffer)) >= 0) { out.write(buffer, 0, n); } return out.toString(); } } 核心业务逻辑如下: private WeatherResponse doGetWeatherData(String uri) { ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class); String strBody = null; if (response.getStatusCodeValue() == 200) { try { strBody = StringUtil.conventFromGzip(response.getBody()); } catch (IOException e) { e.printStackTrace(); } } ObjectMapper mapper = new ObjectMapper(); WeatherResponse weather = null; try { weather = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { e.printStackTrace(); } return weather; } 2. 使用 Apache HttpClient 使用 Apache HttpClient 作为 REST 客户端。Apache HttpClient 内置了对于 GZIP 的支持 @Configuration public class RestConfiguration { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate( new HttpComponentsClientHttpRequestFactory()); // 使用HttpClient,支持GZIP restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); // 支持中文编码 return restTemplate; } } 核心业务逻辑如下: private WeatherResponse doGetWeatherData(String uri) { ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class); String strBody = null; if (response.getStatusCodeValue() == 200) { strBody = response.getBody(); } ObjectMapper mapper = new ObjectMapper(); WeatherResponse weather = null; try { weather = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { e.printStackTrace(); } return weather; } 当然,使用该方案,需要引入 Apache HttpClient 的依赖。 最终效果,完美! 在 Spring Boot 中所使用的差异 也有学员问到,为啥我在“基于Spring Cloud的微服务实战”课程中,没有同样也是使用 RestTemplate, 调用同样的接口,为啥没有出现乱码的问题? 其实,细心的学员应该发现,在课程中,我们同样也是使用了 Apache HttpClient,由于 Spring Cloud 本身也是基于 Spring Boot 来构建的,所以屏蔽了很多消息转换的细节而言。 以下是 Spring Boot 中通过 RestTemplateBuilder 来构建 RestTemplate 的方式: @Configuration public class RestConfiguration { @Autowired private RestTemplateBuilder builder; @Bean public RestTemplate restTemplate() { return builder.build(); } } 所以学习编码,知其然要知其所以然! 源码 本文示例源码,见 “Spring 5 案例大全”(https://github.com/waylau/spring-5-book) 的 “基于 RestTemplate 的天气预报服务”例子 参考引用: 《Spring Boot 教程》:https://github.com/waylau/spring-boot-tutorial 《基于Spring Boot的博客系统实战》:http://coding.imooc.com/class/125.html 原文同步至https://waylau.com/spring-resttemplate-gzip/
“Stop Trying to Reinvent the Wheel(不要重复造轮子 )”, 可能是每个程序员入行被告知的第一条准则。在公司里面,我也会对团队里面每个新进的成员反复灌输这个理念。但要真正做到这一点也非易事。 寻找轮子 所谓“轮子”可以理解为行业里面的技术解决方案。特别是当今开源社区的盛行,开源软件以及开源技术方案层出不穷,这为寻找轮子提供了丰富的途径。 避免重复造轮子 一个轮子能够个被复用,体现了软件的复用性。 使用轮子,本身就是“站在巨人的肩膀上”,最大化享受当今技术所带来的便利,避免了从零开始开发的繁琐以及复杂,有效降低成本。 以ORM框架为例,在Java邻域,真正做到强大的唯有Hibernate几家,一方面,开发如何自己来实现ORM在技术有一定的难度,另一方面,开发自己的ORM需要很多的人力以及技术投入,一般的小公司根本没有能力来做这方面的研发。 以 GNU/Linux 操作系统为例,如果不是 Linux 作为操作系统内核,提供给了 GNU 计划,那么 GNU 也许不知道要延迟多少年才能推出自己的操作系统;另一方面,GNU 计划,提供了很多 GNU/Linux 操作系统的生态软件,让 GNU/Linux 操作系统更为普通用户所接受。 轮子难找 因为选择很多,所以很难选。目前,软件行业空前发展,以分布式消息服务框架,光开源的产品就有很多,包括Apache ActiveMQ、RabbitMQ、Apache Kafka、Apache RocketMQ等。这就对个人的信息检索能力有非常高的要求。同时,也正因为有很多选择性,这也对用户各种技术要有一定的了解,各种技术的优缺点要了如指掌,才能做出合理的选择。这对于用户的技术要有很高的要求。老卫的《分布式系统常用技术及案例分析》一书,为开发者提供了非常好的选择依据。 其次,这个轮子好不好用,需要时间来论证。不能一眼就判断出一个项目的质量以及易用性,这其实需要大量项目经验的积累。即便做出了选择,也需要一定的时间来验证合理性。毕竟项目很多问题,只能在运行中才能体现出来。 最后一点是,好轮子需要打磨。要想将一个开源项目成功整合到自己的项目中,需要对这个项目有比较深入的了解。大部分的开源项目的文档质量参差不齐,当使用轮子时,只看文档往往是不够的,还需要阅读源代码甚至深度修改定制。即便集成到了自己的项目中,也可能需要不断的进行修正调优以符合自己项目的实际。 掌握IT技能的“复制-粘贴-改” 合理复用现有的好轮子,以最大化降低开发项目的成本。如果说,轮子代表了框架级别的可复用的项目,那么“复制-粘贴-改”则是更加精细颗粒度的代码层次的复用。 很多开发者都很鄙视“复制-粘贴-改”,认为所有的代码必须要从0开始敲出来才能真正牛人。这其实有失偏驳。比如,开发人员C君在A项目里面写了一段方法,经历住了上线的压力,目前也在线上能够稳定运行。那么如果项目B也需要同样功能的方法,C君有必要再从头去敲一遍代码呢?为什么不直接“复制-粘贴”,除非有特殊的需求或者有对该方法的更好的改进,那么大胆的“复制-粘贴-改”即可。或者将该方法重构出来,放到项目A和B共享的公用项目里面不是很好?从头敲一遍已有的代码,除了增加自己的敲错的机率意外,实在不知道还有什么好处。当然,可能锻炼了打字速度。 也掌握“复制-粘贴-改”的技能,最难是要做到“改”这一点,因为这一点做不好就是抄袭了。以前,老是说腾讯的软件大多数模仿云云,但恰恰是人家“改”的功能出神入化,时间证明了,那些曾经被腾讯模拟的公司、项目,要不死了要不半死不活,而腾讯却稳坐大佬的位置,可见“改”功的重要性。 很多配置文件,其实能少打就不打。比如 Maven 的 pom 文件坐标,你想要什么类库直接上中央仓库去查找并“复制-粘贴-改”下即可,实现是没有必要手打,打错了XML也不会给你提示哦。 以下是 MvnRepository 提供的Maven坐标。 真实案例 理解了复用了轮子的理论重要性,我们来看下真实的案例。 据统计,Google 有软件工程师平均一天写 100 ~ 150 行代码,而且已经算是业界高效的了。这从侧面反馈了,写代码并不是一天中工作的重心,高效的工程师更加注重于花时间在代码的思考上面。 笔者的视频课程《基于Spring Boot的博客系统实战》(http://coding.imooc.com/class/125.html),遵循了业界软件开发的真实开发方式,采用敏捷的渐进式开发的,后续章节会基于前一个章节的内容来延生。这个不是重复,而是最大化重用现有的代码。递进的每个章节的内容都是前后关联的,让前几章的代码内容,为后续章节所用,即为“学以致用”。 当然,这种开发方式也不是见得为所有的开发者所接受。正如开篇所讲的,热衷于重复造论的人还是不少。 参考引用: 《分布式系统常用技术及案例分析》:https://github.com/waylau/distributed-systems-technologies-and-cases-analysis 《基于Spring Boot的博客系统实战》:http://coding.imooc.com/class/125.html 原文同步至https://waylau.com/stop-trying-to-reinvent-the-wheel/
Jetty 是高性能的 Servlet 容器,经常会在开发环境中作为服务器来使用。在本文中,我们将使用 Spring Web MVC 技术来实现 REST 接口,并使用 使用 Jetty 作为内嵌服务器,方便测试。 接口设计 我们将会在系统中实现两个接口: GET http://localhost:8080/hello GET http://localhost:8080/hello/way 其中,第一个接口“/hello”将会返回“Hello World!” 的字符串;而第二个接口“/hello/way”则会返回一个包含用户信息的JSON字符串。 系统配置 我们需要在应用中添加如下依赖: <properties> <spring.version>5.0.4.RELEASE</spring.version> <jetty.version>9.4.9.v20180320</jetty.version> <jackson.version>2.9.4</jackson.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> <version>${jetty.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> </dependencies> 其中, spring-webmvc 是为了使用 Spring MVC 的功能。 jetty-servlet是为了提供内嵌的 Servlet 容器,这样我们就无需依赖外部的容器,可以直接运行我们的应用。 jackson-core 和 jackson-databind 为我们的应用提供 JSON 序列化的功能。 后台编码实现 领域模型 创建一个 User 类,代表用户信息。 public class User { private String username; private Integer age; public User(String username, Integer age) { this.username = username; this.age = age; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } } 控制器 创建 HelloController 用于处理用户的请求。 @RestController public class HelloController { @RequestMapping("/hello") public String hello() { return "Hello World! Welcome to visit waylau.com!"; } @RequestMapping("/hello/way") public User helloWay() { return new User("Way Lau", 30); } } 其中,映射到“/hello”的方法将会返回“Hello World!” 的字符串;而映射到“/hello/way”则会返回一个包含用户信息的JSON字符串。 应用配置 在本应用中,我们采用基于 Java 注解的配置。 AppConfiguration 是我们的主应用配置: import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @ComponentScan(basePackages = { "com.waylau.spring" }) @Import({ MvcConfiguration.class }) public class AppConfiguration { } AppConfiguration 会扫描“com.waylau.spring”包下的文件,并自动将相关的 bean 进行注册。 AppConfiguration 同时又引入了 MVC 的配置类 MvcConfiguration: @EnableWebMvc @Configuration public class MvcConfiguration implements WebMvcConfigurer { public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new MappingJackson2HttpMessageConverter()); } } MvcConfiguration 配置类一方面启用了 MVC 的功能,另一方面添加了 Jackson JSON 的转换器。 最后,我们需要引入 Jetty 服务器 JettyServer: import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; import com.waylau.spring.mvc.configuration.AppConfiguration; public class JettyServer { private static final int DEFAULT_PORT = 8080; private static final String CONTEXT_PATH = "/"; private static final String MAPPING_URL = "/*"; public void run() throws Exception { Server server = new Server(DEFAULT_PORT); server.setHandler(servletContextHandler(webApplicationContext())); server.start(); server.join(); } private ServletContextHandler servletContextHandler(WebApplicationContext context) { ServletContextHandler handler = new ServletContextHandler(); handler.setContextPath(CONTEXT_PATH); handler.addServlet(new ServletHolder(new DispatcherServlet(context)), MAPPING_URL); handler.addEventListener(new ContextLoaderListener(context)); return handler; } private WebApplicationContext webApplicationContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(AppConfiguration.class); return context; } } JettyServer 将会在 Application 类中进行启动: public class Application { public static void main(String[] args) throws Exception { new JettyServer().run();; } } 11.13.6 运行 在编辑器中,直接运行 Application 类即可。启动之后,应能看到如下控制台信息: 2018-03-21 23:14:52.665:INFO::main: Logging initialized @203ms to org.eclipse.jetty.util.log.StdErrLog 2018-03-21 23:14:52.868:INFO:oejs.Server:main: jetty-9.4.9.v20180320; built: 2018-03-20T20:21:10+08:00; git: 1f8159b1e4a42d3f79997021ea1609f2fbac6de5; jvm 1.8.0_112-b15 2018-03-21 23:14:52.902:INFO:oejshC.ROOT:main: Initializing Spring root WebApplicationContext 三月 21, 2018 11:14:52 下午 org.springframework.web.context.ContextLoader initWebApplicationContext 信息: Root WebApplicationContext: initialization started 三月 21, 2018 11:14:52 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh 信息: Refreshing Root WebApplicationContext: startup date [Wed Mar 21 23:14:52 CST 2018]; root of context hierarchy 三月 21, 2018 11:14:52 下午 org.springframework.web.context.support.AnnotationConfigWebApplicationContext loadBeanDefinitions 信息: Registering annotated classes: [class com.waylau.spring.mvc.configuration.AppConfiguration] 三月 21, 2018 11:14:53 下午 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry register 信息: Mapped "{[/hello]}" onto public java.lang.String com.waylau.spring.mvc.controller.HelloController.hello() 三月 21, 2018 11:14:53 下午 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry register 信息: Mapped "{[/hello/way]}" onto public com.waylau.spring.mvc.vo.User com.waylau.spring.mvc.controller.HelloController.helloWay() 三月 21, 2018 11:14:53 下午 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter initControllerAdviceCache 信息: Looking for @ControllerAdvice: Root WebApplicationContext: startup date [Wed Mar 21 23:14:52 CST 2018]; root of context hierarchy 三月 21, 2018 11:14:53 下午 org.springframework.web.context.ContextLoader initWebApplicationContext 信息: Root WebApplicationContext: initialization completed in 983 ms 2018-03-21 23:14:53.893:INFO:oejshC.ROOT:main: Initializing Spring FrameworkServlet 'org.springframework.web.servlet.DispatcherServlet-6aaa5eb0' 三月 21, 2018 11:14:53 下午 org.springframework.web.servlet.FrameworkServlet initServletBean 信息: FrameworkServlet 'org.springframework.web.servlet.DispatcherServlet-6aaa5eb0': initialization started 三月 21, 2018 11:14:53 下午 org.springframework.web.servlet.FrameworkServlet initServletBean 信息: FrameworkServlet 'org.springframework.web.servlet.DispatcherServlet-6aaa5eb0': initialization completed in 15 ms 2018-03-21 23:14:53.910:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@2796aeae{/,null,AVAILABLE} 2018-03-21 23:14:54.037:INFO:oejs.AbstractConnector:main: Started ServerConnector@42054532{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} 2018-03-21 23:14:54.038:INFO:oejs.Server:main: Started @1578ms 分别在浏览器中访问 “http://localhost:8080/hello” 和 “http://localhost:8080/hello/way” 地址进行测试,能看到图1和图2的响应效果。 图1 “/hello”接口的返回内容 图2 “/hello/way”接口的返回内容 参考应用 源码:见《Spring 5 案例大全》(https://github.com/waylau/spring-5-book)的 “s5-ch11-mvc-rest”应用。 原文同步至:https://waylau.com/spring-mvc-use-jetty/
本文总结了 JDBC 事务隔离级别。 事务隔离级别定义了在一个事务中,哪些数据是对当前执行的语句“可见”的。在并发访问数据库时,事务隔离级别定义了多个事务之间对于同个目标数据源访问时的可交叉程度。 可交叉程度可分为以下几类。 可交叉程度 dirty reads(脏读) 当一个事务能看见另外一个事务未提交的数据时,就称为脏读,换言之,一个事务修改数据后再未提交之前,就能被其它事务看见。如果这个事务被回滚了而不是提交了,那么其它事务看到的数据则是不正确的,是“脏”的。 nonrepeatable reads(不可重复读) 假设事务 A 读取了一行数据,接下来事务 B 改变了这行数据,之后事务 A 又再一次读取这行数据,这时候事务 A 就取到了两个不同的结果。 phantom reads(幻读) 假设事务 A 通过一个 where 条件读取到了一个结果集,事务 B 这时插入了一条符合事务 A 的 where 条件的数据,之后事务 A 通过同样的 where 条件再次进行查询时,发现了多出来一条数据。 事务隔离级别 JDBC 规范增加了 TRANSACTION_NONE 隔离级别,来满足了 SQL:2003 定义的 4 种事务隔离级别。隔离级别从最宽松到最严格,排序如下所示: TRANSACTION_NONE 这意味着当前的 JDBC 驱动不支持事务,也意味着这个驱动不符合 JDBC 规范。 TRANSACTION_READ_UNCOMMITTED 允许事务看到其它事务修改了但未提交的数据,这意味着有可能是脏读、不可重复读或者幻读。 TRANSACTION_READ_COMMITTED 一个事务在未提交之前,所做的修改不会被其它事务所看见。这能避免脏读,但避免不了不可重复读和幻读。 TRANSACTION_REPEATABLE_READ 避免了脏读和不可重复读,但幻读依然是有可能发生的。 TRANSACTION_SERIALIZABLE 避免了脏读、不可重复读以及幻读。 参考资料 JDBC 4.2 Specification(JSR 221):https://github.com/waylau/jdbc-specification 原文同步至:https://waylau.com/jdbc-transaction-isolation-levels/
有时,为了提升整个网站的性能,我们会将经常需要访问数据缓存起来,这样,在下次查询的时候,能快速的找到这些数据。缓存的使用与系统的时效性有着非常大的关系。当我们的系统时效性要求不高时,则选择使用缓存是极好的。当系统要求的时效性比较高时,则并不适合用缓存。本文,我们将演示如何通过集成 Redis 服务器来进行数据的缓存,以提高微服务的并发访问能力。 为啥我们需要缓存 在之前的文章中,我们已经介绍了如何使用 Spring Boot 来快速实现一个天气预报服务应用micro-weather-basic(见 https://waylau.com/spring-boot-weather-report/)。通过该应用,能实现简单的天气查询。 天气数据接口,本身时效性不是很高,而且又因为是 Web 服务,在调用过程中,本身是存在延时的。所以,采用缓存,一方面可以有效减轻访问天气接口服务带来的延时问题,另一方面,也可以减轻天气接口的负担,提高并发访问量。 特别地,我们是使用的第三方免费的天气 API,这些 API 往往对用户的调用次数及频率有一定的限制。所以为了减轻天气 API 提供方的负荷,我们并不需要实时去调用其第三方接口。 在micro-weather-basic的基础上,我们构建了一个micro-weather-redis项目,作为示例。 开发环境 为了演示本例子,需要采用如下开发环境: JDK 8 Gradle 4.0 Spring Boot Web Starter 2.0.0.M4 Apache HttpClient 4.5.3 Spring Boot Data Redis Starter 2.0.0.M4 Redis 3.2.100 项目配置 Spring Boot Data Redis 提供了 Spring Boot 对 Redis 的开箱即用的功能。在原有的依赖的基础上,添加 Spring Boot Data Redis Starter 的依赖。 // 依赖关系 dependencies { //... // 添加 Spring Boot Data Redis Starter 依赖 compile('org.springframework.boot:spring-boot-starter-data-redis') //... } 下载安装、运行 Redis 在 Linux 平台上安装 Redis 比较简单,可以参考官方文档来即可,详见https://github.com/antirez/redis。 而在 Windows 平台,微软特别为 Redis 制作了安装包,下载地址见 https://github.com/MicrosoftArchive/redis/releases。本书所使用的案例,也是基于该安装包来进行的。双击 redis-server.exe 文件,就能快速启动 Redis 服务器了。 安装后,Redis 默认运行在 """"""<>""<>"""""" """""""""""""""""""""""<<>""""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""""""""""""""""""""""""""""<<>""""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""""""""""""""""""""""""""""<<>""""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>""""""""""""""""""""""""<<>"""""""""""""""""""""""""""
Spring Data Elasticsearch 与 Elasticsearch 其实是两个不同的产品。本文带你简单的了解下,Spring Data Elasticsearch 与 Elasticsearch 的关系。 Elasticsearch Elasticsearch 是 NoSQL 之一,是用于构建大数据全文检索的利器。见课程(https://coding.imooc.com/class/125.html)。Elasticsearch 是采用 Java 编写的,提供丰富的 API 可供用户选择。 学习使用这些 API 是有学习成本的。要完全掌握整套 API 可能需要数月的时间。那么,怎么办呢?是否有速成的方式,来让用户快速上手呢?答案就是 Spring Data Elasticsearch。 Spring Data Elasticsearch 如果你恰好是一个 Spring 的应用(就像 https://waylau.com/spring-boot-blog-video-release/ 或者 https://waylau.com/spring-cloud-video-release/),那么使用 Spring Data Elasticsearch 就是非常不错的选择。因为, Spring Data Elasticsearch 就是整个 Spring Data 家族的一员,有着与 Spring 良好的兼容。 Spring Data Elasticsearch 拥有与 Spring Data 家族其他成员(如 Spring Data JPA)通用的接口,这样,只要你学会了 Spring Data的接口,就可以胜任任何存储设备的使用,不管是关系型数据库MySQL、SQL Server、Oracle 还是NoSQL 譬如Elasticsearch 、MongoDB 等,都可以享用统一接口带来的便利,简直简单的不要不要哦~ 以下就是一个例子: interface PersonRepository extends Repository<Person, Long> { List<Person> findByLastname(String lastname); } 我们要先声明一个业务相关的接口 PersonRepository。 PersonRepository 继承自 Repository 即可。无需写具体的实现代码。 class SomeClient { private final PersonRepository repository; SomeClient(PersonRepository repository) { this.repository = repository; } void doSomething() { List<Person> persons = repository.findByLastname("Lau"); } } 而后,注入这个接口即可使用了,而具体的实现 Spring Data Elasticsearch 会帮我们提供。是否很 Cool! 当然,这个 findByLastname 方法名称的定义还是有讲究的,要符合 Spring Data 的语意。这样 Spring Data 就会自动解析,猜测按照你定义的这个方法名,来生成相应的查询语句。从这个方法名字,我们一眼就能看出来,这个一个根据 Lastname 字段来查找 Person 的语句。 总结 任何技术都有优缺点,使用技术一定要符合自己业务的需要。使用 Spring Data Elasticsearch 还是 Elasticsearch 原生 API ,要自己去做评估。 Elasticsearch 原生 API 好处就是可以第一时间用到 Elasticsearch 的新特性。缺点的学习成本高。 用 Spring Data 的好处是,用统一的接口,适配所有不同的存储类型,如SQL、NoSQL 等等。缺点是,有时候适配的版本要比原生的 API 要慢。这个取决于 Spring Data Elasticsearch 团队的开发速度了。见“Spring Data Elasticsearch与Elasticsearch的版本关系”(https://www.imooc.com/article/19873)。 参考资料 Spring Boot 教程:https://github.com/waylau/spring-boot-tutorial Spring Cloud 教程:https://github.com/waylau/spring-cloud-tutorial https://waylau.com/spring-boot-blog-video-release/ https://waylau.com/spring-cloud-video-release/ https://www.imooc.com/article/19873 原文同步至:https://waylau.com/spring-data-elasticsearch-and-elasticsearch/
很多同学都了解了Spring ,了解了 Spring Boot, 但对于 Spring Cloud 是什么还是比较懵逼的。 本文带你简单的了解下,什么是Spring Cloud。 Spring Cloud 是什么 从字面理解,Spring Cloud 就是致力于分布式系统、云服务的框架。 Spring Cloud 是整个 Spring 家族中新的成员,是最近云服务火爆的必然产物。 Spring Cloud 为开发人员提供了快速构建分布式系统中一些常见模式的工具,例如: 配置管理 服务注册与发现 断路器 智能路由 服务间调用 负载均衡 微代理 控制总线 一次性令牌 全局锁 领导选举 分布式会话 集群状态 分布式消息 ...... 使用 Spring Cloud 开发人员可以开箱即用的实现这些模式的服务和应用程序。这些服务可以任何环境下运行,包括分布式环境,也包括开发人员自己的笔记本电脑以及各种托管平台。 Spring Cloud 与 Spring Boot Spring Cloud 基于 Spring Boot 来进行构建服务。这样,开发Spring Cloud 组件时,就能依托 Spring Boot 来实现快速开发。 有关 Spring Boot 内容,可见笔者的视频课程《基于Spring Boot的博客系统实战》(https://waylau.com/spring-boot-blog-video-release/)。 Spring Cloud 与微服务 Spring Cloud 是构建分布式系统的利器,而微服务是当下最火热的分布式系统的类型之一,所以,Spring Cloud 天然是支持微服务的构建的。 在早些年,国内互联网公司盛行采用 Dubbo 来架构微服务。如今,有了更好的选择,那就是 Spring Cloud。有数据显示,Spring Cloud不管是在国内,还是国外,用户数都呈现出爆发式增长。而且,Dubbo 主要只是为了解决服务通信、服务注册等问题,而 Spring Cloud 却是提供微服务架构的完整的解决方案。 那么什么是微服务? 所谓微服务,就是: 微服务架构风格就像是把小的服务开发成单一应用的形式, 运行在其自己的进程中,并采用轻量级的机制进行通信(一般是 HTTP 资源 API)。这些服务都是围绕业务能力来构建,通过全自动部署工具来实现独立部署。这些服务,其可以使用不同的编程语言和不同的数据存储技术,并保持最小化集中管理。 更多有关微服务的理论,可见笔者的博客:https://waylau.com/ahout-microservices/ 。 Spring Cloud 如何实现微服务 说了那么多理论,那么微服务架构如何真实的落地呢?课程《基于Spring Cloud的微服务实战》(https://waylau.com/spring-cloud-video-release/)给出了真实的答案。 在《基于Spring Cloud的微服务实战》课程中,作者基于Spring Boot + Spring Cloud 技术栈来实现了一个完整的天气预报系统。在课程中,先从 Spring Boot 入手,从0到1 快速搭建了具备高并发能力、界面友好的天气预报系统。而后剖析单块架构的利弊,从而引入微服务架构的概念,并实从1到0实现微服务的拆分。最后引入Spring Cloud 技术来实现对这些微服务的治理,重点讲解了服务注册与发现、服务交互、服务消费、负载均衡、API网关、配置中心、服务熔断、自动扩展等方面的话题。 通过学习该课程,学员不但可以学会 Spring Boot 及 Spring Cloud 最新的周边技术栈(本课程基于最新的 Spring Boot 2.0.0.M4 以及 Spring Cloud Finchley.M2),掌握如何运用上述技术进行整合,搭建框架的能力,熟悉单体架构及微服务架构的特点,并最终实现掌握构建微服务架构的实战能力。 搭建 Spring Cloud 微服务系统需要哪些技术 本课程所涉及的相关的技术有 : XML解析:JABX JSON序列化:Jackson 缓存:Redis 定时器:Quartz Scheduler Java模版技术Thymeleaf 前端样式:Bootstrap API网关:Zuul 服务注册与发现:Eureka Server、Eureka Client 服务交互:RestTemplate、Apache HttpClient 服务消费:Ribbon、OpenFeign 负载均衡:Ribbon 配置中心:Config Server、Config Client 服务熔断:Hystrix 项目构建:Gradle 通过本课程的学习,能够掌握架构微服务系统的能力! 参考资料 要学习 Spring Cloud 微服务,除了上面的课程之前,老卫还撰写了多门微服务相关的开源书籍,注意,都是免费的书籍哦!!! 简述 Microservices(微服务):https://waylau.com/ahout-microservices/ Spring Boot 教程:https://github.com/waylau/spring-boot-tutorial Spring Cloud 教程:https://github.com/waylau/spring-cloud-tutorial Gradle 3 用户指南:https://github.com/waylau/gradle-3-user-guide Spring Security 教程:https://github.com/waylau/spring-security-tutorial Thymeleaf 教程:https://github.com/waylau/thymeleaf-tutorial 小伙伴们,快点学习起来!!!
在一个项目组中,有些人会觉得很闲,无所事事;另外一些人又觉得很忙,像无头的苍蝇。太忙或者太闲,都有可能存在问题。 你为啥会觉得自己很闲 在一个项目组中,作为新人的你,会觉得比较闲,好像领导也没有分配啥任务,或者分配的任务也没啥难度。感觉这样行业也就是这样的,早九晚五很快进入了安逸的状态。 殊不知,危机正悄然临近。 实际上,在国内的软件开发行业,基本上不可能存在空闲的时候,特别是互联网公司,九九六的工作制度大有存在。如果你觉得很闲,可能有以下原因: 1. 领导不愿意分配任务给你 如果你是领导的得力干将,那领导会想法设法的找你办事情,因为你办事让领导觉得靠谱;如果相反,领导都不愿分配任务给你,只能说明。。。 2. 领导分配轻量级的任务给你 同上,如果领导只是分配些简单的任务给你,说明,你在他眼里,只是具备了处理简单任务的能力。 综上,如果你觉得闲了,你要思考下自己的行为方式:是否具备了好员工的条件?是否具备了替团队解决问题的能力?是否具备了替领导分忧的能力? 如果不具备,那么,努力吧,少年! “IT 之路不好走,天天加班累成狗!” 抓紧学习 Spring Boot。 你为啥会觉得自己很忙 在项目组中,你经常会觉得自己很忙。一会而又要参加XXX会议,一会又要紧急修复线上的BUG,同时,还有几个新人要跟你请求如何编码的问题,而后又瞄了几眼问题单,瞬间脑子都大了。 你不得不加班加点。别人都回去了,你还在苦逼的编码。最后,为了能挤上最后的一班公交,你不得不放弃还未解决的任务,心里盘算着明天早点过来,在上班前解决掉。 结果,第二天,由于睡眠不足,脑子发晕,遗留的问题始终没有解决,被Boss在早会上批了。 于是,你开始思考人生,作为老黄牛的你,为啥会得不到上级的满意。你比他人付出了更多的时间,甚至牺牲了个人时间去帮助别人,到头来却没有老板的赏识。你开始感叹世事不公,人情无常。你开始厌恶你的领导,厌恶这个团队,跟着厌恶这家公司。最后,你收拾包袱走人了。 这种案例很多,跟之前的很闲的那帮人相比而言,太忙的人,结局也好不到那里去。 虽然,他已经在很努力的干事,但实际上,很多事情都是“无用功”,看上去很忙,产出很少。因为他缺乏一种能力,做事的能力。 做事能力第一原则,就是目标导向 何为目标导向?简言之,“何时、何人、做何事”。 比如,早会中,我们基本上会确立我们在当天的一个目标。设计XX系统接口、输出项目规则书、实现数据同步功能等等,都可以是不同的目标。有能力的人一天可以实现多个目标,能力差的人可能需要几天才能达成一个目标。但不管怎么样,只要有目标,才有做事的方向、动力。 比如,在你的计划是要今天完成一二三件事,并且要实现评估好,今天是具备做好这三件事的条件的。为啥要评估条件?比如,你今天要完成一个详细设计文档,那么这个工作的前提是要有概要设计说明书。如果你今天确定是能拿到这个概要设计说明书,那么自然,你就能做你想要的事情;否则,这个事情,就会受限制。 这就是要评估做事可能性的原因。 只要确定了这个目标,那就要义无反顾的去做 不要想着,这个事情好简单哦,先玩几分钟手机,吹下逼先。如果你有这种思维,那么八成你的事情是做不完的,因为你做事过程中容易受外界干扰。人家给你一条微信,你就忍不住去看下是什么信息,你的脑子经常被打断。 在执行当天的任务的过程中,要学会去拒绝新的任务。比如,你已经向你的Boss承诺了今天要完成三件事。结果,其他人想安排第四件事情给你,此时,你就要学会say no 。不好意思,今天没有时间,我帮你排到明天。 如果老板说,不行啊,小刘,这个活很重要,影响发工资。擦,那没有办法,这个时候就要学会调整你的工作计划了。毕竟,这个老板的事是优先级第一位嘛。你可能要将计划今天完成的优先级低的任务,排到了明天了。毕竟计划赶不上变化嘛。 但这样子,带来的好处是什么呢?首先,你的任务是可控的。“何时、何人、做何事”是非常清楚的,既然,时间上临时可能有推迟,但仍在你的计划单上;其次,你目标是明确的,细到了当天要完成的内容。完成了当天的任务之后,你信心满满,可以准时回家了。如果,任务提前完成了,你如果有时间还能帮助下同事,或者提前计划下第二天的工作。 这样,老板信任你;你做事有条理不慌乱;进度可控,不用加班;同事满意。这样的你,会觉得很忙吗? 参考引用 https://waylau.com/busy-or-easy/
本文演示了如何使用IK Analysis 插件来实现在 Elasticsearch 中的中文分词功能。 使用中文分词 在“Spring Boot应用企业级博客系统”课程中(http://coding.imooc.com/class/125.html),所有的博客功能都已经完成了。读者朋友们开始愉快地使用博客来发表博客了。但如果朋友们足够细心的话,发现在输入中文的标签的时候,存在一定的问题。 比如,当我们想将某个博客标记为“大神”时,博客系统却将这个单词粗暴的分成了如图21-2所示的两个词“大”和“神”。显然,这并不符合用户的使用习惯。 这是 Elasticsearch 语言分析器上的限制,它并不能友好的处理所有语言,特别是中文。这种情况下,我们就需要额外的中文分词器来协助我们了。 本章节,我们将演示使用 IK Analysis 插件来帮助我们实现中文分词。 IK Analysis 插件 IK Analysis 插件(https://github.com/medcl/elasticsearch-analysis-ik/)就是一款专门用于 Elasticsearch 的分词器,可以友好的处理中文。 IK Analysis 插件将 Lucene IK 分析仪(http://code.google.com/p/ik-analyzer/)集成到了 Elasticsearch 中,从而支持自定义分词。 安装 IK Analysis 安装 IK Analysis 非常简单。主要有两种方式。 使用 elasticsearch-plugin 如果 Elasticsearch 是 5.5.1 以后的版本,可以使用 elasticsearch-plugin 来安装,安装方式如下: ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.1/elasticsearch-analysis-ik-5.5.1.zip 下载解压到指定目录 另外一种方式是,下载安装包,解压到 Elasticsearch 安装目录的 /plugins/目录下即可。 下图展示了 IK 的安装目录。 需要注意的是,不管是哪种安装方式,相应的插件,要对应相应的 Elasticsearch 版本,否则可能会安装不成功。下载地址为 https://github.com/medcl/elasticsearch-analysis-ik/releases。本章节,我们使用 IK Analysis for Elasticsearch 5.5.0 版本。 使用 IK Analysis 要使用 IK Analysis,需要在文档类里面,指定相应的分词器。我们在 EsBlog 的 tags 属性上,添加了searchAnalyzer = "ik_smart", analyzer = "ik_smart"的注解内容就可以了。 public class EsBlog implements Serializable { ... @Field(type = FieldType.text,fielddata = true, searchAnalyzer = "ik_smart", analyzer = "ik_smart") private String tags; // 标签 下图展示了使用了IK分词的标签效果 IK Analysis 类型 ik_smart 是 IK Analysis 其中一种分词形式。IK Analysis主要有两种类型的分词形式,分别是 ik_max_word 和 ik_smart。 ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国”、“中华人民”、“中华”、“华人”、“人民共和国”、“人民”、“人”、“民”,、“共和国”、“共和”、“和”、“国歌”等,会穷尽各种可能的组合; ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国”、“国歌”。 参考 原文同步至:https://waylau.com/elasticsearch-use-ik-analysis/ https://waylau.com/spring-boot-blog-video-release/ http://coding.imooc.com/class/125.html
Eureka 是 Netflix 出品的用于实现服务注册和发现的工具。 Spring Cloud 集成了 Eureka,并提供了开箱即用的支持。其中, Eureka 又可细分为 Eureka Server 和 Eureka Client。 本例子将演示如何通过 Spring Cloud Eureka 来快速实现服务的注册和发现。 开发环境 Gradle 4.0 Spring Boot 2.0.0.M3 Spring Cloud Netflix Eureka Server Finchley.M2 Spring Cloud Netflix Eureka Client Finchley.M2 从 Spring Initializr 进行项目的初始化 访问http://start.spring.io/ 进行项目的初始化。我们将该项目命名为micro-weather-eureka-server。 更改配置 根据下面两个博客的指引来配置,加速项目的构建。 Gradle Wrapper 引用本地的发布包 : https://waylau.com/change-gradle-wrapper-distribution-url-to-local-file/ 使用Maven镜像 : https://waylau.com/use-maven-mirrors/ 启用 Eureka Server 为启用 Eureka Server ,在 Application 上增加@EnableEurekaServer注解即可。 @SpringBootApplication @EnableEurekaServer public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 修改项目配置 修改 application.properties,增加如下配置。 server.port: 8761 eureka.instance.hostname: localhost eureka.client.registerWithEureka: false eureka.client.fetchRegistry: false eureka.client.serviceUrl.defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ 其中: server.port: 指明了应用启动的端口号 eureka.instance.hostname: 应用的主机名称 eureka.client.registerWithEureka: 值为false意味着自身仅作为服务器,不作为客户端 eureka.client.fetchRegistry: 值为false意味着无需注册自身 eureka.client.serviceUrl.defaultZone: 指明了应用的URL 启动 Eureka Server 启动应用,访问http://localhost:8761/,可以看到 Eureka Server 自带的 UI 管理界面。 创建 Eureka Client 我们在micro-weather-eureka-server基础上,将创建一个micro-weather-eureka-client 作为客户端,并演示如何让将自身向注册服务器进行注册,让其可以其他服务都调用。 更改配置 增加如下配置: dependencies { //... compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client') //... } 一个最简单的 Eureka Client @SpringBootApplication @EnableDiscoveryClient @RestController public class Application { @RequestMapping("/hello") public String home() { return "Hello world"; } public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 其中@EnableDiscoveryClient启用了服务发现的功能,只要 Eureka Client 启动了,就能被 Eureka Server 所感知。 项目配置: spring.application.name: micro-weather-eureka-client eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka/ 运行 分别在 8081 和 8082 上启动了客户端示例。 java -jar micro-weather-eureka-client-1.0.0.jar --server.port=8081 java -jar micro-weather-eureka-client-1.0.0.jar --server.port=8082 可以在 Eureka Server 上看到这两个实体的信息。 源码 本章节源码,见https://github.com/waylau/spring-cloud-tutorial samples 目录下的micro-weather-eureka-server 和 micro-weather-eureka-client 。 原文同步至https://waylau.com/eureke-server-register-and-server-discovery/
本文,我们将基于 Spring Boot 技术来实现一个微服务天气预报服务接口——micro-weather-basic。micro-weather-basic 的作用是实现简单的天气预报功能,可以根据不同的城市,查询该城市的实时天气情况。 开发环境 Gradle 4.0 Spring Boot 1.5.6 Apache HttpClient 1.5.3 数据来源 理论上,天气的数据是天气预报的实现基础。本应用与实际的天气数据无关,理论上,可以兼容多种数据来源。但为求简单,我们在网上找了一个免费、可用的天气数据接口。 天气数据来源为中华万年历。例如: 通过城市名字获得天气数据 :http://wthrcdn.etouch.cn/weather_mini?city=深圳 通过城市id获得天气数据:http://wthrcdn.etouch.cn/weather_mini?citykey=101280601 城市ID列表。每个城市都有一个唯一的ID作为标识。见 http://cj.weather.com.cn/support/Detail.aspx?id=51837fba1b35fe0f8411b6df> 或者 。 调用天气服务接口示例,我们以“深圳”城市为例,可用看到如下天气数据返回。 { "data": { "yesterday": { "date": "1日星期五", "high": "高温 33℃", "fx": "无持续风向", "low": "低温 26℃", "fl": "<![CDATA[<3级]]>", "type": "多云" }, "city": "深圳", "aqi": "72", "forecast": [ { "date": "2日星期六", "high": "高温 32℃", "fengli": "<![CDATA[<3级]]>", "low": "低温 26℃", "fengxiang": "无持续风向", "type": "阵雨" }, { "date": "3日星期天", "high": "高温 29℃", "fengli": "<![CDATA[5-6级]]>", "low": "低温 26℃", "fengxiang": "无持续风向", "type": "大雨" }, { "date": "4日星期一", "high": "高温 29℃", "fengli": "<![CDATA[3-4级]]>", "low": "低温 26℃", "fengxiang": "西南风", "type": "暴雨" }, { "date": "5日星期二", "high": "高温 31℃", "fengli": "<![CDATA[<3级]]>", "low": "低温 27℃", "fengxiang": "无持续风向", "type": "阵雨" }, { "date": "6日星期三", "high": "高温 32℃", "fengli": "<![CDATA[<3级]]>", "low": "低温 27℃", "fengxiang": "无持续风向", "type": "阵雨" } ], "ganmao": "风较大,阴冷潮湿,较易发生感冒,体质较弱的朋友请注意适当防护。", "wendu": "29" }, "status": 1000, "desc": "OK" } 我们通过观察数据,来了解每个返回字段的含义。 "city": 城市名称 "aqi": 空气指数, "wendu": 实时温度 "date": 日期,包含未来5天 "high":最高温度 "low": 最低温度 "fengli": 风力 "fengxiang": 风向 "type": 天气类型 以上数据,是我们需要的天气数据的核心数据,但是,同时也要关注下面两个字段: "status": 接口调用的返回状态,返回值“1000”,意味着数据是接口正常 "desc": 接口状态的描述,“OK”代表接口正常 重点关注返回值不是“1000”的情况,说明,这个接口调用异常了。 初始化一个 Spring Boot 项目 初始化一个 Spring Boot 项目 micro-weather-basic,该项目可以直接在我们之前章节课程中的 basic-gradle 项目基础进行修改。同时,为了优化项目的构建速度,我们对Maven中央仓库地址和 Gradle Wrapper 地址做了调整。其中细节暂且不表,读者可以自行参阅源码,或者学习笔者所著的《Spring Boot 教程》(https://github.com/waylau/spring-boot-tutorial)。其原理,我也整理到我的博客中了: https://waylau.com/change-gradle-wrapper-distribution-url-to-local-file/ https://waylau.com/use-maven-mirrors/ 项目配置 添加 Apache HttpClient 的依赖,来作为我们Web请求的客户端。 // 依赖关系 dependencies { //... // 添加 Apache HttpClient 依赖 compile('org.apache.httpcomponents:httpclient:4.5.3') //... } 创建天气信息相关的值对象 创建com.waylau.spring.cloud.vo包,用于相关值对象。创建天气信息类 Weather public class Weather implements Serializable { private static final long serialVersionUID = 1L; private String city; private String aqi; private String wendu; private String ganmao; private Yesterday yesterday; private List<Forecast> forecast; public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getAqi() { return aqi; } public void setAqi(String aqi) { this.aqi = aqi; } public String getWendu() { return wendu; } public void setWendu(String wendu) { this.wendu = wendu; } public String getGanmao() { return ganmao; } public void setGanmao(String ganmao) { this.ganmao = ganmao; } public Yesterday getYesterday() { return yesterday; } public void setYesterday(Yesterday yesterday) { this.yesterday = yesterday; } public List<Forecast> getForecast() { return forecast; } public void setForecast(List<Forecast> forecast) { this.forecast = forecast; } } 昨日天气信息: public class Yesterday implements Serializable { private static final long serialVersionUID = 1L; private String date; private String high; private String fx; private String low; private String fl; private String type; public Yesterday() { } public String getDate() { return date; } public void setDate(String date) { this.date = date; } public String getHigh() { return high; } public void setHigh(String high) { this.high = high; } public String getFx() { return fx; } public void setFx(String fx) { this.fx = fx; } public String getLow() { return low; } public void setLow(String low) { this.low = low; } public String getFl() { return fl; } public void setFl(String fl) { this.fl = fl; } public String getType() { return type; } public void setType(String type) { this.type = type; } } 未来天气信息: public class Forecast implements Serializable { private static final long serialVersionUID = 1L; private String date; private String high; private String fengxiang; private String low; private String fengli; private String type; public String getDate() { return date; } public void setDate(String date) { this.date = date; } public String getHigh() { return high; } public void setHigh(String high) { this.high = high; } public String getFengxiang() { return fengxiang; } public void setFengxiang(String fengxiang) { this.fengxiang = fengxiang; } public String getLow() { return low; } public void setLow(String low) { this.low = low; } public String getFengli() { return fengli; } public void setFengli(String fengli) { this.fengli = fengli; } public String getType() { return type; } public void setType(String type) { this.type = type; } public Forecast() { } } WeatherResponse 作为整个消息的返回对象 public class WeatherResponse implements Serializable { private static final long serialVersionUID = 1L; private Weather data; // 消息数据 private String status; // 消息状态 private String desc; // 消息描述 public Weather getData() { return data; } public void setData(Weather data) { this.data = data; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } } 服务接口及实现 定义了获取服务的两个接口方法 public interface WeatherDataService { /** * 根据城市ID查询天气数据 * @param cityId * @return */ WeatherResponse getDataByCityId(String cityId); /** * 根据城市名称查询天气数据 * @param cityId * @return */ WeatherResponse getDataByCityName(String cityName); } 其实现为: @Service public class WeatherDataServiceImpl implements WeatherDataService { @Autowired private RestTemplate restTemplate; private final String WEATHER_API = "http://wthrcdn.etouch.cn/weather_mini"; @Override public WeatherResponse getDataByCityId(String cityId) { String uri = WEATHER_API + "?citykey=" + cityId; return this.doGetWeatherData(uri); } @Override public WeatherResponse getDataByCityName(String cityName) { String uri = WEATHER_API + "?city=" + cityName; return this.doGetWeatherData(uri); } private WeatherResponse doGetWeatherData(String uri) { ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class); String strBody = null; if (response.getStatusCodeValue() == 200) { strBody = response.getBody(); } ObjectMapper mapper = new ObjectMapper(); WeatherResponse weather = null; try { weather = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { e.printStackTrace(); } return weather; } } 返回的天气信息采用了 Jackson 来进行反序列化成为 WeatherResponse 对象。 控制器层 控制器层暴露了RESTful API 地址。 @RestController @RequestMapping("/weather") public class WeatherController { @Autowired private WeatherDataService weatherDataService; @GetMapping("/cityId/{cityId}") public WeatherResponse getReportByCityId(@PathVariable("cityId") String cityId) { return weatherDataService.getDataByCityId(cityId); } @GetMapping("/cityName/{cityName}") public WeatherResponse getReportByCityName(@PathVariable("cityName") String cityName) { return weatherDataService.getDataByCityName(cityName); } } @RestController自动会将返回的数据,序列化成 JSON数据格式。 配置类 RestConfiguration 是 RestTemplate 的配置类。 @Configuration public class RestConfiguration { @Autowired private RestTemplateBuilder builder; @Bean public RestTemplate restTemplate() { return builder.build(); } } 访问API 运行项目之后,访问项目的 API : http://localhost:8080/weather/cityId/101280601 http://localhost:8080/weather/cityName/惠州 能看到如下的数据返回 源码 本章节的源码,见 https://github.com/waylau/spring-cloud-tutorial/ samples目录下的micro-weather-basic。 参考引用 https://waylau.com/spring-boot-weather-report/ https://github.com/waylau/spring-boot-tutorial
Webix 是一个JavaScript UI 库,提供了多达88个UI小部件和功能丰富的 CSS/HTML5 JavaScript 控件。使用 Webix 可以有效地加快 Web 应用的开发。文本将演示了如何通过 Webix 框架,来创建一个 Email 客户端原型。 安装 Webix 可以下载 Webix 的 JS、CSS 文件,但最快的方式是使用 Webix 的 CDN, 如下: <!DOCTYPE HTML> <html> <head> <link rel="stylesheet" href="http://cdn.webix.com/edge/webix.css" type="text/css"> <script src="http://cdn.webix.com/edge/webix.js" type="text/javascript"></script> </head> ... </html> 快速开始 我们为我们的第一个应用创建第一个页面 index.html。在 <body> 中定义 js 脚本,来放置 UI 配置: <!DOCTYPE HTML> <html> <head> <link rel="stylesheet" href="http://cdn.webix.com/edge/webix.css" type="text/css"> <script src="http://cdn.webix.com/edge/webix.js" type="text/javascript"></script> </head> <body> <script type="text/javascript" charset="utf-8"> /* UI 配置 */ </script> </body> </html> 接着,我们编写 UI 配置: webix.ui({ rows: [ { view: "template", type: "header", template: "我的应用!" }, { view: "datatable", autoConfig: true, data: [ { title: "Way Lau", year: 1987, votes: 533848, rating: 8.9, rank: 5 }, { title: "老卫", year: 1987, votes: 53248, rating: 5.9, rank: 6 } ] } ] }); 为了能更加友好显示中文,我们在<head>标签里面加上<meta charset="UTF-8">。 这样,我们完整的第一个应用的代码如下: <!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="http://cdn.webix.com/edge/webix.css" type="text/css"> <script src="http://cdn.webix.com/edge/webix.js" type="text/javascript"></script> </head> <body> <script type="text/javascript" charset="utf-8"> /* UI 配置 */ webix.ui({ rows: [ { view: "template", type: "header", template: "我的应用!" }, { view: "datatable", autoConfig: true, data: [ { title: "Way Lau", year: 1987, votes: 533848, rating: 8.9, rank: 5 }, { title: "老卫", year: 1987, votes: 53248, rating: 5.9, rank: 6 } ] } ] }); </script> </body> </html> 用浏览器直接打开我们的index.html 页面,可以看到如下效果: 探索项目 那么,我们来简单介绍下 Webix 的原理。 Webix 的应用程序都是放置在 script 脚本中: webix.ui({ // 组件 }); 需要注意的是,如果想让 Webix 脚本在 HTML 文档加载完了再执行,可以使用 webix.ready(function(){ ....}) 来包裹我们的 Webix,用法如下: webix.ready(function(){ webix.ui({ .... }); }); 下面代码是用 Webix 中的 view 来定义一个视图组件,多个 view 可以实现复杂的应用布局结构: rows: [ { view: "template", type: "header", template: "我的应用!" }, { view: "datatable", autoConfig: true, data: [ { title: "Way Lau", year: 1987, votes: 533848, rating: 8.9, rank: 5 }, { title: "老卫", year: 1987, votes: 53248, rating: 5.9, rank: 6 } ] } ] 在上述例子中,我们用到了两种类型的 view,其中, rows 代码垂直布局的多个列,这个,我们每个行(row)就是一个view; ui.template 是一个用于包裹 HTML 内容的容器。这里我们用来类型为header的template来说明这个是应用头。更多 template 的类型,可以自行参阅https://docs.webix.com/samples/80_docs/template_types.html; ui.datatable 是一个功能丰富的数据表格组件; autoConfig 设置为 true,表明表格会根据数据来自适应; data 就是表格中放置的数据 进阶 在快速了解 Webix 的相关概念之后,我们就要来创建一个稍微复杂一点的应用,就是本文的主要内容“Email 客户端”。 布局 从大布局开始,再逐步求精,是构建前端应用的基本思路。我们创建了如下布局结构: webix.ui({ type: "space", rows: [ /* 1st row. Toolbar */ { template: "Toolbar", height: 45, }, /* 2nd row. The rest of application */ { type: "wide", cols: [ /* 1st column of the second row. /* Folder tree and Calendar */ { type: "clean", rows: [ { template: "Tree", width: 280 }, { template: "Calendar" } ] }, /* 2nd column of the second row. /* Email list, Buttons, and Message reader */ { type: "wide", rows: [ { template: "Email List" }, { height: 45, cols: [ { template: "Button1" }, { template: "Button2" }, {}, { template: "Button 3" } ] }, { template: "Message" } ] } ] } ] }); 其中: cols 就是列,每行(row)可能包含了多个列(col); height 和 width 属性来定义视图所需的大小了 type,它定义了布局边框。如果使用clean将获得无边框的单元格,使用wide将获得有边框的、有更大空间的单元格。 实现 Toolbar Toolbar(工具栏)可以包含各种元素,如按钮或下拉菜单等。 记住,要使用Webix创建组件,必须使用view:“component_name”代码行,元素属性允许选择工具栏的内容。 ... { view: "toolbar", height: 45, elements:[ {view: "label", label: "Webix Email 客户端"} ] }, ... elements 用来放置子的view组件。 label 就是显示普通的文本标签 这里,我们使用了 ui.chart,来创建图表。 实现 Tree 创建菜单目录树: ... { view:"tree", id: "my_tree", select: true, width:280, data:[ { id:"1", value:"收件箱"}, { id:"2", value:"已发送"}, { id:"3", value:"草稿箱"}, { id:"4", value:"垃圾箱"}, { id:"5", value:"通讯录", open:true, data:[ { id:"5-1", value:"好友"}, { id:"5-2", value:"家人"} ] } ] }, ... 其中: tree 是一个功能丰富的树形组件; open 设置为 true,来让我们的树在初始化时就处于打开状态。 最终效果如下: 实现 Calendar 创建日历组件: ... { view:"calendar", timepicker:true }, ... 其中: calendar 是一个功能丰富的日历组件; timepicker 设置为 true,在日历上显示时间选择器。 最终效果如下: 实现 Email 列表 还记得我们的在“快速开始”部分的那个表格吗?这里同样需要用表格来实现 Email 列表: 创建Email 列表: ... { id: "my_datatable", view: "datatable", scrollX: false, columns: [ { id: "checked", header: { content: "masterCheckbox" }, template: "{common.checkbox()}", width: 40 }, { id: "name", width: 250, header: "发件人" }, { id: "subject", header: "主题", fillspace: true }, { id: "date", header: "时间", width: 150 } ], data: [ { id: 1, folder: 1, name: "Way Lau", email: "waylau521@gmail.com", subject: "Invitation", date: "25/07/2017 12:30:20" }, { id: 2, folder: 1, name: "老卫", email: "waylau521@163.com", subject: "Report", date: "25/07/2017 16:10:07" }, { id: 11, folder: 2, name: "Way Lau", email: "waylau521@gmail.com", subject: "Re: Forecast", date: "25/07/2017 14:10:45" }, { id: 12, folder: 2, name: "老卫", email: "waylau521@163.com", subject: "Party invitation", date: "25/07/2017 17:05:10" } ] }, ... 其中: columns 用来定义表头; header:{ content:"masterCheckbox" } 定义了可以全选列表的 checkbox; template:"{common.checkbox()}" 设置每个列表项都会带有一个 checkbox; scrollX 设置为 false,意味着禁用了水平的滚动条。 fillspace 设置为 true,意味可以自动填充宽度。 最终效果如下: 事件处理 事件,让组件具备交互功能: ... // 绑定事件 $$ ("my_datatable").bind( $$ ("my_tree"),function(obj,filter){ return obj.folder == filter.id; }); // 选中第一个节点 $$ ("my_tree").select(1); ... 其中: "my_datatable" 为 datatable 组件的 id。绑定了"my_tree"的点击事件; ` $$ ("my_tree").select(1)` 意味着树节点会选中第一个节点。 最终效果如下:  ### 按钮实现 按钮实现如下: ... { height: 45, cols: [ { view:"button", label:"回复", width: 95 }, { view:"button", label:"创建", width: 95 }, {}, { view:"button", label:"删除", width: 95 } ] }, ... 其中: "my_datatable" 为 datatable 组件的 id。绑定了"my_tree"的点击事件; ` $$ ("my_tree").select(1)` 意味着树节点会选中第一个节点。 最终效果如下:  ### 展示 Email 正文 展示 Email 正文实现如下: ... { id:"details", template:"No message selected" }, ... 如果想显示文本,可以编写如下脚本: var message = "大道至简 https://waylau.com"; $$ ("details").define("template",message); $$ ("details").render(); 最终效果如下: 编辑窗口 发送邮件,我们需要有一个编辑窗口: webix.ui({ view:"window", move: true, id:"my_win", width:400, head:"创建新邮件", position: "center", body: { view:"form", borderless:true, elements: [ { view:"text", label:'收件人:', name:"address" }, { view:"text", label:'主题:', name:"subject" }, { view:"textarea", height:200, label:"内容:", name:"message"}, { cols: [ { view:"button", value: "发送", }, { view:"button", value: "关闭", click:(" $$ ('my_win').hide();") } ]}, ], } }); 然后在“创建”的按钮上,添加弹出窗口的事件: ... { view:"button", label:"创建", width: 95, click:function(){ $$ ("my_win").getBody().clear(); $$ ("my_win").show(); } }, ... 最终效果如下: 源码 https://github.com/waylau/webix-tutorial 参考文献 https://opensource.com/article/17/5/10-step-guide-webix-framework https://docs.webix.com/tutorials__quick_start.html 本文同步至https://waylau.com/email-client-with-webix
2022年05月
2022年04月
2022年03月
2022年01月
2021年12月
2021年09月
2021年07月
2021年04月
2021年03月
2021年02月
2020年12月
2020年11月
2020年09月