前言的前言最近私下做了好一阵子的项目 UltraFunction，以至于好久没更新过博客了（白天工作太忙了），特此过来总结一番。估摸着算了一下，这个项目从 2024/8/29 开始构建项目，到如今也快 2 个月了，中间发生了不少难题，都一一解决，为了自己的热爱（屹立不倒）在此感谢参与项目的所有人🙏🏻网址: https://ultrafunction.domcer.com/Wiki: https://ultrafunction.domcer.com/wikiDiscord: https://discord.gg/2YZuDJwpMXEmail: admin@domcer.comQQ: 217934413SpigotMC: https://www.spigotmc.org/resources/%E2%AD%90ultrafunction-low-code-development-platforms-1-8-1-21.120276/前言感觉国内貌似对低代码平台不感兴趣，在当前公司，也曾有过低代码平台（对于前端），当时闹得风风火火，领导都很看中。节后开发组的人就不见了踪影。（3名前端主力，听说都被优化了，我是当时支持的后端，瑟瑟发抖）在国外经常刷到一些视频，说一群孩子使用 iPad 做一些好玩的东西（应该很多年了），感觉国外一直都很力推技术教育，常常在孩子最具有创造力和想象力的年龄，去深挖他们的潜力。而国内貌似就没有这些内容了，甚至很难推进。24年有幸被网易邀请到上海喝茶聊天，我就提过少儿教育这类的事情，他们的回复的大概意思是，国内政府很难说通，很难办，如果在大学应该还好。可是我想说，如果到大学，他还真的会对这些感兴趣吗？这是一个什么项目Wiki: [这个项目致力于简化 Minecraft 插件的创建过程，通过低代码平台，让用户轻松生成自定义逻辑数据包插件。无需任何编程基础，只需选择预定好的模块，并配置相关行为，即可实现独特的游戏功能。无论是添加新的游戏机制，还是打造个性化的互动体验，我们的工具都能满足大部分的需求。直观的界面和强大的自定义选项，让每一位 Minecraft 爱好者都能轻松实现创意，提升游戏乐趣，快来打造属于您的专属世界吧！]说白了就是一个可以通过拖拽积木块的方式，来达到自己的逻辑行为。帮助那些不会写代码，但是想要做游戏内逻辑行为的玩家，提供一个社区、平台，帮助他们完成想要做的事情。名字和LOGO的来源对于定义这个项目，其实也想了一些时间，最终定下了 UltraFunction，这个不仅仅只有直译的 极致功能/极致方法，也有英文可拆解的 Ultra Fun（有趣）ction，让大家在使用这个平台的时候，更多感受到的是一种乐趣。关于技术选型和架构关于前端前端共有 2 个部分：官网页面、画布编辑官网页面采用的是 Vue画布编辑使用的是 Google Blockly：[Blockly 是 Google 推出的一款可视化编程编辑器，使用拖放块。它是为全球最受欢迎的编程教育计划提供支持的引擎。]因为我并不是做前端的，所以一些细节我也没办法讲述太多。。。关于后端后端共有 3 个部分：平台业务层、核心执行层、插件处理层平台业务层：采用 SpringBoot + MySQL 的设计，对平台的业务进行处理，包括但不限于（用户账号功能、工作台管理功能、文件触发器编辑功能等）核心执行层：采用 antlr 定义自己的领域语言（DSL），对接前端的 Blockly，通过积木块生成 DSL 内容插件处理层：使用 Bukkit 包进行开发，对文件、事件等进行统一分发和处理为什么选择 antlr性能实测每次都经过compile: 一组包含20条语句的代码块，有20个入参数与2个出参。每次执行都经过输入、编译、执行、输出四个步骤。循环执行1000000万次，时间是122212ms，平均每条执行时间0.12ms每次不经过compile: 一组包含20条语句的代码块，有20个入参数与2个出参。每次执行都经过输入、执行、输出三个步骤。循环执行1000000万次，时间是6151ms，平均每条执行时间0.006ms所以，可以将即将执行的代码块进行预编译，存储到缓存内，每次取其中的 ParserTree 直接运行高性能原理分析模块原理antlr4词法匹配DFAantlr4语法匹配Adative LL()词法分析（Lexical Analysis）词法分析器（Lexer）将输入的字符流分割成令牌（Tokens）。这一步类似于正则表达式的匹配，但ANTLR4使用的是其自己的模式匹配算法。词法分析器规则（Lexer rules）可以用类似正则表达式的语法来定义，但它们不是直接使用正则表达式库来实现的，而是ANTLR4自身的匹配机制。语法分析（Parsing）语法分析器（Parser）根据定义的文法规则（Grammar rules）将令牌流解析成语法树（Parse Tree）。语法分析使用的是一种叫做LL()的解析算法，这是一种自顶向下的解析算法，能够处理许多上下文无关文法。由于不进行回溯，LL 解析器通常比较高效，特别是在处理适合 LL(1) 或 LL() 的文法时流程图在平台上，用户需要对自己的文件进行编辑，最终前端的 schema 与 后端的 expression （DSL）均会保存在云端通过构建文件，会生成一个 xxx.ultrafunction 文件放入服务端内，UltraFunction 会加载这个数据文件，并且预编译里面的内容，存入到缓存内等待事件监听器的分发，当触发某个事件后，即开始运行缓存内的语法后话目前市场上已有太多各种功能的插件了，当时没做项目之前的想法微乎其微，认为做任何功能的插件都有很多竞争对手，并且赛道上已经有太多人了，目前没有什么新颖的点。倒不如开发一个低代码平台，让用户发挥自己的想象，随便配置。我仍记得好几年之前，有个网站也做了个类似的低代码平台，但是我当时还小，用不懂，而且没有一个稳定的社区平台。所以在我们开创这个项目的时候，主打的就是社交媒体，完善帮助文档，让用户可以在任何地方都能联系到我们。给予用户帮助显然成本是很高的，但是这可以帮助项目持久的运营下去。
IntentProvide an interface for creating families of related or dependent objects without specifying[指定] their concrete[具体] classes.ExplanationReal-world exampleImagine a furniture[家具] company that produces various styles of furniture: modern, Victorian[维多利亚时代], and rustic[乡村的]. Each style includes products like chairs, tables, and sofas. To ensure[确保] consistency[一致性] within each style, the company uses an Abstract Factory pattern.In this scenario[脚本], the Abstract Factory is an interface for creating families of related furniture objects (chairs, tables, sofas). Each concrete factory (ModernFurnitureFactory, VictorianFurnitureFactory, RusticFurnitureFactory) implements the Abstract Factory interface and creates a set of products that match the specific style. This way, clients can create a whole set of modern or Victorian furniture without worrying about the details of their instantiation[实例化]. This maintains[保持] a consistent style and allows easy swapping[交换] of one style of furniture for another.In plain wordsA factory of factories; a factory that groups the individual[单独的] but related/dependent factories together without specifying their concrete classes.Wikipedia saysThe abstract factory pattern provides a way to encapsulate[概括、封装] a group of individual factories that have a common theme without specifying their concrete classesProgrammatic ExampleTo create a kingdom we need objects with a common theme. The elven[精灵] kingdom needs an elven king, elven castle, and elven army whereas[而、反之] the orcish[兽人] kingdom needs an orcish king, orcish castle, and orcish army. There is a dependency between the objects in the kingdom.Translating the kingdom example above. First of all, we have some interfaces and implementation for the objects in the kingdom.public interface Castle { String getDescription(); } public interface King { String getDescription(); } public interface Army { String getDescription(); } // Elven implementations -> public class ElfCastle implements Castle { static final String DESCRIPTION = "This is the elven castle!"; @Override public String getDescription() { return DESCRIPTION; } } public class ElfKing implements King { static final String DESCRIPTION = "This is the elven king!"; @Override public String getDescription() { return DESCRIPTION; } } public class ElfArmy implements Army { static final String DESCRIPTION = "This is the elven Army!"; @Override public String getDescription() { return DESCRIPTION; } } // Orcish implementations similarly -> ...Then we have the abstraction and implementations for the kingdom factory.public interface KingdomFactory { Castle createCastle(); King createKing(); Army createArmy(); } public class ElfKingdomFactory implements KingdomFactory { @Override public Castle createCastle() { return new ElfCastle(); } @Override public King createKing() { return new ElfKing(); } @Override public Army createArmy() { return new ElfArmy(); } } // Orcish implementations similarly -> ...Now, we can design a factory for our different kingdom factories. In this example, we created FactoryMaker, responsible for returning an instance of either ElfKingdomFactory or OrcKingdomFactory. The client can use FactoryMaker to create the desired[所需] concrete[具体] factory which, in turn, will produce different concrete objects (derived[来源] from Army, King, Castle). In this example, we also used an enum to parameterize[用参数表示] which type of kingdom factory the client will ask for.public static class FactoryMaker { public enum KingdomType { ELF, ORC } public static KingdomFactory makeFactory(KingdomType type) { return switch (type) { case ELF -> new ElfKingdomFactory(); case ORC -> new OrcKingdomFactory(); }; } }Here is the main function of our example application:LOGGER.info("elf kingdom"); createKingdom(Kingdom.FactoryMaker.KingdomType.ELF); LOGGER.info(kingdom.getArmy().getDescription()); LOGGER.info(kingdom.getCastle().getDescription()); LOGGER.info(kingdom.getKing().getDescription()); LOGGER.info("orc kingdom"); createKingdom(Kingdom.FactoryMaker.KingdomType.ORC); LOGGER.info(kingdom.getArmy().getDescription()); LOGGER.info(kingdom.getCastle().getDescription()); LOGGER.info(kingdom.getKing().getDescription());The program output:07:35:46.340 [main] INFO com.iluwatar.abstractfactory.App -- elf kingdom 07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the elven army! 07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the elven castle! 07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the elven king! 07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- orc kingdom 07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the orc army! 07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the orc castle! 07:35:46.343 [main] INFO com.iluwatar.abstractfactory.App -- This is the orc king!Class diagramApplicabilityUse the Abstract Factory pattern when:The system should be independent of how its products are created, composed[组成], and represented[表达方式].You need to configure the system with one of multiple families of products.A family of related product objects must be used together, enforcing[加强] consistency.You want to provide a class library of products, exposing only their interfaces, not their implementations.The lifetime of dependencies is shorter than the consumer's lifetime.Dependencies need to be constructed using runtime values or parameters.You need to choose which product to use from a family at runtime.Adding new products or families should not require changes to existing code.TutorialsAbstract Factory Design Pattern in Java (DigitalOcean)Abstract Factory(Refactoring Guru)ConsequencesBenefits:Flexibility: Easily switch between product families without code modifications.Decoupling: Client code only interacts with abstract interfaces, promoting portability and maintainability.Reusability: Abstract factories and products facilitate component reuse across projects.Maintainability: Changes to individual product families are localized, simplifying updates.Trade-offs:Complexity: Defining abstract interfaces and concrete factories adds initial overhead.Indirectness: Client code interacts with products indirectly through factories, potentially reducing transparency.Known usesJava Swing's LookAndFeel classes for providing different look-and-feel options.Various implementations in the Java Abstract Window Toolkit (AWT) for creating different GUI components.javax.xml.parsers.DocumentBuilderFactoryjavax.xml.transform.TransformerFactoryjavax.xml.xpath.XPathFactoryRelated patternsFactory Method: Abstract Factory uses Factory Methods to create products.Singleton: Abstract Factory classes are often implemented as Singletons.Factory Kit: Similar to Abstract Factory but focuses on configuring and managing a set of related objects in a flexible way.CreditsDesign Patterns: Elements of Reusable Object-Oriented SoftwareDesign Patterns in JavaHead First Design Patterns: Building Extensible and Maintainable Object-Oriented SoftwareJava Design Patterns: A Hands-On Experience with Real-World Examples
意图 Intent抽象文档设计模式是一种结构设计模式，旨在通过为各种文档类型定义公共接口，提供一种一致的方式来处理分层和树状数据结构。它将核心文档结构与特定的数据格式分离，实现动态更新和简化维护。The Abstract Document design pattern is a structural design pattern that aims to provide a consistent way to handle hierarchical and tree-like data structures by defining a common interface for various document types. It separates the core document structure from specific data formats, enabling dynamic updates and simplified maintenance.解释 Explanation抽象文档模式允许处理附加的非静态属性。此模式使用特性的概念来实现类型安全，并将不同类的属性分离到一组接口中。The Abstract Document pattern enables handling additional, non-static properties. This pattern uses concept of traits to enable type safety and separate properties of different classes into set of interfaces.真实世界的例子 Real world example想象一下，在一个图书馆系统中，书籍可以有不同的格式和属性：实体书、电子书和有声读物。每种格式都有独特的属性，如实体书的页数、电子书的文件大小和有声读物的持续时间。摘要文档设计模式允许图书馆系统灵活地管理这些不同的格式。通过使用这种模式，系统可以动态存储和检索属性，而不需要为每种书籍类型提供刚性结构，从而在未来更容易添加新的格式或属性，而不会对代码库进行重大更改。Imagine a library system where books can have different formats and attributes: physical books, eBooks, and audiobooks. Each format has unique properties, such as page count for physical books, file size for eBooks, and duration for audiobooks. The Abstract Document design pattern allows the library system to manage these diverse formats flexibly. By using this pattern, the system can store and retrieve properties dynamically, without needing a rigid structure for each book type, making it easier to add new formats or attributes in the future without significant changes to the codebase.用简单的话说 In plain words抽象文档模式允许在对象不知情的情况下将属性附加到对象。Abstract Document pattern allows attaching properties to objects without them knowing about it.维基百科称 Wikipedia says一种面向对象的结构设计模式，用于在松散类型的键值存储中组织对象，并使用类型化视图公开数据。该模式的目的是在强类型语言中的组件之间实现高度的灵活性，在这种语言中，可以动态地将新属性添加到对象树中，而不会失去类型安全的支持。该模式利用特征将类的不同属性分离到不同的接口中。An object-oriented structural design pattern for organizing objects in loosely typed key-value stores and exposing the data using typed views. The purpose of the pattern is to achieve a high degree of flexibility between components in a strongly typed language where new properties can be added to the object-tree on the fly, without losing the support of type-safety. The pattern makes use of traits to separate different properties of a class into different interfaces.程序性示例 Programmatic Example考虑一辆由多个部件组成的汽车。然而，我们不知道具体的汽车是否真的拥有所有部件，或者只是其中的一部分。我们的汽车动力十足，非常灵活。Consider a car that consists of multiple parts. However, we don't know if the specific car really has all the parts, or just some of them. Our cars are dynamic and extremely flexible.我们先定义基类Document和AbstractDocument。它们基本上使对象包含一个属性贴图和任意数量的子对象。Let's first define the base classes Document and AbstractDocument. They basically make the object hold a property map and any amount of child objects.public interface Document { Void put(String key, Object value); Object get(String key); <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor); } public abstract class AbstractDocument implements Document { private final Map<String, Object> properties; protected AbstractDocument(Map<String, Object> properties) { Objects.requireNonNull(properties, "properties map is required"); this.properties = properties; } @Override public Void put(String key, Object value) { properties.put(key, value); return null; } @Override public Object get(String key) { return properties.get(key); } @Override public <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor) { return Stream.ofNullable(get(key)) .filter(Objects::nonNull) .map(el -> (List<Map<String, Object>>) el) .findAny() .stream() .flatMap(Collection::stream) .map(constructor); } // Other properties and methods... }接下来，我们定义一个枚举Property和一组用于类型、价格、型号和部件的接口。这允许我们创建Car类的静态接口。Next we define an enum Property and a set of interfaces for type, price, model and parts. This allows us to create static looking interface to our Car class.public enum Property { PARTS, TYPE, PRICE, MODEL } public interface HasType extends Document { default Optional<String> getType() { return Optional.ofNullable((String) get(Property.TYPE.toString())); } } public interface HasPrice extends Document { default Optional<Number> getPrice() { return Optional.ofNullable((Number) get(Property.PRICE.toString())); } } public interface HasModel extends Document { default Optional<String> getModel() { return Optional.ofNullable((String) get(Property.MODEL.toString())); } } public interface HasParts extends Document { default Stream<Part> getParts() { return children(Property.PARTS.toString(), Part::new); } }现在我们准备好介绍这辆车了。Now we are ready to introduce the Car.public class Car extends AbstractDocument implements HasModel, HasPrice, HasParts { public Car(Map<String, Object> properties) { super(properties); } }最后，在一个完整的例子中，我们将介绍如何构造和使用car。And finally here's how we construct and use the Car in a full example. public static void main(String[] args) { LOGGER.info("Constructing parts and car"); var wheelProperties = Map.of( Property.TYPE.toString(), "wheel", Property.MODEL.toString(), "15C", Property.PRICE.toString(), 100L); var doorProperties = Map.of( Property.TYPE.toString(), "door", Property.MODEL.toString(), "Lambo", Property.PRICE.toString(), 300L); var carProperties = Map.of( Property.MODEL.toString(), "300SL", Property.PRICE.toString(), 10000L, Property.PARTS.toString(), List.of(wheelProperties, doorProperties)); var car = new Car(carProperties); LOGGER.info("Here is our car:"); LOGGER.info("-> model: {}", car.getModel().orElseThrow()); LOGGER.info("-> price: {}", car.getPrice().orElseThrow()); LOGGER.info("-> parts: "); car.getParts().forEach(p -> LOGGER.info("\t{}/{}/{}", p.getType().orElse(null), p.getModel().orElse(null), p.getPrice().orElse(null)) ); }The program output:07:21:57.391 [main] INFO com.iluwatar.abstractdocument.App -- Constructing parts and car 07:21:57.393 [main] INFO com.iluwatar.abstractdocument.App -- Here is our car: 07:21:57.393 [main] INFO com.iluwatar.abstractdocument.App -- -> model: 300SL 07:21:57.394 [main] INFO com.iluwatar.abstractdocument.App -- -> price: 10000 07:21:57.394 [main] INFO com.iluwatar.abstractdocument.App -- -> parts: 07:21:57.395 [main] INFO com.iluwatar.abstractdocument.App -- wheel/15C/100 07:21:57.395 [main] INFO com.iluwatar.abstractdocument.App -- door/Lambo/300类图 Class diagram适用性 Applicability此模式在具有不同类型的文档的场景中特别有用，这些文档共享一些共同的属性或行为，但也具有特定于其个别类型的独特属性或行为。下面是一些可以应用抽象文档设计模式的场景：内容管理系统(CMS)：在CMS中，您可能有各种类型的内容，如文章、图像、视频等。每种类型的内容可以具有创建日期、作者和标签等共享属性，也可以具有特定属性，如图像的图像尺寸或视频的视频时长。文件系统：如果您设计的文件系统需要管理不同类型的文件，如文档、图像、音频文件和目录，抽象文档模式可以帮助提供一致的方式来访问文件大小、创建日期等属性，同时允许特定的属性，如图像分辨率或音频持续时间。电子商务系统：电子商务平台可能有不同的产品类型，如实体产品、数字下载和订阅。每种类型可以共享共同的属性，如名称、价格和描述，而具有独特的属性，如实体产品的运输重量或数字产品的下载链接。医疗记录系统：在医疗保健中，患者记录可能包括各种类型的数据，如人口统计数据、病史、测试结果和处方。抽象文档模式可以帮助管理共享属性，如患者ID和出生日期，同时适应特定属性，如测试结果或处方药物。配置管理：在处理软件应用程序的配置设置时，可以有不同类型的配置元素，每个配置元素都有自己的一组属性。抽象文档模式可用于管理这些配置元素，同时确保以一致的方式访问和操作它们的属性。教育平台：教育系统可能有各种类型的学习材料，如基于文本的内容、视频、测验和作业。标题、作者和出版日期等公共属性可以共享，而视频时长或作业截止日期等独特属性可以特定于每种类型。项目管理工具：在项目管理应用程序中，您可能有不同类型的任务，如待办事项、里程碑和问题。抽象文档模式可用于处理一般属性，如任务名称和受理人，同时允许特定属性，如里程碑日期或问题优先级。文件具有多样化和不断演变的属性结构。动态添加新属性是一种常见要求。将数据访问与特定格式脱钩至关重要。可维护性和灵活性对代码库至关重要。This pattern is particularly useful in scenarios where you have different types of documents that share some common attributes or behaviors, but also have unique attributes or behaviors specific to their individual types. Here are some scenarios where the Abstract Document design pattern can be applicable:Content Management Systems (CMS): In a CMS, you might have various types of content such as articles, images, videos, etc. Each type of content could have shared attributes like creation date, author, and tags, while also having specific attributes like image dimensions for images or video duration for videos.File Systems: If you're designing a file system where different types of files need to be managed, such as documents, images, audio files, and directories, the Abstract Document pattern can help provide a consistent way to access attributes like file size, creation date, etc., while allowing for specific attributes like image resolution or audio duration.E-commerce Systems: An e-commerce platform might have different product types such as physical products, digital downloads, and subscriptions. Each type could share common attributes like name, price, and description, while having unique attributes like shipping weight for physical products or download link for digital products.Medical Records Systems: In healthcare, patient records might include various types of data such as demographics, medical history, test results, and prescriptions. The Abstract Document pattern can help manage shared attributes like patient ID and date of birth, while accommodating specialized attributes like test results or prescribed medications.Configuration Management: When dealing with configuration settings for software applications, there can be different types of configuration elements, each with its own set of attributes. The Abstract Document pattern can be used to manage these configuration elements while ensuring a consistent way to access and manipulate their attributes.Educational Platforms: Educational systems might have various types of learning materials such as text-based content, videos, quizzes, and assignments. Common attributes like title, author, and publication date can be shared, while unique attributes like video duration or assignment due dates can be specific to each type.Project Management Tools: In project management applications, you could have different types of tasks like to-do items, milestones, and issues. The Abstract Document pattern could be used to handle general attributes like task name and assignee, while allowing for specific attributes like milestone date or issue priority.Documents have diverse and evolving attribute structures.Dynamically adding new properties is a common requirement.Decoupling data access from specific formats is crucial.Maintainability and flexibility are critical for the codebase.抽象文档设计模式背后的关键思想是提供一种灵活且可扩展的方式来管理具有共享和不同属性的不同类型的文档或实体。通过定义公共接口并跨各种文档类型实现它，您可以实现一种更有组织、更一致的方法来处理复杂的数据结构。The key idea behind the Abstract Document design pattern is to provide a flexible and extensible way to manage different types of documents or entities with shared and distinct attributes. By defining a common interface and implementing it across various document types, you can achieve a more organized and consistent approach to handling complex data structures.后遗症 Consequences优点：灵活性：适应不同的文档结构和属性。可扩展性：在不破坏现有代码的情况下动态添加新属性。可维护性：由于关注点的分离，促进了干净和可适应的代码。可重用性：类型化视图允许代码重用以访问特定的属性类型。Benefits:Flexibility: Accommodates varied document structures and properties.Extensibility: Dynamically add new attributes without breaking existing code.Maintainability: Promotes clean and adaptable code due to separation of concerns.Reusability: Typed views enable code reuse for accessing specific attribute types.权衡：复杂性：需要定义接口和视图，增加实现开销。性能：与直接数据访问相比，可能会带来轻微的性能开销。Trade-offs:Complexity: Requires defining interfaces and views, adding implementation overhead.Performance: Might introduce slight performance overhead compared to direct data access.CreditsDesign Patterns: Elements of Reusable Object-Oriented SoftwareJava Design Patterns: A Hands-On Experience with Real-World ExamplesPattern-Oriented Software Architecture Volume 4: A Pattern Language for Distributed Computing (v. 4)Patterns of Enterprise Application ArchitectureAbstract Document Pattern (Wikipedia)Dealing with Properties (Martin Fowler)
前言做开发也有了一段时间，好的代码和烂的代码也看了很多，所以想记录下来平日里面自己开发的一些规范，方便后续其他人阅读学习或借鉴。之前有读过《代码整洁之道》，感觉受益匪浅，里面讲的很多内容都是日常会犯的一些小毛病，而且书中教了大家该如何让代码看起来更舒服，细节决定了更多事情，如果一个人写代码的风格换来换去，只会让这一片代码受到污染，即使是小的空格和空行，也需要多加注意。先记录下来，慢慢更新吧！或许是强迫症还是洁癖使然，让我对代码规范比较敏感例如这里是否多个空行，那里是否少个空格有问过其他同事，他们貌似对这些并无所谓，觉得能开发出来就好开门见山1. 空格所有特殊符号前后都应有空格，最常见的可以直接使用 idea 自带的代码整理，自动帮忙规范。规定：定义变量的等于号前后都需要有空格循环或者IF这种特殊词与括号都需要有空格来隔离。[scode type="red"]坏示例[/scode]String name="example"; int age= 1; int age =1; if(A ==B) if(A<B)[scode type="green"]好示例[/scode]String name = "example"; int age = 1; if (A == B)2. 类开始和结束的空行有很多人喜欢类开始的时候，直接开始写代码，在结束的时候又莫名奇妙多出来很多空行，亦或是在分割方法的时候没有空行，或者多个空行，这样都会让代码显得很不规范。规定：在新的类写代码时，应先空一行，再开始编写代码在类结束时，应该与上一个最近的方法紧贴，没有空行每个方法之间只且需要一个空行来隔离不同类型的变量应该用空行来隔离变量与方法之间应该用空行来隔离如果出现 List 类型的变量，应该以 xxxList 为命名，取消类似英文复数的定义，比如pages等，这种很容易让人引起歧义如果出现 Map 类型的变量，应该以 xxxMap 为命名[scode type="red"]坏示例[/scode]public class ApiRequestTotal extends BaseBasicDashboardCalculate { private static final String NAME = "example"; private String name; private String type; @Override public Map<String, Object> getConditions() { Map<String, Object> map = new HashMap<>(); map.put("finalDecisionCode", "Accept"); return map; } @Override public Map<String, Object> getNestedConditions() { return new HashMap<>(); } @Override public BasicDashboardMetricTypeEnum getMetricType() { return BasicDashboardMetricTypeEnum.API_REQUEST_TOTAL; } }[scode type="green"]好示例[/scode]public class ApiRequestTotal extends BaseBasicDashboardCalculate { private static final String NAME = "example"; private String name; private String displayName; @Override public Map<String, Object> getConditions() { Map<String, Object> map = new HashMap<>(); map.put("finalDecisionCode", "Accept"); return map; } @Override public Map<String, Object> getNestedConditions() { return new HashMap<>(); } @Override public BasicDashboardMetricTypeEnum getMetricType() { return BasicDashboardMetricTypeEnum.API_REQUEST_TOTAL; } }3. 属性和变量的定义这块的定义其实是很糟糕的，经常看到很多代码，打开一个类的时候，迎头一棒全是定义，导致要一直往下翻找代码，且定义的很不规范，一样类型的内容应该遵守从上到下的结构。规定：顺序规范应遵从：public 常量 -> private 常量 -> private 属性同一种类型的内容应该放在同一块去定义，且没有空行分割不同类型的内容应该有空行进行分割常量定义的名字都应该遵从全部大写，且使用下划线进行分割[scode type="red"]坏示例[/scode]@Resource private BasicDashboardCalculateExecutor calculateExecutor; public static final String CACHE_HOUR_KEY = "BasicDashboardWarningTask_Hour"; @Resource private CacheService cacheService; private static final String CACHE_NOTICE_KEY = "BasicDashboardWarningTask_Notice"; private static final List<String> ENABLE_METRICS = Stream.of( BasicDashboardMetricTypeEnum.SCAN_TOTAL, BasicDashboardMetricTypeEnum.SCAN_DENY_RATIO, BasicDashboardMetricTypeEnum.SCAN_REVIEW_RATIO, BasicDashboardMetricTypeEnum.API_REQUEST_TOTAL ).map(BasicDashboardMetricTypeEnum::name).collect(Collectors.toList()); @Resource private WarningConfigCache cache;[scode type="green"]好示例[/scode]public static final String CACHE_HOUR_KEY = "BasicDashboardWarningTask_Hour"; private static final String CACHE_NOTICE_KEY = "BasicDashboardWarningTask_Notice"; private static final List<String> ENABLE_METRICS = Stream.of( BasicDashboardMetricTypeEnum.SCAN_TOTAL, BasicDashboardMetricTypeEnum.SCAN_DENY_RATIO, BasicDashboardMetricTypeEnum.SCAN_REVIEW_RATIO, BasicDashboardMetricTypeEnum.API_REQUEST_TOTAL ).map(BasicDashboardMetricTypeEnum::name).collect(Collectors.toList()); @Resource private CacheService cacheService; @Resource private WarningConfigCache cache; @Resource private BasicDashboardCalculateExecutor calculateExecutor;4. 注释的使用注释真的是一个很重要的东西，在写这一段的时候我有在思考，为什么注释五花八门，很多人都写不好，注释的意义到底是什么？我认为注释的意义在于让别人看懂这段东西的含义，但是现在注释存在着很多问题，可能他会影响本身代码的整洁性，亦或是存留在这里多年没有被人更新，最后导致最新的这块代码早已和之前写的注释互不相关，让人揪心。在好的代码中，应该通过命名来告诉他人这个是做什么的，在名字中即可提现具体的含义，注释不该被大量定义、解释重复的内容，这会造成大量的代码污染。规定：不得在行尾添加注释，不管什么注释都应该另起一行，尤其是 // 这种注释// 注释开始后，应该有一个空格，然后再继续写注释，双斜杠与文字之间不能紧连着注释也属于一块代码或者一个方法的整体，不能算作空行，例如方法之间要有空行，如果要在方法上写注释，额外起一行注释应该频繁更新，不要造成注释与代码含义不同的情况，在更新代码的同时去更新注释注释不该去解释重复的内容，例如private String name;，上边就不需要写注释了，经常有同事在上边还要写一个 名字 意思的注释，感觉很奇怪尽可能减少注释，注释本身就不该被大量编写，好的代码在命名上即可体现出它所要干的事情在编写 TODO 时，应保持以下结构 // TODO | 存储至数据库[scode type="red"]坏示例[/scode]private String displayName; //展示名 无意义的注释、不该在行尾、没有空格分割 /** * 展示名 无意义的注释 */ private String displayName;[scode type="green"]好示例[/scode]/** * every hour/day, for example: time is 2023/x/x xx:00:00 00.00.00 * while storage all metric value in mysql when after aggregation */ @Scheduled(cron = "0 0 * * * ?") public void hourTask() { ... } // TODO | async consumer trigger5. 代码中的空行代码中的空行是指在某一个方法中空行的使用，在代码编写习惯里面，经常会有人从上到下写完一个空行都没有，我的评价是连贯，太xx的连贯了，条理清晰，逻辑通畅，全文从上到下，我是没办法维护哈哈哈。这种代码看上去就像是一篇没有逗号和句号的文章一样，让人喘不上来气，也看不懂他究竟要做什么。更有甚者如果这个方法超过了100行将是地狱难度，所以应该在某个部分完成后，适当的使用空行来作为分割线，告诉后来人这里的代码告一段落，下一段要干什么事情。规定：方法应适当减少行数，尽可能保持在20行以内为最佳多重复杂的代码应该对其进行拆分，在一个部分完成后，使用空行来分割空行应只有一行，不要为了分割多搞好几个空行在方法开始后直接写代码，不要有空行；代码写完后，也不要有空行在最终即将要return什么内容的时候，上边需要有一个空行来承接（前提是之前有很多的大量代码逻辑）在某个if或者某个循环结束时，可以考虑在其后边增添一个空行[scode type="red"]坏示例[/scode]public Response<String> execute(ConsoleListAddCmd cmd){ StringBuffer rosterDefineName = new StringBuffer(); StringBuffer rosterDefineCode = new StringBuffer(); rosterDefineCode.append("console"); rosterDefineName.append("CONSOLE"); if(cmd.getListType()==1){ rosterDefineCode.append("_black"); rosterDefineName.append("黑名单"); } if(cmd.getListType()==3){ rosterDefineCode.append("_white"); rosterDefineName.append("白名单"); } //appMap 所有渠道 List<Map<String, Object>> list = commonService.getAPPs(); Map<String,String> appMap = new HashedMap(); for (Map<String, Object> map : list) { appMap.put(map.get("name").toString(),map.get("dName").toString()); } appMap.put("all","全部渠道"); rosterDefineCode.append("_"+cmd.getApp()).append("_"+cmd.getDataType()); rosterDefineName.append(appMap.get(cmd.getApp())+cmd.getDataType().toUpperCase()); List<RosterDefineDO> rosterDefineDOS = rosterDefineRepository.selectValidAll(); List<RosterDefineDO> collect = rosterDefineDOS.stream().filter(p -> p.getCode().equals(rosterDefineCode.toString())).collect(Collectors.toList()); Response addRosterDefineRes = new Response(); ... }[scode type="green"]好示例[/scode]protected void sendMessage(List<String> messages) { List<WarningUser> warningUserList = cache.getWarningUserList(); StringBuilder stringBuilder = new StringBuilder(); AtomicInteger times = new AtomicInteger(1); messages.forEach(message -> { stringBuilder.append(times.get()) .append(". ") .append(message) .append("\n"); times.getAndAdd(1); }); Map<WarningTypeEnum, List<WarningUser>> collect = warningUserList.stream() .collect(Collectors.groupingBy(warningUser -> WarningTypeEnum.valueOf(warningUser.getWarningType()))); String result = stringBuilder.toString(); if (StringUtils.isNotEmpty(result)) { mailWarningSender.execute(result, collect.get(mailWarningSender.getWarningType())); dingWarningSender.execute(result, collect.get(dingWarningSender.getWarningType())); } }6. 注解的使用在类上的注解，经常使人感觉难受，每个人都有不同定义的顺序，有时候经常出现乱塞，乱使用的情况，导致影响整体美感。应从下到上根据关联关系的重要性进行排序，使其让它更有意义。规定：注解的使用应保持从下到上的关联重要性排序如果不确定关联重要性可根据注解长度进行排序不要定义没有意义的注解，或定义不使用的注解注: 第二条额外解释，例如在 SpringBoot 项目中，定义某个 Service 时，@Service 注解应该离类最近，其次是其他的条件，然后是日志[scode type="red"]坏示例[/scode]@EnableScheduling @SpringBootApplication @Import(cn.hutool.extra.spring.SpringUtil.class) public class StartApplication { // @SpringBottApplication 应该与类最近 }[scode type="green"]好示例[/scode]@Slf4j @EnableScheduling @MapperScan(basePackages = "com.domcer.infrastructure.mapper") @SpringBootApplication(scanBasePackages = {"com.domcer"}) public class AthenaApplication { } @RequestMapping("/api/alert/config") @RestController public class AlertConfigController { } @Slf4j @ConditionalOnClass(RedisOperations.class) @Service public class RiskEventStatServiceImpl implements RiskEventStatService { }7. 代码复杂度规范复杂度在 sonar 中一直是个比较有意思的内容，它又名为 complex，在 sonar 中规范一个方法中复杂度不可大于 15 以上。而计算方式并非是统计用到了多少个 IF 或者 FOR循环，当你在使用两个 IF 嵌套起来时，他们复杂度计算方式为 1+2=3，所以在越深的层级里面，它所代表的复杂度权重就越高。应极力避免复杂度的出现，在公司修复标准产品线时，曾看见过约 200 复杂度的长方法，这种代码阅读、维护都需要消耗大量的成本，得不偿失。尽量拆解成各类小方法，使其具有唯一性，一个方法只敢一件事情，让方法共用，也可以让代码看着更简洁。规定：在同一个方法中，复杂度不可超过 15 以上在同一个方法中，最多仅允许出现 3 层深度结构在同一个方法中，请尽量减少 else 单独出现在同一个方法中，请尽可能保证代码行数在 25 以内[scode type="red"]坏示例[/scode]public static void main(String[] args) { if (true) { // do sth. if (true) { // do sth. if (true) { // do sth. } else if (true) { // do sth. } } else if (true) { // do sth. } else { // do sth. } else { // do sth. } } else { // do sth. } }[scode type="green"]好示例[/scode]public static void main(String[] args) { if (true) { // do sth. return; } // do sth. }8. 方法的规范有看过之前很多代码，很隐晦又难懂，好的方法应该如同变量一样，在名字上就能体现出它要做的事情，而且应该尽职尽责，何为尽职尽责？它应只做这一件事情，且只做这件事情，并把这件事情完整的做完，这才叫尽职尽责。往往有很多人抽离方法的时候，总是到一半就结束了，然后再通过其他代码去加工，显得特别奇怪。方法名也应该尽可能的表达出你要做的事情，不要使用简写等，一个好的方法让人一看就懂，不需要再点进去看具体写了什么代码，让阅读更加迅速。并且，在方法的入参上应该尽可能的减少，在 sonar 中，方法入参最大规定为 7 个，但是我认为，应该尽可能保持在 4 个左右为最佳，尽可能减少入参所带来不必要的成本。规定：方法应尽职尽责，只做一件事，且只做这件事，并做完这件事方法名在定义上应保证可阅读，不管名字多长，只要完整的表达出即可方法的入参应尽可能保证在 4 以下，以避免后续的维护成本方法中的代码应该短小精悍，避免大量的糟糕代码出现，抽出可隔离的代码[scode type="red"]坏示例[/scode]public void test(A, B, C, D, E, F, G) { }[scode type="green"]好示例[/scode]public void storageTest(A, B) { // do sth. } public void addCoins(Long coins) { this.coins += coins; }9. stream流使用规范10. 内部变量名字规范all, list后话代码风格只是为了让代码看的更舒服，方便后续阅读，也可以让自己有一种风格，让别人看见代码就知道是谁写的。一个人的风格可以永久的保持下去，不管是到未来还是以后，都是对一些细节的较真。如果一个人写代码没有任何的风格，随便定义或修改代码，只会让代码越来越糟糕。代码的开发往往是小成本，在后期的更新维护才需要大量的成本投入。所以我这个人比较神经，包括但不限于：经常看其他人代码经常看自己已经写过的代码经常优化自己的代码经常翻找是否有冗余的内容（例如最上方多余的import或者变量定义）经常在想如何更简洁所以，尽可能保持自己的代码简洁吧！让自己和别人看见你的代码时，都会感觉舒爽！未完结[scode type="red"]坏示例[/scode][scode type="green"]好示例[/scode]
前言最近成为了 Warning Master (调侃）因为最近接手了3个预警项目，都是要做各个类别数值统计计算指标的预警感觉挺有趣的，就想记录下来这三个项目分别为：某A外企啤酒公司某B国际航空公司某C会员超市公司[scode type="blue"]小小的项目背景[/scode]某A外企啤酒公司项目要做预警是由于在2023年底的时候爆出来了一个生产问题，是由于当时代码分支记录混乱，最后上线的包版本缺少了部分 commit 提交，最后引发了比较惨烈的生产问题，结果是领导也没有深究（毕竟是领导在分支上管理不太好，也没有同步消息，修复的BUG在一个bugfix分支，没有在项目分支上）。紧接着引发了客户投诉，说是出现了好几天的数据异常，导致他们判断有误，所以客户要求我们赠送一个预警系统（简易），以此来兜住这种类似的问题。项目管理说最好以最小的成本完成该内容，甚至没有前端，纯后端进行数据计算统计、触发预警等。开发周期大概5-7天，由我来主导设计架构。在2024年初的时候，某A外企啤酒公司项目又爆了一个生产问题，本次生产事故是产品缺陷，导致报错影响了主流程判断。是由于客户项目是在好几年前买了的，是1.1.0版本，目前我们的版本已经是1.5.4+了，这个问题已经在新版本修复，但是旧版本没有修复，最终导致本次报错。客户认为该内容属于产品缺陷，我们应在修复产品缺陷时回归同步项目代码，公司侧认为只在新版本进行更新和维护，旧版本出现且新版本已修复问题应按需所求去同步。因此客户发起二期预警的需求，本次二期预警改动量较大，影响第一次的整体内容（由于第一次项目经理要求以最简单的方式开发，没有留太多拓展的口子）某B国际航空公司项目是客户购买的看板趋势产品，要求携带预警功能，该预警功能设计架构与某A外企啤酒公司项目又不一致，且我在有一段时间既参加A有参加B的开发任务，导致思维也比较跳跃。某C会员超市公司项目，目前主管只让我评估了对应的工作量，还没有进行开发，所以目前也没有开始设计。方案设计下面我会讲解两种方案设计，分别是 某A外企啤酒公司 与 某B国际航空公司 项目现在的预警方案设计。他们其中有一些共同点，也有一些不同的底层设计，具体是由于需求所导向。某A外企啤酒公司每天数据量: 25w+数据来源: Elasticsearch需求说明: 支持天类型与小时类型的预警 （天类型：当前天开始至目前时间的数据量 与 过去七天均值进行对比，看环比上升或下降率是否满足预警要求；小时类型：当前小时段与过去七天该小时段均值进行对比，看环比上升或下降率是否满足预警要求）额外补充: 没有前端页面展示，需要手动在某配置中写 Json，仅后端去做数据计算和触发[scode type="green"]说明[/scode]这个项目主要就是两个定时任务来运转整体预警系统的，定时任务分别去做存储和触发两个事情。我们的数据来源基本上是通过读取 Elasticsearch 获取的，在读取数据时已经进行了大量的聚合操作，并在返回结果后再进行一次加工。由于该项目预警需求最小时间片是按照小时进行对比的，所以在读取 Elasticsearch 数据后，可以按照 1 小时分片单位将对应的数据存入 MySQL 中。在触发预警的定时任务中，因为分成天类型与小时类型，我将其分成了两部分，该小时的数据实时的从 Elasticsearch 中读取，该小时之前的数据从 MySQL 中读取，最后将两个数据进行合并以及加工，再进行对比，计算出最终结果。至于为什么分着读取，是由于如果一直实时的去操作 Elasticsearch 可能会对其产生一些影响，且这些数据在上个小时后就已经结束了使命，是一个"死数据"，完全可以存储到 MySQL 当中，为之后预警的其他操作提供一个稳定的数据来源。这种形式也可以加快预警触发的效率，如果在 Elasticsearch 中数据量很大，并且做了极多的 Aggregation 聚合操作，会导致相应慢、节点阻塞等情况。在后续的 MySQL 中，可以定期清理往期数据，保证一个清洁的数据源。由于没有前端的交互，整个流程的触发和操作基本上都是从后端角度去考虑的，包括后续对读取数据行为的优化。整体开发所需的工作量并不是很多，但是一些细节仍需多加考虑。在最终的预警通知中，也应提供高拓展、多可用的方式，或许以后会兼容 邮箱、钉钉、AI电话、短信等方式，对不同的通知人群进行不同的预警方式。其实在这个设计之初我还设计了类似群组用户之类的功能，目前的预警，只要其中一个指标触发了阈值、满足条件后，就会推送给所有配置好的“推送人”，其实这里应该要去分开，不同的预警配置推送给不同的用户，并且应新增一个用户群组，例如运维组、管理组，可以设置全局推送等权限；部分某项目上的人员可以单独对某指标或多个指标进行监控。可惜该功能最后被项目经理按下，因为这个是预警一期的设计，当时只考虑制作一个最简易的预警系统，去兜住因此导致的生产问题。且该功能是客户要求去赠送的，并非实际算作新的需求（说白了就是不会给额外的钱），导致这个功能并没有实际去开发。在设计之初甚至考虑了是否做预警记录、预警处理以及收费推送的概念，以及可以去支持多时间、多条件的配置，可惜这些都没有让开发，还有一些小可惜。某B国际航空公司每天数据量: 7-12w数据来源: Elasticsearch + Kafka需求说明: 支持多个预警配置，不同的时间分片的指标统计，实时或准实时触发预警，可配置多条预警通知对象等额外补充: 该项目有前端交互界面，项目工作量预计220+人力天以上[scode type="green"]说明[/scode]整个流程图看下来是比较复杂的，但是思路非常清晰，目前的概念就是当数据推送到 Kafka 后，去实时消费该条数据并暂存到 Redis 当中。定时任务会按照时间分片读取这个期间的所有数据（从 Redis 读取），然后在 Java 内存里面去做聚合和计算。这里的聚合是完全通过把大量的数据读取到内存，然后进行分组过滤筛选等，个人认为这种操作还是很费时间的。当如果这个时间片范围内出现了大量的数据，可能会产生不太稳定的影响。与某A外企啤酒公司不同的是，A是从 Elasticsearch 读取数据的时候就已经进行的聚合，只需要后面的加工计算，但是B需要自己先聚合一次再加工计算。B项目使用的方式是实时消费 Kafka 消息，在 Java 中做聚合和计算，最终也是要落入数据库中。相同的点在于，A与B在读历史数据进行对比的时候，都是从 MySQL 中读取，道理是一样的。我主要在这里负责预警配置CRUD（比较无聊，而且项目代码很烂，MVC，看着很难受）以及预置指标逻辑开发（上层传递数据，我来做计算加工统计，最后落入数据库）。定时任务那块并没有参与，感觉预警触发那里的开发会更有意思一些。后话本质上A与B其实有很多相似之处，主要都是在落库行为上以及读取数据合并加工等操作上。可能后续我也会参加C项目预警的开发，这期间的经理还是很不错的。有的时候觉得开发或许很无聊，当被分到纯CRUD的时候，甚至不是很想去做，可能也是一种心态导致。感觉这种东西闭着眼睛都能搞完，也不会费很多时间，但是上边经理又分了很多时间，就导致我很不想搞，一点都没有新颖感。但是当我去独立设计一个流程的时候，我会觉得这个内容很有意思。各种东西互相关联，最终开发一个完整的系统，破具有爽感。朝着一定目标走去是“志”，一鼓作气中途绝不停止是“气”，两者合起来就是“志气”。一切事业的成败都取决于此。——卡耐基
题目/** * 圣诞活动预热开始啦，汉堡店推出了全新的汉堡套餐。为了避免浪费原料，请你帮他们制定合适的制作计划。 * * 给你两个整数 tomatoSlices 和 cheeseSlices，分别表示番茄片和奶酪片的数目。不同汉堡的原料搭配如下： * * 巨无霸汉堡：4 片番茄和 1 片奶酪 * 小皇堡：2 片番茄和 1 片奶酪 * 请你以 [total_jumbo, total_small]（[巨无霸汉堡总数，小皇堡总数]）的格式返回恰当的制作方案，使得剩下的番茄片 tomatoSlices 和奶酪片 cheeseSlices 的数量都是 0。 * * 如果无法使剩下的番茄片 tomatoSlices 和奶酪片 cheeseSlices 的数量为 0，就请返回 []。 * * * * 示例 1： * * 输入：tomatoSlices = 16, cheeseSlices = 7 * 输出：[1,6] * 解释：制作 1 个巨无霸汉堡和 6 个小皇堡需要 4*1 + 2*6 = 16 片番茄和 1 + 6 = 7 片奶酪。不会剩下原料。 * 示例 2： * * 输入：tomatoSlices = 17, cheeseSlices = 4 * 输出：[] * 解释：只制作小皇堡和巨无霸汉堡无法用光全部原料。 * 示例 3： * * 输入：tomatoSlices = 4, cheeseSlices = 17 * 输出：[] * 解释：制作 1 个巨无霸汉堡会剩下 16 片奶酪，制作 2 个小皇堡会剩下 15 片奶酪。 * 示例 4： * * 输入：tomatoSlices = 0, cheeseSlices = 0 * 输出：[0,0] * 示例 5： * * 输入：tomatoSlices = 2, cheeseSlices = 1 * 输出：[0,1] * * * 提示： * * 0 <= tomatoSlices <= 10^7 * 0 <= cheeseSlices <= 10^7 * * @Author hang.wang * @Date 2023/12/25 15:07 */ public class Class1276 { public static void main(String[] args) { Class1276 clazz = new Class1276(); System.out.println(clazz.numOfBurgers(16, 7)); System.out.println(clazz.numOfBurgers(17, 4)); System.out.println(clazz.numOfBurgers(4, 17)); System.out.println(clazz.numOfBurgers(0, 0)); System.out.println(clazz.numOfBurgers(2, 1)); } public List<Integer> numOfBurgers(int tomatoSlices, int cheeseSlices) { if (tomatoSlices % 2 != 0) { return Collections.emptyList(); } if (cheeseSlices * 2 > tomatoSlices) { return Collections.emptyList(); } if (cheeseSlices * 4 < tomatoSlices) { return Collections.emptyList(); } int small = cheeseSlices; int big = 0; while (small * 2 + big * 4 != tomatoSlices) { if (small == 0) { return Arrays.asList(0, 0); } small--; big++; } return Arrays.asList(big, small); } }解析有点类似鸡兔同笼，但是其中的边界值需要考虑，例如如果给的值为0，返回的结果都为0；如果给的条件不符合（例如奇数），返回空List先按照最小的汉堡消耗进行计算，然后依次减少小汉堡数量，增加大汉堡数量（根据条件已知：奶酪数=大汉堡数+小汉堡数）做该题目时优先考虑把各种情况排除掉奇数时，返回空值当奶酪全做小汉堡时，又超过西红柿数，返回空值当奶酪全做大汉堡时，又小于西红柿数，返回空值当做循环中途，小汉堡数为0时，返回 0, 0当循环结束后，返回对应数值
前言最近在公司做了一个类似大盘、看板的功能，所有的数据全部存储在ES里面，完全需要自己去ES里面捞数据，并没有一个结构化、过滤好的数据源，完全需要自己对ES的数据进行过滤、筛选、统计，所以就需要用到聚合了，下面是功能图，在图里面其实展示了很多重要信息根据时间范围、其他条件进行筛选数据指标数据以每天为单位进行展示统计所有数据，变成总量数据这里就需要考验到对ES聚合的使用了，因为不可能查询每个指标，都访问一遍ES，不仅指标多，指标也要根据日期进行分桶，不可能每次都发一次请求，即便是多线程，那也对ES压力太大了，使用了过多的网络资源来满足需求，况且时间范围越多，请求次数也越多，所以很不合理这个时候就体现出来聚合的重要性了，ES底层自己支持大量的聚合方式，可以满足按天为单位聚合数据，展示每天数据量有多少，也可以在其上层进行过滤，展示某个数据每天有多少 （不得不说，这简直太棒了）底层的指标都是我来计算提供数据的，所以对这块很熟，也使用了策略设计模式去做这个内容，满足其拓展性聚合是什么首先要明白，聚合是什么聚合分析是数据库中重要的功能特性，完成对一个查询的数据集中数据的聚合计算，如：找出某字段（或计算表达式的结果）的最大值、最小值，计算和、平均值等。ES作为搜索引擎兼数据库，同样提供了强大的聚合分析能力。对一个数据集求最大、最小、和、平均值等指标的聚合，在ES中称为指标聚合(metric)而关系型数据库中除了有聚合函数外，还可以对查询出的数据进行分组group by，再在组上进行指标聚合。在ES中group by称为分桶，桶聚合bucketing通过聚合，我们可以统计出绝大部分的数据，也可以查出来自己想要的指标，满足其使用性用法按天聚合数据{ "aggs": { "daily_hits_true": { "filter": { "bool": { "must": [ { "range": { "statDate": { "gte": 1700668800000, "lt": 1701359999000 } } }, { "nested": { "path": "searchFields", "query": { "bool": { "must": [ { "term": { "searchFields.name": "C_S_TDRESULT" } }, { "term": { "searchFields.value.raw": "true" } } ] } } } } ] } }, "aggs": { "daily_hits": { "date_histogram": { "field": "statDate", "interval": "1d", "format": "yyyy-MM-dd" } } } } } }上面的聚合就是一个非常简单的例子，我根据时间范围(statDate)进行筛选数据，判断C_S_TDRESULT是否为true进行过滤最后添加一个 daily_hits 聚合条件统计出每天的数据这里的聚合使用的是ES的 date_histogram，他可以按照指定时间范围的内容进行统计，例如几小时，也是完全可以的field: 根据哪个字段进行统计interval: 时间周期是多少，1d就是按照一天为单位统计数据format: 展示出来的时间格式是什么，如果按照1小时统计，需要填写小时的单位，需求是按照天统计，所以不关注时分秒的内容去重聚合去重聚合顾名思义，假如你数据存在一个 PHONE_NUMBER 字段第1条数据: PHONE_NUMBER = qwe第2条数据: PHONE_NUMBER = abc第3条数据: PHONE_NUMBER = qwe最终去重聚合统计出来的结果是2，这个2的数据就是聚合后的结果{ "aggs": { "daily_hits": { "date_histogram": { "field": "statDate", "interval": "1d", "format": "yyyy-MM-dd" }, "aggregations": { "searchFields": { "nested": { "path": "searchFields" }, "aggregations": { "relativeInAcNumFilter": { "filter": { "bool": { "must": [ { "term": { "searchFields.name": "S_S_MOBINO" } } ] } }, "aggregations": { "relativeInAcNum": { "cardinality": { "field": "searchFields.value.raw" } } } } } } } } } }顶部的 daily_hits 就是按天聚合，主要是看最里面的 relativeInAcNum 聚合其中的 cardinality 才是去重的关键，他根据 searchFields.name = S_S_MOBINO 内容进行去重后话虽然这样聚合可以满足我们的指标结果，当指标类型过多时，还是会产生一定的ES压力，并且也要根据时间范围而定，当数据量特别大的情况下，ES查询效率也会变慢，有时候倒不如直接多线程执行多条ES操作了主要选择还是在于自身的体量上，一般来说都放在ES上直接进行大聚合，一次操作，查询多个结果，这样也要求ES需要极高的效率，虽然ES是一个搜索效率极高的组件，有时候也需要考虑他的性能这里其实还有很多可以优化的点，因为按天搜索出来的数据，只有当天的数据需要保证实时性，也就是聚合的数据只需要查找当天的即可，所以可以在每天的半夜，统计昨天一整天的数据，落库存储至数据库，以此来减轻ES的压力。当存储在ES的数据量越多，聚合的效率也会降低，压力也会飙升，如若时间范围更长，筛选的数据也会更多，所以将死数据存入数据库类似的介质或许是一种选择
LeetCode | [20]-Easy 给定一个只包括 '('，')'，'{'，'}'，'['，']' 的字符串 s ，判断字符串是否有效
题目/** * @Author hang.wang * @Date 2023/11/24 10:22 * * 给定一个只包括 '('，')'，'{'，'}'，'['，']' 的字符串 s ，判断字符串是否有效。 * * 有效字符串需满足： * * 左括号必须用相同类型的右括号闭合。 * 左括号必须以正确的顺序闭合。 * 每个右括号都有一个对应的相同类型的左括号。 * * * 示例 1： * 输入：s = "()" * 输出：true * * 示例 2： * 输入：s = "()[]{}" * 输出：true * * 示例 3： * 输入：s = "(]" * 输出：false * * 提示： * 1 <= s.length <= 104 * s 仅由括号 '()[]{}' 组成 */代码public class Class20 { public static void main(String[] args) { Class20 class20 = new Class20(); System.out.println(class20.isValid("([)]")); System.out.println(class20.isValid("()")); System.out.println(class20.isValid("()[]{}")); System.out.println(class20.isValid("(]")); } public char exchange(char c) { switch (c) { case '(': return ')'; case ')': return '('; case '[': return ']'; case ']': return '['; case '{': return '}'; case '}': return '{'; } return ' '; } public boolean isValid(String s) { if (s.length() % 2 != 0) { return false; } if (s.length() == 1) { if (isPost(s.charAt(0))) { return false; } } Deque<Character> queue = new ArrayDeque<>(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (isPost(c)) { try { Character remove = queue.removeFirst(); if (exchange(c) != remove) { return false; } } catch (Exception e) { return false; } } else { queue.addFirst(c); } } return queue.isEmpty(); } public boolean isPost(char c) { switch (c) { case ')': case ']': case '}': return true; } return false; } }解析利用堆栈的特性，如果判断是前括号，就存进去，如果是后括号，就取出来一个值，然后判断这俩是否匹配
LeetCode | [14]-Easy 编写一个函数来查找字符串数组中的最长公共前缀
代码/** * @Author hang.wang * @Date 2023/11/24 13:38 * 编写一个函数来查找字符串数组中的最长公共前缀。 * 如果不存在公共前缀，返回空字符串 ""。 * 示例 1： * 输入：strs = ["flower","flow","flight"] * 输出："fl" * * 示例 2： * 输入：strs = ["dog","racecar","car"] * 输出："" * 解释：输入不存在公共前缀。 * * 提示： * 1 <= strs.length <= 200 * 0 <= strs[i].length <= 200 * strs[i] 仅由小写英文字母组成 */ public class Class14 { public static void main(String[] args) { Class14 class14 = new Class14(); System.out.println(class14.longestCommonPrefix(new String[]{"flower", "flow", "flight"})); System.out.println(class14.longestCommonPrefix(new String[]{"dog", "racecar", "car"})); } public String longestCommonPrefix(String[] strs) { List<String> collect = Arrays.stream(strs).sorted(Comparator.comparing(String::length)).collect(Collectors.toList()); String smallest = collect.get(0); int index = smallest.length(); for (int i = 0; i < smallest.length(); i++) { String substring = smallest.substring(0, index); AtomicBoolean flag = new AtomicBoolean(true); collect.forEach(str -> { if (str.indexOf(substring) != 0) { flag.set(false); } }); if (flag.get()) { return substring; } if (index == 1) { return ""; } index--; } return ""; } }思路取List中最短长度的值，以此判断满不满足，节约资源，当不满足后，长度-1，把末尾的字符去掉，一直到变成空字符串
示例/** * 示例 1: * * 输入: s = "III" * 输出: 3 * 示例 2: * * 输入: s = "IV" * 输出: 4 * 示例 3: * * 输入: s = "IX" * 输出: 9 * 示例 4: * * 输入: s = "LVIII" * 输出: 58 * 解释: L = 50, V= 5, III = 3. * 示例 5: * * 输入: s = "MCMXCIV" * 输出: 1994 * 解释: M = 1000, CM = 900, XC = 90, IV = 4. * @param args */代码public static void main(String[] args) { Class13 class13 = new Class13(); System.out.println(class13.romanToInt("III")); System.out.println(class13.romanToInt("IV")); System.out.println(class13.romanToInt("IX")); System.out.println(class13.romanToInt("LVIII")); System.out.println(class13.romanToInt("MCMXCIV")); } public int romanToInt(String s) { Map<String, Integer> map = new HashMap<>(); map.put("I", 1); map.put("V", 5); map.put("X", 10); map.put("L", 50); map.put("C", 100); map.put("D", 500); map.put("M", 1000); int total = 0; List<Integer> list = new ArrayList<>(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); String s1 = String.valueOf(c); Integer tempValue = map.get(s1); list.add(tempValue); if (i + 1 < s.length()) { char tempChar = s.charAt(i + 1); String s2 = String.valueOf(tempChar); if (map.get(s2) > tempValue) { total -= tempValue; } else { total += tempValue; } } else { total += tempValue; } } System.out.println(list); return total; }解析罗马数字主要特点ABA若A小于B，那么数值是B-A+A，根据位置的不同计算不同的数值，需要判断数字在哪个位置，与下一个数字进行比较可以先把所有罗马字转换成数值，然后再进行对比、计算