技术白皮书:现代企业架构设计

【注】本文节译自:APIs and microservices: How to create modern enterprise architectures (bitpipe.com)

    创建和发展现代化的企业架构并非易事:这意味着打破单一的、集中的系统,转而使用 API 和微服务等工具支持更多云功能和自适应环境。在本指南中,我们研究了这些类型的云连接架构的关键属性,并提供了有助于培养以 API 为中心的现代架构的实用建议。接下来,我们来探讨一下渐进式 Web 应用程序的出现。
    云应用程序被认为是一种易于部署、修改、扩展和计量的服务。这为现代企业架构和应用程序设计须如何发展提供了线索。这就是为什么,以及未来是什么样子。
    云服务本身建立在高度分布式的基础架构上,旨在对故障具有弹性,并且在不影响用户的情况下轻松移动和调整大小。使用应用程序接口或 API 访问和控制抽象服务,将用户与实现细节完全隔离。用户几乎不知道或不关心服务在哪里运行或在什么类型的系统上运行。这些核心属性巩固了云服务相对于传统单体软件的众多优势。
    然而,这些相同的特性可以而且应该应用于构建在一个或多个云上的企业应用程序。事实上,使用共享、按需、可伸缩的云服务的细粒度微服务设计是任何现代企业架构的虚拟需求,这些架构期望满足数字业务的需求,服务于数百万移动客户、智能机器和连接的传感器。

互联的数字企业

    企业架构必须改变的原因是高速连接和数十年来计算能力指数级摩尔定律改进的融合。这使得廉价智能手机市场趋于饱和,公用事业规模的 IT 服务提供商得以创建云服务。这些技术共同推动了业务的巨大变化。无论您将其称为“新连接经济学 ”(Gartner) 还是“无界企业 ”(AT&T Bell Labs),它都意味着企业以及随之而来的 IT 系统和应用程序将越来越多地与人交互,还将以自动化业务流程和智能设备的形式与设备、虚拟对象和其他软件交互。这是一种交互的爆炸式增长,Gartner 称之为数字网格。
    这对企业架构的影响变得更像是大型云服务——想想谷歌、Facebook 和 AWS——而不是使用单体的、集中的系统来处理不断增长和越来越不可预测的工作负载。事实上,随着双模 IT、DevOps 和敏捷开发等概念的引入以加快数字业务创新的步伐,其影响将远远超出企业架构,深入到 IT 如何开展业务的核心。但是我们这里的重点是体系结构框架。

处理“连通性”

    但是,如果组织选择实施它,演进的现代企业架构将类似于云。应用程序是否最终运行在 AWS 和 Azure 等共享的公共服务上;基于 Azure Stack、OpenStack 或 vCloud 等软件构建的私有基础设施;或者两者结合,架构必须假定共享的可用性,即使仅在组织内部,计量软件服务也可以立即实例化、修改、扩展和互连。随着组织构建的数字服务看起来更像 Facebook 和 Uber,应用架构必须类似于云原生服务,而不是隔离在单个盒子上的单体系统。
    现代架构应具有以下关键属性:

  • 将客户端接口与业务服务分离:应用程序将采用移动和浏览器设计范式,客户端专门用于 UI 和后端服务的业务处理。
  • 天然分布式:后端应用程序将能够运行多个实例,提供弹性和可扩展性,同时支持持续、敏捷开发流程所需的无中断更新。
  • 使用微服务:后端服务将针对特定功能而设计。它还可以在许多消费者之间共享,并可以轻松链接到特定应用程序的服务链中。将微服务视为用于构建任意复杂功能的构件。服务可以在自己的 VM 中隔离运行,也可以作为共享操作系统实例的容器运行。它们还具有固有的自计量功能,以支持基于消费的计费、细粒度的性能跟踪和自动扩展。
  • 异步服务间通信:服务将使用消息总线交换信息,而不是到网络套接字和文件共享的持久连接。
  • 丰富的服务组合:服务将公开元数据,允许它们作为广泛服务组合的一部分进行管理、使用和编排,包括移动和物联网应用后端、数据存储和分析、消息传递、监控和安全。
  • 应用程序模板和设计模式库:
        该组合是模板的基础,这些模板将服务组合在一起,以解决各种应用开发需求和设计模式,比如 Azure 提供的。开发人员库还包括标准数据模型,用于选择合适的数据服务,如对象存储、NoSQL、SQL、大数据,例如 Hadoop 和 Spark;以及满足特定需求的数据转换管道。
  • 以 API 为中心:API 是主要的,通常也是唯一的服务接口。 客户端访问通常是无状态的,但任何状态都通过消息总线和后端数据服务处理,并且只有最少的客户端支持。
  • 自动化基础设施:服务链可以从用于部署、扩展、移动和停用云实例的基础设施模板中实例化。 服务计量和仪表用于对资源使用情况发出警报、活动、错误状态、并触发自动修复,例如缩放、重启和人工通知。
  • 细粒度的安全性:对单个服务的访问通过基于角色的访问控制列表进行控制,策略定义为应用程序模板的一部分。 用户身份和角色被集中管理,并可能与来自业务合作伙伴或公共在线服务的外部身份管理系统联合。

    待办事项清单

        拥有正确的企业架构可能是未来数字业务计划成败的关键。 对于 CIO 和 IT 高管来说,这意味着架构应该是执行层的优先级,它是通过 IT、应用程序开发人员和业务线经理之间的密切协作开发。 对于 IT 经理和技术专业人士来说,企业架构的重要性需要一个由多学科专家组成的专门团队——例如 IT 运营、AppDev、DevOps、云运营和数据科学家等——他们的重点是开发、改进、更新和实施架构。
        不断发展的企业架构是不断发展的 IT 的一部分,它必须专注于灵活的服务创新、开发和实施,以响应动态的数字业务需求。因此,它可能会伴随 IT 内部的结构和文化变化,例如双模式和 DevOps 组织以及持续交付流程。对于 IT 专业人员来说,这是最具挑战性但又最激动人心的时刻之一。

    在网页和原生应用之间架起桥梁

        移动应用和网站在用户体验方面历来采用不同的模式。让用户访问网站比安装应用程序要容易得多。但移动应用往往会促进更具吸引力的用户体验。一种全新的全渠道应用交付模式,称为渐进式 Web 应用,有望提供两全其美的服务,并且是旧金山 O’Reilly Fluent 会议的热门话题。
        谷歌 Chrome 团队的一名资深软件工程师 亚历克斯·拉塞尔(Alex Russell)表示,他们之所以选择“渐进式 Web 应用”这个词,是因为它们可以在标签中开始生活,并逐渐变得像应用程序一样。有几个因素正在推动这一趋势,包括更好的缓存技术和使 Web 应用的行为更像移动应用的新技术。
        一些先驱企业已经证明了使用新模式有利于增加用户访问量和在网站上花费的时间。Flipkart 开始实施渐进式应用,并且发现用户花在新网站上的时间增加了三倍,每周访问者增长 40%。他们还发现 63% 转化率来自主界面访问者,他们甚至还没有发布推送通知。

    解决分布式问题

        “我认为分发是软件中最困难的问题,”Russell 说。在早期,用户必须从软盘安装应用程序。现在,让用户安装应用程序要容易得多,但要让用户主屏上花费更多的精力和空间,仍然有很多阻力。网络是最便捷的分发平台,但令人惊讶的是,我们还没有转向通过 URL 分发移动应用。Web 应用程序可以更定期地更新,也可以通过单击链接进行更新,开发无需通过应用商店的审批流程。
        但根据 comScore Inc. 的研究,用户往往会将大约 87% 的时间花在移动应用上,而花在智能手机上的移动 Web 应用上的时间只占 13%。另一方面,大多数用户将大部分时间花在顶级应用上,而其他人则处于休眠状态。由于这些趋势,企业最终会花费大量资金让用户安装其软件的移动应用版本。许多这些应用最终成为永远不会被使用的僵尸应用。这需要付出很大代价,因为其中许多应用都无法进入顶层。
        与此同时,移动网站也吸引了许多未安装该应用的独立访问者。这对于应用使用较少的企业(例如零售商)很重要。用户平均每月使用 27 个应用,但访问超过 100 个移动网站。Russell 表示,用户对下载原生移动应用的空间、带宽和时间感到焦虑。

    超越技术

        一些处理密集型应用可能会像原生移动一样表现得更好。但 Russell 表示,他相信移动网络平台对于当今的大多数应用来说已经足够了。他说,阻碍移动网络应用使用的三个主要因素是缺少主屏、缺少推送通知托盘访问和缺少离线访问。

    主屏访问

        Chrome 团队最近发布了 Web 应用清单(Web App Manifest)规范。早期版本可在所有 Android 平台上运行。这提供了一个框架,用于告诉移动浏览器某个特定链接实际上是一个移动应用。 这样可以更轻松地将站点作为单独的窗口重新启动,该窗口的外观和行为类似于其他本机应用程序。
        开发人员只需在站点上包含一个清单文件,其中包含一个指向图标和应用名称的链接。Russell 说这是必要的,因为 Web 还没有应用结构的概念。直觉上,人们知道博客与博客中的条目是不同的。清单提供了一种向浏览器显式声明这种区别的方法。

    突破新极限

        去年,Chrome 团队在 Chrome 桌面版和 Android 版上推出了推送通知。 这使得 Web 应用程序可以更轻松地将更新定向到推送通知托盘中。 此功能也可用于三星和 Firefox 浏览器,并生成行为与本机应用程序非常相似的通知——即使在浏览器关闭时也是如此。 用户必须提供从特定站点接收通知的权限。
        这种策略对零售商的网站很有效,例如,他们可能想向访问商店的用户推出优惠券。它将允许用户接收这些更新,而无需在主屏上安装任何应用程序。零售商 Beyond The Rack 报告称,其 50% 的访客来自发布推送通知。他们还发现在网站上花费的时间和转化率有所增加。

    使用 Service worker 进行更好的缓存

        最后一个阈值是支持有效的离线功能。谷歌多年来一直致力于解决这个问题。最初,Google Gears 允许通过一组狭窄的方式来考虑打造离线体验。应用程序缓存与现在被纳入 HTML5 规范的设计相同。
        谷歌现在引入了服务工作者的概念。这种方法允许开发人员指定服务工作者在移动设备离线或在线时的行为方式。Russell 说离线只是网络连接不稳定的一个特例。这种方法允许应用程序提供令人满意的用户体验,而不管其连通性如何。应用程序外壳可以在重复访问时立即缓存和加载。“现在,有了 Service Worker,你可以在没有 DNS、http 或 TLS 辅助的情况下将内容投放到屏幕上,”Russell 说。个人、网络广播、播客、视频、虚拟贸易展览、研究报告等——利用技术提供商的丰富研发资源来应对市场趋势、挑战和解决方案。我们的现场活动和虚拟研讨会为您能够就您每天面临的问题和挑战获得中立的供应商评论、专家评论和建议。我们的社交社区 IT 知识交流使您可以与同行和专家实时共享现实世界的信息。

Java 异常机制

【注】本文译自:Java Exceptions – DZone Java

Java Exception

    Java Exception 是为处理异常应用程序行为而创建的类。在本文中,我将解释如何使用 Java Exception 类以及如何在考虑现有 Java Exceptions 设计的情况下创建异常结构。Java 异常概念是 Java 中的重要里程碑之一,每个开发人员都必须了解它。

Java 异常结构比你想象的要有用

    Java 异常的结构非常有用,可以告诉开发人员一组重要的事情(如果开发人员正确使用此结构)。所以,在这里,您可以看到基本结构:

    可以捕获所有可能情况的主要父类是 Throwable,它有 2 个子类:Error 和 Exception。

Java Error

    Java Error 代表异常情况。一旦出现错误,应用程序可能会关闭。

Java Exception

    与错误不同,Java 异常有机会从问题中恢复应用程序,并尝试保持应用程序运行。异常也分为两类:

    异常由运行时和非运行时异常表示,也称为已检异常。此分类与错误异常非常相似,但在该分类中,已检异常在恢复方面更为乐观。

已检和未检异常

    在 Java 中,有两种类型的异常。已检异常迫使开发人员创建处理程序异常或重新抛出它们。如果重新抛出已检查的异常,则 java 函数必须在其签名中声明它。未检异常 unline checked 不需要任何处理。 这样的设计意味着无法处理未经检查的异常,并且注定会被抛出到顶级父类。

Error 异常调查

    有两种方法可以处理抛出的异常:在当前方法中处理它或者只是重新抛出它。没有比这更好的方法了:您可能有一个父处理程序或以某种方式处理它,例如创建重试逻辑。

好的、坏的、丑的

    介绍完之后,我们可以将所有异常分为 3 组:Checked、Runtime 和 Error。主要思想是,他们每个人都会陷入不同的情况。最乐观的是 Checked 异常。运行时将属于恢复机会很小的情况。而且,最悲观的是Error。

Checked, Runtime, Error … 然后呢?

    了解异常类的类型后,我们可能会回答下一个问题:

  • 情况有多糟糕,问题的原因是什么。
  • 如何解决这个问题。
  • 我们需要重启 JVM 吗?
  • 我们需要重写代码吗?
        知道异常类,我们可以预测可能出错的地方。考虑潜在的原因,我们可以假设问题的原因是什么以及如何解决它。让我们回顾一下最流行的场景,看看这些异常可以告诉我们什么。在接下来的段落中,我们将回顾著名的异常并调查潜在的代码是什么。在我们的调查中,我们假设应用程序足够稳定并且开发阶段已经完成和测试。

Error 异常调查

    我们从最悲观的案例或我们的丑男开始。Error 真的有那么丑吗? 让我们来看看最常见的 Java 错误:

    因此,在大多数情况下,您需要做的就是更改 JVM 配置或添加缺少的依赖项。仍然存在需要更改代码的情况,但它们不太可能在每种情况下应用更改。

Checked 异常调查

    对于受检异常,我们期望有机会恢复问题;例如,再试一次。在这一部分,我们回顾最著名的已检异常。提供的例外可能是彼此的父类,但是,在这里,我只列出最常见的案例,而不关心它们的关系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gV698acA-1634022073043)(https://i.ibb.co/JCRS30f/30-2.png)]
    好吧,有很多异常,但是,正如我所承诺的,我把最常见的异常放在这里。那么,这张表说明了什么?如果我们查看最可能的原因,我们会发现其中的大多数不仅不需要任何代码更改,甚至不需要重新启动应用程序。所以,显然,已检异常应该是好人。

Runtime 异常调查

    最常见也是个人最悲观的例外:运行时。Checked 和 Error 异常错误不会导致任何代码更改。但是,在大多数情况下,运行时异常突出了代码中的真正问题,如果不重写代码就无法修复这些问题。让我们通过查看最流行的运行时异常来找出原因:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZHw0QsNQ-1634022073045)(https://i.ibb.co/SJb6tTX/30-3.png)]
    一个例子可能给人的印象是任何运行时异常都会导致应用程序失败。在大多数情况下,这是正确的,因为不更改代码就无法恢复应用程序。最终,运行时异常是我们的坏人,它会导致新的代码更改、开发人员的压力和业务损失。

些许批评

    在这次审查期间,我们做出了一个重大假设:代码已准备好投入生产并经过充分测试。但是,在实践中,这很难实现。所以,我们所做的结论并不是100% 可靠,但是代码越稳定,结果就越真实。

已检异常和代码污染

    根据已检查异常,设计开发人员必须使所有可恢复的异常都是可检查的。因此,每次调用带有已检查异常签名的方法都会为 Try Catch 结构添加 3-4 行。这种方法使代码变得丑陋且可读性较差。就个人而言,我更喜欢使用运行时异常。即使在设计库的情况下,您仍然可以在方法签名中保留运行时异常,并在 API 中添加一些注释。在这种情况下,您的 API 用户将能够决定如何处理它。

DataOps(数据运维)指南 – 数据管理的新时代

【注】本文译自:
A Guide to DataOps – DZone Big Data

DataOps 不仅仅是另一种开发方法。它通过民主化的访问和巨大的潜力从根本上改变了组织使用数据的方式。

    最近一项关于企业面临的大数据挑战的调查揭示了一些有关数据利用的惊人事实。38% 的企业“缺乏”令人信服的商业案例来使用他们的数据。34% 的公司没有足够成熟的流程来处理大数据技术,其中 24% 的公司无法为最终用户提供大数据!

    说这些发现令人震惊是轻描淡写。如果调查结果属实,那么很大一部分企业不知道他们可以做什么——他们必须做什么——利用他们拥有的数据,并继续从客户那里收集数据。与竞争对手相比,这使他们处于严重劣势。

    在数据驱动的竞争格局中,忽视数据的好处,甚至无法充分发挥其潜力,对组织来说只会意味着灾难性的结局。可以肯定的是,其中许多组织正在收集大量数据。他们只是不想、不知道或没有适当的流程来使用它。

    部分问题是遗留数据管道。随着数据在数据管道中从源移动到目标,每个阶段对数据的含义以及如何使用它都有自己的想法。这种不连贯的数据视图使数据管道变得脆弱且难以改变,从而使组织在面对变化时反应迟缓。

    应对这一挑战的解决方案是 DataOps。

什么是DataOps(数据运维)?

    DataOps 是 data operationalization(数据操作化)的缩写,是一种协作数据管理方法,强调组织内数据管道的通信、集成和自动化。

    与数据存储管理不同,DataOps 并不主要关注“存储”数据。它更关注“交付”,即让所有利益相关者都可以轻松获得、访问和使用数据。它的目标是创建可预测的数据、数据模型和相关工件的交付和变更管理,以便在整个组织和消费者中更快地交付价值。

    DataOps 通过采用技术将数据的设计、部署、管理和交付自动化来实现这一目标,以改进其使用和提供的价值。这使所有使用数据的利益相关者都可以轻松访问数据,并加快数据分析的周期时间。

    通过这样做,DataOps 大大提高了组织对市场变化的响应时间,并使他们能够更快地应对挑战。

DataOps 解决的挑战和问题

    大数据最重要的承诺——快速且可靠的数据驱动的可操作业务洞察——仍未实现,因为存在众多挑战,这些挑战可大致分为组织、技术和人员(使用数据的人)的挑战。

    DataOps 通过结合来自敏捷、DevOps 和精益制造方法的学习和实践,帮助克服这些挑战。以下是 DataOps 所要应对的最重要挑战:

  • 速度

    现代组织依赖(至少必须依赖)来自许多不同来源和许多不同形式的数据。清理、改进和使用数据可能是一个如此复杂和漫长的过程,以至于当最终从中产生洞察力时,它们与快速发展的业务环境不再相关。

    DataOps 从根本上提高了从数据中获取洞察力的速度。

  • 数据类型

    有时,组织收集的数据可能是非结构化格式,这使得从中提取见解变得极其困难。此类数据源完全有可能甚至有可能为新兴业务挑战提供线索。因此,仅仅使用易于处理的结构化数据是不够的。

    DataOps 使组织能够识别、收集和使用来自每个可用数据源的数据。

  • 数据孤岛

    DataOps 打破组织内的数据孤岛并集中所有数据。同时,它构建了弹性系统,为每个需要访问数据的利益相关者提供自助服务。这些系统随着组织内外的变化而发展,并且为“数据用户”提供了可预测的方式来查找和使用他们需要的数据。

DataOps 的业务优势

    通过克服挑战,DataOps 使 DataOps 团队能够将数据交付给需要它的人——数据工程师、数据科学家、ML 工程师,甚至客户——并且速度比以前快得多。这一成就为数据驱动型企业带来了多项好处,其中包括:

  • 最大限度地利用数据

    DataOps 为所有数据“用户”解锁数据,无论是分析师、高管,还是客户。它使数据交付自动化,并在此过程中允许每个部门从数据中提取最大价值。 结果是提高了竞争力、对变化的响应能力和更高的投资回报率。

  • 在正确的时间获得正确的见解

    迄今为止,大数据的一个常见问题是在错误的时间获得正确的见解。来得太晚的见解是无用的。DataOps 将数据快速提供给需要它的每个人。因此,他们可以比以往任何时候都更快地做出更明智的决策,使组织能够快速发展以适应市场变化。

  • 提高数据生产力

    DataOps 使用自动化工具将数据交付作为自助服务进行操作。因此,消除了数据请求和数据访问之间的任何固有延迟,从而使所有团队能够迅速做出数据驱动的决策。

    DataOps 还摆脱了手动数据管道变更管理流程的组织。相反,对数据管道的所有更改都经过简化和自动化,以提供快速、有针对性的更改。

  • 针对结果优化的数据管道

    DataOps 在数据管道中加入了一个反馈循环,允许各种数据消费者识别他们需要的特定数据并从中获得定制的见解。然后,每个团队都可以使用这些洞察来降低成本、发现新机会、增加收入并提高组织的盈利能力。

DataOps 的原则

    在技术方面,DataOps 实现了组织最具开创性的里程碑之一——使他们的数据程序具有高度可扩展性,而不会影响数据分析的速度或质量。 因为它借鉴了 DevOps 的经验教训和实践,所以 DataOps 在许多关键方面与前者重叠。这在 DataOps 的三个基本原则中可见:

  • 持续集成

    DataOps 动态地识别、整理、集成和提供来自各种来源的数据。当团队为 DataOps 团队添加新数据源进行处理时,新数据会自动集成到数据管道中,并使用 AI/ML 工具提供给各个利益相关者。

    由于自动化,从数据发现到数据管理、转换和洞察定制的所有内容都得到了完全简化。实际上,可以将数据以实时流的形式直接传输到预测算法,以便向用户尤其是消费者,提供实时的见解。

    这种优化的数据集成过程可确保在数据发现和数据利用之间不会浪费时间。

  • 持续交付

    组织数据的价值取决于从中产生的见解。访问它的团队越多,从中提取的见解就越多。然而,数据可访问性也伴随着数据治理挑战。DataOps 在整个组织内实施数据治理,同时使数据可访问性民主化并增强其安全性和隐私性。

    数据以协作方式有目的地交付给内部和外部数据消费者,同时符合内部数据质量和数据屏蔽规则。通常,使用“智能”数据平台来实现这一目标。当数据的质量、隐私和安全得到保证时,各种利益相关者可以使用它来获得准确的见解,而不必担心数据治理的影响。

  • 持续部署

    数字企业依靠一系列数据驱动的应用程序来实时做出功能决策,这对组织的未来具有深远的影响。关键任务功能,如欺诈检测、AI(人工智能)聊天机器人、销售、供应链管理等,需要随时可用的最新数据来进行决策。持续部署使所有用户都能无缝访问新数据。

DevOps 对比 DataOps

    虽然 DataOps 借鉴了 DevOps 的知识和操作流程,但两者之间存在显著差异。列举如下:

  • 人为因素

    尽管 DataOps 参与者可能精通技术,但他们更专注于为数据用户创建算法、模型和视觉辅助工具。另一方面,DevOps 参与者是具有操作思维的软件工程师。

  • 流程

    DataOps 流程的特点是数据管道和分析开发编排,而 DevOps 流程几乎不涉及编排。

  • 测试

    与 DevOps 不同的是,DataOps 严重依赖数据屏蔽来进行测试,因此,测试数据管理变得至关重要。此外,DataOps 通常在部署之前在数据管道和分析开发过程中测试和验证数据。

  • 工具

    DevOps 拥有成熟的工具生态系统,尤其是测试工具。DataOps 是一种新方法,通常需要团队从头开始构建工具或根据其目的修改 DevOps 工具。

DataOps 平台的演进

    在数据分析的早期,ETL(提取、转换、加载)工具成为管理大量(相对而言)传入数据的强大工具。然而,随着传入数据的多样性、准确性和数量激增,对可扩展性和高速数据分析的需求变得更加迫切。数据连接器固有的缺陷也被证明是一个限制因素。

    云的出现将解决数据摄取、管理和分析的挑战。当 ETL 工具与云资源结合时,它加快了分析速度。然而,一个日益严峻的挑战仍然存在——数据可访问性。仅仅使用数据来产生见解是不够的; 每个人都应该能够获得这些见解。

    于是,DataOps 诞生了!

    DataOps 实现了数据访问的大众化。所有利益相关者都可以访问受组织数据治理政策约束的安全、高质量的数据,而不是少数人有权访问数据。

Java 17 与 Java 11 相比有什么变化?

【注】本文译自: What’s New Between Java 11 and Java 17?

    9 月 14 日 Java 17 发布。是时候仔细看看自上一个 LTS 版本(即 Java 11)以来的变化。我们先简要介绍许可模型,然后重点介绍 Java 11 和 Java 17 之间的一些变化,主要是通过例子。享受吧!

1. 介绍

    首先,让我们仔细看看 Java 许可和支持模型。Java 17 是一个 LTS(长期支持)版本,就像 Java 11 一样。Java 11 开始了一个新的发布节奏。Java 11 支持到 2023 年 9 月,扩展支持到 2026 年 9 月。此外,在 Java 11 中,Oracle JDK 不再免费用于生产和商业用途。每 6 个月发布一个新的 Java 版本,即所谓的非 LTS 发布,从 Java 12 直至并包括 Java 16。但是,这些都是生产就绪版本。与 LTS 版本的唯一区别是支持在下一个版本发布时结束。例如。 Java 12 的支持在 Java 13 发布时结束。当您想要保持支持时,您或多或少必须升级到 Java 13。当您的某些依赖项尚未为 Java 13 做好准备时,这可能会导致一些问题。大多数情况下,对于生产用途,公司将等待 LTS 版本。但即便如此,一些公司也不愿意升级。最近 Snyk 的一项调查显示,只有 60% 的人在生产中使用 Java 11,而这距离 Java 11 发布已经过去了 3 年!60% 的公司仍在使用 Java 8。另一个值得注意的有趣事情是,下一个 LTS 版本将是 Java 21,它将在 2 年内发布。关于库在 Java 17 是否存在问题的一个很好的概述,可以在此处找到。

    随着 Java 17 的推出,Oracle 许可模式发生了变化。Java 17 是根据新的 NFTC(Oracle 免费条款和条件)许可发布的。因此,再次允许免费将 Oracle JDK 版本用于生产和商业用途。在同一个 Snyk 调查中,有人指出 Oracle JDK 版本在生产环境中仅被 23% 的用户使用。请注意,对 LTS 版本的支持将在下一个 LTS 版本发布一年后结束。看看这将如何影响升级到下一个 LTS 版本将会很有趣。

    Java 11 和 Java 17 之间发生了什么变化?可以在 OpenJDK 网站上找到 JEP(Java 增强提案)的完整列表。在这里,您可以阅读每个 JEP 的详细信息。 有关自 Java 11 以来每个版本更改的完整列表,Oracle 发行说明提供了一个很好的概述。

    在接下来的部分中,将通过示例解释一些更改,但主要取决于您对这些新功能进行试验以熟悉它们。这篇文章中使用的所有资源都可以在 GitHub 上找到。

    最后一件事是 Oracle 发布了 dev.java,所以不要忘记看一下。

2. Text Blocks(本文块)

    为了使 Java 更具可读性和更简洁,已经进行了许多改进。文本块无疑使代码更具可读性。首先,我们来看看问题。假设您需要一些 JSON 字符串到您的代码中并且您需要打印它。这段代码有几个问题:

  • 双引号的转义;
  • 字符串连接,使其具有或多或少的可读性;
  • JSON 的复制粘贴是一项劳动密集型的工作(您的 IDE 可能会帮助您解决该问题)。
    private static void oldStyle() {
        System.out.println("""
                *************
                * Old Style *
                *************""");
        String text = "{\n" +
                      "  \"name\": \"John Doe\",\n" +
                      "  \"age\": 45,\n" +
                      "  \"address\": \"Doe Street, 23, Java Town\"\n" +
                      "}";
        System.out.println(text);
    }

    上面代码的输出是格式良好的 JSON。

{
    "name": "John Doe",
    "age": 45,
    "address": "Doe Street, 23, Java Town"
}

    文本块用三个双引号定义,其中结尾的三个双引号不能与起始的在同一行。首先,只需打印一个空块。为了可视化发生了什么,文本被打印在两个双管之间。

    private static void emptyBlock() {
        System.out.println("""
                ***************
                * Empty Block *
                ***************""");
        String text = """
                """;
        System.out.println("|" + text + "|");
    }

    输出是:

||||

有问题的 JSON 部分现在可以写成如下,这样可读性更好。不需要转义双引号,它看起来就像会被打印。

    private static void jsonBlock() {
        System.out.println("""
                **************
                * Json Block *
                **************""");
        String text = """
                {
                  "name": "John Doe",
                  "age": 45,
                  "address": "Doe Street, 23, Java Town"
                }
                """;
        System.out.println(text);
    }

    输出当然是相同的。

{
    "name": "John Doe",
    "age": 45,
    "address": "Doe Street, 23, Java Town"
}

    在前面的输出中,没有前面的空格。但是,在代码中,前面有空格。如何确定剥离前面的空格? 首先,将结尾的三个双引号向左移动更多。

    private static void jsonMovedBracketsBlock() {
        System.out.println("""
                *****************************
                * Json Moved Brackets Block *
                *****************************""");
        String text = """
                  {
                    "name": "John Doe",
                    "age": 45,
                    "address": "Doe Street, 23, Java Town"
                  }
                """;
        System.out.println(text);
    }

    输出现在在每行之前打印两个空格。这意味着结尾的三个双引号表示文本块的开始。

{
    "name": "John Doe",
    "age": 45,
    "address": "Doe Street, 23, Java Town"
}
123

    当你将结尾的三个双引号向右移动时会发生什么?

    private static void jsonMovedEndQuoteBlock() {
        System.out.println("""
                ******************************
                * Json Moved End Quote Block *
                ******************************""");
        String text = """
                  {
                    "name": "John Doe",
                    "age": 45,
                    "address": "Doe Street, 23, Java Town"
                  }
                       """;
        System.out.println(text);
    }

    前面的间距现在由文本块中的第一个非空格字符决定。

{
    "name": "John Doe",
    "age": 45,
    "address": "Doe Street, 23, Java Town"
}

3. Switch 表达式

    Switch 表达式将允许您从 switch 返回值并在赋值等中使用这些返回值。此处显示了一个经典的 switch,其中,根据给定的 Fruit 枚举值,需要执行一些操作。故意忽略了 break。

    private static void oldStyleWithoutBreak(FruitType fruit) {
        System.out.println("""
                ***************************
                * Old style without break *
                ***************************""");
        switch (fruit) {
            case APPLE, PEAR:
                System.out.println("Common fruit");
            case ORANGE, AVOCADO:
                System.out.println("Exotic fruit");
            default:
                System.out.println("Undefined fruit");
        }
    }

    使用 APPLE 调用该方法。

oldStyleWithoutBreak(Fruit.APPLE);

    这将打印每个 case,因为没有 break 语句,case 就失效了。

Common fruit
Exotic fruit
Undefined fruit

    因此,有必要在每个 case 中添加一个 break 语句,以防止这种失效。

    private static void oldStyleWithBreak(FruitType fruit) {
        System.out.println("""
                ************************
                * Old style with break *
                ************************""");
        switch (fruit) {
            case APPLE, PEAR:
                System.out.println("Common fruit");
                break;
            case ORANGE, AVOCADO:
                System.out.println("Exotic fruit");
                break;
            default:
                System.out.println("Undefined fruit");
        }
    }

    运行此方法会为您提供所需的结果,但现在代码的可读性稍差。

Common fruit

    这可以通过使用 Switch 表达式来解决。用箭头 (->) 替换冒号 (:) 并确保在大小写中使用表达式。Switch 表达式的默认行为是没有失败,因此不需要 break。

    private static void withSwitchExpression(FruitType fruit) {
        System.out.println("""
                **************************
                * With switch expression *
                **************************""");
        switch (fruit) {
            case APPLE, PEAR -> System.out.println("Common fruit");
            case ORANGE, AVOCADO -> System.out.println("Exotic fruit");
            default -> System.out.println("Undefined fruit");
        }
    }

    这已经不那么啰嗦了,结果是相同的。

    Switch 表达式也可以返回一个值。在上面的示例中,您可以返回 String 值并将它们分配给变量 text。在此之后,可以打印 text 本变量。不要忘记在最后一个案例括号后添加一个分号。

    private static void withReturnValue(FruitType fruit) {
        System.out.println("""
                *********************
                * With return value *
                *********************""");
        String text = switch (fruit) {
            case APPLE, PEAR -> "Common fruit";
            case ORANGE, AVOCADO -> "Exotic fruit";
            default -> "Undefined fruit";
        };
        System.out.println(text);
    }

    而且,更短的是,上面的内容可以用一个语句重写。这是否比上面的更具可读性取决于您。

    private static void withReturnValueEvenShorter(FruitType fruit) {
        System.out.println("""
                **********************************
                * With return value even shorter *
                **********************************""");
        System.out.println(
            switch (fruit) {
                case APPLE, PEAR -> "Common fruit";
                case ORANGE, AVOCADO -> "Exotic fruit";
                default -> "Undefined fruit";
            });
    }

    当您需要在 case 中做不止一件事情时,您会怎么做? 在这种情况下,您可以使用方括号来表示 case 块,并在返回值时使用关键字 yield。

    private static void withYield(FruitType fruit) {
        System.out.println("""
                **************
                * With yield *
                **************""");
        String text = switch (fruit) {
            case APPLE, PEAR -> {
                System.out.println("the given fruit was: " + fruit);
                yield "Common fruit";
            }
            case ORANGE, AVOCADO -> "Exotic fruit";
            default -> "Undefined fruit";
        };
        System.out.println(text);
    }

    输出现在有点不同,执行了两个打印语句。

the given fruit was: APPLE
Common fruit

    您可以在“旧” switch 语法中使用 yield 关键字也很酷。这里不需要 break。

    private static void oldStyleWithYield(FruitType fruit) {
        System.out.println("""
                ************************
                * Old style with yield *
                ************************""");
        System.out.println(switch (fruit) {
            case APPLE, PEAR:
                yield "Common fruit";
            case ORANGE, AVOCADO:
                yield "Exotic fruit";
            default:
                yield "Undefined fruit";
        });
    }

4. Records(记录)

    Records 将允许您创建不可变的数据类。目前,您需要例如 使用 IDE 的自动生成函数创建 GrapeClass 以生成构造函数、getter、hashCode、equals 和 toString,或者您可以使用 Lombok 达到同样的目的。最后,您会得到一些样板代码,或者您的项目最终会依赖 Lombok。

public class GrapeClass {

    private final Color color;
    private final int nbrOfPits;

    public GrapeClass(Color color, int nbrOfPits) {
        this.color = color;
        this.nbrOfPits = nbrOfPits;
    }

    public Color getColor() {
        return color;
    }

    public int getNbrOfPits() {
        return nbrOfPits;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GrapeClass that = (GrapeClass) o;
        return nbrOfPits == that.nbrOfPits && color.equals(that.color);
    }

    @Override
    public int hashCode() {
        return Objects.hash(color, nbrOfPits);
    }

    @Override
    public String toString() {
        return "GrapeClass{" +
                "color=" + color +
                ", nbrOfPits=" + nbrOfPits +
                '}';
    }

}

    使用上述 GrapeClass 类执行一些测试。创建两个实例,打印它们,比较它们,创建一个副本并也比较这个。

    private static void oldStyle() {
        System.out.println("""
                *************
                * Old style *
                *************""");
        GrapeClass grape1 = new GrapeClass(Color.BLUE, 1);
        GrapeClass grape2 = new GrapeClass(Color.WHITE, 2);
        System.out.println("Grape 1 is " + grape1);
        System.out.println("Grape 2 is " + grape2);
        System.out.println("Grape 1 equals grape 2? " + grape1.equals(grape2));
        GrapeClass grape1Copy = new GrapeClass(grape1.getColor(), grape1.getNbrOfPits());
        System.out.println("Grape 1 equals its copy? " + grape1.equals(grape1Copy));
    }

    测试的输出是:

Grape 1 is GrapeClass{color=java.awt.Color[r=0,g=0,b=255], nbrOfPits=1}
Grape 2 is GrapeClass{color=java.awt.Color[r=255,g=255,b=255], nbrOfPits=2}
Grape 1 equals grape 2? false
Grape 1 equals its copy? true

    GrapeRecord 具有与 GrapeClass 相同的功能,但要简单得多。您创建一个记录并指出字段应该是什么,然后您就完成了。

record GrapeRecord(Color color, int nbrOfPits) {
}

    一个记录可以在它自己的文件中定义,但是因为它非常紧凑,所以在需要的地方定义它也是可以的。上面用记录重写的测试变成如下:

    private static void basicRecord() {
        System.out.println("""
                ****************
                * Basic record *
                ****************""");
        record GrapeRecord(Color color, int nbrOfPits) {}
        GrapeRecord grape1 = new GrapeRecord(Color.BLUE, 1);
        GrapeRecord grape2 = new GrapeRecord(Color.WHITE, 2);
        System.out.println("Grape 1 is " + grape1);
        System.out.println("Grape 2 is " + grape2);
        System.out.println("Grape 1 equals grape 2? " + grape1.equals(grape2));
        GrapeRecord grape1Copy = new GrapeRecord(grape1.color(), grape1.nbrOfPits());
        System.out.println("Grape 1 equals its copy? " + grape1.equals(grape1Copy));
    }

    输出与上面相同。重要的是要注意记录的副本应该以相同的副本结束。添加额外的功能,例如 grape1.nbrOfPits() 为了做一些处理并返回与初始 nbrOfPits 不同的值是一种不好的做法。虽然这是允许的,但您不应该这样做。

    构造函数可以通过一些字段验证进行扩展。请注意,将参数分配给记录字段发生在构造函数的末尾。

    private static void basicRecordWithValidation() {
        System.out.println("""
                ********************************
                * Basic record with validation *
                ********************************""");
        record GrapeRecord(Color color, int nbrOfPits) {
            GrapeRecord {
                System.out.println("Parameter color=" + color + ", Field color=" + this.color());
                System.out.println("Parameter nbrOfPits=" + nbrOfPits + ", Field nbrOfPits=" + this.nbrOfPits());
                if (color == null) {
                    throw new IllegalArgumentException("Color may not be null");
                }
            }
        }
        GrapeRecord grape1 = new GrapeRecord(Color.BLUE, 1);
        System.out.println("Grape 1 is " + grape1);
        GrapeRecord grapeNull = new GrapeRecord(null, 2);
    }

    上述测试的输出向您展示了此功能。 在构造函数内部,字段值仍然为 null,但在打印记录时,它们被分配了一个值。验证也做它应该做的事情,并在颜色为 null 时抛出 IllegalArgumentException。

Parameter color=java.awt.Color[r=0,g=0,b=255], Field color=null
Parameter nbrOfPits=1, Field nbrOfPits=0
Grape 1 is GrapeRecord[color=java.awt.Color[r=0,g=0,b=255], nbrOfPits=1]
Parameter color=null, Field color=null
Parameter nbrOfPits=2, Field nbrOfPits=0
Exception in thread "main" java.lang.IllegalArgumentException: Color may not be null
    at com.mydeveloperplanet.myjava17planet.Records$2GrapeRecord.(Records.java:40)
    at com.mydeveloperplanet.myjava17planet.Records.basicRecordWithValidation(Records.java:46)
    at com.mydeveloperplanet.myjava17planet.Records.main(Records.java:10)

5. Sealed Classes(密封类)

    密封类将让您更好地控制哪些类可以扩展您的类。密封类可能更像是一个对库所有者有用的功能。一个类在 Java 11 final 中或者可以扩展。如果您想控制哪些类可以扩展您的超类,您可以将所有类放在同一个包中,并赋予超类包可见性。现在一切都在您的控制之下,但是,不再可能从包外部访问超类。让我们通过一个例子来看看这是如何工作的。

    在包
com.mydeveloperplanet.myjava17planet.nonsealed 中创建一个具有公共可见性的抽象类 Fruit。在同一个包中,创建了最终的类 Apple 和 Pear,它们都扩展了 Fruit。

public abstract class Fruit {
}
public final class Apple extends Fruit {
}
public final class Pear extends Fruit {
}

    在包
com.mydeveloperplanet.myjava17planet 中创建一个带有 problemSpace 方法的 SealedClasses.java 文件。如您所见,可以为 Apple、 Pear 和 Apple 创建实例,可以将 Apple 分配给 Fruit。除此之外,还可以创建一个扩展 Fruit 的 Avocado 类。

public abstract sealed class FruitSealed permits AppleSealed, PearSealed {
}
public non-sealed class AppleSealed extends FruitSealed {
}
public final class PearSealed extends FruitSealed {
}

    假设您不希望有人扩展 Fruit。 在这种情况下,您可以将 Fruit 的可见性更改为默认可见性(删除 public 关键字)。在将 Apple分配给 Fruit 和创建 Avocado 类时,上述代码将不再编译。后者是需要的,但我们确实希望能够将一个 Apple 分配给一个 Fruit。这可以在带有密封类的 Java 17 中解决。

在包
com.mydeveloperplanet.myjava17planet.sealed 中,创建了 Fruit、Apple 和 Pear 的密封版本。唯一要做的就是将 sealed 关键字添加到 Fruit 类中,并使用 permits 关键字指示哪些类可以扩展此 Sealed 类。子类需要指明它们是 final、 sealed 还是 non-sealed。超类无法控制子类是否可以扩展以及如何扩展。

public abstract sealed class FruitSealed permits AppleSealed, PearSealed {
}
public non-sealed class AppleSealed extends FruitSealed {
}
public final class PearSealed extends FruitSealed {
}

    在 sealedClasses 方法中,仍然可以将 AppleSealed 分配给 FruitSealed,但 Avocado 不允许扩展 FruitSealed。 然而,允许扩展 AppleSealed 因为这个子类被指示为非密封的。

    private static void sealedClasses() {
        AppleSealed apple = new AppleSealed();
        PearSealed pear = new PearSealed();
        FruitSealed fruit = apple;
        class Avocado extends AppleSealed {};
    }

6. instanceof 的模式匹配

    通常需要检查对象是否属于某种类型,如果是,首先要做的是将对象强制转换为该特定类型的新变量。可以在以下代码中看到一个示例:

private static void oldStyle() {
System.out.println("""
 *************
 * Old Style *
 *************""");
Object o = new GrapeClass(Color.BLUE, 2);
if (o instanceof GrapeClass) {
GrapeClass grape = (GrapeClass) o;
System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
 }
 }

    输出是:

This grape has 2 pits.

    使用 instanceof 的模式匹配,上面的可以改写如下。如您所见,可以在 instanceof 检查中创建变量,并且不再需要用于创建新变量和转换对象的额外行。

    private static void patternMatching() {
        System.out.println("""
                ********************
                * Pattern matching *
                ********************""");
        Object o = new GrapeClass(Color.BLUE, 2);
        if (o instanceof GrapeClass grape) {
            System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
        }
    }

    输出当然与上面相同。

    仔细查看变量的范围很重要。它不应该是模棱两可的。在下面的代码中,&& 之后的条件只会在 instanceof 检查结果为 true 时进行评估。 所以这是允许的。将 && 更改为 || 不会编译。

    private static void patternMatchingScope() {
        System.out.println("""
                *******************************
                * Pattern matching scope test *
                *******************************""");
        Object o = new GrapeClass(Color.BLUE, 2);
        if (o instanceof GrapeClass grape && grape.getNbrOfPits() == 2) {
            System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
        }
    }

    下面的代码显示了另一个有关范围的示例。如果对象不是 GrapeClass 类型,则抛出 RuntimeException。在这种情况下,永远不会到达打印语句。在这种情况下,也可以使用 grape 变量,因为编译器肯定知道 grape 存在。

    private static void patternMatchingScopeException() {
        System.out.println("""
                **********************************************
                * Pattern matching scope test with exception *
                **********************************************""");
        Object o = new GrapeClass(Color.BLUE, 2);
        if (!(o instanceof  GrapeClass grape)) {
            throw new RuntimeException();
        }
        System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
    }

7.有用的空指针异常

    有用的 NullPointerException 将为您节省一些宝贵的分析时间。以下代码导致 NullPointerException。

public class HelpfulNullPointerExceptions {

    public static void main(String[] args) {
        HashMap grapes = new HashMap<>();
        grapes.put("grape1", new GrapeClass(Color.BLUE, 2));
        grapes.put("grape2", new GrapeClass(Color.white, 4));
        grapes.put("grape3", null);
        var color = ((GrapeClass) grapes.get("grape3")).getColor();
    }
}

    对于 Java 11,输出将显示 NullPointerException 发生的行号,但您不知道哪个链式方法解析为 null。你必须通过调试的方式找到自己。

Exception in thread "main" java.lang.NullPointerException
        at com.mydeveloperplanet.myjava17planet.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)

    在 Java 17 中,相同的代码会产生以下输出,其中准确显示了 NullPointerException 发生的位置。

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.mydeveloperplanet.myjava17planet.GrapeClass.getColor()" because the return value of "java.util.HashMap.get(Object)" is null
    at com.mydeveloperplanet.myjava17planet.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)

 8. 精简数字格式支持

    NumberFormat 中添加了一个工厂方法,以便根据 Unicode 标准以紧凑的、人类可读的形式格式化数字。 SHORT 格式样式如下面的代码所示:

        NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);
        System.out.println(fmt.format(1000));
        System.out.println(fmt.format(100000));
        System.out.println(fmt.format(1000000));

    输出是:

1K
100K
1M

    LONG 格式样式:

fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));

    输出是:

1 thousand
100 thousand
1 million
荷兰语替换英语的 LONG 格式:
fmt = NumberFormat.getCompactNumberInstance(Locale.forLanguageTag("NL"), NumberFormat.Style.LONG);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));

    输出是:

1 duizend
100 duizend
1 miljoen

9. 添加了日周期支持

    添加了一个新模式 B 用于格式化 DateTime,该模式根据 Unicode 标准指示日期时间段。

    使用默认的中文语言环境,打印一天中的几个时刻:

System.out.println("""
 **********************
 * Chinese formatting *
 **********************""");
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("B");
System.out.println(dtf.format(LocalTime.of(8, 0)));
System.out.println(dtf.format(LocalTime.of(13, 0)));
System.out.println(dtf.format(LocalTime.of(20, 0)));
System.out.println(dtf.format(LocalTime.of(23, 0)));
System.out.println(dtf.format(LocalTime.of(0, 0)));

    输出是:

上午
下午
晚上
晚上
午夜

    现在使用荷兰语本地环境:

System.out.println("""
 ********************
 * Dutch formatting *
 ********************""");
dtf = DateTimeFormatter.ofPattern("B").withLocale(Locale.forLanguageTag("NL"));
System.out.println(dtf.format(LocalTime.of(8, 0)));
System.out.println(dtf.format(LocalTime.of(13, 0)));
System.out.println(dtf.format(LocalTime.of(20, 0)));
System.out.println(dtf.format(LocalTime.of(0, 0)));
System.out.println(dtf.format(LocalTime.of(1, 0)));

    输出如下。请注意,英国之夜从 23 点开始,荷兰之夜从 01 点开始。可能是文化差异;-)。

’s ochtends
’s middags
’s avonds
middernacht
’s nachts

10. Stream.toList()

    为了将 Stream 转换为 List,您需要使用 collect 的 Collectors.toList() 方法。这非常冗长,如下面的示例所示。

    private static void oldStyle() {
        System.out.println("""
                        *************
                        * Old style *
                        *************""");
        Stream stringStream = Stream.of("a", "b", "c");
        List stringList =  stringStream.collect(Collectors.toList());
        for(String s : stringList) {
            System.out.println(s);
        }
    }

    在 Java 17 中,添加了一个 toList 方法来替换旧的行为。

    private static void streamToList() {
        System.out.println("""
                        *****************
                        * stream toList *
                        *****************""");
        Stream stringStream = Stream.of("a", "b", "c");
        List stringList =  stringStream.toList();
        for(String s : stringList) {
            System.out.println(s);
        }
    }

11. 结论

    在本文中,您快速浏览了自上一个 LTS 版本 Java 11 以来添加的一些功能。现在由您开始考虑迁移到 Java 17 的计划,以及了解有关这些新功能的更多信息以及您如何 可以将它们应用到您的日常编码习惯中。提示:IntelliJ 会帮你解决这个问题!
java17java17

话说软件开发中的规范性


    这个话题要从最近工作中接触到的一些问题说起:大概情况是这样的,公司年前收购了一家公司(以下简称Y公司),虽然行业领域相同,但从业务模式到技术框架都是截然不同的。我们原有采用的是C#、.Net架构,Y公司全栈采用Java,Dubbo微服务架构,本人刚好在公司主导了几个Java项目实践,被分配到接手Y公司的部分项目。几个月下来,感触颇多,简记如下:

  • 项目模块缺乏整体规划,同一功能出现在项目的多个模块中,重复代码比比皆是;
  • 与第三方平台接口不规范,比如与X团的API接口中需要获取Token及加密,这样的接口本可以在一个地方统一实现,但却散落在各个地方,且用法不一;最奇葩的是明明对方的结果有返回代码(returnCode)和响应消息(message),却偏偏要自己定义一个枚举类来解释返回代码,最要命的是这个枚举是不完整的,结果就是有些代码找不到就抛出了一个异常”找不到对应的代码“云云,在调试过程中一头雾水—难道不能直接用人家的代码和消息吗?
  • 对于底层协议调用没有统一封装,最典型的比如HTTP协议,几乎所有的十几个项目中都有HttpUtil,从Header到PUT/GET/POST都要层层处理,不知道有OkHttp,更有上层封装Retrofit可以直接用吗?还有Json处理,甚至String处理都要写一堆,还各个项目各自为政,结果是出现了一堆无用的代码,臃肿还不能保证效率和正确性。

    上面的问题都可以归结为代码的规范性,如果大家是一个团队,就应该互通有无,有所分工,不要重复造轮子,首先要利用已经有的开源框架,其次要有统一的底层框架;这样既能提高整体效率,也保障了代码的稳定性和可维护性。试想,如果一个地方出了问题,比如第三方接口做了变化,那么上述的编码方式岂不是要改几十个地方?

    其实规范性的重要性不言而喻,大家都懂,特别是对于团队开发而言,更是如此。那为什么实际执行下来就这么难呢?其实是一个团队领导力的问题,当今互联网行业,许多是赚快钱,只求快速上线,不出bug就行,至于怎么实现的,什么规范性、复用性,不值一提。但是仔细想想,日积月累,这些东西就变得了“负”能量,时刻在影响着你的产品质量,当然也包括成本。到最后,大家都改不动了,习惯了差,没有了改的动力,到这时候就是整个团队的悲哀了。说到底是一种文化和精神,如果一个公司只求眼前利益,没有对细节和质量的追求,那么永远也成为不了一家伟大的公司,为之工作的员工也只能浑浑噩噩,打更混日子了!

我的前半生(技术人生)

(-)缘起

    冬季的北京,有些晴冷、干燥,位于望京的这个科技园有些荒凉,倒是旁边的街道上停得满满当当的车显得不甚协调。好在只在这里呆两天,现在已经坐在回上海的高铁上,窗外飞速飘过一排排光秃的树梢,黄土地上还有一些亮晶晶晶的积雪,心情有些放松。旅途的意义就在于让你能在周而复始的平日生活中得到一丝喘息,换一种方式放空心情。

    回想一天前还彻夜未眠地赶一个上线的项目,直到早上8点多,上车后两个小时、上班前一刻才搞定。这一切只是想在出差的两天能够让上海的两个小伙伴能够继续工作。没人催促,只是凭着一腔热情、卑微的自尊心和责任感自我驱动。这样的情况最近两年,每半年就会出现两三次,就是遇到一个技术问题,就会不眠不休,直到攻克为止。妻在一旁直心疼,总是提醒我注意身体,我也隐约中觉得不妥,可事情总是这样重复发生。有时候会忍不住想是不是年纪大了,珍惜时间?心中总不免泛出一丝的酸楚和无奈,另一方面,却还是有些成就感的。

    写博客的想法一直有,总觉得就这么每天干些和年轻人一样的事,写写代码,攻克一个又一个自己觉得了不起、别人也许不屑一顾的难题,有点蹉跎人生的感觉。加上这段时间同事、上级也鼓励我搞搞副业、做做分享,人生多一种可能,也给自己留点后路,传道授业总是越老越值钱的,这是一个可以做到老的事业。马云退休后不也当了“乡村教师”嘛,于人是一种帮助,于己也是一种慰藉,何乐不为?加上如今无处不在的媒体渠道,各种网红层出不穷,这些东西已经不泛传播渠道了。

    要写的内容也很多,但主要围绕技术主题,夹杂着一些人生体会,自勉也希望能给他人以启发。

(二)现状

    本人现在年近半百,明年就是本命年,两鬓已然斑白;目前就职一家民企信息技术部,抬头还是资深开发工程师,手下两名刚入行两三年的小伙伴,收入在上海勉强能够在中等水平;名下入门级小车一辆,50多平的蜗居一套附带百万房贷,膝下有一子即将高考;另有小女尚在咿呀学语,甚是喜人,余生唯此而有奋斗之动力。

    此等境况可谓不堪,但比起不写代码、管理不上路、尽是茫然空虚的那些年,重拾技术让我又觉得脚踏实地,找到了失落的自信。

———回到了湿漉漉的上海,闻到了熟悉的味道,暂且分割,回头再续———


(三)新起点  

    —相隔大半年,终于想起还有这一未完篇。期间见识了旷世未见、影响世界的疫情大战,以及随之而来的中美各种混战,仿佛变了天。所幸还有赖以生存的工作和温馨舒适的家,每日有妻女相伴,小儿也度过高考之役,不算太坏。外面的世界风云变幻,愈发显得小环境的安宁祥和,有生如斯,夫复何求?

     昨日看一个技术视频讲程序员如何做副业,说到公众号、培训等等无不是技术分享,和前期所想的技术博客并无二致,是原创还是转载、主题怎么确定?这些都是问题,但关键是要开始探索,终究会找到适合自己的方向的。

    早上上班途中想起了博客的笔名或者名号,就叫“信码由缰”吧,借用这个成语暗含码农人生、自由不羁之意,内容上也是关于软件行业的各种随想。第一篇是关于最近在工作中看别人的代码引发的规范性、封装性和框架(第三方)方面的感想,以此为起点,开启技术博主的人生旅程吧。

继续阅读“我的前半生(技术人生)”