本文共:1976字 预计阅读时间:5分钟
你好,我是看山。
不出意外,看到这篇文章的人八成阳了。
先放个统计,12月23号的时候,部门发起了一次投票,480人参与,只有20%的壮士还没阳。
在这一波高峰期前,我们公司已经居家好几个周了,所以投票结果基本上能够反应社会情况。
今天就想聊两件事:
什么情况下可以放开?基本上有三种情况:
现在显然不是第一种情况。
我们先来看个数据:
新冠病毒原始毒株的R0值在3-5;德尔塔毒株的R0值为6-8;奥密克戎BA.1的R0值约为9.5;奥密克戎BA.2的R0值约为13.3;奥密克戎BA.4/5的R0值可能达到18.6……
R0值(R naught)是基本传染数(basic reproductive number)的简称,又译作基本再生数,指的是在没有采取任何干预措施的情况下,平均每位感染者在传染期内使易感者个体致病的数量。数字越大说明传播能力越强,控制难度越大。
简单说就是,R0是2时,一个传染俩,两个传四个,四个传八个,以此类推……经过4个循环,病例数就增加到64人。
咱们把新冠病毒的R0值做个图:
这就是所谓的指数爆炸:
当R0超过一定范围,不是我们想防就能防得住的。就比如年初的上海,奥密克戎的高传播性迅速击溃了精准防控,演变成后来的封城。
奥密克戎的变异株的传染能力还在增强,据说现在是21了(没有查到官方数据)。
这个时候,我们如果还想像前几年似的,想通过封控来阻断传染,就要举全国之力了。那种情况下,大家就只能待在家里,各种行业停摆。显然是不现实的,不是每家都有能够吃一个月的战略储备粮,也不是每家都能够啥也不干白来一个月钱的。
也就是说,现在是防不住了。
然后我们再看看致病性,鉴于大家或者周围人都阳过,就算是有经验。我身边人阳了之后的征兆基本上这这几种:
可以看到,这些症状和普通的流感很像。甚至没有抗原的情况下,都不能断定自己是流感还是新冠。
也就是说,大部分人阳了之后症状都很轻,且属于上呼吸道的症状,基本上对肺部没啥影响。
这样就符合第二种情况了,我们大多数人可以与新冠病毒共生了。
从经济的角度看,我们不可能无限期的收紧防控措施,很多企业在这段封控期间艰难度日,每一次收紧,企业都会受到冲击,我们没有办法不计成本的封控,孩子要长大,成人要工作,企业要发展,人们要生活。时移世易,当封控成本远远大于收益时,当感染后对身体影响很小时,我们就要选择更适合的方式。
再来说下我对放开的看法:坚决拥护,但也会做好个人防护。
首先说个人防护,相信大家从各种渠道了解过,病毒是RNA复制,变异方向多变且速度快,就有概率出现传染性强且致死率高的毒株。只要我们做好防护,别让病毒在我们体内有复制的机会,那变异的机会就会少很多。
然后说坚决拥护,就这几周看过来,商场在渐渐恢复,大家生活轻松很多,连前几天几乎瘫痪的物流快递业,现在也逐渐恢复了。一切都在向好的方向发展,有什么理由不拥护呢?
给大家看一下北京地铁最近15日客流量和往年日均客流量图。
最近15日客流量每个工作日都在递增,在1月6日已经达到769万。再看年日均客流,2023年刚开始7天,日均客流量已经达到去年平均水平。
回想前段时间,2022年11月25日客流量跌到了108万,在放开之后,逐步恢复生机。
从上帝视角看,中央踩刹车打方向盘的举措是正确的。就这几天复工经历看,同事们带着口罩,偶尔听到几声咳嗽,也都没人在意,我们以较小的成本实现了与新冠共存。
放一个新华社的新冠防疫手册,虽然病毒毒性弱了,但是阳的时候还是很难受的,能不重阳就不重阳吧。
最后,多年之后,当我们回忆起这三年,可能有感慨、有怀念、有悲伤,更多的可能是对当下美好生活的珍惜。
祝大家身体健康。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:羊不羊的关我什么事?
你好,我是看山。
前面介绍了 Spring 的 MessageSource 组件的用法、执行逻辑和源码,本文我们将根据前面的知识,实现自己的动态刷新的国际化组件。
现在大家都用的是微服务,为了高可用,每个服务部署时最少两个实例。
ResourceBundleMessageSource
实现国际化,每次修改配置文件,都需要重启服务。ReloadableResourceBundleMessageSource
,我们可以借助多个服务挂在同一个磁盘或同一个卷读取同一个配置文件,借助远程工具,修改文件实现动态配置内容的修改。但是这就涉及到文本编辑,不能很好的实现审计记录和文件管控。所以,我们要实现一个适用于微服务架构、方便修改、具备审计功能的动态的国际化配置组件。本文选择 Nacos 实现,Nacos 有配置中心的能力,适合在微服务架构中使用,同时也具备方便修改和审计能力,只要我们实现从 Nacos 加载国际化配置的能力,就可以轻松实现目标。
Spring 提供的默认实现中,ReloadableResourceBundleMessageSource
实现了动态刷新的能力,只不过是从文件读取内容,我们可以借助ReloadableResourceBundleMessageSource
的逻辑实现,只是将其改为从 Nacos 读取内容。
这个实现,通常可以有两种方案(假设我们新实现的类命名为NacosBundleMessageSource
):
ReloadableResourceBundleMessageSource
:重写读取配置的方法,然后通过 Spring 注入新的方法。这种方式有优点和缺点:ReloadableResourceBundleMessageSource
相似的结构和执行逻辑,当 Spring 进行升级的时候,我们直接通过继承获取了能力;applicationContext.getBean(ReloadableResourceBundleMessageSource.class)
会找到ReloadableResourceBundleMessageSource
和NacosBundleMessageSource
两个 Bean,Spring 容器就不知道该返回哪个了;NacosBundleMessageSource
就可能需要重写。ReloadableResourceBundleMessageSource
:完全实现自己的一个动态加载类。与第一种的优缺点正好相反:applicationContext.getBean
可以通过 class 类型获取;ReloadableResourceBundleMessageSource
的类定义没有关系,除非 Spring 修改底层逻辑,否则不会因为ReloadableResourceBundleMessageSource
的变动出现不兼容的情况;ReloadableResourceBundleMessageSource
进行升级,提出更加优化的写法,我们就需要重写NacosBundleMessageSource
了;ReloadableResourceBundleMessageSource
完全相同的重复代码。考虑到两种方案的优缺点,结合业务中的逻辑,最终选择方案二。
我们要实现的国际化组件,在 Spring 的实现中,使用的是Locale
表示指定的区域,在这个类中,定义了三个不同的维度language
、country
、variant
,翻译过来是语言
、国家
、变种
,结合BaseLocale
的定义,可以将country
看做是语言大类。
根据前面的介绍,我们的国际化配置文件定义格式是根据Locale
格式定义的。比如,basename 是 messages,Locale 是 de_AT_oo 的话,对应的配置文件可以是“messages_de_AT_OO”、“messages_de_AT”、“messages_de”。
所以我们需要用到递归的方式获取文件名:
protected List<String> calculateFilenamesForLocale(String basename, Locale locale) { List<String> result = new ArrayList<>(3); String language = locale.getLanguage(); String country = locale.getCountry(); String variant = locale.getVariant(); StringBuilder temp = new StringBuilder(basename); temp.append('_'); if (language.length() > 0) { temp.append(language); result.add(0, temp.toString()); } temp.append('_'); if (country.length() > 0) { temp.append(country); result.add(0, temp.toString()); } if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) { temp.append('_').append(variant); result.add(0, temp.toString()); } return result;}
既然是从 Nacos 读取配置,那配置文件的内容就需要通过 Nacos 获取。这里使用了NacosConfigManager
获取:
protected Properties loadProperties(String filename) throws IOException, NacosException { final Properties props = newProperties(); final String dataId = filename + NacosConstants.PROPERTIES_SUFFIX; logger.info("Loading properties for " + dataId); final String config = nacosConfigManager.getConfigService().getConfig(dataId, nacosGroup, 5000); if (StringUtils.hasText(config)) { logger.info("No properties found for " + dataId); throw new NoSuchFileException(dataId); } try (Reader reader = new StringReader(config)) { this.propertiesPersister.load(props, reader); logger.info("Loaded properties for " + dataId); } return props;}
方法传入的filename
就是从上一节中获取的文件名。当然,文件名是我们计算出来的,可能不存在,此处直接抛出NoSuchFileException
,由上层逻辑捕捉处理:
protected PropertiesHolder refreshProperties(String filename, @Nullable PropertiesHolder propHolder) { long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis()); try { Properties props = loadProperties(filename); propHolder = new PropertiesHolder(props, -1); } catch (NacosException ex) { if (logger.isWarnEnabled()) { logger.warn("Could not get properties form nacos ", ex); } // Empty holder representing "not valid". propHolder = new PropertiesHolder(); } catch (IOException ex) { if (logger.isInfoEnabled()) { logger.info("Could not get properties form nacos, the message is " + ex.getMessage()); } // Empty holder representing "not valid". propHolder = new PropertiesHolder(); } propHolder.setRefreshTimestamp(refreshTimestamp); this.cachedProperties.put(filename, propHolder); logger.info("Refreshed properties for " + filename); return propHolder;}
我们可以看到,从 Nacos 中读取配置逻辑的上层会捕捉Exception
,然后创建一个空的配置管理器。此处会有两个异常:
NacosException
:读取 Nacos 失败抛出的异常,包括 Nacos 链接不正确或者是读取失败等;IOException
:没有指定文件名的配置时会抛出 IOException。前一节讲了加载 Nacos 配置文件的方式,本节说一下怎么实现 Nacos 配置文件的监听,动态刷新配置内容。
我们先介绍一下实现 Nacos 监听的几个类:
NacosConfigManager#getConfigService
通过反射创建的,用于存储 Nacos 监听器,实现监听逻辑;AbstractSharedListener
的匿名子类,实现动态刷新的逻辑就在这个类里。首先,我们当前组件是 MessageSource,用于实现国际化的组件。这个组件是在整个应用就绪后再加载就行,所以,我们监听 Spring 的ApplicationReadyEvent
事件即可。
@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) { // many Spring context if (this.ready.compareAndSet(false, true)) { for (Locale defaultLocale : this.nacosBundleMessageSource.defaultLocales()) { this.nacosBundleMessageSource.getMergedProperties(defaultLocale); } this.registerNacosListenersForApplications(); }}
然后是我们需要对所有可能的配置文件进行监听,就需要用到前文的calculateFilenamesForLocale
方法,计算所有可能出现的名字。
private void registerNacosListenersForApplications() { if (!isRefreshEnabled()) { return; } this.nacosBundleMessageSource.getBasenameSet().stream() .map(basename -> this.nacosBundleMessageSource.defaultLocales().stream() .map(locale -> this.nacosBundleMessageSource.calculateAllFilenames(basename, locale)) .flatMap(List::stream) .collect(Collectors.toList()) ) .flatMap(List::stream) .forEach(x -> registerNacosListener(nacosBundleMessageSource.getNacosGroup(), x + NacosConstants.PROPERTIES_SUFFIX));}
最后,我们需要向 Nacos 的运行管理器中注册文件监听,实现动态刷新的能力。
private void registerNacosListener(final String groupKey, final String dataKey) { final String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey); final Listener listener = listenerMap.computeIfAbsent(key, lst -> new AbstractSharedListener() { @Override public void innerReceive(String dataId, String group, String configInfo) { refreshCountIncrement(); nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo); try { nacosBundleMessageSource.forceRefresh(dataId, configInfo); if (log.isDebugEnabled()) { log.debug("Refresh Nacos config group={},dataId={},configInfo={}", group, dataId, configInfo); } } catch (IOException e) { log.warn("Nacos refresh failed, dataId: {}, group: {}, configInfo: {}", dataId, group, configInfo, e); } } }); try { configService.addListener(dataKey, groupKey, listener); } catch (NacosException e) { log.warn("register fail for nacos listener ,dataId=[{}],group=[{}]", dataKey, groupKey, e); }}
至此,我们获取了国际化组件 MessageSource 与 Nacos 组合实现动态配置能力。本文中的实例已经传到 GitHub,关注公众号「看山的小屋」,回复spring
获取源码。
接下来,我们再重头想一下,我们最初想要实现一个适用于微服务架构、方便修改、具备审计功能的动态的国际化配置组件。选择 Nacos 作为例子,是因为 Nacos 本身实现了动态监听的能力,可以快速复刻ReloadableResourceBundleMessageSource
的能力。那 Nacos 具备什么能力呢?
只要具备上面三个特性,我们可以通过各种组合实现,比如:
本文从实践角度出发,实现了一个适用于微服务架构、方便修改、具备审计功能的动态的国际化配置组件。文中的实例已经传到 GitHub,关注公众号「看山的小屋」,回复spring
获取源码。如果是你,你会采用哪种方案呢?欢迎一起讨论。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:SpringBoot 实战:国际化组件 MessageSource 与 Nacos 组合实现动态配置能力
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:SpringBoot 实战:国际化组件 MessageSource 与 Nacos 组合实现动态配置能力
你好,我是看山。
前文介绍了 SpringBoot 中的国际化组件MessageSource
的使用,本章我们一起看下ResourceBundleMessageSource
和ReloadableResourceBundleMessageSource
的执行逻辑。SpringBoot 的 MessageSource 组件有很多抽象化,源码看起来比较分散,所以本文会通过流程图的方式进行讲解。
配置文件是基础,会影响执行逻辑,我们先来看下配置项:
MessageFormat.format
函数对国际化信息格式化,如果注入参数,输出结果是经过格式化的。比如MessageFormat.format("Hello, {0}!", "Kanshan")
输出结果是“Hello, Kanshan!”。该参数控制的是,当输入参数为空时,是否还是使用MessageFormat.format
函数对结果进行格式化,默认是 false;NoSuchMessageException
异常。这些配置参数都有各自的默认值。如果没有特殊的需求,可以直接直接按照默认约定使用。
接下来我们看下流程图,下面的流程图绿色部分是 cacheDuration 没有配置的情况。对于 ResourceBundleMessageSource 是只加载一次配置文件,ReloadableResourceBundleMessageSource 会根据文件修改时间判断是否需要重新加载。
@Overridepublic final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { String msg = getMessageInternal(code, args, locale); if (msg != null) { return msg; } if (defaultMessage == null) { return getDefaultMessage(code); } return renderDefaultMessage(defaultMessage, args, locale);}@Overridepublic final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { String msg = getMessageInternal(code, args, locale); if (msg != null) { return msg; } String fallback = getDefaultMessage(code); if (fallback != null) { return fallback; } throw new NoSuchMessageException(code, locale);}@Overridepublic final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { String[] codes = resolvable.getCodes(); if (codes != null) { for (String code : codes) { String message = getMessageInternal(code, resolvable.getArguments(), locale); if (message != null) { return message; } } } String defaultMessage = getDefaultMessage(resolvable, locale); if (defaultMessage != null) { return defaultMessage; } throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);}
第一个getMessage
方法,是可以传入默认值defaultMessage
的,也就是当所有 basename 的配置文件中不存在 code 指定的值,就会使用defaultMessage
值进行格式化返回。
第二个getMessage
方法,是通过判断useCodeAsDefaultMessage
配置,如果设置了 true,在所有 basename 的配置文件中不存在 code 指定的值的情况下,会返回 code 作为返回值。但是当设置为 false 时,code 不存在的情况下,会抛出NoSuchMessageException
异常。
第三个getMessage
方法,传入的是MessageSourceResolvable
接口对象,查找的 code 更加多种多样。不过如果最后还是找不到,会抛出NoSuchMessageException
异常。
我们看源码不仅仅是为了看功能组件的实现,还是学习更加优秀的编程方式。比如下面这段内存缓存的使用,Spring 源码中很多地方都用到了这种内存缓存的使用方式:
// 两层 Map,第一层是 basename,第二层是 localeprivate final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles = new ConcurrentHashMap<>();@Nullableprotected ResourceBundle getResourceBundle(String basename, Locale locale) { if (getCacheMillis() >= 0) { // Fresh ResourceBundle.getBundle call in order to let ResourceBundle // do its native caching, at the expense of more extensive lookup steps. return doGetBundle(basename, locale); } else { // Cache forever: prefer locale cache over repeated getBundle calls. // 先从缓存中获取第一层 basename 的缓存 Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename); if (localeMap != null) { // 如果命中第一层,在通过 locale 获取第二层的值 ResourceBundle bundle = localeMap.get(locale); if (bundle != null) { // 如果命中第二层缓存,直接返回 return bundle; } } try { // 走到这里,说明没有命中缓存,就根据 basename 和 locale 创建对象 ResourceBundle bundle = doGetBundle(basename, locale); if (localeMap == null) { // 如果 localeMap 为空,说明第一级就不存在,通过 Map 的 computeIfAbsent 方法初始化 localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>()); } // 将新建的 ResourceBundle 对象放入 localeMap 中 localeMap.put(locale, bundle); return bundle; } catch (MissingResourceException ex) { if (logger.isWarnEnabled()) { logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage()); } // Assume bundle not found // -> do NOT throw the exception to allow for checking parent message source. return null; } }}
还有一种使用 Map 实现内存缓存的写法,比如我们就对上面的这个方法进行改写:
public class ResourceBundleMessageSourceExt extends ResourceBundleMessageSource { private final Map<BasenameLocale, ResourceBundle> cachedResourceBundles = new ConcurrentHashMap<>(); @Override protected ResourceBundle getResourceBundle(String basename, Locale locale) { if (getCacheMillis() >= 0) { // Fresh ResourceBundle.getBundle call in order to let ResourceBundle // do its native caching, at the expense of more extensive lookup steps. return doGetBundle(basename, locale); } else { // Cache forever: prefer locale cache over repeated getBundle calls. final BasenameLocale basenameLocale = new BasenameLocale(basename, locale); ResourceBundle resourceBundle = this.cachedResourceBundles.get(basenameLocale); if (resourceBundle != null) { return resourceBundle; } try { ResourceBundle bundle = doGetBundle(basename, locale); this.cachedResourceBundles.put(basenameLocale, bundle); return bundle; } catch (MissingResourceException ex) { if (logger.isWarnEnabled()) { logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage()); } // Assume bundle not found // -> do NOT throw the exception to allow for checking parent message source. return null; } } } public record BasenameLocale(String basename, Locale locale) { @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } BasenameLocale that = (BasenameLocale) o; return basename.equals(that.basename) && locale.equals(that.locale); } @Override public int hashCode() { return Objects.hash(basename, locale); } }}
我们可以利用 Map 是通过equals
判断 key 是否一致的原理,创建一个包含 basename、locale 的对象BasenameLocale
,然后改写cachedResourceBundles
为一层 Map,会简化一些判断逻辑。
此处的
BasenameLocale
是record
类型,具体语法可以参考 Java16 的新特性 中的 Record 类型一节。
本文先介绍了 MessageSource 的配置项,然后通过流程图的方式介绍了ResourceBundleMessageSource
和ReloadableResourceBundleMessageSource
的执行逻辑,最后分享了两个使用 Map 实现内存缓存的方式。
下一节我们将扩展 MessageSource,实现从 Nacos 加载配置内容,同时实现动态修改配置内容的功能。
本文中的实例已经传到 GitHub,关注公众号「看山的小屋」,回复spring
获取源码。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:SpringBoot 实战:国际化组件 MessageSource 的执行逻辑与源码
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:SpringBoot 实战:国际化组件 MessageSource 的执行逻辑与源码
你好,我是看山。
咱们今天一起来聊聊 SpringBoot 中的国际化组件 MessageSource。
先看一下类图:
从类图可以看到,Spring 内置的MessageSource
有三个实现类:
在 SpringBoot 中,默认创建 ResourceBundleMessageSource 实例实现国际化输出。标准的配置通过MessageSourceProperties
类注入:
MessageFormat.format
函数对国际化信息格式化,如果注入参数,输出结果是经过格式化的。比如MessageFormat.format("Hello, {0}!", "Kanshan")
输出结果是“Hello, Kanshan!”。该参数控制的是,当输入参数为空时,是否还是使用MessageFormat.format
函数对结果进行格式化,默认是 false;NoSuchMessageException
异常。从上面我们知道了一些简单的配置,但是还是没有办法知道 MessageSource 到底是什么,本节我们举个例子小试牛刀。
首先从https://start.spring.io/创建一个最少依赖spring-boot-starter-web
的 SpringBoot 项目。
然后在 resources 目录下定义一组国际化配置文件,我们这里使用默认配置,所以 basename 是 messages:
## messages.propertiesmessage.code1=[DEFAULT]code onemessage.code2=[DEFAULT]code twomessage.code3=[DEFAULT]code threemessage.code4=[DEFAULT]code fourmessage.code5=[DEFAULT]code fivemessage.code6=[DEFAULT]code six## messages_en.propertiesmessage.code2=[en]code two## messages_en_US.propertiesmessage.code3=[en_US]code three## messages_zh.propertiesmessage.code4=[中文] 丁字号## messages_zh_CN.propertiesmessage.code5=[大陆区域中文] 戊字号## messages_zh_Hans.propertiesmessage.code6=[简体中文] 己字号
一个定义了六个配置文件:
从上面配置文件的命名可以看出,都是以 basename 开头,后面跟上语系和地区,三个参数以下划线分隔。
可以支持的语言和国家可以从java.util.Locale
查找。
最后我们定义一个 Controller 实验:
@RestControllerpublic class HelloController { @Autowired private MessageSource messageSource; @GetMapping("m1") public List<String> m1(Locale locale) { final List<String> multi = new ArrayList<>(); multi.add(messageSource.getMessage("message.code1", null, locale)); multi.add(messageSource.getMessage("message.code2", null, locale)); multi.add(messageSource.getMessage("message.code3", null, locale)); multi.add(messageSource.getMessage("message.code4", null, locale)); multi.add(messageSource.getMessage("message.code5", null, locale)); multi.add(messageSource.getMessage("message.code6", null, locale)); return multi; }}
我们通过不同的请求查看结果:
### 默认GET http://localhost:8080/m1### 结果是:[ "[DEFAULT]code one", "[DEFAULT]code two", "[DEFAULT]code three", "[中文] 丁字号", "[大陆区域中文] 戊字号", "[简体中文] 己字号"]### local: enGET http://localhost:8080/m1Accept-Language: en### 结果是:[ "[DEFAULT]code one", "[en]code two", "[DEFAULT]code three", "[DEFAULT]code four", "[DEFAULT]code five", "[DEFAULT]code six"]### local: en-USGET http://localhost:8080/m1Accept-Language: en-US### 结果是:[ "[DEFAULT]code one", "[en]code two", "[en_US]code three", "[DEFAULT]code four", "[DEFAULT]code five", "[DEFAULT]code six"]### local: zhGET http://localhost:8080/m1Accept-Language: zh### 结果是:[ "[DEFAULT]code one", "[DEFAULT]code two", "[DEFAULT]code three", "[中文] 丁字号", "[DEFAULT]code five", "[DEFAULT]code six"]### local: zh-CNGET http://localhost:8080/m1Accept-Language: zh-CN### 结果是:[ "[DEFAULT]code one", "[DEFAULT]code two", "[DEFAULT]code three", "[中文] 丁字号", "[大陆区域中文] 戊字号", "[DEFAULT]code six"]
从上面的结果可以看出:
zh-Hans
,所以结果是简体中文优先;我们在 message.properties 中添加一行配置:
message.multiVars=var1={0}, var2={1}
在刚才的 Controller 中增加一个请求:
@GetMapping("m2")public List<String> m2(Locale locale) { final List<String> multi = new ArrayList<>(); multi.add("参数为 null: " + messageSource.getMessage("message.multiVars", null, locale)); multi.add("参数为空:" + messageSource.getMessage("message.multiVars", new Object[]{}, locale)); multi.add("只传一个参数:" + messageSource.getMessage("message.multiVars", new Object[]{"第一个参数"}, locale)); multi.add("传两个参数:" + messageSource.getMessage("message.multiVars", new Object[]{"第一个参数", "第二个参数"}, locale)); multi.add("传超过两个参数:" + messageSource.getMessage("message.multiVars", new Object[]{"第一个参数", "第二个参数", "第三个参数"}, locale)); return multi;}
我们看看结果:
###GET http://localhost:8080/m2### 结果是:[ "参数为 null: var1={0}, var2={1}", "参数为空:var1={0}, var2={1}", "只传一个参数:var1=第一个参数,var2={1}", "传两个参数:var1=第一个参数,var2=第二个参数", "传超过两个参数:var1=第一个参数,var2=第二个参数"]
我们可以看到,我们在配置文件中定义了带参数的配置信息,此时,我们可以不传参数、传少于指定数量的参数、传符合指定数量的参数、传超过指定数量的参数,都可以正常返回国际化信息。
此处可以理解为,MessageFormat.format
执行过程是for-index
循环,从配置值中找格式为{数字}
的占位符,然后用对应下标的输入参数替换,如果属于参数没了,就保持原样。
如果我们的配置文件中没有配置或者对应语言及其父级都没有配置呢?
这个就要靠前面说的useCodeAsDefaultMessage
配置了,如果为 true,就会返回输入的 code,如果为 false,就会抛出异常。默认是 false,所以如果找不到会抛异常。比如:
@GetMapping("m3")public List<String> m3(Locale locale) { final List<String> multi = new ArrayList<>(); multi.add("不存在的 code: " + messageSource.getMessage("message.notExist", null, locale)); return multi;}
这个时候我们执行 http 请求:
###GET http://localhost:8080/m3### 结果是:{ "timestamp": "2022-06-19T09:14:14.977+00:00", "status": 500, "error": "Internal Server Error", "path": "/m3"}
这是报错了,异常栈是:
org.springframework.context.NoSuchMessageException: No message found under code 'message.notExist' for locale 'zh_CN_#Hans'. at org.springframework.context.support.AbstractMessageSource.getMessage(AbstractMessageSource.java:161) ~[spring-context-5.3.20.jar:5.3.20] at cn.howardliu.effective.spring.springbootmessages.controller.HelloController.m3(HelloController.java:47) ~[classes/:na] ……此处省略
本文开头说过,MessageSource 有三种实现,Spring 默认使用了 ResourceBundleMessageSource,我们可以自定义使用 ReloadableResourceBundleMessageSource。
既然是在 SpringBoot 中,我们可以依靠 SpringBoot 的特性定义:
@Configuration(proxyBeanMethods = false)@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)@ConditionalOnProperty(name = "spring.messages-type", havingValue = "ReloadableResourceBundleMessageSource")@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@Conditional(ReloadResourceBundleCondition.class)@EnableConfigurationPropertiespublic class ReloadMessageSourceAutoConfiguration { private static final Resource[] NO_RESOURCES = {}; @Bean @ConfigurationProperties(prefix = "spring.messages") public MessageSourceProperties messageSourceProperties() { return new MessageSourceProperties(); } @Bean public MessageSource messageSource(MessageSourceProperties properties) { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); if (StringUtils.hasText(properties.getBasename())) { final String[] originBaseNames = StringUtils .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())); final String[] baseNames = new String[originBaseNames.length]; for (int i = 0; i < originBaseNames.length; i++) { if (originBaseNames[i].startsWith("classpath:")) { baseNames[i] = originBaseNames[i]; } else { baseNames[i] = "classpath:" + originBaseNames[i]; } } messageSource.setBasenames(baseNames); } if (properties.getEncoding() != null) { messageSource.setDefaultEncoding(properties.getEncoding().name()); } messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale()); Duration cacheDuration = properties.getCacheDuration(); if (cacheDuration != null) { messageSource.setCacheMillis(cacheDuration.toMillis()); } messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat()); messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage()); return messageSource; } protected static class ReloadResourceBundleCondition extends SpringBootCondition { private static final ConcurrentReferenceHashMap<String, ConditionOutcome> CACHE = new ConcurrentReferenceHashMap<>(); @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages"); ConditionOutcome outcome = CACHE.get(basename); if (outcome == null) { outcome = getMatchOutcomeForBasename(context, basename); CACHE.put(basename, outcome); } return outcome; } private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) { ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle"); for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) { for (Resource resource : getResources(context.getClassLoader(), name)) { if (resource.exists()) { return ConditionOutcome.match(message.found("bundle").items(resource)); } } } return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll()); } private Resource[] getResources(ClassLoader classLoader, String name) { String target = name.replace('.', '/'); try { return new PathMatchingResourcePatternResolver(classLoader) .getResources("classpath*:" + target + ".properties"); } catch (Exception ex) { return NO_RESOURCES; } } }}
我们可以看到,我们在执行messageSource.setBasenames(baseNames);
的时候,baseNames
中的值都是设置成classpath:
开头的,这是为了使ReloadableResourceBundleMessageSource
能够读取 CLASSPATH 下的配置文件。当然也可以使用绝对路径或者相对路径实现,这个是比较灵活的。
我们可以通过修改配置文件内容,查看变化,这里就不再赘述。纸上得来终觉浅,绝知此事要躬行。
本文通过几个小例子介绍了MessageSource
的使用。这里做一下预告,下一章我们会从源码角度分析MessageSourc
e 的实现类ResourceBundleMessageSource
和ReloadableResourceBundleMessageSource
的执行逻辑;然后我们自定义扩展,从 Nacos 中读取配置内容,实现更加灵活的配置。
本文中的实例已经传到 GitHub,关注公众号「看山的小屋」,回复spring
获取源码。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:SpringBoot 实战:国际化组件 MessageSource
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:SpringBoot 实战:国际化组件 MessageSource
你好,我是看山。
transmittable-thread-local 是阿里开源一个线程池复用场景下,处理异步执行时上下文传递数据问题的解决方案。可以从官方文档https://github.com/alibaba/transmittable-thread-local获取更多信息。
本文主要是变更 transmittable-thread-local 使用方式时出现的一个异常。
看异常之前,先简单说下项目大概情况。
项目是 Java 栈,使用了 SpringBoot+MyBatis 的框架结构,构建工具是 Maven。因为项目中使用了比较多的多线程逻辑,所以引入了 transmittable-thread-local,解决上下文传递数据问题。后来做项目升级,接入公司的监控系统,启动时增加了启动参数-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
,通过零侵入的方式解决多线程上下文传值问题。
于是,有些逻辑出错了。
我们看看异常栈(日志做了删改,隐藏项目信息):
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor' available: expected single matching bean but found 3: executor1,executor2,executor3 at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1200) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:420) at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349) at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342) at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1127) …… at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88) ……
异常日志很清楚,就是通过AbstractApplicationContext.getBean
获取 Bean 的时候,因为存在多个同类型的ThreadPoolTaskExecutor
,Spring 容器不知道返回哪个 Bean,就抛出了NoUniqueBeanDefinitionException
异常。
我们再来看看调用代码:
public static void doSth(Object subtag, Object extra, long time) { ApplicationContextContainer.getBean(ThreadPoolTaskExecutor.class) .execute(() -> { // 一些业务代码 });}@Componentpublic class ApplicationContextContainer implements ApplicationContextAware { private static ApplicationContext applicationContext; public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ApplicationContextContainer.applicationContext = applicationContext; }}
可以看出来,applicationContext.getBean
时只传入了 class 类型,没有指明 Bean 的名字。推测是项目中定义了多个ThreadPoolTaskExecutor
类型的 Bean,名字分别是 executor1、executor2、executor3(名字改过了,大家写代码时尽量使用见名知意的起名方式)。
@Configurationpublic class ExecutorConfig { @Bean(value = "executor1") public Executor executor1() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); // 一些初始化方法 taskExecutor.initialize(); return taskExecutor; } @Bean(value = "executor2") public Executor executor2() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); // 一些初始化方法 taskExecutor.initialize(); return TtlExecutors.getTtlExecutor(taskExecutor); } @Bean(value = "executor3") public Executor executor3() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); // 一些初始化方法 taskExecutor.initialize(); return TtlExecutors.getTtlExecutor(taskExecutor); }}
从上面的代码可以发现,确实有 executor1、executor2、executor3 三个Executor
,executor1 是ThreadPoolTaskExecutor
类型的,executor2 和 executor3 是经过TtlExecutors.getTtlExecutor
包装的ThreadPoolTaskExecutor
。
我们来看看TtlExecutors.getTtlExecutor
方法:
public static Executor getTtlExecutor(@Nullable Executor executor) { if (TtlAgent.isTtlAgentLoaded() || null == executor || executor instanceof TtlEnhanced) { return executor; } return new ExecutorTtlWrapper(executor, true);}
根据错误反推,经过TtlExecutors.getTtlExecutor
之后返回的还是ThreadPoolTaskExecutor
类型。也就是上面代码走了if
语句,直接返回了输入参数。
但是,这里就碰到了两个开发十大未解之谜中的两个:
首先,我们需要知道,代码的终点不是玄学。我们现在用的计算机还不会撒谎,只要报错了,就一定是有问题。
我们仔细看看TtlExecutors.getTtlExecutor
方法中的if
判断:
TtlEnhanced
类型,输入的是ThreadPoolTaskExecutor
类型,不符合。所以,重点看看 ttlAgentLoaded 标识:
public static boolean isTtlAgentLoaded() { return ttlAgentLoaded;}
从全局找到修改ttlAgentLoaded
的地方是:
public final class TtlAgent { public static void premain(final String agentArgs, @NonNull final Instrumentation inst) { kvs = splitCommaColonStringToKV(agentArgs); Logger.setLoggerImplType(getLogImplTypeFromAgentArgs(kvs)); final Logger logger = Logger.getLogger(TtlAgent.class); try { logger.info("[TtlAgent.premain] begin, agentArgs: " + agentArgs + ", Instrumentation: " + inst); final boolean disableInheritableForThreadPool = isDisableInheritableForThreadPool(); // 省略非相关代码 ttlAgentLoaded = true; } catch (Exception e) { String msg = "Fail to load TtlAgent , cause: " + e.toString(); logger.log(Level.SEVERE, msg, e); throw new IllegalStateException(msg, e); } } // 省略非相关代码}
有一定 javaagent 知识的应该知道,premain
方法是 java 启动时,加载 javaagent 后执行的方法。
这就吻合了。
报错之前的服务器代码,ExecutorConfig
类中定义的 executor1 是ThreadPoolTaskExecutor
类型,executor2 和 executor3 是ExecutorTtlWrapper
类型,使用applicationContext.getBean(clazz)
能够得到名字是 executor1 的 Bean。
然后使用-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
方式实现零侵入的transmittable-thread-local
注入能力。ExecutorConfig
类中定义的 executor2 和 executor3 是ThreadPoolTaskExecutor
类型,使用applicationContext.getBean(clazz)
就会查到三个ThreadPoolTaskExecutor
类型的 Bean,Spring 容器没有办法判断返回哪一个,于是抛出了NoUniqueBeanDefinitionException
异常。
本地启动是加上-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
命令,问题复现。
解决上面的报错比较简单,就是使用applicationContext.getBean(beanName, clazz)
方法,通过输入指定的 Bean 的名字和类型,获取确定 Bean,代码修改为:
public static void doSth(Object subtag, Object extra, long time) { ApplicationContextContainer.getBean("executor1", ThreadPoolTaskExecutor.class) .execute(() -> { // 一些业务代码 });}
流水线发版回归测试,问题解决。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:小心 transmittable-thread-local 的这个坑
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:小心 transmittable-thread-local 的这个坑
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
与 if-else 一样,switch 语法是用来做条件判断的。当条件清晰简洁时,能够有效地提升代码可读性。switch 语法从 Java5 开始,Java12 增加了 Switch 表达式(Java14 提供正式版),Java17 增加 Switch 模式匹配(预览版)。
本文的目标是期望读者可以掌握 Switch 语法的所有能力,在需要的时候信手拈来。
我们先来个简单的例子看看 if-else 和 switch 语法的使用:
public static void demoOfIf(int num) { if (num == 0) { System.out.println("0"); } else if (num == 1) { System.out.println("1"); } else if (num == 2) { System.out.println("2"); } else { System.out.println("3"); }}public static void demoOfSwitch(int num) { switch (num) { case 0: { System.out.println("0"); break; } case 1: { System.out.println("1"); break; } case 2: { System.out.println("2"); break; } default: { System.out.println("3"); } }}
上面的示例很简单,下面我们就来着重学习一下 Swith 语法。
switch(integral-selector) {case value1: statement1; break;case value2: statement2; break;// ……default: default-statement;}
switch 语句是一种多路选择的简洁表达式,在 Java7 之前,switch 语句的表达式必须是整数值,这样会有很多的限制。于是在 Java7 中增加了 String 格式的支持,使应用场景更加丰富。
每个 case 执行语句末尾都有一个break
关键字,它会让执行流程跳到 switch 的末尾。如果不加break
,后面的 case 语句会继续执行,直到第一个break
关键字。
比如:
public static void noBreak(int num) { switch (num) { case 0: { System.out.println("0"); } case 1: { System.out.println("1"); } case 2: { System.out.println("2"); break; } default: { System.out.println("3"); } }}
执行noBreak(0)
的结果会是:
012
基于这种特性,我们可以合并多个值的执行逻辑,比如下面这种写法:
public static void noBreak2(int num) { switch (num) { case 0: case 1: case 2: { System.out.println("0 or 1 or 2"); break; } default: { System.out.println("3"); } }}
当参数是 0 或 1 或 2 时,结果相同,都是:
0 or 1 or 2
Switch 语句出现的姿势是条件判断、流程控制组件,与现在很流行的新语言对比,其写法显得非常笨拙,所以 Java 推出了 Switch 表达式语法,可以让我们写出更加简化的代码。这个扩展在 Java12 中作为预览版首次引入,需要在编译时增加-enable-preview
开启,在 Java14 中正式提供,功能编号是 JEP 361。
比如,我们通过 switch 语法简单计算工作日、休息日,在 Java12 之前需要这样写:
@Testvoid testSwitch() { final DayOfWeek day = DayOfWeek.from(LocalDate.now()); String typeOfDay = ""; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: typeOfDay = "Working Day"; break; case SATURDAY: case SUNDAY: typeOfDay = "Rest Day"; break; } Assertions.assertFalse(typeOfDay.isEmpty());}
在 Java12 中的 Switch 表达式中,我们可以直接简化:
@Testvoid testSwitchExpression() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Working Day"; case SATURDAY, SUNDAY -> "Day Off"; }; Assertions.assertEquals("Day Off", typeOfDay);}
是不是很清爽。不过,这里有一点不足的是,如果是需要执行一段业务逻辑,然后返回一个结果呢?于是 Java13 使用yield
关键字补齐了这个功能:
@Testvoid testSwitchExpression13() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> { System.out.println("Working Day: " + day); yield "Working Day"; } case SATURDAY, SUNDAY -> { System.out.println("Day Off: " + day); yield "Day Off"; } }; Assertions.assertEquals("Day Off", typeOfDay);}
到 Java17 时,又提供了 Switch 模式匹配功能。与 instanceof 模式匹配有些类似,是能够在 Switch 表达式实现类型自动转换。
比如:
static String formatterPatternSwitch(Object o) { return switch (o) { case null -> "null"; case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.getClass().getSimpleName() + " " + o; };}public static void main(String[] args) { System.out.println(formatterPatternSwitch(null)); System.out.println(formatterPatternSwitch("1")); System.out.println(formatterPatternSwitch(2)); System.out.println(formatterPatternSwitch(3L)); System.out.println(formatterPatternSwitch(4.0)); System.out.println(formatterPatternSwitch(new AtomicLong(5)));}
结果是:
nullString 1int 2long 3double 4.000000AtomicLong 5
可以看到,不只是类型自动转换,还可以直接判断是否是null
,省了前置判断对象是否是null
了。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Switch 块、Switch 表达式、Switch 模式匹配,越来越好用的 Switch
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Switch 块、Switch 表达式、Switch 模式匹配,越来越好用的 Switch
你好,我是看山。
前段时间介绍了从 Java8 到 Java17 每个版本比较有特点的新特性(收录在 从小工到专家的 Java 进阶之旅 专栏),今天看到 JRebel 发布了《2022 年 Java 发展趋势和分析》,于是借此分析一下 Java 行业的现状,希望给大家一些参考。
JRebel 是通过调研问卷的方式总结的报告,涉及了不同国家、不同岗位、不同公司规模、不同行业,相对来说,该调查报告是有一定参考意义的。
我们先来看下大家都在使用的 Java 版本(包括 JVM 语言:Kotlin、Groovy、Scala):
从结果我们可以看到,Java8 占比 37%,Java11 占比 29%,甚至有 12%的被调查者使用了高于 Java12 的版本。
Java8 是 2014 年发布,相较于之前版本,增加了 Lambda 表达式、Stream 流式处理等一种优秀的 API。至今已 8 年时间,Java 版本也是从 8 一直升到了 17。版本号一直在增加,却没有增加特别吸引人的语言特性。
哪些因素是大家升级的动力呢?
可以看到,主要的升级因素包括 LTS 版本(25%)、安全性(23%)、性能(20%),新特性(18%)和随大流(14%)占比低一些。
从这点我们也就知道为什么 Java11 之后的版本占比并不高了,随着 Java17 的发布,可能 Java8 和 Java11 的占比会降低。安全性方面,除非是严重的漏洞,一般 Java 开发团队会通过补丁的方式升级,不会影响大版本号占比。
性能方面,Java 团队一直在不断优化,随着 G1、ZGC、Shenandoah 等一众优秀的 GC 被添加进来,这也成为大家从 Java8 升级的重要原因。
就功能特性而言,Java11 之后增加了 Record 类型、密封类、instanceof 模式匹配、Swtich 表达式和模式匹配等一些语法糖。这些新特性,也能够提升升级到 Java17 的意愿。
Java17 是 2021 年下半年发布的 LTS 版本(长期支持版)。
我们看下大家升级的意愿:
从结果可以看出来,有 37%的人会在未来 6 个月内升级,有 25%的人会在 6-12 个月内升级,不会升级的占比仅占 8%。
可见,有 62%的人会在未来一年内升级到 Java17,大家的升级意愿还是比较强的。
我们都知道,市面上有很多的 JDK 版本,在 Oracle 起诉 Google 侵权之前,非企业特供的情况下,我们基本上用的都是 OracleJDK,后来因为容器中使用 JDK 版本的版权问题,容器中大部分使用了 OpenJDK。
从问卷结果也反映了这种情况:
OracleJDK 的版本占比 36%,OpenJDK 的版本占比 43%,其中包括标准 OpenJDK 和 AdoptOpenJDK 版本。
有些同学会疑惑 OracleJDK 和 OpenJDK 的区别在哪?我们日常用到的部分,没有任何区别。
这个问题的结果有些出乎我的预料:
各种架构风格中,微服务架构仅占 32%,单体架构占比 22%,模块化单体架构占比 13%,SOA 架构占比 12%。
从结果来看,这个问卷的对架构风格的定义和分类比较细腻。
很多公司把系统的服务化、模块化也统称为了微服务,这是一种很严重的错误,在之前的文章 《微服务架构的陷阱:从单体到分布式单体》 中介绍过这种错误。
推荐阅读:
这里不对架构风格做出评价,架构只有合适与否,没有优劣之分。
既然微服务架构占比高一些,我们就来看一下微服务架构的应用趋势。
从结果来看,有 44%的人团队已经是完全微服务架构了,还有 44%的团队在向微服务架构迁移。可见,在 Java 行业中,微服务架构是得到大家普遍认可的。
但是这个结果与上面的架构风格占比结果有出入,可能是问卷题目设计问题,或者问题回答者的主观原因,不能够苛求结果准确性。
既然是微服务架构,每个应用中服务数量必然超过 1 个。从结果可以看出来,有 54%的应用中少于 10 个服务,还有 22%的应用服务数量超过 20 个。
按照公司规模维度,越是大公司,每个应用中服务数量越多,结果符合康威定律的。从大家普遍实践结果看,当团队规模较小时,要尽量减少微服务数量。市面上很多老师会告诉我们,微服务架构要按照业务域拆分,但是你要知道,如果团队规模不大,即使拆分了业务域,可能最终开发调试维护也只有你一个。
从结果看,SpringBoot 几乎霸占了整个微服务市场。所以,大家在日常工作学习过程中,还是主要看看 SpringBoot 栈吧。
在国内,SpringBoot 技术栈还会细分为 SpringNetflixCloud 栈、SpringAlibabaCloud 栈、SpringBoot+Dubbo 栈等。
不同的技术栈中组件有些差异,所以我们需要掌握的不是简单的应用,还要了解其中的原理。原理掌握了,不同的组价只是在应用层面的差异。
随着公司业务的增长,应用中会增加各种各样的新功能。问卷中有个问题是关于随着时间推移,微服务启动时间的变化:
可见,有 60%的服务启动时间都在增加,甚至有 13%的应用启动时间增长超过 50%,有 30%的应用启动时间增长范围在 10%-50%。
为什么启动时间会增长呢?这个与公司业务增长后,代码增加了很多新功能有关。随着功能增加、类的增加,系统体积增大、加载类数量增大,启动时间会随之增加。这会引起系统的腐化,当腐化到一定程度,可能就需要重构了。或者随着业务增长,原来的微服务边界划分不合适了,需要重新划分系统边界,拆分微服务。
既然微服务总体的启动时间在增长,那启动时间一般是多久呢?
可以看到,只有 9%的服务在 1 分钟内启动成功,有 26%的服务启动时间需要 10 分钟以上。
从上图可以看出来,人员规模大于 100 人的公司中,服务启动时间普遍长于少于 100 人的公司。产生这种情况的原因有这么几个:
采用微服务其中一个好处是服务足够小,启动时间比较少。但是,从上面两个问卷结果来看,普遍情况是启动时间比较长,而且在变得更长。
从问卷结果可以看到,Docker 使用率是 41%,Kubernetes 使用率是 26%,VMware 使用率是 16%,Vagrant 使用率是 3%,即有 86%已经实现了虚拟化,其中 Docker、Kubernetes 占比最高。
所以在 Java 升级版本特性中,实现了容器感知的能力,使 Java 服务容器化更容易一些。
JRebel 的这个问卷调查是全球性质的,从全球范围看,AWS 当之无愧的 NO.1。AWS 作为亚马逊曾经的附属产业,已经成为了亚马逊的重要业务之一。
与亚马逊的经历类似,阿里巴巴从电商切入,然后布局云服务(阿里云)。如果还是走亚马逊的老路,势必没法超越。不过阿里从很多年前开始布局 CPU 和芯片领域,如果能够有所突破,就可以破开西方技术的封锁,依托我国的发展潜力,未必不能撼动亚马逊的 PaaS 服务商地位。
前面关于微服务的问题中,SpringBoot 是众多微服务框架中的首选,SpringBoot 默认的应用容器是 Tomcat。加之 Tomcat 的开源方式,将近半数应用服务器选择 Tomcat 也是预料之中。
Maven 和 Gradle 到底该用哪个?这个问题似乎争论许久。从问卷结果看,Maven 占有率是 68%,Gradle 占有率是 23%,Maven 还是有绝对的优势。
Gradle 采用了约定大于配置的方式,与 SpringBoot 的理念一致。但是从市场接受度和发展而言,并没有形成替换 Maven 的风潮。Android 项目默认使用 Gradle,能够看出 Google 对 Gradle 的推崇,也从侧面印证 Gradle 的优秀。但是,Gradle 并没有绝对优势。
我是从 2015 年开始使用 IntelliJ IDEA,试用之后立马抛弃了 Eclipse。首先是快捷键的设计,可以很大程度摆脱鼠标。内置的插件市场,可以找到任何需要的插件,提升编程体验。更关键的是,JetBrains 公司出品的 IDE,可以无缝对接,实现不同语言的编程支持。
Eclips 也不是一无是处,它的插件体系也是相当丰富,很多低代码开发工具都是基于 Eclipse 开发的。如果是普通开发,推荐使用 IntelliJ IDEA;如果想要做低代码工具,可以考虑对 Eclipse 进行二次开发。
这一部分属于 JRebel 有私心的部分,JRebel 一个优势功能是提供热部署能力,所以会在问卷中询问被调查者重新部署应用的时间。
很多时候,我们可能只改动一行代码,然后验证功能是不是正常,这个时候需要重新部署应用。JRebel 统计了重新部署需要花费的时间。
从结果上看,重新部署需要超过 3 分钟时间的占 50%,其中 21%的比率需要 10 分钟以上。那这段时间,大家会干什么?
有 28%会增加新功能;有 20%会优化系统性能;有 19%会完善测试覆盖。这些都是正向的,大概率的是那些回答其他的:喝咖啡、喝啤酒、开趴、睡觉、钓鱼……
不过也是符合我们工作的原因:我们工作是为了生活,而不是为了加班。所以,假如每天给你 1 小时的自由时间,你会用来做什么呢?欢迎评论区讨论。
技术不断发展,我们需要学习的东西越来越多,很多时候感觉学不动了。但既然选择了这个行业,拿着高于其他行业的薪资,也承担着各种裁员的风险,总归是要有一些技能傍身,才不至于被历史的车轮碾成粉末。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:2022 年 Java 行业分析报告
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:2022 年 Java 行业分析报告
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
Java17 是在 2021 年 9 月发布的一个 LTS 版本(长期支持版本),上一个长期支持版是 Java11,于 2018 年 9 月发布。从目前来看,Java11 在市场占有率已经过半,如果错过了升级 Java11,我们可不要错过这次的升级。接下来我们看看 Java17 为我们带来了哪些新增特性:
接下来我们一起看看这些特性。
这个特性是利好科学计算中的浮点运算,保证浮点运算中的 strict 或 strictfp 在每个平台上都能够得到相同的结果,也就是可以把 strictfp 扔了。
在 Java1.2 之前,精确浮点计算是通过迂回的方式实现的。大约从 2001 年开始,奔腾 4 和更高版本的处理器中提供了 SSE2 扩展(数据流单指令多数据扩展指令集 2),可以直接支持严格的 JVM 浮点运算,不需要额外的开销。那个时候 Intel 和 AMD 还不支持这种扩展,于是 Java1.2 的浮点运算就分叉了。
到后来 Intel 和 AMD 也开始支持 SSE2 和更高版本的扩展指令集,Java 语言就可以恢复到严格的浮点运算了。连 Java 之父 James Gosling 在 Twitter 也发文庆祝:
这个特性是为伪随机数生成器 RPNG(Pseudo-Random Number Generators)增加了新的接口类型和实现,可以更容易地互换使用不同的算法,而且它还为基于流的编程方式提供了更好的支持。这个特性的目标有四个:
java.util.Random
类的现有行为,做好向下兼容。新增了java.util.random.RandomGenerator
接口,作为所有 PRNG 算法的统一 API,提供了工厂类java.util.random.RandomGeneratorFactory
,借助java.util.ServiceLoader.load()
的能力加载各种 PRNG 算法实现,可以构造RandomGenerator
实例。
我们遍历一下看看有哪些 PRNG 算法:
RandomGeneratorFactory.all().forEach(factory -> { System.out.println(factory.group() + ":" + factory.name());});
结果是:
LXM:L32X64MixRandomLXM:L128X128MixRandomLXM:L64X128MixRandomLegacy:SecureRandomLXM:L128X1024MixRandomLXM:L64X128StarStarRandomXoshiro:Xoshiro256PlusPlusLXM:L64X256MixRandomLegacy:RandomXoroshiro:Xoroshiro128PlusPlusLXM:L128X256MixRandomLegacy:SplittableRandomLXM:L64X1024MixRandom
Legacy:Random
就是我们常用的java.util.Random
,我们来试试看:
RandomGenerator randomGenerator = RandomGeneratorFactory.of("Random") .create(System.currentTimeMillis());System.out.println(randomGenerator.getClass());System.out.println(randomGenerator.nextInt(10));
结果是:
class java.util.Random6 (这个值随不同的运行结果不同)
我们还可以使用流式编程方式批量获取随机数:
final IntStream ints = RandomGeneratorFactory.of("L128X128MixRandom") .create() .ints(10, 0, 100);System.out.println(Arrays.toString(ints.toArray()));
结果会得到 10 个随机数字数组(每次运行结果不同):
[50, 16, 73, 4, 79, 32, 55, 34, 40, 53]
MacOS 为了提升图形渲染性能,在 2018 年 9 月放弃之前的 OpenGL 渲染库,选用了 Apple Metal。从 Java17 开始,Swing API 内部用于渲染 Java 2D 的 API 开始使用新的 Apple Metal 加速渲染 API。
默认情况下,这个功能不启用,需要主动开启:
-Dsun.java2d.metal=true
这个特性改动是属于 API 内部实现,使用上没有任何差别。而且对 MacOS 的系统版本有要求,需要在 MacOS10.14 版本或以上,否则还是会使用 OpenGL 渲染图形。
苹果在 2020 年 6 月的 WWDC 的演讲中宣布,将开启长期将 Macintosh 系列从 x64 过渡到 AArch64 的计划,该特性主要是为了适应这种改变。
Linux 的 AArch64 支持是在 Java9 提供的(参见 Java9 的新特性),Windows 的 AArch64 支持是在 Java16 提供的(参见 Java16 的新特性)。
在 Java12 的时候对 AArch64 的支持库进行了统一,只保留了一套维护代码(参见 Java12 的新特性)。
在 Java16 中为了改进 JDK 的安全性和可维护性,对内部 API 进行了封装,但是也留了后门,可以使用启动参数--illegal-access
控制内部 API 的封装程度。(参见 Java16 的新特性)
到了 Java17 中,除了sun.misc.Unsafe
可以使用,其他的内部 API 都变成了强封装模式,而且--illegal-access
命令也被移除,如果还在命令中添加该参数,会直接报错:
~ $ java -versionopenjdk version "17.0.1" 2021-10-19OpenJDK Runtime Environment (build 17.0.1+12-39)OpenJDK 64-Bit Server VM (build 17.0.1+12-39, mixed mode, sharing)~ $ java --illegal-accessUnrecognized option: --illegal-accessError: Could not create the Java Virtual Machine.Error: A fatal exception has occurred. Program will exit.
密封类特性是在 Java15 提供预览版,Java16 提供第二版预览,终于在 Java17 中成为正式功能。该特性限制哪些其他类或接口可以扩展或实现密封组件。
JEP 409 并没有对密封类有新的特性,可以参考 Java15 的新特性、Java16 的新特性,这里不再重复。
Java 对象序列化是一个非常重要的功能,可以透明化远程处理,也促进了 JavaEE 的成功。序列化过程没有问题,但是反序列化过程可能存在危险:
终于在 Java17 中增加了反序列化过滤器,允许应用程序使用 JVM 范围的过滤器工厂,配置特定于上下文和动态选择的反序列化过滤器,该工厂用于为每个反序列化操作选择一个过滤器。
简单点说,就是提前说好可以反序列化哪些类,如果序列化数据流中包含不被允许的类对象,就直接报错。
这个特性功能很赞,在 Java14 中正式提供 Switch 表达式特性(参见 Java14 的新特性),本次提供的是 Switch 模式匹配与 instanceof 模式匹配有些类似,是能够在 Switch 表达式实现类型自动转换。
比如:
static String formatterPatternSwitch(Object o) { return switch (o) { case null -> "null"; case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.getClass().getSimpleName() + " " + o; };}public static void main(String[] args) { System.out.println(formatterPatternSwitch(null)); System.out.println(formatterPatternSwitch("1")); System.out.println(formatterPatternSwitch(2)); System.out.println(formatterPatternSwitch(3L)); System.out.println(formatterPatternSwitch(4.0)); System.out.println(formatterPatternSwitch(new AtomicLong(5)));}
结果是:
nullString 1int 2long 3double 4.000000AtomicLong 5
可以看到,不只是类型自动转换,还可以直接判断是否是null
,省了前置判断对象是否是null
了。
期待这个功能早日转正。
Java 程序可以通过该 API 与 Java 运行时之外的代码和数据互操作。通过有效地调用外部函数(即 JVM 外部的代码),并通过安全地访问外部内存(即不由 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会有 JNI 的脆弱性和危险。
通过更加优雅的方式访问外部函数是从 Java14 开始的,经历了多个孵化版本:
可以看出来,虽然一直在孵化,但是功能越来越强大,这一旦孵出来,岂不是超级神兽了。
这一系列的功能都是为了能够在 Java 类中调用 C 语言类库:
private static final SymbolLookup libLookup;static { // loads a particular C library var path = JEP412.class.getResource("/print_name.so").getPath(); System.load(path); libLookup = SymbolLookup.loaderLookup();}
第一步,需要加载我们希望通过 API 调用的目标库。
第二步,我们需要指定目标方法的签名,并最终调用它:
public String getPrintNameFormat(String name) { var printMethod = libLookup.lookup("printName"); if (printMethod.isPresent()) { var methodReference = CLinker.getInstance() .downcallHandle( printMethod.get(), MethodType.methodType(MemoryAddress.class, MemoryAddress.class), FunctionDescriptor.of(CLinker.C_POINTER, CLinker.C_POINTER) ); try { var nativeString = CLinker.toCString(name, newImplicitScope()); var invokeReturn = methodReference.invoke(nativeString.address()); var memoryAddress = (MemoryAddress) invokeReturn; return CLinker.toJavaString(memoryAddress); } catch (Throwable throwable) { throw new RuntimeException(throwable); } } throw new RuntimeException("printName function not found.");}
Vector 向量计算 API 是为了处理 SIMD(Single Instruction Multiple Data,单指令多数据)类型的操作,即并行执行的各种指令集。它利用支持向量指令的专用 CPU 硬件,并允许以管道的形式执行此类指令。这种运算方式可以让开发人员实现更高效的代码,充分利用底层硬件的潜力。日常使用包括科学代数线性应用程序、图像处理、字符处理、繁重的算术应用程序,以及任何需要对多个独立操作数应用一个运算的应用程序。
Vector 向量计算 API 是在 Java16 引入(参见 Java16 的新特性),可以在运行时借助 CPU 向量运算指令,实现更优的计算能力。在 Java17 中,针对性能和实现进行了改进,包括字节向量与布尔数组之间进行转换。
原来的向量运算我们需要这样写:
for (var i = 0; i < a.length; i++) { c[i] = a[i] * b[i];}
现在我们可以这样写:
final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;for (var i = 0; i < a.length; i += SPECIES.length()) { var m = SPECIES.indexInRange(i, a.length); var va = FloatVector.fromArray(SPECIES, a, i, m); var vb = FloatVector.fromArray(SPECIES, b, i, m); var vc = va.mul(vb); vc.intoArray(c, i, m);}
Applet 是用 Java 编写可以嵌入到网页中的小应用,属于已经过时的技术,很多浏览器已经取消支持。Applet API 在 Java9 的时候标记了过期,在 Java17 标记为删除(@Deprecated(since = "9", forRemoval = true)
)。
记得我上学的时候,课本上还有这部分内容。
RMI 激活机制在 Java15 标记了过期(参见 Java15 的新特性),到 Java17 正式删除。这里只是删除了 RMI 激活机制,对于其他 RMI 功能不受影响。
在 Java9 的 JEP 295 中,引入了实验性的提前编译 jaotc 工具,但是这个特性自从引入依赖用处都不太大,而且需要大量的维护工作,所以在 Java17 中决定删除这个特性。
但是保留了实验性的 Java 级 JVM 编译器接口(JVMCI),这样开发人员也可以继续使用外部构建的编译器版本,并使用 Graal 编译器(GraalVM)进行 JIT 编译。
Security Manager 在 JDK1.0 时就已经引入,但是它一直都不是保护服务端以及客户端 Java 代码的主要手段,对于如此鸡肋的功能,最终决定标记为删除(@Deprecated(since="17", forRemoval=true)
)。
本文介绍了 Java17 新增的特性,完整的特性清单可以从 https://openjdk.java.net/projects/jdk/17/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
Java17 是 LTS(长期支持版),上个 LTS 版本是 Java11,很多团队已经在生产上切换,相信接下来会有一些团队在测试环境尝鲜。
有人认为 Java8 是神,有人则喜欢不断地尝鲜,你是哪种呢?欢迎在留言说下你在用哪个版本?
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java17 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java17 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
Java16 是在 2021 年 3 月发布的一个短期版本,新增特性如下:
接下来我们一起看看这些特性。
在 JDK15 之前,JDK 中使用的 C++语言限制在 C++98/03 版本,没有办法使用更高级的特性,从 JDK16 开始,可以支持 C++14 的语言特性。
这一点更新对应用开发者可能关系不大,但是对于底层组件的开发者意义重大。Java 的版本更新迅速,C++的特性也是飞速更新,如果 JDK 还是限制在 C++98/03 版本,没有办法使用 C++11/14 中的高级特性,也是一种损失。
这是两个提案,JEP 357 是将 OpenJDK 社区的源代码版本控制工具,从 Mercurial(hg)迁移到 Git,JEP 369 是将 OpenJDK 项目定向到 GitHub 中的仓库,我们可以看到从 OpenJDK 的 JIRA 工具中,代码提交和 Issue 预览的都是在 https://github.com/openjdk 中,有一部分是从 https://git.openjdk.java.net 重定向到 GitHub。
Mercurial(hg)是一个 Python 编写的跨平台的分布式版本控制软件,与 Git 是同一时代开始的工具,功能也是很强大,只是在发展过程中,有些方面稍弱于 Git,比如元数据的占用、与现代工具链的集成。所以 OpenJDK 转而投向了 Git 的怀抱。
ZGC 是在 Java11 引入的(参见 Java11 新特性),在 Java15 中正式特性(参见 Java15 新特性),可以用命令-XX:+UseZGC
启用 ZGC。
ZGC 是一个并发的垃圾回收器,可以极大地提升 GC 的性能,支持任意堆大小而保持稳定的低延迟。在 128G 堆大小的测试中,ZGC 优势明显,找了一张网上的图片:
ZGC 的目标是实现垃圾回收与程序同时运行,将 STW 降低为 0,即不存在中断。目前在标记、重定位、参考处理、类卸载和跟处理阶段删除安全点处理。目前 ZGC 中仍然依靠安全点执行的包括部分的根处理和有时间限制的标记终止操作。这些根处理中有一项就是 Java 线程堆栈处理。随着线程数量增加,停顿时间增长。所以,我们需要实现并发的堆栈处理。目标包括:
对于本地进程间通信,Unix 套接字比 TCP/IP 更加安全高效。Unix 套接字一直是大多数 Unix 平台的一个特性,现在在 Windows 10 和 Windows Server 2019 也提供了支持。
所以在 Java16 中为java.nio.channels
包的SocketChannel
和ServerSocketChannel
添加了 Unix(AF_UNIX)套接字支持。Unix 套接字用于同一主机上的进程间通信(IPC), 在很大程度上类似于 TCP/IP,区别在于套接字是通过文件系统路径名而不是 Internet 协议(IP)地址和端口号寻址的。对于本地进程间通信,Unix 套接字比 TCP/IP 环回连接更安全、更有效。
这些移植的价值不在于移植本身,而在于支持平台的多样性。Java 的口号是一次编写到处运行。既然要到处运行,就得支持各种平台。而且,针对不同的操作系统支持,还能给我们提供更多的选择。
Alpine Linux 是一个独立非商业的 Linux 发行版,体系非常小,一个容器需要不超过 8MB 的空间,磁盘最小仅需 130MB 存储。如果我们通过 jlink 生产 JDK,Docket 镜像可以减小到 38MB,这样在微服务部署过程中,可以减少很多磁盘占用,也能减少镜像传输、部署时间。
这是在 HotSpot 中的空间分配上的优化,将未使用的元空间(metaspace,也叫类的元空间)中的内容存返回给操作系统。
应用程序如果存在大量类加载和类卸载的动作时,会占用大量的元空间内存,这部分内存得不到释放,造成内存利用率低。现在的应用系统为了应对高并发的流量,动辄部署数十上百台实例,这将造成极大的资源浪费。
元空间的内存方式使用的是基于区域的内存管理方式(Region-based memory management),也就是每个分配的对象都被分配到一个区域中。这里的区域有不同的叫法:zone(区域)、arena(竞技场)、memory context(内存上下文)等。
当类被回收后,其元空间区域中的内存块会返回自由列表中,以便以后重新使用。当然,可能很长使用不会被重新使用。这样就会造成元空间中很多的内存碎片,这些都是被标记为占用的内存。如果没有碎片的内存空间,是可以返回给操作系统的。
在 JEP 387 特性中,提出使用基于伙伴的内存分配算法(Buddy memory allocation)改善元空间的内存使用,这种方式是一种在 Linux 内核中经过验证的成熟算法。这种算法是在很小的块(chunk)中分配内存,这会降低类加载器的开销。
同时,JEP 387 增加了内存延迟提交给内存区域的特性,这样就会减少那种申请了内存却不使用的情况。
最后,JEP 387 将元空间的内存区域设计为不同大小,可以满足不同大小需求的内存申请。
这些操作与 Java13 中对 ZGC 的增强特性很类似(参见 Java13 的新特性)。他山之石可以攻玉,我们不妨学习一下这些方式,对我们在以后的开发中提供思路。
将基于值的类的公共构造函数设置启用移除警告。
比如Interger
的构造函数上设置了@Deprecated(since="9", forRemoval = true)
。如果某个类使用了Integer integer = new Integer(1);
这种写法,通过javac
命令编译时,会收到警告:[removal] Integer 中的 Integer(int) 已过时,且标记为待删除
这种警告信息。
基于值的类在类定义上都会有@jdk.internal.ValueBased
注解,比如java.lang.Integer
、java.lang.Double
等。这样的改动是为 Valhalla 项目做准备。
打包工具是在 Java14 中引入的孵化功能(参见 Java14 的新特性),可以打包成自包含的 Java 应用程序,比如 Windows 的 exe 和 msi、Mac 的 pkg 和 dmg、Linux 的 deb 和 rpm 等。
我们可以使用jlink
创建一个最小可运行的模块包,然后使用jpackage
将其构建成安装包:
jpackage --name myapp --input lib --main-jar main.jar
这里需要注意一点,因为已经成为正式功能,模块名从jdk.incubator.jpackage
改为jdk.jpackage
。
instanceof 模式匹配首先在 Java14 中提供预览功能(参见 Java14 特性),可以提供instanceof
更加简洁高效的实现,在 Java15 中进行了第二次预览,用于收集反馈,终于是多年的媳妇熬成婆,在 Java16 中成为正式功能。
我们再简单复习一下instanceof
模式匹配的功能(详细使用可以移步 Java14 特性):
@Testvoid test1() { final Object obj1 = "Hello, World!"; int result = 0; if (obj1 instanceof String str) { result = str.length(); } else if (obj1 instanceof Number num) { result = num.intValue(); } Assertions.assertEquals(13, result);}
Record 类型用来增强 Java 语言特性,充当不可变数据载体。与 instanceof 模式匹配一样,Record 类型也是在 Java14 中提供预览功能(参见 Java14 新特性),在 Java15 中进行了第二次预览,用于收集反馈。
我们再简单复习一下 Record 类型的功能,比如,我们定义一个Person
类:
public record Person(String name, String address) {}
我们转换为之前的定义会是一坨下面这种代码:
public final class PersonBefore14 { private final String name; private final String address; public PersonBefore14(String name, String address) { this.name = name; this.address = address; } public String name() { return name; } public String address() { return address; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PersonBefore14 that = (PersonBefore14) o; return Objects.equals(name, that.name) && Objects.equals(address, that.address); } @Override public int hashCode() { return Objects.hash(name, address); } @Override public String toString() { return "PersonBefore14{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; }}
Record 类型特性有四个特性:
equals
、getter
、setter
等方法;我们不能将 Record 类型简单的理解为去除“样板化”代码的功能,它不是解决 JavaBean 命名约定的中很多模板化方法的冗余繁杂问题,它的目标不是类似 Lombok 等工具自动生成代码的功能,是从开发人员专注模型的角度出发的。
这个功能特性是为了改进 JDK 的安全性和可维护性,是 Jigsaw 项目的主要目标之一。所以在 Java16 中,默认强封装 JDK 的绝大部分内部 API,有些关键性的 API,比如sun.misc.Unsafe
暂时可以放心使用。
我们可以使用启动参数--illegal-access
控制内部 API 的封装程度:
--illegal-access=permit
:JDK 8 中存在的每个包对未命名模块中的代码开放。也就是放心大胆地使用。Java9 中默认就是这个等级;--illegal-access=warn
:与许可相同,不同之处在于每次非法反射访问操作都会发出警告消息;--illegal-access=debug
:与 warn 相同,不同的是,每个非法反射访问操作都会发出警告消息和堆栈跟踪;--illegal-access=deny
:禁用所有非法访问操作,但由其他命令行选项(例如--add-opens
)启用的操作除外。密封类首次在 Java15 中预览(参见 Java15 新特性),在 Java16 中进行第二次预览,我们在复习一下功能:
public sealed interface JungleAnimal permits Monkey, Snake {}public final class Monkey implements JungleAnimal {}public non-sealed class Snake implements JungleAnimal {}
sealed
关键字与permits
关键字结合使用,以确定允许哪些类实现此接口。在我们的例子中,是Monkey
和Snake
。
sealed
:必须使用permits
关键字定义允许继承的子类;final
:最终类,不再有子类;non-sealed
:普通类,任何类都可以继承它。这是为向量计算专门定义的 API,可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量硬件指令,从而获得优于同等标量计算的性能,充分利用单指令多数据(SIMD)技术(大多数现代 CPU 上都可以使用的一种指令)。该 API 将使开发人员能够轻松地用 Java 编写可移植的高性能向量算法。
尽管 HotSpot 支持自动向量化,但是可转换的标量操作集有限且易受代码更改的影响。
final int[] a = {1, 2, 3, 4};final int[] b = {5, 6, 7, 8};final int[] c = new int[3];IntVector vectorA = IntVector.fromArray(IntVector.SPECIES_128, a, 0);IntVector vectorB = IntVector.fromArray(IntVector.SPECIES_128, b, 0);IntVector vectorC = vectorA.mul(vectorB);vectorC.intoArray(c, 0);
这个功能在 Java17 中进行了第二次孵化,基于使用安全的考虑,我们在短时间内用不上这个特性了。
这个特性提供了静态类型、纯 Java 访问原生代码的 API,大大简化绑定原生库的原本复杂且容易出错的过程。从 Java1.1 开始,我们可以通过原生接口(JNI)调用原生方法,但是并不好用,现在提供了外部链接器 API,可以不再使用 JNI 粘合代码了。
和向量 API 一样,暂时用不上了,等啥时候转正了,咱们重点说说怎么玩。
外部存储器访问 API 在 Java14 开始孵化(参见 Java14 新特性),在 Java15 中孵化第二版(参见 Java15 新特性),在 Java16 中进行第三版孵化。
外部存储器访问 API 使 Java 程序能够安全有效地对各种外部存储器(例如本机存储器、持久性存储器、托管堆存储器等)进行操作。外部内存通常是说那些独立 JVM 之外的内存区域,可以不受 JVM 垃圾收集的影响,通常能够处理较大的内存。
这次带来的特性包括:
MemorySegment
和MemoryAddress
接口之间更加清晰的职责分离;MemoryAccess
,提供了常见的静态内存访问器,以便在简单的情况下尽量减少对VarHandle
的需求;这些新的 API 虽然不会直接影响多数的应用类开发人员,但是他们可以在内存的第三方库中提供支持,包括分布式缓存、非结构化文档存储、大型字节缓冲区、内存映射文件等。
本文介绍了 Java16 新增的特性,完整的特性清单可以从 https://openjdk.java.net/projects/jdk/16/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java16 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java16 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
Java15 是在 2020 年 9 月发布的一个短期版本,新增特性如下:
接下来我们一起看看这些特性。
Edwards-Curve 数字签名算法(EdDSA),一种根据 RFC 8032 规范所描述的 Edwards-Curve 数字签名算法(EdDSA)实现加密签名。
EdDSA 是一种现代的椭圆曲线方案,与 JDK 中的现有签名方案相比,EdDSA 具有更高的安全性和性能,因此备受关注。它已经在 OpenSSL 和 BoringSSL 等加密库中得到支持,目前在区块链领域用的比较多。
我们看下官方给的例子:
byte[] msg = "Hello, World!".getBytes(StandardCharsets.UTF_8);// example: generate a key pair and signKeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");KeyPair kp = kpg.generateKeyPair();// algorithm is pure Ed25519Signature sig = Signature.getInstance("Ed25519");sig.initSign(kp.getPrivate());sig.update(msg);System.out.println(Hex.encodeHexString(sig.sign()));// example: use KeyFactory to contruct a public keyKeyFactory kf = KeyFactory.getInstance("EdDSA");NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519");EdECPublicKeySpec pubSpec = new EdECPublicKeySpec(paramSpec, new EdECPoint(true, new BigInteger("1")));PublicKey pubKey = kf.generatePublic(pubSpec);System.out.println(pubKey.getAlgorithm());System.out.println(Hex.encodeHexString(pubKey.getEncoded()));System.out.println(pubKey.getFormat());
例子中 Ed25519 是使用 SHA-512(SHA-2)和 Curve25519 的 EdDSA 签名方案。旨在提供与高质量 128 位对称密码相当的抗攻击能力,公钥长度为 256 位,签名长度为 512 位。
Java15 引入了一个新的特性:隐藏类(Hidden Classes),一个专为框架而设计的特性。大多数开发人员不会直接使用这个特性,一般是通过动态字节码或 JVM 语言来使用隐藏类。
隐藏类有下面三个特点:
隐藏类的功能特性还是比较有意思的,会涉及类加载、卸载、不可见、反射等很多内容,后续会开文单独聊,文章会放在 从小工到专家的 Java 进阶之旅 专栏中。
老的 DatagramSocket API 在 Java15 中被重写,是继 Java14 重写 Socket API 的后续不走。这个特性是 Loom 项目的先决条件。
目前,DatagramSocket
和MulticastSocket
将所有的套接字委托为java.net.DatagramSocketImpl
的实现,根据不同的平台,Unix 平台使用PlainDatagramSocketImpl
,Windows 平台使用TwoStackPlainDatagramSocketImpl
和DualPlainDatagramSocketImpl
。抽象类DatagramSocketImpl
是 Java1.1 提供的,功能很少且有一些过时方法,阻碍了 NOI 的实现。
类似于 Java14 中对 Socket API 的重写(参见 Java14 新特性),会在DatagramSocket
内部封装一个DatagramSocket
实例,将所有调用直接委托给该实例。包装实例或者使用 NIO 的DatagramChannel::socket
创建套接字,或者是使用原始DatagramSocket
类的实现DatagramSocketImpl
实现功能(用于实现向后兼容)。
我们可以看下新的依赖图:
在 Java15 中,默认禁用偏向锁,弃用了所有相关命令行选项。
偏向锁是 HotSpot 中一种用于减少非竞争锁定开销的优化技术,不过在如今的应用程序中,优化增益不太明显了。
根据官方说法,使用偏向锁增益最多的是大量使用早期同步组件(比如Hashtable
、Vector
等),随着新的 API 实现和针对多线程场景引入的支持并发的数据结构,偏向锁的锁定及撤销,会带来性能的开销,从而是优化收益降低。
而且随着越来越多的功能特性引入,偏向锁在同步子系统中引入的大量代码,侵入 HotSpot 其他组件,带来代码的复杂性和维护成本,成为代码优化的阻碍。所以官方要将其移除。
不过,有些应用在禁用偏向锁后会出现性能下降,可以使用-XX:+UseBiasedLocking
手动开启。
ZGC 是在 Java11 引入的(参见 Java11 新特性),一直处于试验阶段,想要体验,需要在参数中使用-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
组合启用,在 Java15 中,ZGC 成为正式特性,想要使用可以直接用命令-XX:+UseZGC
就行。
ZGC 是一个重新设计的并发的垃圾回收器,可以极大的提升 GC 的性能,支持任意堆大小而保持稳定的低延迟。从 https://openjdk.java.net/jeps/333 给出的数据可以看出来,在 128G 堆大小的测试中,ZGC 优势明显,找了一张网上的图片:
虽然 ZGC 愿景很好,但是还有很长的路要走,所以默认的垃圾收集器还是 G1。
Shenandoah 是在 Java12 引入的(参见)Java12 的新特性,本次和 ZGC 一起转正。同样的,想要使用 Shenandoah,不再需要参数-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
组合,只使用-XX:+UseShenandoahGC
即可。需要注意的是,Shenandoah 只在 OpenJDK 中提供,OracleJDK 中并不包含。
文本块是千呼万唤终于转正,在 Java13 中首次引入(参见 Java13 的新特性),在 Java14 中又增加了预览特性(参见 Java14 的新特性),终于在 Java15 确定下来,可以放心使用了。
我们再复习一下:
@Testvoid testTextBlock() { final String singleLine = "你好,我是看山,公众号「看山的小屋」。这行没有换行,而且我的后面多了一个空格 \n 这次换行了"; final String textBlockSingleLine = """ 你好,我是看山,公众号「看山的小屋」。\ 这行没有换行,而且我的后面多了一个空格、s 这次换行了"""; Assertions.assertEquals(singleLine, textBlockSingleLine);}
这个功能特性是代码可读性的优化。
目前,Java 没有提供对继承的细粒度控制,只有 public、protected、private、包内控制四种非常粗粒度的控制方式。
为此,密封类的目标是允许单个类声明哪些类型可以用作其子类型。这也适用于接口,并确定哪些类型可以实现它们。该功能特性新增了sealed
和non-sealed
修饰符和permits
关键字。
我们可以做如下定义:
public sealed class Person permits Student, Worker, Teacher {}public sealed class Student extends Person permits Pupil, JuniorSchoolStudent, HighSchoolStudent, CollegeStudent, GraduateStudent {}public final class Pupil extends Student {}public non-sealed class Worker extends Person {}public class OtherClass extends Worker {}public final class Teacher extends Person {}
我们可以先定义一个sealed
修饰的类Person
,使用permits
指定被继承的子类,这些子类必须是使用final
或sealed
或non-sealed
修饰的类。其中Student
是使用sealed
修饰,所以也需要使用permits
指定被继承的子类。Worker
类使用non-sealed
修饰,成为普通类,其他类都可以继承它。Teacher
使用final
修饰,不可再被继承。
从类图上看没有太多区别:
但是从功能特性上,起到了很好的约束作用,我们可以放心大胆的定义可以公开使用,但又不想被非特定类继承的类了。
instanceof 模式匹配首先在 Java14 中提供预览功能(参见 Java14 特性),可以提供instanceof
更加简洁高效的实现,在 Java15 中没有新增特性,主要是为了再次收集反馈,根据结果看,大家还是很期待这个功能,在 Java16 中正式提供。
我们再简单看下instanceof
的改进:
@Testvoid test1() { final Object obj1 = "Hello, World!"; int result = 0; if (obj1 instanceof String str) { result = str.length(); } else if (obj1 instanceof Number num) { result = num.intValue(); } Assertions.assertEquals(13, result);}
Record 类型用来增强 Java 语言特性,充当不可变数据载体。在 Java14 中提供预览功能(参见 Java14 新特性),在 Java15 中提供第二次预览,这次预览的目标是收集用户反馈。
比如,我们定义一个Person
类:
public record Person(String name, String address) {}
我们转换为之前的定义会是一坨下面这种代码:
public final class PersonBefore14 { private final String name; private final String address; public PersonBefore14(String name, String address) { this.name = name; this.address = address; } public String name() { return name; } public String address() { return address; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PersonBefore14 that = (PersonBefore14) o; return Objects.equals(name, that.name) && Objects.equals(address, that.address); } @Override public int hashCode() { return Objects.hash(name, address); } @Override public String toString() { return "PersonBefore14{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; }}
Record 类型特性有四个特性:
equals
、getter
、setter
等方法;我们不能将 Record 类型简单的理解为去除“样板化”代码的功能,它不是解决 JavaBean 命名约定的中很多模板化方法的冗余繁杂问题,它的目标不是类似 Lombok 等工具自动生成代码的功能,是从开发人员专注模型的角度出发的。
外部存储器访问 API 在 Java14 开始孵化(参见 Java14 新特性),在 Java15 中继续孵化状态,这个版本中增加了几个特性:
VarHandle
API,用于定制内存访问句柄;Spliterator
接口实现并行处理内存段;外部内存通常是说那些独立 JVM 之外的内存区域,可以不受 JVM 垃圾收集的影响,通常能够处理较大的内存。
这些新的 API 虽然不会直接影响多数的应用类开发人员,但是他们可以在内存的第三方库中提供支持,包括分布式缓存、非结构化文档存储、大型字节缓冲区、内存映射文件等。
Nashorn JavaScript 引擎最初在 Java8 中引入(参见 Java8 新特性),在 Java11 被标记为过期,在 Java15 中被删除,包括 Nashorn JavaScript 引擎、API、jjs 工具等内容。
Nashorn JavaScript 引擎是一个 JavaScript 脚本引擎,用来取代 Rhino 脚本引擎,对 ECMAScript-262 5.1 有完整的支持,增强了 Java 和 JavaScript 的兼容性,而且有很强的性能。
随着 GraalVM 和其他虚拟机技术最近的引入,Nashorn 引擎不再在 JDK 生态系统中占有一席之地。而且,ECMAScript 脚本语言结构、API 改变速度太快,Nashorn JavaScript 引擎维护成本太高,所以,直接删了。
Solaris 和 SPARC 都已被 Linux 操作系统和英特尔处理器取代。放弃对 Solaris 和 SPARC 端口的支持将使 OpenJDK 社区的贡献者能够加速开发新功能,从而推动平台向前发展。
Solaris 和 SPARC 端口 API 在 Java14 中标记过时,在 Java15 中彻底移除。仅仅半年就痛下杀手,可见社区对于维护这些 API 深受折磨。
RMI Activation 在 Java15 中标记为废除,会在未来版本删除。之所以被删除,是因为在现代的 web 应用中,已经不需要这种激活机制,继续维护,增加了 Java 开发人员的维护负担。在 Java8 的时候,已经将其设置为非必选项。
从开发系统的角度看,虽然 RMI Activation 是一个还不错的设计,但是已经有其他替代方案,继续维护开发下去,成本收益完全不匹配,及早舍弃,可以选择更加优秀的方案。有些类似于零边际成本的思想。
本文介绍了 Java15 新增的特性,完整的特性清单可以从 https://openjdk.java.net/projects/jdk/15/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java15 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java15 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
本文讲解一下 Java14 的特性,这个版本带来了不少新特性、功能实用性的增强、GC 的尝试、性能优化等:
接下来我们一起看看这些特性。
Switch 表达式在 Java12 和 Java13 都处于功能预览阶段,到 Java14 终于转正了,从另一个角度,我们可以在生产环境中使用这个功能了。
我们以“判断是否工作日”的例子展示一下,在 Java14 之前:
@Testvoid testSwitch() { final DayOfWeek day = DayOfWeek.from(LocalDate.now()); String typeOfDay = ""; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: typeOfDay = "Working Day"; break; case SATURDAY: case SUNDAY: typeOfDay = "Rest Day"; break; } Assertions.assertFalse(typeOfDay.isEmpty());}
在 Java14 中:
@Testvoid testSwitchExpression() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> { System.out.println("Working Day: " + day); yield "Working Day"; } case SATURDAY, SUNDAY -> "Day Off"; }; Assertions.assertEquals("Day Off", typeOfDay);}
注意看,例子中我们使用了两种写法,一种是通过yield
关键字表示返回结果,一种是在->
之后直接返回。
和前面的版本一样,Java14 中也提供了一些预览功能,我们可以在预览环境中试用一下。
这次的预览功能包括文本块(第二版预览)、instanceof 模式匹配、Record 声明,接下来我们分别说一下。
文本块在 Java13 中首次出现(参见 Java13),本次又提供了两个扩展:
\
:表示当前语句未换行,与 shell 脚本中的习惯一致;\s
:表示单个空格。我们看个例子:
@Testvoid testTextBlock() { final String singleLine = "你好,我是看山,公众号「看山的小屋」。不没有换行,而且我的后面多了一个空格 "; final String textBlockSingleLine = """ 你好,我是看山,公众号「看山的小屋」。\ 不没有换行,而且我的后面多了一个空格、s"""; Assertions.assertEquals(singleLine, textBlockSingleLine);}
个人感觉、是比较实用的,这个功能在 Java15 中转正,值得期待。
instanceof
主要用来检查对象类型,作为类型强转前的安全检查。
比如:
@Testvoid test() { final Object obj1 = "Hello, World!"; int result = 0; if (obj1 instanceof String) { String str = (String) obj1; result = str.length(); } else if (obj1 instanceof Number) { Number num = (Number) obj1; result = num.intValue(); } Assertions.assertEquals(13, result);}
可以看到,我们每次判断类型之后,需要声明一个判断类型的变量,然后将判断参数强制转换类型,赋值给新声明的变量。这种写法显得繁琐且多余。
于是在 Java14 中对instanceof
进行了改进:
@Testvoid test1() { final Object obj1 = "Hello, World!"; int result = 0; if (obj1 instanceof String str) { result = str.length(); } else if (obj1 instanceof Number num) { result = num.intValue(); } Assertions.assertEquals(13, result);}
不仅如此,instanceof
模式匹配的作用域还可以扩展。在if
条件判断中,我们都知道&&
与判断是会执行所有的表达式,所以使用instanceof
模式匹配定义的局部变量继续判断。
比如:
if (obj1 instanceof String str && str.length() > 20) { result = str.length();}
与原来的写法对比,Java14 提供的写法代码更加简洁、可读性更高,能够提出很多冗余繁琐的代码,非常实用的一个特性,这个功能会在 Java16 中转正。
在 Java14 预览功能中新增了一个关键字record
,它是定义不可变数据类型封装类的关键字,主要用在特定领域类上。这个关键字最终会在 Java16 中正式提供。
我们都知道,在 Java 开发中,我们需要定义 POJO 作为数据存储对象,根据规范,POJO 中除了属性是个性化的,其他的比如getter
、setter
、equals
、hashCode
、toString
都是模板化的写法,所以为了简便,很多类似 Lombok 的组件提供 Java 类编译时增强,通过在类上定义@Data
注解自动添加这些模板化方法。在 Java14 中,我们可以直接使用record
解决这个问题。
比如,我们定义一个Person
类:
public record Person(String name, String address) {}
我们转换为之前的定义会是一坨下面这种代码:
public final class PersonBefore14 { private final String name; private final String address; public PersonBefore14(String name, String address) { this.name = name; this.address = address; } public String name() { return name; } public String address() { return address; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PersonBefore14 that = (PersonBefore14) o; return Objects.equals(name, that.name) && Objects.equals(address, that.address); } @Override public int hashCode() { return Objects.hash(name, address); } @Override public String toString() { return "PersonBefore14{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; }}
我们可以发现 Record 类有如下特征:
equals()
、hashCode()
方法toString()
方法final
修饰,所以构造函数是包含所有属性的,而且没有 setter 方法在Class
类中也新增了对应的处理方法:
getRecordComponents()
:返回一组java.lang.reflect.RecordComponent
对象组成的数组,该数组的元素与Record
类中的组件相对应,其顺序与在记录声明中出现的顺序相同,可以从该数组中的每个RecordComponent
中提取到组件信息,包括其名称、类型、泛型类型、注释及其访问方法。isRecord()
:返回所在类是否是 Record 类型,如果是,则返回 true。看起来,Record 类和 Enum 很像,都是一定的模板类,通过语法糖定义,在 Java 编译过程中,将其编译成特定的格式,功能很好,但如果没有习惯使用,可能会觉得限制太多。
在没有考虑完全场景的情况下,很容易碰到空指针异常(NullPointerException,简称 NPE)。一般碰到这个异常,根据异常栈信息我们很容易定位到发生异常的代码行,比如:
@Testvoid test1() { Student s = null; Assertions.assertThrows(NullPointerException.class, ()-> s.getName()) .fillInStackTrace() .printStackTrace();}
如果是在 Java14 之前,这个时候打印出来的异常信息是:
Exception in thread "main" java.lang.NullPointerException at cn.howardliu.tutorials.java14.NpeTest.test1(NpeTest.java:20)
对于上面例子,我们可以直接定位到s
是null
,但是下面这个例子呢:
@Testvoid test2() { Student s = new Student(); s.setName("看山"); Assertions.assertThrows(NullPointerException.class, ()-> s.getClazz().getNo()) .fillInStackTrace() .printStackTrace();}
我们很难判断s
或者s
中的clazz
是null
,需要查看上下文代码,或者复杂情况还需要添加日志辅助定位问题。
在 Java14 中,对 NullPointerException 异常栈信息做了增强,通过分析程序的字节码信息,能够做到准确地定位到出现 NullPointerException 的变量,并且根据实际源代码打印出详细异常信息。此时,上面例子的异常信息是:
java.lang.NullPointerException: Cannot invoke "cn.howardliu.tutorials.java14.NpeTest$Clazz.getNo()" because the return value of "cn.howardliu.tutorials.java14.NpeTest$Student.getClazz()" is null at cn.howardliu.tutorials.java14.NpeTest.test2(NpeTest.java:30)
这样一目了然。
孵化功能是 Java 开发团队让我们提前尝鲜、公测的功能,在 Java9 模块化之后,孵化功能会放在jdk.incubator.
中。
Java 对象是驻留在堆上,但是有时候因为其算法或者内存结构的原因,使用效率低下、性能低下、受垃圾收集器 GC 算法影响。所以很多时候我们会使用本机内存或者称为直接内存。
在 Java14 之前,使用直接内存我们会用到ByteBuffer
或者Unsafe
,但是这两个都存在一些问题。
ByteBuffer
管理内存最大不能够超过 2G;ByteBuffer
管理的这部分内存需要使用垃圾收集器回收内存,使用不当可能造成内存泄漏;Unsafe
是非标准的 Java API,可能会因为不合法的内存使用致使系统崩溃。“天下苦秦久矣”,于是在 Java14 中提供了新的 API:
MemorySegment
:用来申请内存区域,可以是堆内存,也可以是对外内存;MemoryAddress
:从MemorySegment
实例获取已申请内存的内存地址用于执行操作,例如从底层内存段的内存中检索数据;MemoryLayout
:用来描述内存段的内容,它允许我们定义如何将内存分解为元素,并提供每个元素的大小。这部分功能截止到 Java17 还是孵化功能,而且内容比较多,后续会单独开一篇介绍。
一般来说,Java 程序会以一个 Jar 的形式提供,web 服务可能是 war 或者 ear 包,但是有时候我们的 Java 程序可能是在自己的 PC 机(比如 Windows 或者 MacOS)上运行,期望可以通过双击打开的方式。
于是 Java14 引入了引入了 jdk.incubator.jpackage.jmod
,基于 JavaFX javapackager tool,其目的就是创建一个打包工具,可以将 jar 包构建成 exe、pkg、dmg、deb、rpm 格式的安装文件。
我们可以使用jlink
创建一个最小可运行的模块包,然后使用jpackage
将其构建成安装包:
jpackage --name myapp --input lib --main-jar main.jar
ZGC 最初是在 Java11 中引入(参见 Java11 的新特性),在后续版本中,不断升级优化,实现可伸缩、低延迟的目标,使用了内存读屏障、染色指针、内存多重映射等技术。在之前,ZGC 只支持 Linux/x64 平台,在 Java14 之后,支持了 macOS/x64 和 Windows/x64 系统中。
开启参数:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
Java14 改进了非一致性内存访问(Non-uniform Memory Access,NUMA)系统上的 G1 垃圾收集器的整体性能,主要是对年轻代的内存分配做出优化,从而提升 CPU 计算过程中内存访问速度。
NUMA 主要是指在多插槽物理计算机体系中,处理器一般是多核,且越来越多具备 NUMA 访问体系结构,即内存与每个插槽或内核之间的距离不等。套接字之间的内存访问有不同的性能特征,更远的套接字访问会有更多的时间消耗。这样的结果是,每个核对于某一区域的内存访问速度会随核与物理内存的位置远近有所差异。
Java 分配内存时,G1 会申请一块 region,作为对象存放区域。如果能够感知 NUMA,就可以优先在当前线程绑定的 NUMA 节点空闲内存执行申请内存操作,用于提升访问速度。
启用参数是-XX:+UseNUMA
。
飞行记录器(Flight Recorder)是在 Java11 中引入(参见 Java11 的新特性)。
本次增强可以实现 JFR 数据的公开访问,可以通过使用jdk.jfr.consumer
中的方法持续读取或流式传输读取记录,用于持续监控。这样的话,我们可以与现有监控系统集成,实现 JFR 数据的持续监听,不用非得等着收集完成后再解析分析。
对FileChannel
进行扩展,定义了jdk.nio.mapmode.ExtendedMapMode
,用来创建MappedByteBuffer
实例,可以对非原子性的字节缓冲区映射(Non-Volatile Mapped Byte Buffers,NVM)实现持久化。
CMS 是老年代垃圾回收算法,通过标记-清除的方式进行内存回收,在内存回收过程中能够与用户线程并行执行。在 G1 之前,CMS 几乎占据了 GC 的全部江山。在使用过程中,一般是 CMS 与 Parallel New 搭配使用。
CMS 由于其算法特性,会产生内存碎片和浮动垃圾,随着时间推移,可能出现的情况是,虽然老年代还有空间,但是没有办法分配足够内存给大对象。
所以在 Java9 中开始放弃使用 CMS,在 Java14 中彻底删除,并删除与 CMS 有关的参数。从 Java14 开始,CMS 成为了历史。
Parallel Scavenge 是并行收集算法,SerialOld 提供老年代串行收集,这种年轻代使用并行算法、老年代使用串行算法的混搭的方式,使用场景少且有风险。但是却需要大量工作量维护,所以在 Java14 中,删除了这两种 GC 组合。
删除组合的方式是通过启用组合参数-XX:+UseParallelGC -XX:-UseParallelOldGC
,并在单独使用-XX:-UseParallelOldGC
时会收到警告信息。
these were deprecated for removal in Java 11, and now removed
删除java.util.jar
包中的pack200
和unpack200
工具以及 Pack200 API。这些工具和 API 已在 Java11 时标记弃用,删除也是意料之中。
Solaris 和 SPARC 都已被 Linux 操作系统和英特尔处理器取代。放弃对 Solaris 和 SPARC 端口的支持将使 OpenJDK 社区的贡献者能够加速开发新功能,从而推动平台向前发展。
这些 API 在 Java14 中标记弃用,在 Java15 中彻底删除。这样做,也是为了让很多正在进行的项目尽早适应新的架构。
本文介绍了 Java14 新增的特性,完整的特性清单可以从 https://openjdk.java.net/projects/jdk/14/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java14 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java14 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
本文讲解一下 Java13 的特性,这个版本在语法特性上增加不多,值得关注的是两个预览功能:Switch 表达式和文本块,另外可以关乎的是性能优化方面的:动态类数据共享(CDS)存档、ZGC 动态释放未使用内存、Socket API 重构。这些方面可以看出,Java 的升级方向有两个,一是增加功能,增加新的语法特性;二是增强功能,提升已有功能性能。
Java13 引入了两个新的语法特性:Switch 表达式和文本块。这些预览功能是为了让开发者尝鲜的同时,可以快速调整,反馈好就留下,不好就移除。目前来看,这些特性还是挺香的。
在 Java12 中 Switch 表达式首次以预览版的身份出现,在 Java13 中又做了增强,在 Java14 正式提供。Java13 添加了yield
关键字,用来返回值。
yield
与return
的区别在于,yield
只会跳出switch
块,return
是跳出当前方法或循环。
比如下面的例子,在 Java12 之前,要判断日期可以这样写:
@Testvoid testSwitch() { final DayOfWeek day = DayOfWeek.from(LocalDate.now()); String typeOfDay = ""; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: typeOfDay = "Working Day"; break; case SATURDAY: case SUNDAY: typeOfDay = "Rest Day"; break; } Assertions.assertFalse(typeOfDay.isEmpty());}
在 Java12 提供的 Switch 表达式预览功能,我们可以简化一下:
@Testvoid testSwitchExpression() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Working Day"; case SATURDAY, SUNDAY -> "Day Off"; }; Assertions.assertEquals("Day Off", typeOfDay);}
这样可以实现判断,但是没有办法在表达式中实现其他逻辑了。于是 Java13 补齐了这个功能:
@Testvoid testSwitchExpression13() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> { System.out.println("Working Day: " + day); yield "Working Day"; } case SATURDAY, SUNDAY -> { System.out.println("Day Off: " + day); yield "Day Off"; } }; Assertions.assertEquals("Day Off", typeOfDay);}
这里需要说明一下,既然是预览功能,会与正式提供功能有些出入。上面的代码是在 Java14 环境中编写,与 Java13 发布的功能描述有些差异,这点不必深究,已经废弃的约束就是不存在。
一直以来,Java 中的字符串定义都是以双引号括起来的形式,不支持多行书写,所以在需要多行字符串中,需要使用转义符表示,既不好看、还不好读,更不好维护。
千呼万唤始出来,终于有了文本块功能。
比如,我们想要写一段 Json 格式的数据,Java13 之前需要写成:
String json = "{\n" + " \"wechat\": \"hellokanshan\",\n" + " \"wechatName\": \"看山、",\n" + " \"mp\": \"kanshanshuo\",\n" + " \"mpName\": \"看山的小屋、"\n" + "}\n";
但是在 Java13 预览版中可以写作:
String json2 = """ { "wechat": "hellokanshan", "wechatName": "看山", "mp": "kanshanshuo", "mpName": "看山的小屋" } """;
少了很多的+、换行、转移等字符,看着更加直观。
这个功能在 Java15 中正式提供。
CDS 是 Java5 引入的一种类预处理方式,可以将一组类共享到一个归档文件中,借助内存映射加载类数据,减少启动时间,并可实现在多 JVM 之间共享的功能。在 Java10 对其进行扩展,增大了 CDS 使用范围,即 AppCDS(参见 Java10 新特性)。到了 Java12,将 CDS 归档文件作为了默认功能开放出来(参见 Java12 新特性)。
但是这个功能在使用的时候还是有些麻烦。为了生成归档文件,开发人员必须先对应用程序进行试运行,创建一个类列表,然后将其转储到归档文件中。然后,这个归档才可以用来在 JVM 之间共享元数据。
Java13 简化了这个过程:允许 Java 应用在运行结束后动态归档,即将已被加载但不属于 CDS 的类(包括自定义类和引用库的类)动态添加到 CDS 归档文件中。不用再提供归档类的列表,通过更加简洁的方式创建包含应用程序的归档。
我们可以使用-XX:ArchiveClassesAtExit
参数控制应用程序退出时创建 CDS 归档文件:
java -XX:ArchiveClassesAtExit=<archive filename> -cp <app jar> AppName
也可以使用-XX:SharedArchiveFile
来使用动态存档功能:
java -XX:SharedArchiveFile=<archive filename> -cp <app jar> AppName
ZGC 是 Java11 中引入的一个可伸缩、低延迟的垃圾收集器,主要目标包括:GC 停顿时间不超过 10ms;可以处理从几百 MB 的小堆,到几个 TB 的大堆;应用吞吐能力不会下降超过 15%等(参见 Java11 的新特性)。
但是 ZGC 并没有像 Hotspot 中的 G1 和 Shenandoah 那样,可以主动释放未使用的内存,对于多数应用程序来说,CPU 和内存都是稀缺资源,尤其是现在云上环境和虚拟化技术,如果应用程序占用的内存长期处于空闲状态,还紧握住不释放,就是极大的浪费。
在 Java13 中对其进行改进,包括:
我们来看下 ZGC 的内部逻辑。
ZGC 堆由一组称为 ZPages 的堆区域组成,每个 ZPage 都与提交的堆内存的可变数量相关联。当 ZGC 压缩堆时,ZPages 被释放并插入到页面缓存 ZPageCache 中,页面缓存中的 ZPages 可以重新使用,以满足新的堆分配。
ZPageCache 中的 ZPages 集合代表堆中未使用的部分,这部分可以释放回操作系统。ZPageCache 中的 ZPages 根据 LRU(最近最少使用)排序,并按照大中小进行分组。这样的话就可以根据算法按顺序释放未使用的内存。
Java13 还提供了-XX:ZUncommitDelay=<seconds>
命令,用于指定释放多长时间(默认是 5 分钟)未使用的内存,这个参数类似于 Shenandoah 中的-XX:ShenandoahUncommitDelay=<milliseconds>
。
在 Java13 中,ZGC 内存释放功能默认开启,可通过参数-XX:-ZUncommit
关闭该功能。由于 ZGC 释放内存时,不会低于最小堆内存,即当最小堆内存(-Xms)与最大堆内存(-Xmx)一样时,不会自动释放。
Java 中的 Socket 是从 Java1.0 开始就有的,是 Java 中不可或缺的网络 API,算起来已经服役 20 多年了。在这段时间内,信息技术已经发生了很多变化,这些上古 API 有一定的局限性,而且不容易维护和调试。
Java 的 Socket API 主要包括java.net.ServerSocket
和java.net.Socket
,ServerSocket
用来监听连接请求的端口,连接成功后返回的是Socket
对象,可以通过操作Socket
对象实现数据发送和读取。Java 是通过SocketImpl
实现这些功能。
在 Java13 之前,通过SocketImpl
的子类PlainSocketImpl
实现。在 Java13 中,引入NioSocketImpl
实现,该实现以 NIO 为基础,与高速缓冲机制集成,实现非阻塞式网络。
如果想用回PlainSocketImpl
,可以设置启动参数-Djdk.net.usePlainSocketImpl=true
即可。
本文介绍了 Java13 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk/13/查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java13 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java13 的新特性
你好,我是看山。
腊月二十七宰鸡赶大集,响应国家号召,就地过年。
难免思乡,于是翻了翻百度地图提供的迁徙数据。我们从迁出地和迁徙趋势看一看这几年的春运变化。
我们先来看看从 2020 年到现在(2022 年)的春运迁徙图:
从三张图中,我们不难看出一些规律,待我慢慢道来。
这几年中,比较热门的迁出地有北京、上海、广州、深圳,稍微热门的是杭州、武汉、重庆、长沙、西安等。不难看出,热门迁出地对应着一线、新一线城市。这些城市的外来务工人员比较多,在春节选择回家过年。
其中,北上广深是一线城市,很多人或为生活、或为梦想,选择了一线城市打拼。辛苦一年,趁着年关回到家,无论家乡是好是孬,总归是一个平静的港湾。至于年后,是选择以梦为马潇洒天涯,还是选择背起行囊漂泊他乡,都是年后的事情。
当人们在寻找工作和生活的平衡点时,很多城市也是快速发展,成为了新一线城市,成为了中国发展的新兴动力城市,也给了我们更多的选择。人们不在单纯的考虑收入,会更多的考虑感受、家人,更多的考虑幸福。
无论我们在哪,做什么,都是为了追求幸福,幸福才是我们心底最期望的东西。
疫情发生前(2019 年),我们可以看到,年前迁移流量持续增长,大年初一稍微少了一点,到初六假期结束,迁移量达到峰值。我们以此为参照,看下疫情的影响。
2020 年疫情爆发,武汉封城,春节期间全国人民上下齐心,共抗疫情。这个时候,春运流动基本上停止。作为普通民众,我们能够做到,就是待在家里,不给国家添麻烦。
2021 年是疫情第二年,咱们国家提出的“清零”政策取得了好成绩,生活工作基本上恢复正常,也有了十一小长假出行旅游复苏的场景。外国友人们躲在家里看我们堵在路上,心里一定是各种羡慕。而且,在年底开始全员接种疫苗,给我们增加一层保障。
不过由于冬天天气转冷,病毒的存活能力增强,为了保住来之不易的战果。各地倡导就地过年。所以能够看到,春运的流量比 2019 年减少了将近一半。
今年的春运刚刚开始,我们只能够根据趋势推测一下,到今天为止,迁徙流量的发展基本上和 2019 年相似。这不得不说在抗疫方面,2021 年取得了好成绩。全民接种加强针疫苗,各地的清零政策也是严格执行。虽然年底有些地方出现了本土疫情,但是都是在控制范围内,没有爆发的征兆。有了 2021 年春运、五一、十一等各种假期迁徙的经验,各地采取“有温度的严格控制”,让远在他乡想在春节回家的游子们,一解乡愁。
可以预见,等到了 2023 年春运的时候,我们可能就不再纠结能不能回、让不让回,只需要带着必要的证明,正常计划归乡日期就好。
按照习俗,给大家拜个早年,愿大家新年胜旧年,欢愉且胜意,万事尽可期。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:从春运迁徙图看到的一些东西
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:从春运迁徙图看到的一些东西
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
你好,我是看山。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
本文讲解一下 Java12 的特性,作为第一个长期支持版 Java11 之后的第一个版本,增加的功能也不少,除了一些小幅度的 API 增强,增加了另一个试验阶段的垃圾收集器 Shenandoah、对 G1 做了优化、增加微基准套件等。
Java12 提供了很多的语法特性,既有小而美的增强 API,又有特别方便的工具扩展。本节我们跟着代码看看比较好玩的功能。
在 Java12 中,String 又增强了两个方法。之所以说又,是因为在 Java11 中已经增加过小而美的方法,想要详细了解的可以查看 Java11 新特性。
这次增加的方法是indent
(缩进)和transform
(转换)。
顾名思义,indent
方法是对字符串每行(使用\r
或\n
分隔)数据缩进指定空白字符,参数是 int 类型。
如果参数大于 0,就缩进指定数量的空格;如果参数小于 0,就将左侧的空字符删除指定数量,即右移。
我们看下源码:
public String indent(int n) { if (isEmpty()) { return ""; } Stream<String> stream = lines(); if (n > 0) { final String spaces = " ".repeat(n); stream = stream.map(s -> spaces + s); } else if (n == Integer.MIN_VALUE) { stream = stream.map(s -> s.stripLeading()); } else if (n < 0) { stream = stream.map(s -> s.substring(Math.min(-n, s.indexOfNonWhitespace()))); } return stream.collect(Collectors.joining("\n", "", "\n"));}
这里会使用到 Java11 增加的lines
、repeat
、stripLeading
等方法。indent
最后会将多行数据通过Collectors.joining("\n", "", "\n")
方法拼接,结果会有两点需要注意:
\r
会被替换成\n
;\n
,最后会补上一个\n
,即多了一个空行。我们看下测试代码:
@Testvoid testIndent() { final String text = "\t\t\t 你好,我是看山。\n \u0020\u2005Java12 的 新特性。\r 欢迎三连+关注哟"; assertEquals(" \t\t\t 你好,我是看山。\n \u0020\u2005Java12 的 新特性。\n 欢迎三连+关注哟、n", text.indent(4)); assertEquals("\t 你好,我是看山。\n\u2005Java12 的 新特性。\n 欢迎三连+关注哟、n", text.indent(-2)); final String text2 = "山水有相逢"; assertEquals("山水有相逢", text2);}
我们再来看看transform
方法,源码一目了然:
public <R> R transform(Function<? super String, ? extends R> f) { return f.apply(this);}
通过传入的Function
对当前字符串进行转换,转换结果由Function
决定。比如,我们要对字符串反转:
@Testvoid testTransform() { final String text = "看山是山"; final String reverseText = text.transform(s -> new StringBuilder(s).reverse().toString()); assertEquals("山是山看", reverseText);}
其实这个方法在 Java8 中提供的Optional
实现类似的功能(完整的 Optional 功能可以查看 Optional 的 6 种操作):
@Testvoid testTransform() { final String text = "看山是山"; final String reverseText2 = Optional.of(text) .map(s -> new StringBuilder(s).reverse().toString()) .orElse(""); assertEquals("山是山看", reverseText2);}
在 Java12 中,Files
增加了mismatch
方法,用于对比两个文件中的不相同字符的位置,如果内容相同,返回-1L
,是long
类型的。
我们来简单看下怎么用:
@Testvoid testMismatch() throws IOException { final Path pathA = Files.createFile(Paths.get("a.txt")); final Path pathB = Files.createFile(Paths.get("b.txt")); // 写入相同内容 Files.write(pathA, "看山".getBytes(), StandardOpenOption.WRITE); Files.write(pathB, "看山".getBytes(), StandardOpenOption.WRITE); final long mismatch1 = Files.mismatch(pathA, pathB); Assertions.assertEquals(-1L, mismatch1); // 追加不同内容 Files.write(pathA, "是山".getBytes(), StandardOpenOption.APPEND); Files.write(pathB, "不是山".getBytes(), StandardOpenOption.APPEND); final long mismatch2 = Files.mismatch(pathA, pathB); Assertions.assertEquals(6L, mismatch2); Files.deleteIfExists(pathA); Files.deleteIfExists(pathB);}
我们可以看到,当第一次在两个文件中写入相同内容,执行mismatch
方法返回的是-1L
。当第二次追加进去不同的内容后,返回的是6L
。之所以是 6,是因为测试代码中使用的字符集是UTF-8
,大部分汉子是占用 3 个字符,前两个字相同,从第三个字开始不同,下标从 0 开始,所以开始位置是 6。
我们看下teeing
的定义:
public static <T, R1, R2, R> Collector<T, ?, R> teeing( Collector<? super T, ?, R1> downstream1, Collector<? super T, ?, R2> downstream2, BiFunction<? super R1, ? super R2, R> merger)
这个方法有三个参数,前两个是Collector
对象,用于对输入数据进行预处理,第三个参数是BiFunction
,用于将前两个处理后的结果作为参数传入BiFunction
中,运算得到结果。
我们来看下例子:
@Testvoid testTeeing() { var result = Stream.of("Sunday", "Monday", "Tuesday", "Wednesday") .collect(Collectors.teeing( Collectors.filtering(n -> n.contains("u"), Collectors.toList()), Collectors.filtering(n -> n.contains("n"), Collectors.toList()), (list1, list2) -> List.of(list1, list2) )); assertEquals(2, result.size()); assertTrue(isEqualCollection(List.of("Sunday", "Tuesday"), result.get(0))); assertTrue(isEqualCollection(List.of("Sunday", "Monday", "Wednesday"), result.get(1)));}
我们对输入的几个字符串进行过滤,然后将过滤结果组成一个新的队列。
这个工具比较好玩,可以对数字进行按需格式化。提供了public static NumberFormat getCompactNumberInstance(Locale locale, NumberFormat.Style formatStyle)
方法用于初始化:
SHORT
和LONG
,不过对于中文展示,似乎没啥区别。我们一起看下例子:
@Testvoid testFormat() { final NumberFormat zhShort = NumberFormat.getCompactNumberInstance(Locale.CHINA, Style.SHORT); assertEquals("1 万", zhShort.format(10_000)); assertEquals("1 兆", zhShort.format(1L << 40)); final NumberFormat zhLong = NumberFormat.getCompactNumberInstance(Locale.CHINA, Style.LONG); assertEquals("1 万", zhLong.format(10_000)); assertEquals("1 兆", zhLong.format(1L << 40)); final NumberFormat usShort = NumberFormat.getCompactNumberInstance(Locale.US, Style.SHORT); usShort.setMaximumFractionDigits(2); assertEquals("10K", usShort.format(10_000)); assertEquals("1.1T", usShort.format(1L << 40)); final NumberFormat usLong = NumberFormat.getCompactNumberInstance(Locale.US, Style.LONG); usLong.setMaximumFractionDigits(2); assertEquals("10 thousand", usLong.format(10_000)); assertEquals("1.1 trillion", usLong.format(1L << 40));}
我们也可以继续使用NumberFormat
中的方法定义,比如示例中保留小数点后 2 位。
Java12 引入了一个实验阶段的垃圾收集器:Shenandoah,作为一个低停顿的垃圾收集器。
Shenandoah 垃圾收集器是 RedHat 在 2014 年宣布进行的垃圾收集器研究项目,其工作原理是通过与 Java 应用执行线程同时运行来降低停顿时间。简单的说就是,Shenandoah 工作时与应用程序线程并发,通过交换 CPU 并发周期和空间以改善停顿时间,使得垃圾回收器执行线程能够在 Java 线程运行时进行堆压缩,并且标记和整理能够同时进行,因此避免了在大多数 JVM 垃圾收集器中所遇到的问题。
Shenandoah 垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置为 200MB 还是 200GB,都将拥有一致的系统暂停时间,不过实际使用性能将取决于实际工作堆的大小和工作负载。
Java12 中 Shenandoah 处于实验阶段,想要使用需要编译时添加--with-jvm-features=shenandoahgc
,然后启动时使用-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
以开启。
后续会补充 Java 中各种垃圾收集器的文章,其中会有介绍 Shenandoah 的,敬请关注公众号「看山的小屋」。如果想要提前了解,欢迎访问https://wiki.openjdk.java.net/display/shenandoah。
Java12 中添加一套基准测试套件,该基准测试套件基于 JMH(Java Microbenchmark Harness),使开发人员可以轻松运行现有的基准测试并创建新的基准测试,其目标是提供一个稳定且优化的基准。
在这套基准测试套件中包括将近 100 个基准测试的初始集合,并且能够轻松添加新基准、更新基准测试和提高查找已有基准测试的便利性。
微基准套件与 JDK 源代码位于同一个目录中,并且在构建后将生成单个 Jar 文件。它是一个单独的项目,在支持构建期间不会执行,以方便开发人员和其他对构建微基准套件不感兴趣的人在构建时花费比较少的构建时间。
Switch 语句出现的姿势是条件判断、流程控制组件,与现在很流行的新语言对比,其写法显得非常笨拙,所以 Java 推出了 Switch 表达式语法,可以让我们写出更加简化的代码。这个扩展在 Java12 中作为预览版首次引入,需要在编译时增加-enable-preview
开启,在 Java14 中正式提供,功能编号是 JEP 361。
比如,我们通过 switch 语法简单计算工作日、休息日,在 Java12 之前需要这样写:
@Testvoid testSwitch() { final DayOfWeek day = DayOfWeek.from(LocalDate.now()); String typeOfDay = ""; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: typeOfDay = "Working Day"; break; case SATURDAY: case SUNDAY: typeOfDay = "Rest Day"; break; } Assertions.assertFalse(typeOfDay.isEmpty());}
在 Java12 中的 Switch 表达式中,我们可以直接简化:
@Testvoid testSwitchExpression() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Working Day"; case SATURDAY, SUNDAY -> "Day Off"; }; Assertions.assertEquals("Day Off", typeOfDay);}
是不是很清爽。文末提供的源码中,pom.xml
定义的maven.compiler
版本写的是14
,这是因为 Switch 表达式是 Java14 正式提供,我没有重新编译 Java,所以只能指定 Java14 来实现这个功能代码的演示。
Java12 中引入 JVM 常量 API,用来更容易地对关键类文件和运行时构件的描述信息进行建模,特别是对那些从常量池加载的常量,这是一项非常技术性的变化,能够以更简单、标准的方式处理可加载常量。
具体来说就是java.base
模块新增了java.lang.constant
包,引入了ConstantDesc
接口以及Constable
接口。ConstantDesc
的子接口包括:
ClassDesc
:Class 的可加载常量标称描述符;MethodTypeDesc
:方法类型常量标称描述符;MethodHandleDesc
:方法句柄常量标称描述符;DynamicConstantDesc
:动态常量标称描述符。继续挖坑,这部分内容会在进阶篇再详细介绍,敬请关注公众号「看山的小屋」。
Java12 中将只保留一套 AArch64 实现,之前版本中,有两个关于 aarch64 的实现,分别是ope/src/hotspot/cpu/arm
以及open/src/hotspot/cpu/aarch64
,它们的实现重复了。为了集中精力更好地实现 aarch64,删除了open/src/hotspot/cpu/arm
中与 arm64(64-bit Arm platform)实现相关的代码,只保留 32 位 ARM 端口和 64 位 aarch64 的端口。
这样做,可以让开发人员将目标集中在剩下的这个 64 位 ARM 实现上,消除维护两套端口所需的重复工作。
目标聚焦,力量集中。
在 Java10 的新特性 中我们介绍过类数据共享(CDS,Class Data Sharing),其作用是通过构建时生成默认类列表,在运行时使用内存映射,减少 Java 的启动时间和减少动态内存占用量,也能在多个 Java 虚拟机之间共享相同的归档文件,减少运行时的资源占用。
在 Java12 之前,想要使用需要三步走手动开启,到了 Java12,将默认开启 CDS 功能,想要关闭,需要使用参数-Xshare:off
。
G1 垃圾收集器可以在大内存多处理器的工作场景中提升回收效率,能够满足用户预期降低 STW 停顿时间。
其内部是采用一个高级分析引擎来选择在收集期间要处理的工作量,此选择过程的结果是一组称为 GC 回收集(collection set,CSet)的区域。一旦收集器确定了 GC 回收集 并且 GC 回收、整理工作已经开始,则 G1 收集器必须完成收集集合集的所有区域中的所有活动对象之后才能停止;但是如果收集器选择过大的 GC 回收集,可能会导致 G1 回收器停顿时间超过预期时间。
在 Java12 中,GC 回收集拆分为必需和可选两部分,使 G1 垃圾回收器能中止垃圾回收过程。其中必需处理的部分包括 G1 垃圾收集器不能递增处理的 GC 回收集的部分,同时也可以包含老年代以提高处理效率。在 G1 垃圾回收器完成收集需要必需回收的部分之后,G1 垃圾回收器可以根据剩余时间决定是否停止收集。
在 Java11 中,G1 仅在进行 Full GC 或并发处理周期时才能向操作系统返还堆内存,但是这两种场景都是 G1 极力避免的,所以如果我们使用 G1 收集器,基本上很难返还 Java 堆内存,这样对于那种周期性执行大量占用内存的应用,会造成比较多的内存浪费。
Java12 中,G1 垃圾收集器将在应用程序不活动期间定期生成或持续循环检查整体 Java 堆使用情况,以便 G1 垃圾收集器能够更及时的将 Java 堆中不使用内存部分返还给操作系统。对于长时间处于空闲状态的应用程序,此项改进将使 JVM 的内存利用率更加高效。
本文介绍了 Java12 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk/12/查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java12 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java12 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
Java11 是 2018 年 9 月发布的,是自 Java8 之后第一个长期支持版(long-term support,LTS)。相比于其他版本 6 个月维护期,长期支持版的维护期是 3 年。
长期支持版的更新会比较多,而且都是相对稳定的更新。今天我们就一起看看 Java11 都有哪些喜人的变化:增强的 API、全新的 HTTP 客户端、基于嵌套关系的访问控制优化、低开销的堆性能采用工具、ZGC、Epsilon 垃圾收集器、飞行记录器等。
首先说下String
中新增的方法:repeat
、strip
、stripLeading
、stripTrailing
、isBlank
、lines
。这些方法还是挺有用的,以前我们可能需要借助第三方类库(比如 Apache 出品的 commons-lang)中的工具类,现在可以直接使用嫡亲方法了。
repeat
是实例方法,顾名思义,这个方法是返回给定字符串的重复值的,参数是int
类型,传参的时候需要注意:
IllegalArgumentException
异常;Integer.MAX_VALUE
,会抛出OutOfMemoryError
错误。用法很简单:
@Testvoid testRepeat() { String output = "foo ".repeat(2) + "bar"; assertEquals("foo foo bar", output);}
小而美的一个工具方法。
strip
方法算是trim
方法的增强版,trim
方法可以删除字符串两侧的空白字符(空格、tab 键、换行符),但是对于Unicode
的空白字符无能为力,strip
补足这一短板。
用起来是这样的:
@Testvoid testTrip() { final String output = "\n\t hello \u2005".strip(); assertEquals("hello", output); final String trimOutput = "\n\t hello \u2005".trim(); assertEquals("hello \u2005", trimOutput);}
对比一下可以看到,trim
方法的清理功能稍弱。
stripLeading
和stripTrailing
与strip
类似,区别是一个清理头,一个清理尾。用法如下:
@Testvoid testTripLeading() { final String output = "\n\t hello \u2005".stripLeading(); assertEquals("hello \u2005", output);}@Testvoid testTripTrailing() { final String output = "\n\t hello \u2005".stripTrailing(); assertEquals("\n\t hello", output);}
这个方法是用于判断字符串是否都是空白字符,除了空格、tab 键、换行符,也包括Unicode
的空白字符。
用法很简单:
@Testvoid testIsBlank() { assertTrue("\n\t\u2005".isBlank());}
最后这个方法是将字符串转化为字符串Stream
类型,字符串分隔依据是换行符:\n
、\r
、\r\n
,用法如下:
@Testvoid testLines() { final String multiline = "This is\n \na multiline\nstring."; final String output = multiline.lines() .filter(Predicate.not(String::isBlank)) .collect(Collectors.joining(" ")); assertEquals("This is a multiline string.", output);}
本次更新在Files
中增加了两个方法:readString
和writeString
。writeString
作用是将指定字符串写入文件,readString
作用是从文件中读出内容到字符串。是一个对Files
工具类的增强,封装了对输出流、字节等内容的操作。
用法比较简单:
@Testvoid testReadWriteString() throws IOException { final Path tmpPath = Path.of("./"); final Path tempFile = Files.createTempFile(tmpPath, "demo", ".txt"); final Path filePath = Files.writeString(tempFile, "看山 howardliu.cn\n 公众号:看山的小屋"); assertEquals(tempFile, filePath); final String fileContent = Files.readString(filePath); assertEquals("看山 howardliu.cn\n 公众号:看山的小屋", fileContent); Files.deleteIfExists(filePath);}
readString
和writeString
还可以指定字符集,不指定默认使用StandardCharsets.UTF_8
字符集,可以应对大部分场景了。
java.util.Collection
提供了集合转数组的方法有两个:
Object[] toArray()
:可以直接转数组,但是转换后是Object
类型,后续使用的时候,需要强转,太不优雅了;<T> T[] toArray(T[] a)
:传入一个指定类型的数组,一般会有另种实现:Arrays.copyOf
创建列表长度的数组,这个数组与传入数组参数没有关系System.arraycopy
将列表写入数组,超过长度的数组元素置为null
。我们一般这样用:
@Testvoid testArray() { final List<String> vars = Arrays.asList("1", "2", "3"); final Object[] objArray = vars.toArray(); final String[] strArray = vars.toArray(new String[0]); Assertions.assertTrue(Arrays.asList(strArray).contains("1")); Assertions.assertTrue(Arrays.asList(strArray).contains("2")); Assertions.assertTrue(Arrays.asList(strArray).contains("3"));}
在 Java11 中,又新增了一种实现,相当于对<T> T[] toArray(T[] a)
做了增强,其源码是:
default <T> T[] toArray(IntFunction<T[]> generator) { return toArray(generator.apply(0));}
可以看到,是通过传入一个IntFunction
类型的函数,然后调用<T> T[] toArray(T[] a)
创建数组,其实是采用了我们常用的给toArray
传入空数组的方式,用法如下:
@Testvoid testArray() { final List<String> vars = Arrays.asList("1", "2", "3"); final String[] strArray2 = vars.toArray(String[]::new); Assertions.assertTrue(Arrays.asList(strArray2).contains("1")); Assertions.assertTrue(Arrays.asList(strArray2).contains("2")); Assertions.assertTrue(Arrays.asList(strArray2).contains("3"));}
从使用上,似乎没有太多的提升,但是写法上,使用了函数式编程,是不是很优雅。
这个也是方法增强,在以前,我们在Stream
中的filter
方法判断否的时候,一般需要!
运算,比如我们想要找到字符串列表中的数字,可以这样写:
final List<String> list = Arrays.asList("1", "a");final List<String> nums = list.stream() .filter(NumberUtils::isDigits) .collect(Collectors.toList());Assertions.assertEquals(1, nums.size());Assertions.assertTrue(nums.contains("1"));
想要找到非数字的,filter
方法写的就会用到!
非操作:
final List<String> notNums = list.stream() .filter(x -> !NumberUtils.isDigits(x)) .collect(Collectors.toList());Assertions.assertEquals(1, notNums.size());Assertions.assertTrue(notNums.contains("a"));
Java11 中为Predicate
增加not
方法,可以更加简单的实现非操作:
final List<String> notNums2 = list.stream() .filter(Predicate.not(NumberUtils::isDigits)) .collect(Collectors.toList());Assertions.assertEquals(1, notNums2.size());Assertions.assertTrue(notNums2.contains("a"));
有些教程还会推崇静态引入,比如在头部使用import static java.util.function.Predicate.not
,这样在函数式编程时,可以写更少的代码,语义更强,比如:
final List<String> notNums2 = list.stream() .filter(not(NumberUtils::isDigits)) .collect(toList());
喜好随人,没有优劣。
局部变量是 Java10 中增加的特性,具体可以查看 Java10 的新特性 中的介绍,但是不支持在 Lambda 中使用局部变量。
在 Lambda 中,我们可以这样操作:
(String s1, String s2) -> s1 + s2
也可以这样:
(s1, s2) -> s1 + s2
到 Java11 之后,我们还能这样:
(var s1, var s2) -> s1 + s2
单纯从语法上,似乎没啥特点,但是如果再加上一些别的用法,比如:
(@Nonnull var s1, @Nullable var s2) -> s1 + s2
是不是就能看出差别了,我们可以有如下的操作:
@Testvoid testLocalVariable() { final List<String> sampleList = Arrays.asList("Hello", "World"); final String resultString = sampleList.stream() .map((@NotNull var x) -> x.toUpperCase()) .collect(Collectors.joining(", ")); Assertions.assertEquals("HELLO, WORLD", resultString);}
不过,这里还是有一些限制,比如:
如果是多个参数,不能有的使用var
修饰,有的不指定类型:
// 错误写法(var s1, s2) -> s1 + s2
或者,不能混合使用,一个使用var
修饰,一个使用明确的类型:
// 错误写法(var s1, String s2) -> s1 + s2
如果是单个参数,如果是单行操作,我们可以不写{}
,但是使用var
修饰的时候,就不能省略{}
了:
// 错误写法var s1 -> s1.toUpperCase()
还是有一些限制的,我们在便利的同时,需要符合一定的约束。自由和规范不冲突。
在 Java9 的新特性 中说过,Java 中有一个全新的 HTTP 客户端,当时还在孵化模块中,到 Java11 可以正式使用了。
新客户端用法简单、性能可靠,而且支持功能也多。我们先简单看下使用:
@Testvoid testHttpClient() throws IOException, InterruptedException { final HttpClient httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(20)) .build(); final HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create("https://www.howardliu.cn/robots.txt")) .build(); final HttpResponse<String> httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); final String responseBody = httpResponse.body(); assertTrue(responseBody.contains("Allow"));}
这部分是遗留的技术债务,从 Java1.1 开始,到 Java11 修复,属于 Valhalla 项目的一部分,我们在 Java11 中基于嵌套关系的访问控制优化 一文中有详细解释,这里就不再赘述了。
在 Java11 之前,想要运行源文件,需要先通过javac
命令编译,然后使用java
命令运行,先可以直接使用java
运行了:
$ java HelloWorld.javaHello Java 11!
为了使 JVM 对动态语言更具吸引力,Java 指令集引入了 invokedynamic。
不过 Java 开发人员通常不会注意到此功能,因为它隐藏在 Java 字节代码中。通过使用 invokedynamic,可以延迟方法调用的绑定。例如,Java 语言使用该技术来实现 Lambda 表达式,这些表达式仅在首次使用时才显示出来。这样做,invokedynamic 已经演变成一种必不可少的语言功能。
Java 11 引入了类似的机制,扩展了 Java 文件格式,以支持新的常量池:CONSTANT_Dynamic,它在初始化的时候,像 invokedynamic 指令生成代理方法一样,委托给 bootstrap 方法进行初始化创建,对上层软件没有很大的影响,降低开发新形式的可实现类文件约束带来的成本和干扰。
此功能可提高性能,并面向语言设计人员和编译器实现人员。
Java 11 中提供一种低开销的 Java 堆分配采样方法,能够得到堆分配的 Java 对象信息,并且能够通过 JVMTI 访问堆信息。
引入这个低开销内存分析工具是为了达到如下目的:
对用户来说,了解堆中内存分布是非常重要的,特别是遇到生产环境中出现的高 CPU、高内存占用率的情况。目前有一些已经开源的工具,允许用户分析应用程序中的堆使用情况,比如:Java Flight Recorder、jmap、YourKit 以及 VisualVM tools.。但是这些工具都有一个明显的不足之处:无法得到对象的分配位置,headp dump 以及 heap histogram 中都没有包含对象分配的具体信息,但是这些信息对于调试内存问题至关重要,因为它能够告诉开发人员他们的代码中发生的高内存分配的确切位置,并根据实际源码来分析具体问题,这也是 Java 11 中引入这种低开销堆分配采样方法的原因。
ZGC 是一个可伸缩、低延迟的垃圾收集器,性能由于 G1 收集器,从 Java11 开始可以在 Linux/x64 平台体验,全平台支持是从 Java17 开始。详细介绍可以从https://wiki.openjdk.java.net/display/zgc/Main查看。
在 Java11 中尚处于试验阶段,没有包含在 JDK 构建中,想要启用,需要在 JDK 编译时添加参数--with-jvm-features=zgc
。显式启用了 ZGC 之后,我们可以使用构建好的 JDK 启动,需要添加参数-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
。
ZGC 有下面几个目标:
从https://openjdk.java.net/jeps/333给出的数据可以看出来,在 128G 堆大小的测试中,ZGC 优势明显,找了一张网上的图片:
这里预告一下,Java12 中也增加了一个实现阶段的垃圾收集器 Shenandoah,到时候咱们看一下。
Java 11 优化了 ARM64 或 Arch64 处理器上现有的字符串和数组内部函数。还为java.lang.Math
的sin
、cos
和log
方法实现了新的内部函数。
我们像其他函数一样使用内在函数,但是,编译器会以特殊的方式处理内部函数,将使用 CPU 体系结构特定的汇编代码来提高性能。可以关注一下HotSpotIntrinsicCandidate
这个注解。
Java11 引入了一个新的实验性垃圾收集器:Epsilon。Epsilon 垃圾收集器提供一个完全消极的 GC 实现,分配有限的内存资源,最大限度的降低内存占用和内存吞吐延迟时间,适用于模拟内存不足错误的场景。
Epsilon 垃圾收集器有几个使用场景:
可以通过-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
参数开启。
飞行记录器(Flight Recorder)可是个好东西,之前是 Oracle JDK 中的一个商用产品,现已在 Open JDK 中开源。这是一种低开销的事件信息收集框架,主要用于对应用程序和 JVM 进行故障检查、分析。
飞行记录器记录的主要数据源于应用程序、JVM 和操作系统,这些事件信息保存在单独的事件记录文件中,故障发生后,能够从事件记录文件中提取出有用信息对故障进行分析。有些类似于飞机上的黑匣子。
比如,我们可以使用以下参数开启一个时长为 120 秒的记录:
-XX:StartFlightRecording=duration=120s,settings=profile,filename=recording.jfr
生成的文件可以使用 JMC 工具可视化查看,也可以自己写代码通过RecordedEvent
解析。不过嘛,有可视化的,干嘛还要自己敲代码呢?
我们也可以在运行时通过jcmd
命令启动记录:
$ jcmd <pid> JFR.start$ jcmd <pid> JFR.dump filename=recording.jfr$ jcmd <pid> JFR.stop
收到监控,想推广一下之前写的开源监控组件 Cynomys,源码在https://github.com/howardliu-cn/cynomys,里面包含通过 Netty 实现的 RPC 框架、javaagent 实现的探针、使用 javassist 操作字节码、JMX 实现 JVM 内部监控等,可以对操作系统、网络、JVM、请求、SQL 等内容进行监控。
社会在发展,技术在进步。又有一些功能或组件不合时宜,要么移除、要么标记过期。标记过期的最好不要再用了,不知道哪天就会被移除,想要升级依赖反而麻烦。
本文介绍了 Java11 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk/11/查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java11 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java11 的新特性
你好,我是看山。
Java 语言很强大,但是,有人的地方就有江湖,有猿的地方就有 bug,Java 的核心代码并非十全十美。比如在 JDK 中居然也有反模式接口常量 中介绍的反模式实现,以及本文说到的这个技术债务:嵌套关系(NestMate)调用方式。
在 Java 语言中,类和接口可以相互嵌套,这种组合之间可以不受限制的彼此访问,包括访问彼此的构造函数、字段、方法等。即使是private
私有的,也可以彼此访问。比如下面这样定义:
public class Outer { private int i; public void print1() { print11(); print12(); } private void print11() { System.out.println(i); } private void print12() { System.out.println(i); } public void callInnerMethod() { final Inner inner = new Inner(); inner.print4(); inner.print5(); System.out.println(inner.j); } public class Inner { private int j; public void print3() { System.out.println(i); print1(); } public void print4() { System.out.println(i); print11(); print12(); } private void print5() { System.out.println(i); print11(); print12(); } }}
上例中,Outer
类中的字段i
、方法print11
和print12
都是私有的,但是可以在Inner
类中直接访问,Inner
类的字段j
、方法print5
是私有的,也可以在Outer
类中使用。这种设计是为了更好的封装,在用户看来,这几个彼此嵌套的类/接口是一体的,分开定义是为了更好的封装自己,隔离不同特性,但是有因为彼此是一体,所以私有元素也应该是共有的。
我们使用 Java8 编译,然后借助javap -c
命令分别查看Outer
和Inner
的结果。
$ javap -c Outer.class Compiled from "Outer.java"public class cn.howardliu.tutorials.java8.nest.Outer { public cn.howardliu.tutorials.java8.nest.Outer(); Code: 0: aload_0 1: invokespecial #4 // Method java/lang/Object."<init>":()V 4: return public void print1(); Code: 0: aload_0 1: invokespecial #2 // Method print11:()V 4: aload_0 5: invokespecial #1 // Method print12:()V 8: return public void callInnerMethod(); Code: 0: new #7 // class cn/howardliu/tutorials/java8/nest/Outer$Inner 3: dup 4: aload_0 5: invokespecial #8 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner."<init>":(Lcn/howardliu/tutorials/java8/nest/Outer;)V 8: astore_1 9: aload_1 10: invokevirtual #9 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.print4:()V 13: aload_1 14: invokestatic #10 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.access$000:(Lcn/howardliu/tutorials/java8/nest/Outer$Inner;)V 17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 20: aload_1 21: invokestatic #11 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.access$100:(Lcn/howardliu/tutorials/java8/nest/Outer$Inner;)I 24: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 27: return static int access$200(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: getfield #3 // Field i:I 4: ireturn static void access$300(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: invokespecial #2 // Method print11:()V 4: return static void access$400(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: invokespecial #1 // Method print12:()V 4: return}
再来看看Inner
的编译结果,这里需要注意的是,内部类会使用特殊的命名方式定义Inner
类,最终会将编译结果存储在两个文件中:
$ javap -c Outer\$Inner.classCompiled from "Outer.java"public class cn.howardliu.tutorials.java8.nest.Outer$Inner { final cn.howardliu.tutorials.java8.nest.Outer this$0; public cn.howardliu.tutorials.java8.nest.Outer$Inner(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: aload_1 2: putfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 5: aload_0 6: invokespecial #4 // Method java/lang/Object."<init>":()V 9: return public void print3(); Code: 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 7: invokestatic #6 // Method cn/howardliu/tutorials/java8/nest/Outer.access$200:(Lcn/howardliu/tutorials/java8/nest/Outer;)I 10: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 17: invokevirtual #8 // Method cn/howardliu/tutorials/java8/nest/Outer.print1:()V 20: return public void print4(); Code: 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 7: invokestatic #6 // Method cn/howardliu/tutorials/java8/nest/Outer.access$200:(Lcn/howardliu/tutorials/java8/nest/Outer;)I 10: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 17: invokestatic #9 // Method cn/howardliu/tutorials/java8/nest/Outer.access$300:(Lcn/howardliu/tutorials/java8/nest/Outer;)V 20: aload_0 21: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 24: invokestatic #10 // Method cn/howardliu/tutorials/java8/nest/Outer.access$400:(Lcn/howardliu/tutorials/java8/nest/Outer;)V 27: return static void access$000(cn.howardliu.tutorials.java8.nest.Outer$Inner); Code: 0: aload_0 1: invokespecial #2 // Method print5:()V 4: return static int access$100(cn.howardliu.tutorials.java8.nest.Outer$Inner); Code: 0: aload_0 1: getfield #1 // Field j:I 4: ireturn}
我们可以看到,Outer
和Inner
中多出了几个方法,方法名格式是access$*00
。
Outer
中的access$200
方法返回了属性i
,access$300
和access$400
分别调用了print11
和print12
方法。这些新增的方法都是静态方法,作用域是默认作用域,即包内可用。这些方法最终被Inner
类中的print3
和print4
调用,相当于间接调用Outer
中的私有属性或方法。
我们称这些生成的方法为“桥”方法(Bridge Method),是一种实现嵌套关系内部互相访问的方式。
在编译的时候,Java 为了保持类的单一特性,会将嵌套类编译到多个 class 文件中,同时为了保证嵌套类能够彼此访问,自动创建了调用私有方法的“桥”方法,这样,在保持原有定义不变的情况下,又实现了嵌套语法。
“桥”方法的实现是比较巧妙的,但是这会造成源码与编译结果访问控制权限不一致,比如,我们可以在Inner
中调用Outer
中的私有方法,按照道理来说,我们可以在Inner
中通过反射调用Outer
的方法,但实际上不行,会抛出IllegalAccessException
异常。我们验证一下:
public class Outer { // 省略其他方法 public void callInnerReflectionMethod() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { final Inner inner = new Inner(); inner.callOuterPrivateMethod(this); } public class Inner { // 省略其他方法 public void callOuterPrivateMethod(Outer outer) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { final Method method = outer.getClass().getDeclaredMethod("print12"); method.invoke(outer); } }}
定义测试用例:
@Testvoid gotAnExceptionInJava8() { final Outer outer = new Outer(); final Exception e = assertThrows(IllegalAccessException.class, outer::callInnerReflectionMethod); e.printStackTrace(); assertDoesNotThrow(outer::callInnerMethod);}
打印的异常信息是:
java.lang.IllegalAccessException: class cn.howardliu.tutorials.java8.nest.Outer$Inner cannot access a member of class cn.howardliu.tutorials.java8.nest.Outer with modifiers "private" at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361) at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591) at java.base/java.lang.reflect.Method.invoke(Method.java:558) at cn.howardliu.tutorials.java8.nest.Outer$Inner.callOuterPrivateMethod(Outer.java:62) at cn.howardliu.tutorials.java8.nest.Outer.callInnerReflectionMethod(Outer.java:36)
通过反射直接调用私有方法会失败,但是可以直接的或者通过反射访问这些“桥”方法,这样就比较奇怪了。所以提出 JEP181 改进,修复这个技术债务的同时,为后续的改进铺路。
我们再来看看 Java11 编译之后的结果:
$ javap -c Outer.class Compiled from "Outer.java"public class cn.howardliu.tutorials.java11.nest.Outer { public cn.howardliu.tutorials.java11.nest.Outer(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void print1(); Code: 0: aload_0 1: invokevirtual #2 // Method print11:()V 4: aload_0 5: invokevirtual #3 // Method print12:()V 8: return public void callInnerMethod(); Code: 0: new #7 // class cn/howardliu/tutorials/java11/nest/Outer$Inner 3: dup 4: aload_0 5: invokespecial #8 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner."<init>":(Lcn/howardliu/tutorials/java11/nest/Outer;)V 8: astore_1 9: aload_1 10: invokevirtual #9 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner.print4:()V 13: aload_1 14: invokevirtual #10 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner.print5:()V 17: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 20: aload_1 21: getfield #11 // Field cn/howardliu/tutorials/java11/nest/Outer$Inner.j:I 24: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 27: return}
是不是很干净,与Outer
类的源码结构是一致的。我们再看看Inner
有没有什么变化:
$ javap -c Outer\$Inner.classCompiled from "Outer.java"public class cn.howardliu.tutorials.java11.nest.Outer$Inner { final cn.howardliu.tutorials.java11.nest.Outer this$0; public cn.howardliu.tutorials.java11.nest.Outer$Inner(cn.howardliu.tutorials.java11.nest.Outer); Code: 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return public void print3(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 7: getfield #4 // Field cn/howardliu/tutorials/java11/nest/Outer.i:I 10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 17: invokevirtual #6 // Method cn/howardliu/tutorials/java11/nest/Outer.print1:()V 20: return public void print4(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 7: getfield #4 // Field cn/howardliu/tutorials/java11/nest/Outer.i:I 10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 17: invokevirtual #7 // Method cn/howardliu/tutorials/java11/nest/Outer.print11:()V 20: aload_0 21: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 24: invokevirtual #8 // Method cn/howardliu/tutorials/java11/nest/Outer.print12:()V 27: return}
同样干净。
我们在通过测试用例验证一下反射调用:
@Testvoid doesNotGotAnExceptionInJava11() { final Outer outer = new Outer(); assertDoesNotThrow(outer::callInnerReflectionMethod); assertDoesNotThrow(outer::callInnerMethod);}
结果是正常运行。
这就是 JEP181 期望的结果,源码和编译结果一致,访问控制一致。
在 Java11 中还新增了几个 API,用于嵌套关系的验证:
这个方法是返回嵌套主机(NestHost),转成普通话就是找到嵌套类的外层类。对于非嵌套类,直接返回自身(其实也算是返回外层类)。
我们看下用法:
@Testvoid checkNestHostName() { final String outerNestHostName = Outer.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.Outer", outerNestHostName); final String innerNestHostName = Inner.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.Outer", innerNestHostName); assertEquals(outerNestHostName, innerNestHostName); final String notNestClass = NotNestClass.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.NotNestClass", notNestClass);}
对于Outer
和Inner
都是返回了cn.howardliu.tutorials.java11.nest.Outer
。
这个方法是返回嵌套类的嵌套成员数组,下标是 0 的元素确定是 NestHost 对应的类,其他元素顺序没有给出排序规则。我们看下使用:
@Testvoid getNestMembers() { final List<String> outerNestMembers = Arrays.stream(Outer.class.getNestMembers()) .map(Class::getName) .collect(Collectors.toList()); assertEquals(2, outerNestMembers.size()); assertTrue(outerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer")); assertTrue(outerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer$Inner")); final List<String> innerNestMembers = Arrays.stream(Inner.class.getNestMembers()) .map(Class::getName) .collect(Collectors.toList()); assertEquals(2, innerNestMembers.size()); assertTrue(innerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer")); assertTrue(innerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer$Inner"));}
这个方法是用于判断两个类是否是彼此的 NestMate,彼此形成嵌套关系。判断依据还是嵌套主机,只要相同,两个就是 NestMate。我们看下使用:
@Testvoid checkIsNestmateOf() { assertTrue(Inner.class.isNestmateOf(Outer.class)); assertTrue(Outer.class.isNestmateOf(Inner.class));}
嵌套关系是作为 Valhalla 项目的一部分,这个项目的主要目标之一是改进 JAVA 中的值类型和泛型。后续会有更多的改进:
Unsafe.defineAnonymousClass()
API 的安全替换,实现将新类创建为已有类的 Nestmate。本文阐述了基于嵌套关系的访问控制优化,其中涉及NestMate
、NestHost
、NestMember
等概念。这次优化是 Valhalla 项目中一部分,主要改进 Java 中的值类型和泛型等。文中涉及源码都上传在 GitHub 上,关注公号「看山的小屋」回复“java”获取源码。
青山不改,绿水长流,咱们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java11 中基于嵌套关系的访问控制优化
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java11 中基于嵌套关系的访问控制优化
你好,我是看山。
一晃又是一年,果然岁数越大,时间越快。有些内容在 原来还能这么干 一文中聊了一些,今天再聊点别的。让我们一起总结过去,把握现在,展望未来。
总结下来,2021 年还算幸运,平平淡淡过一年。工作上按部就班,生活上一如既往。
如果说有什么可以出牛的,就是开始好好写文了。幸运的是在 2021 年最后一天,得到了 InfoQ 官方认可,成为签约作者。
2021 年换了一份工作,感谢前司领导同事的帮助,知道了什么是好好工作,怎样做可以做好工作:
到了现司之后,也有了一些感悟:孤胆英雄是没有办法生存的,团队才是能够好好工作的最小单位。
上面这些,每一条都可以描述很多,既然是年终总结,就先一笔带过,看官可以先自行体会一下。如果有必要,再开文详细聊聊。
2021 年要好好感谢我媳妇,如果有哪位朋友恰好看到这篇文章,记得给小猪转发一下,我猜她一定忽略了我的这份心意。
生活方面没有太多要说的,只有满心的感动和感激。
在 Geek 青年说北京沙龙分享 中聊过,我是 2013 年开始写博客,2018 年停更一年,从 2021 年开始坚持周更。这个过程中,认识了很多志同道合的朋友,见到了很多优秀的博主。
一个人可以走的很快,一群人可以走的很远。
写博客是为了实现自己定的目标,不必太在意结果。不过,正如前面所说,一切用数字说话。下面就晒一下 2021 年的一些成绩(这些成绩和大佬没法比,只能小小的自嗨一下):
C 站粉丝达到 17000,访问量有 870000:
C 站 1024 活动时,收货博客专家勋章:
参加知乎海盐计划,直接升级到 4 级;
参加掘金 11 月更文活动,两次后端模块的周榜前 10;
参加 InfoQ 写作平台签约作者第二季,成功入选。评选结果是在 12 月 31 号公布的,算是给 2021 年的写作之旅画上一个不错的句号。
除了写博客,今年也开始健身了。一开始是维嘉带着练,后来维嘉回了学校是跟着斌哥练,终于看到了 75 公斤的影子。
新的一年,为了对自己负责,对家人负责,对朋友负责,我们总要做出新一年的计划。我 2022 年的计划就是搞定 2021 年那些原定于 2020 年未完成的安排,只为兑现 2019 年时要完成 2018 年许下的诺言,曾说 2017 年之后一定不要像 2016 年那样只会跟着 2015 年去做 2014 年没给 2013 年完成的那个目标。
哈哈哈,上面的文案摘自某音的段子,比较写实。
我真实的计划就不放出来了,心理学上有个研究结果,当多次向别人描述自己的计划,就会产生一种错觉,以为自己已经完成了计划。计划不广而告之,但是一定要有。
没有计划的人生不值得过。
青山不改,绿水长流,咱们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:这一年很幸运,平平淡淡的|2021 年度总结
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:这一年很幸运,平平淡淡的|2021 年度总结
你好,我是看山。
就这样 2022 年了,年纪越大,时间越快,又到了罗胖《时间的朋友》直播的时候,看完后想写点什么,奈何腹中墨水太少,索性不难为自己,随便写写。由于疫情原因,罗胖今年的跨年演讲现场的观众全部都是熊猫娃娃,评论区有整场直播的视频回访和文字稿,有些事情,还是要自己亲自感受一下。
整场演讲的主题是“原来还能这么干”,用我能够理解的话,就是打破思维的墙。
每个人都有自己的思维模式,这是我们赖以生存的根本,也是我们能够更快速、更高效处理普通事务的根本。之前在 别让非理性思维毁了你的人生 中聊过,我们的大脑为了节省能量,会经常处于自动驾驶模式,这让我们可以更有机会省出能量做其他事情。比如,我看看到知道冒白气的水是热的,不会直接喝;我们知道冷了要穿衣服,否则会生病……但是有些时候,我们用常规的办法解决不了问题,怎么办?
想要打破思维的墙,我们需要绝对的理性分析限制我们思维的问题是什么?真正要解决的问题有哪些?就不列举罗胖演讲中的例子了,我们考虑一下网约车出来之前那种打车难的问题。
很多人可能没有经历过打车难的时期,那个时候,我们没有办法知道哪里有出租车,只能就近在路边招手,出租车师傅同样不知道哪有顾客,只能满城转悠,或者在上客概率到的地方等着,比如 CBD、高铁站、机场等。平时还好,等会就等会,如果恰好有急事、或者带着老人小孩、亦或是刮风下雨的时候,就会比较难受。怎么办?
如果我们可以提前和司机师傅约好,在约定地点上车,是不是就可以了。想法有了,接下来就是实现,于是有了一系列的网约车 APP。现在大家打车记录不需要路边拦车了,直接网上下单,指定地点上车即可,方便快捷。
这几年,很多互联网公司的崛起,改变了我们的生活方式,比如:外卖、拼团、移动支付……他们的成功,是走了以前没有人走过的路。
《功勋》中屠呦呦关于常山碱的判断中,认为已经经过反复论证走不通的路,就该果断放弃,立马淘汰,找到更可靠的方式,于是有了后面的青蒿素的发现。
碰到问题,我们要投入百分之百的努力克服困难,但是如果已经不行了,就该考虑换个方式再上。
“行就行,不行再想想办法。”
唯一不变的就是变化。没有什么是永恒不变的,我们能够应对变化的手段,只有提前预知变化,做好准备,当变化来临时,坦然面对。
很多 2020 年风生水起的教培行业,在国家出台双减政策后,一夜之间,大厦轰然倒塌。很多教培行业的老师、研发人员,只能重新考虑未来的发展。其实国家一直有这个信号,我在 想躺平不是错 中也谈过相关的问题。很多人抱怨国家手段强硬,但他们真正抱怨的是,国家没有提前告诉他们要行霹雳手段,改变这个畸形发展的行业。
我们很多人相信风水、星座、命运,其实只是想从中探寻一些未来的可能。罗胖给出了一个观点是,我们没有办法一直追寻改变,只要找到未来的不变,试着靠近他,当未来来临时,我们就已经赶在了潮流的前列。
那怎么找到未来一定发生的事情?个人愚见是翻翻国家政策,比如“十四五”规划,看看规划的未来目标。跟着国家政策走,绝对不会有太大偏差。找到目标了怎么实现呢?有能力上,没能力提升能力也要上,如果还是上不去,就“打破思维的墙”,再想想别的办法。
35 岁焦虑是每个程序员都有的?各种营销号中一直鼓吹一个观点,到了 35 岁,就会一下子变成了没有任何价值的抹布。而且给出很多的理由:
似乎都有道理,但是总感觉哪里不对。罗胖的观点是,年龄大了之后,除了工作能力之外,我们拼的还有软技能。
以编程开发为例,简单的 CRUD,刚毕业的小伙子和 35 岁的人开发结果差不多,但是复杂逻辑呢?但凡有些经验的开发人员,会把场景考虑更加完善,会在开发时考虑更多的设计模式,这些经验,会让程序更加健壮,能够应对更多的变化。而且,经历了社会的毒打之后,我们会比较平和的接受一些职场上的不公平,这不是怂,而是一种心态的转变,“世间事,除了生死,哪一件事不是闲事。”
心态平和了,为人处世才会简单,能够更好的处理人际关系。这就是我们的软技能,如果我们可以在开发之外再有一些亮眼的特点,比如:架构设计、逻辑分析、产品设计、汇报总结等等。
之前看过一篇文章,里面说到,被辞退的员工,不会被告知被辞退的真正原因,只会说是公司效益不好、发展不畅。其实,很多时候是软技能太弱。
既然我们没有办法和 20 多岁的年轻人拼精力,那我们以一个更有生活阅历的年轻人身份在职场中打拼。
不知道从什么时候开始,这种情绪就渗进了我们骨子里面。
写这段内容的时候,写了改,改了删。我企图找到一些证据,证明我的这个想法是对的,我企图找到一个事件,能够代表这种情绪的起点。最后还是删了,这是潜移默化的一个结果。填饱肚子的不是最后一个包子,而是前面 9 个包子的铺垫。
我只表达这种情绪,其他的交给时间。
『1』给重要时刻:
“行万里路,读万遍经。笨鸭早飞,笨牛勤耕。让小的敬老的,拿次的留好的。宁欺官,不欺贤,宁欺贤,不欺天。人多的地方不去,没人的地方不留。赞美成功的人,安慰失败的人。犯病的东西不吃,犯法的事情不做。不要穿金戴银,只要好好做人。墙倒众人推,我不推;枪打出头鸟,我不打。种瓜得瓜瓜儿大,种豆得豆豆儿多。”————《王鼎钧回忆录》
『2』给理性乐观派:
“我的乐观并不需要这些头头是道的逻辑支撑,它就是一种朴素的信念:相信中国会更好。这种信念不是源于学术训练,而是源于司马迁、杜甫、苏轼,源于‘一条大河波浪宽’,源于对中国人勤奋实干的钦佩。它影响了我看待问题的角度和处理信息的方式,我接受这种局限性,没有改变的打算。”————兰小欢
『3』给犹豫不决的人:
“什么是事件?事件就是某种超出了原因的结果。”————齐泽克
『4』给准备出发的人:
“设计是一个不断生成目标和备选方案的过程。”————赫伯特·西蒙
『5』给正在路上的人:
“非常理想,特别现实。”————李希贵
『6』给正在拓荒的人:
“提前一个版本遵守法律”————王永治
『7』给知易行难的人:
“要改变一个成年人的行为,认知、能力、提醒,三者同样重要”————王建硕
『8』给身处困境的人:
“地球上最后一个人独自坐在房间里,这时忽然响起了敲门声……”————弗里蒂克·布朗
『9』给 2022 年的我们:
“让我们泰然自若,与自己的时代狭路相逢”————莎士比亚
生活很难,有时候就需要一种正能量激励我们,哪怕只是轻轻的推一把,齿轮就会转动起来,然后就沿着这种惯性继续下去。
愿大家 2022 年“各从其欲,皆得所愿”。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:原来还能这么干——罗胖2022年《时间的朋友》观后感
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:原来还能这么干——罗胖2022年《时间的朋友》观后感
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
从 Java10 开始,Java 版本正式进入每半年一个版本的更新节奏,更新频率加快,小步快跑。
接下来我们瞅瞅 Java10 都更新了哪些比较有意思的功能。
我们都知道 Java 是强类型语言,有着严格的类型限制。想要定义一个变量,必须明确指明变量类型,用Object
抬杠的可以离开了。但是从 Java10 开始,我们可以在定义局部变量时使用var
限定变量类型,Java 编译器会根据变量的值推断具体的类型。
比如,我们定义一个Map
对象:
Map<Integer, String> map = new HashMap<>();
现在我们可以写做:
var idToNameMap = new HashMap<Integer, String>();
这个功能算是 Java 的一次尝鲜,给 Java 语言增加了更多的可能,让我们的代码更加简洁,更加专注于可读性。
需要注意的是,新增的var
不会把 Java 变成动态语言,在编译时,编译器会自动推断类型,将其转换为确定的类型,不会在运行时动态变化。
目前var
只能用于局部变量,而且等号右侧必须是确定类型的定义,包括:初始化的实例、方法的调用、匿名内部类。
我们再回到刚才的例子:
// 以前的写法Map<Integer, String> map = new HashMap<>();// 现在可以这么写var idToNameMap = new HashMap<Integer, String>();
对于参数的名字,我们可以不在关注类型,可以更多的关注参数的意义,这也是编写可读代码的要求。
这也为我们提出了一些要求,如果是特别长的 Lambda 表达式,还是老老实实的使用明确的类型吧,否则写着写着就迷糊了。
再就是推断类型时没有那么智能,都是基于最明确的推断,比如:
var emptyList = new ArrayList<>();
这个时候推断emptyList
的结果是ArrayList<Object>
,绝对不会按照我们常用写法推断成List<Object>
。
如果是匿名内部类,比如:
var obj = new Object() {};
这个时候obj.getClass()
可就不是Object.class
了,而且匿名内部类的类型了。
所以,小刀虽好,但也要好好用,胡乱用容易误伤。
从 Java9 开始提供不可变集合的实现,Java10 继续扩展。集合是一个容器,作为一个参数传入方法中,我们并不知道方法是否会对容器中的元素进行修改,有了不可变集合,我们就能够在一定程度上进行控制(毕竟对容器中对象的数据进行修改,我们的控制力就没有那么强了)。
针对不可变集合,我们摘取java.util.List
的描述(其他的描述都是类似的):
Unmodifiable Lists
The List.of and List.copyOf static factory methods provide a convenient way to create unmodifiable lists. The List instances created by these methods have the following characteristics:
- They are unmodifiable. Elements cannot be added, removed, or replaced. Calling any mutator method on the List will always cause UnsupportedOperationException to be thrown. However, if the contained elements are themselves mutable, this may cause the List’s contents to appear to change.
- They disallow null elements. Attempts to create them with null elements result in NullPointerException.
- They are serializable if all elements are serializable.
- The order of elements in the list is the same as the order of the provided arguments, or of the elements in the provided array.
- They are value-based. Callers should make no assumptions about the identity of the returned instances. Factories are free to create new instances or reuse existing ones. Therefore, identity-sensitive operations on these instances (reference equality (==), identity hash code, and synchronization) are unreliable and should be avoided.
- They are serialized as specified on the Serialized Form page.
简单翻译一下:
UnsupportedOperationException
异常。但是,but,如果集合中的元素是可变的,那就控不住了。比如,元素是AtomInteger
就没法控制其中的值,集合只是元素不变;如果是String
,那集合是整体不变的。null
,会抛出NullPointerException
copyOf
和of
这些方法中返回的结果可能使用提前定义好的对象,比如空集合、原集合等。换句话说,在不同调用位置返回了相同对象。所以不要相信==
、hashCode
,也不要对其加锁。在java.util.List
、java.util.Map
、java.util.Set
这几个接口中都各自添加了一个copyOf
静态方法,用来创建不可变集合,最终都会是ImmutableCollections
中定义的几个集合实现,与 Java9 中定义的of
方法类似。
对于java.util.Map
、java.util.Set
,这里有一个优化,如果传入的本身就是不可变的集合,将直接返回传入的参数,代码如下:
static <E> Set<E> copyOf(Collection<? extends E> coll) { if (coll instanceof ImmutableCollections.AbstractImmutableSet) { return (Set<E>)coll; } else { return (Set<E>)Set.of(new HashSet<>(coll).toArray()); }}
Java10 很贴心的提供了Stream
中的操作,我们直接创建不可变集合了。比如:
Stream.of("1", "2", "3", "4", "5") .map(x -> "id: " + x) .collect(Collectors.toUnmodifiableList());
toUnmodifiableList
、toUnmodifiableSet
、toUnmodifiableMap
的用法与toList
、toSet
、toMap
没有太多区别,差别在于返回的是不可变集合。
这里说的 Optional 族包括Optional
、OptionalInt
、OptionalLong
、OptionalDouble
几个实现。以前有一个orElseThrow(Supplier<? extends X> exceptionSupplier)
方法,用于获取不到数据时,抛出exceptionSupplier
中定义的异常。
我们会写成:
Stream.of("1", "2", "3", "4", "5") .map(x -> "id: " + x) .findAny() .orElseThrow(() -> new NoSuchElementException("No value present"));
优点是我们可以自定义自己的异常以及异常信息。有时候,我们不关心具体的异常和异常信息,这个时候 Java10 中的新增的orElseThrow
方法就派上用场了:
Stream.of("1", "2", "3", "4", "5") .map(x -> "id: " + x) .findAny() .orElseThrow();
此时如果元素为空,将抛出NoSuchElementException
异常。
Java 当年在性能方面一直被诟病,中间隔着一层虚拟机,实现跨平台运行的功能同时,也致使其执行性能不如 C 语言。所以,Java 一直在性能方面投入大量精力。
我们看看 Java10 中都有哪些优化点。
从 Java9 开始,G1 已经转正,成为默认的垃圾收集器。不过在 Full GC 时,G1 还是采用的单线程串行标记压缩算法,这样 STW 时间会比较长。到 Java10,Full GC 实现了并行标记压缩算法,明显缩短 STW 时间。
CDS(Class-Data Sharing,类数据共享)是在 Java5 引入的一种类预处理方式,可以将一组类共享到一个归档文件中,在运行时通过内存映射加载类,这样做可以减少启动时间。同时在多个 JVM 之间实现同享同一个归档文件,减少动态内存占用。
但是 CDS 有一个限制,就是只能是 Bootstrap ClassLoader 使用,这样就将功能限制了类的范围。在 Java10 中,将这个功能扩展到了系统类加载器(System ClassLoader,或者成为应用类加载器,Application ClassLoader)、内置的平台类加载器(Platform ClassLoader),或者是自定义的类加载器。这样就将功能扩展到了应用类。
想要使用这个功能的话,总共分三步:
hello.jar
中的HelloWorld
类使用的类添加到hello.lst
中:$ java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst \ -cp hello.jar HelloWorld
hello.lst
中的内容创建 AppCDS 文件hello.jsa
:$ java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst \ -XX:SharedArchiveFile=hello.jsa -cp hello.jar
hello.jsa
启动HelloWorld
:$ java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \ -cp hello.jar HelloWorld
Graal 是使用 Java 编写的与 HotSpot JVM 集成的动态编译器,专注于高性能和可扩展性。是从 JDK9 引入的实验性 AOT 编译器的基础。
在 JDK10 中,我们可以在 Linux/x64 平台将 Graal 作为 JIT 编译器使用。开启命令如下:
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
需要注意的是,这是实验特性,相当于公测阶段,可能在某些场景下,性能不如现有的 JIT 编译器。
容器化是目前的趋势,在之前,由于 JVM 不能够感知容器,在同一个主机上部署多个虚拟机时,会造成内存占用、CPU 抢占等问题,这点也成为了很多大会上抨击 Java 语言不适合容器时代的一个点。
现在好了,JVM 可以感知容器了。只是暂时还只支持 Linux 系统(so what,其他平台也还没有用过)。这个功能默认开启,不想使用可以手动关闭:
-XX:-UseContainerSupport
我们还可以手动指定 CPU 核数:
-XX:ActiveProcessorCount=1
还有三个可以控制内存的使用量:
-XX:InitialRAMPercentage-XX:MaxRAMPercentage-XX:MinRAMPercentage
就目前来看,这部分还可以继续完善,相信只是时间问题。
自 Java9 起在 keytool 中加入参数 -cacerts,可以查看当前 JDK 管理的根证书。而 Java9 中 cacerts 目录为空,这样就会给开发者带来很多不便。从 Java10 开始,将会在 JDK 中提供一套默认的 CA 根证书。
作为 JDK 一部分的 cacerts 密钥库旨在包含一组能够用于在各种安全协议的证书链中建立信任的根证书。在 Java10 之前,cacerts 密钥库是空的,默认情况下,关键安全组件(如 TLS)是不起作用的,开发人员需要手动添加一组根证书来使用这些验证。
Java10 中,Oracle 开放了根证书源码,可以让 OpenJDK 构建对开发人员更有吸引力,并减少这些构建与 Oracle JDK 构建之间的差异。
有增有减,这样才能够保证 Java 的与时共进。
javah
命令,这个命令用于创建 native 方法所需的 C 的头文件和资源文件的,使用javac -h
替代。policytool
工具,这个工具用于创建和管理策略文件。可以直接还使用文本编辑器代替。java -Xprof
参数,这个参数本来是用于评测正在运行的程序,并将评测数据发送到标准输出。可以使用jmap
代替。java.security.acl
包标记为过期,标记参数forRemoval
是true
,将在未来版本中删除。目前,这个包内的功能已经被java.security.Policy
取代。java.security
包中的Certificate
、Identity
、IdentityScope
、Signer
的标记参数forRemoval
也是true
。这些都将在后续版本中删除。
从 Java10 开始,Java 正式进入每半年一个版本的更新节奏,主要改动如下:
$FEATURE.$INTERIM.$UPDATE.$PATCH
命名机制:$FEATURE
,每次版本发布加 1,不考虑具体的版本内容;$INTERIM
,中间版本号,在大版本中间发布的,包含问题修复和增强的版本,不会引入非兼容性修改;$PATCH
用于快速打补丁的。本文介绍了 Java10 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk/10/查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java10 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java10 的新特性
你好,我是看山。
今天项目依赖了一个基础组件之后,启动失败,排查过程走了一些弯路,最终确认是因为依赖组件版本冲突造成了java.lang.NoClassDefFoundError
异常。下面是排查过程,希望可以给你提供一些思路。
下面是打印的异常栈信息,从其中提炼可能的关键信息,能够找到“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”,还有“Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]”。继续从异常栈中找一下发生的时机,可以发现是调用AbstractAutowireCapableBeanFactory.createBeanInstance
时,这个方法是创建 Bean 实例。
这块是异常信息(getMessage 的内容,横向太长,手动换行了):org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'methodValidationPostProcessor' defined in class path resource [org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class]: Unsatisfied dependency expressed through method 'methodValidationPostProcessor' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'addressMapper' defined in file [/Users/liuxinghao/Documents/work/code/cn.howardliu/effective-spring/target/classes/cn/howardliu/demo/AddressMapper.class]: Unsatisfied dependency expressed through constructor parameter 0: Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Class'; nested exception is java.lang.IllegalArgumentException: Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]下面是异常栈: at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:799) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:540) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1341) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]其他异常栈信息可以忽略了
我们可以根据目前有效的信息进行排查,首先看下我们的cn.howardliu.demo.AddressMapper
定义是否有问题,再看看依赖它的 Service 有没有问题,什么问题也没有发现。下一个检查点是配置,比如@MapperScan
是否正确、Mapper 类上有没有加上@Mapper
注解,发现也没有问题。
从异常信息找不到思路了,只能从代码入手了。
这里需要说一下,打印异常信息至关重要,直接影响我们排错的思路。如果打印的信息没有办法准确定位,我们将会花费大量的时间查找真正的错误,这就需要走查代码,有时候还需要一些经验。
我们由异常栈ConstructorResolver.createArgumentArray(ConstructorResolver.java:799)
入手,跟着断点往下追,最终会追到org.springframework.util.ClassUtils#forName
方法,其中会抛出异常的代码是下面这块:
try { return Class.forName(name, false, clToUse);}catch (ClassNotFoundException ex) { int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR); if (lastDotIndex != -1) { String innerClassName = name.substring(0, lastDotIndex) + INNER_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); try { return Class.forName(innerClassName, false, clToUse); } catch (ClassNotFoundException ex2) { // Swallow - let original exception get through } } throw ex;}
出现错误的是Class.forName(name, false, clToUse)
这句,name
传的是”cn.howardliu.demo.AddressMapper”字符串,抛出的异常是java.lang.NoClassDefFoundError
,由于不是ClassNotFoundException
异常,不会进入catch
逻辑,会直接向上抛出。
找到错误我们就好定位问题了。
一般来说,java.lang.NoClassDefFoundError
错误是需要加载的类能够找到,但是加载时出现了异常,简单说就是,类的定义有问题。我们借助 JD-GUI 反编译一下运行 jar 包,结果如下:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;import cn.howardliu.demo.Address;import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface AddressMapper extends BaseMapper<Address> {}
观察仔细的话,我们可以看到import com.baomidou.mybatisplus.core.mapper.BaseMapper;
这行没有下划线,也就是说,在反编译工具中追溯不到这个接口,推断出来就是在运行环境中,找不到BaseMapper
这个类定义。
所以,当Class.forName
加载类的时候抛出了java.lang.NoClassDefFoundError
异常。
如果有一定经验,就会立刻想到,大概率出现了依赖 jar 的版本冲突。
我们可以借助 maven 命令行找到版本冲突的依赖:
mvn dependency:tree -Dverbose | grep conflict
打印结果为:
[INFO] | +- (com.baomidou:mybatis-plus:jar:3.1.2:compile - omitted for conflict with 2.1.6)
我们也可以借助 IDEA 的可视化工具,在 pom.xml 上打开依赖图:
我们可以看到 mybatis-plus 的红线指示出冲突信息:
结论就是 Mybatis-Plus 版本冲突了,项目中依赖了 mybatis-plus 的 2.1.6 和 3.1.2 两个版本,由于 2.1.6 路径更短,最终被选中。
此时只需要将低版本的依赖去掉即可。
为什么低版本的 mybatis-plus 会造成类加载失败呢?是因为 mybatis-plus 跨版本更新时,把BaseMapper
的包路径改了:
// 3.1.2 版本import com.baomidou.mybatisplus.core.mapper.BaseMapper;// 2.1.6 版本import com.baomidou.mybatisplus.mapper.BaseMapper;
而且还不止这一个,IService
、ServiceImpl
、TableName
、TableField
、Model
、TableField
等等,很多常用的类都改了位置。所以会造成找不到依赖的类。编译是 3.1.2 依赖还在运行环境中,就会出现编译没有问题,执行时出现加载类异常。
想要工程化的解决这个问题,我们可以创建基础的依赖 bom 配置,定义好基础依赖包,在项目中不在指定版本。这样做到统一版本,可以有效地避免这类问题。
我们还可以在 CI/CD 中加入冲突依赖检查,如果发现冲突依赖,就终止流水线。
接下来我们看下为什么明明是java.lang.NoClassDefFoundError
异常,结果异常栈中打印的是一堆不相干的错误。继续跟着刚才的断点 Debug:
org.springframework.util.ClassUtils#resolveClassName
会捕捉LinkageError
错误,然后包装成IllegalArgumentException
异常,这个时候真是异常还是继续上抛。
然后在org.springframework.beans.TypeConverterSupport#convertIfNecessary
方法会包装成TypeMismatchException
异常,此时,真实异常还在异常cause
参数中,并没有丢失。
等回到org.springframework.beans.factory.support.ConstructorResolver#createArgumentArray
方法后,捕捉异常的方法是:
try { convertedValue = converter.convertIfNecessary(originalValue, paramType, methodParam);}catch (TypeMismatchException ex) { throw new UnsatisfiedDependencyException( mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), "Could not convert argument value of type [" + ObjectUtils.nullSafeClassName(valueHolder.getValue()) + "] to required type [" + paramType.getName() + "]: " + ex.getMessage());}
此时我们可以注意到,在包装成UnsatisfiedDependencyException
异常的时候,只是把捕捉到的TypeMismatchException
通过getMessage
方法追加在异常描述后面,此时经过前面几轮的包装再包装,真实的异常的异常信息仅剩Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]
这段经过处理的信息,完全没有java.lang.NoClassDefFoundError
的影子了。
至此,真实异常消失无踪。
这也给我们一个提醒,我们要保证异常的时候,一定要保留有效信息,否则,排错会非常麻烦。
本文是抓虫文,从问题出发,到解决问题,给出完整的思路。java.lang.NoClassDefFoundError
一般都是出现在版本冲突的时候,这种异常是编译打包没有问题,在运行时加载类失败。在本文中之所以排查时走了一些弯路,是因为Spring
隐藏了真实异常,给我们排错造成了一些阻碍。所以,我们在日常开发时也要重视异常的明确信息,可以给我们排错提供准确的目标。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Mybatis-Plus 版本冲突触发“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”的 java.lang.NoClassDefFoundError 异常
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Mybatis-Plus 版本冲突触发“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”的 java.lang.NoClassDefFoundError 异常