数据库建表时才知道我多菜
最近建库设计表,弄得有点不自信了,好久没干过这种细活了,真是家狗吃不了细糠不是; 下面咱就说道说道这个编码格式及所需字节数之间的关系,一起坐好,上课;(以下内容均来自于网络学习及ai回答,没深入细节,没深入细节,没深入细节,不对的地方,大佬们一定要指正) 数据库编码格式首先说下,咱们目前常用的数据库编码格式,其他字符咱没用过也没见过就不瞎说了。 ISO 8859-1 GB2312 GBK GB18030 UTF8 UTF16 UTF32 还有个啥UTF8mb4;太多了,是不是,先过滤下。 看下u8家族的,当我们在设计数据库时,Unicode码与我们的数据库并不是一一对应的,直接看结果: UTF8数据库中UTF8(实际叫utf8mb3)不等于UTF-8,"utf8"只支持每个字符最多3个字节, 对于超过3个字节的字符就会出错,而我们的**汉字虽然通常在utf8的情况下占三个字节**,但是存在占用四个字节的情况,且某些特殊符号也是四个字节,所以utf8淘汰。 UTF-8UTF-8支持1-4个字节,其最小单元是1个字节,也有说它支持**最大6个字节**; utf16utf16的每个字符必须是2个字节或者4个字节,而*UTF编码在最小单元为多字节中存在字节顺序的问题*, 所以UTF-8没这个困扰,但是utf16最小是2字节,所以我们也pass掉吧,费神不是; utf32utf32呢直接一个字符四个字节,但是呢我们的库表并不需要简单粗暴的定长,而是尽量最优使用存储空间(可以参考oracle);utf8mb4数据库里的utf8mb4有说他就是纯正的UTF-8,特性类似于UTF-8;(我以前根本不懂这玩意,就在哪看过说utf8mb4支持emoj我就用它了,没想到是对的); **那最终我们mysql层面u8家族的就剩一个utf8mb4能打了。** 再说一下我们的utf8mb4什么时候是一个字节呢,就是内容在ASCII编码范围内(就是128个字母数字符号)的时候是一个字符;下面这几个一般在oracle上用了(如果mysql也用就当我没说过这句话)ISO 8859-1占用一个字节,不支持汉字等其他字符,所以直接淘汰 GB2312(国标)汉字占用2个字节,非汉字字符(如字母、数字、标点符号等)占用1个字节主要覆盖简体汉字,(对汉字支持不够全面)所以直接淘汰; GBK(国标扩展)兼容GB2312,所需字节数与GB2312一样,GB2312中的字符在GBK中有相同的编码,相对于GB2312添加了繁体字,生僻字,东亚其他文字的支持;(有时我们会使用它)GB18030ai给的评价是基本覆盖了中国所有的汉字(包括少数民族文字)和常用字符需求;(我的想法是正常普通业务不需要这么大的,如果你喜欢当我没说)好了,不同编码格式存储数据所需要的字节数我们差不多知道了吧,下面我们再看看mysql那些讨人厌的字段类型各自的字节数。[顺便吆喝一个,就是有看机会的哥们可以看看,技术大厂,前后端测试捞人,待遇还可以,不妨一试。]数据库字段类型我直接复制网上一份过来 MySQL 字段类型可以简单分为三大类-  数值类型:整型(TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT)、浮点型(FLOAT 和 DOUBLE)、定点型(DECIMAL)-  字符串类型:CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB 等,最常用的是 CHAR 和 VARCHAR。-  日期时间类型:YEAR、TIME、DATE、DATETIME 和 TIMESTAMP 等。DATETIME 和 TIMESTAMP先捡简单的说:日期类型其实我们常用的就两个DATETIME 和 TIMESTAMP,其他三个就是字面意思年份、时间、日期; 这两个说实话大差不差哈,平时我们都用,整体区别就是DATETIME占八个字节,而 TIMESTAMP占四个字节,DATETIME表示的时间范围更广,TIMESTAMP能表示到2038年,但其可以随时区变化; 接着看我们的字符串类型,我此处捡常用的说char 和 varchar简单说就是char是不可变长度,但是varchar是可变长度,这么看好像没啥区别,比如说我们数字类型的字典,那我给varchar(1)岂不是更方便,然后我一顿捣鼓,终于发现存储的区别,**varchar会用字节空间来记录字符长度**,而char是定长的,不需要记录,这就会让mysql在sql优化的时候会考虑这种情况,所以总能看到前人的总结,固定字符数的用char,字符数不固定就用varchar,*有人说char属性的字段如果字符不够会空格填充,又有人说填充仅限于oracle*;注意哈,虽然char(1)表示一个字符空间,但是存储依然只能存储一个值哈,简单理解就是它叫字符个数,varchar同理一样;总结下哈,定长(char),可变长(varchar);整型这里TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT我就不拆开说了,下图简单看就是一个字节能存储范围是255,那两个就是255*255,依次类推那我们经常定义整型字段后面那个位数是啥比如int(M)的M是啥,怎么说呢,你就当它毫无用处吧,因为有说他们表示的是**显示宽度**,但是mysql8又不推荐了,所以咱们就当不存在,总结就是这里选择的话看你要表示的范围选取合适的,不用管数据库建表时的长度配置;浮点型给我说我就是不推荐,反驳的理由就是精度无法保证,想要精度就别用他,不在乎精度更没必要用它,当然,如果你就说普通的精度控制其实也可以用,但是给我我不会废这个脑子去思考,其实这两个的精度控制在mysql中我欣赏不来,可能是我navicat问题,我还是喜欢在plsql上操作oracle中double的感觉。举例double(M,D)中 M=整数位+小数位,D=小数位;DECIMAL我也是第一次知道这个叫定点数,DECIMAL(M,D)表示M是最大位数(精度)(整数位+小数位+小数点),范围是1到65。可不指定,默认值是10。 D是小数点右边的位数(小数位)。范围是0到30,并且不能大于M,可不指定,默认值是0,  ——转载自:小红帽的大灰狼  
科叼
1 2 开源硬件平台
Cursor最佳工作方法!!!
我正在使用Cursor,所以我在想我是否应该先了解一下使用它的最佳工作方法。这是为了确保我在处理项目时不会被复杂鶥惧憝疖初性压垮。1、使用Cursor之前,让Claude用Markdown 创建一个清晰详细的计划(让它提出一些澄清性问题,然后批评自己的计划,然后重新生成)。将其添加到Instructions.md文件中(这样你就可以经常让Cursor 参考它)。告诉 ChatGPT我想要创建什么,然后让它为另一个负责编码的 AI提供指令。然后我将所有内容粘贴Cursor Composer Agent 中.ChatGPT 基本上增加了另一层规划,从而降低了遇到问题的几率。在一个项目中,Cursor遇到了一些问题,无论如何都无法解决。浪费了几个小时,陷入了循环。然后我从头开始,但这次我让 ChatGPT o1为另一个编码 AI编写了清晰的指令。它非常有效。 2.、使用.cursorrules(它们总是在AI上下文中)来定义大致的规则。请参阅https:/cursor.directory。例如:先编写测试,然后编写代码,然后运行测试并更新代码,直到测试通过。3、让agent以小段“编辑·测试”循环的方式逐步编写代码。定义一个小任务进行增量。编写(或让AI编写)一个在此增量中失败的测试用例。指示AI(通常在agent模式下)编写代码以通过测试。如果测试失败,AI将分析故障原因并尝试修复代码,环回到步4。一旦测试通过,开发者审查更改。4、在prompt中鼓励链式思维5、当你遇到问题时,让Cursor写一份报告,列出所有文件及其功能,并描述遇到的问题,发给Caude或ChatGPT要解决方案。6、使用gitinsect.com以便将所有脚本配置和相关文件(可按扩展名选)整合到一个页面中。7、https:/context7.com/用于参考最新文档。[removed]~8、请经常使用git进行版本控制,避免出现太多未提交的更改。通过@明确添加文件来保持上下文简短。上下文越长,AI提供的细节就越多。。当上下文边长时,开始新聊天频繁地重新同步 /index 代码使用.cursorsignore 来排除不相关的文件。9、使用 /Reference 打开的编辑器快速将它们添加到上下文中记事本是常用的提示符10、示例:启用Yolo模式以伊它编写测试允许任何测试,例如vitest、npmtest、nr test等,也可以使用基本构建命令,例如 build、tsc等也始终允许创建文件和目录,如touch,mkdir等可选:11、在光标设置中的“AI规则”中设置系统提示保持prompt简洁明了使用替代词汇避免不必要的解释优先考虑技术细节而不是通用建议——转载自:行痴·知无畏
科叼
0 5 开源硬件平台
程序员必看:两个思想优化90%的代码
概览在软件开发过程中,代码的可读性和可维护性往往是衡量代码质量的重要指标。本文将介绍两个能够显著提升代码质量的设计原则:组合函数模式(Composed Method Pattern)和抽象层次一致性原则(Single Level of Abstraction Principle, SLAP),并通过实例说明如何在实际开发中应用这些原则。组合函数模式组合函数模式最早由 Kent Beck 在《Smalltalk Best Practice Patterns》一书中提出。这是一个简单易懂且实用的编程原则,能够对代码的可读性和可维护性产生立竿见影的效果。组合函数模式要求:所有公有函数(入口函数)应当读起来像一系列执行步骤的概要具体实现细节应当封装在私有函数中这种模式有助于保持代码精炼并易于复用。阅读这样的代码就像在看一本书,入口函数是目录,指向各自的私有函数,而具体内容则在私有函数中实现。示例代码不良实践: public void processOrder(Order order) { // 验证订单 if (order == null) { throw new IllegalArgumentException("Order cannot be null"); } if (order.getItems().isEmpty()) { throw new IllegalArgumentException("Order must contain at least one item"); } // 计算总价 double total = 0; for (OrderItem item : order.getItems()) { total += item.getPrice() * item.getQuantity(); } // 应用折扣 if (total > 1000) { total *= 0.9; // 10% 折扣 } else if (total > 500) { total *= 0.95; // 5% 折扣 } // 更新订单状态 order.setTotal(total); order.setStatus(OrderStatus.PROCESSED); orderRepository.save(order); // 发送确认邮件 String message = "Your order #" + order.getId() + " has been processed. Total: $" + total; emailService.sendEmail(order.getCustomerEmail(), "Order Confirmation", message); } 良好实践(应用组合函数模式): public void processOrder(Order order) { validateOrder(order); double total = calculateTotal(order); total = applyDiscount(total); updateOrderStatus(order, total); sendConfirmationEmail(order, total); } private void validateOrder(Order order) { if (order == null) { throw new IllegalArgumentException("Order cannot be null"); } if (order.getItems().isEmpty()) { throw new IllegalArgumentException("Order must contain at least one item"); } } private double calculateTotal(Order order) { double total = 0; for (OrderItem item : order.getItems()) { total += item.getPrice() * item.getQuantity(); } return total; } private double applyDiscount(double total) { if (total > 1000) { return total * 0.9; // 10% 折扣 } else if (total > 500) { return total * 0.95; // 5% 折扣 } return total; } private void updateOrderStatus(Order order, double total) { order.setTotal(total); order.setStatus(OrderStatus.PROCESSED); orderRepository.save(order); } private void sendConfirmationEmail(Order order, double total) { String message = "Your order #" + order.getId() + " has been processed. Total: $" + total; emailService.sendEmail(order.getCustomerEmail(), "Order Confirmation", message); } 抽象层次一致性原则(SLAP)抽象层次一致性原则与组合函数密切相关。它要求函数体中的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,代码会显得凌乱,难以理解。按照组合函数和 SLAP 原则,我们应当:在入口函数中只显示业务处理的主要步骤通过私有方法封装具体实现细节确保一个函数中的抽象在同一个水平上,避免高层抽象和实现细节混杂代码金字塔结构满足 SLAP 实际上是构筑了代码结构的金字塔。金字塔结构是一种自上而下的,符合人类思维逻辑的表达方式。在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次。示例: // 顶层抽象 - 业务流程 public void registerNewUser(UserRegistrationRequest request) { User user = createUserFromRequest(request); validateUser(user); saveUser(user); sendWelcomeEmail(user); } // 中层抽象 - 具体步骤 private User createUserFromRequest(UserRegistrationRequest request) { User user = new User(); mapBasicInfo(user, request); mapAddressInfo(user, request); mapPreferences(user, request); return user; } // 底层抽象 - 实现细节 private void mapBasicInfo(User user, UserRegistrationRequest request) { user.setUsername(request.getUsername()); user.setEmail(request.getEmail()); user.setFirstName(request.getFirstName()); user.setLastName(request.getLastName()); // 其他基本信息映射 } 顺便吆喝一句,技术大厂[机遇][机遇],前后端测试捞人,待遇还可以如何进行抽象1. 寻找共性抽象的过程是合并同类项、归并分类和寻找共性的过程。将有内在逻辑关系的事物放在一起,然后给这个分类进行命名,这个名字就代表了这组分类的抽象。示例:重构重复代码 public void processCustomerA(Customer customer) { System.out.println("Processing customer: " + customer.getName()); double discount = customer.getTotal() * 0.1; customer.applyDiscount(discount); notifyCustomer(customer); } public void processVipCustomer(Customer customer) { System.out.println("Processing customer: " + customer.getName()); double discount = customer.getTotal() * 0.2; customer.applyDiscount(discount); notifyCustomer(customer); } // 抽象后的代码 public void processCustomer(Customer customer, double discountRate) { System.out.println("Processing customer: " + customer.getName()); double discount = customer.getTotal() * discountRate; customer.applyDiscount(discount); notifyCustomer(customer); } public void processRegularCustomer(Customer customer) { processCustomer(customer, 0.1); } public void processVipCustomer(Customer customer) { processCustomer(customer, 0.2); } ——转载自:Lvan
科叼
6 16 开源硬件平台
熬夜写了个开源项目,同事看了直呼救命
从 CSS 地狱到开源救赎作为一个被丑陋后台界面折磨到想砸电脑的前端狗,我曾幻想有个管理后台能让我既省心又不辣眼睛。现实呢?公司的后台系统 UI 像 90 年代网页,按钮点下去没反馈,产品经理还吐槽“能不能好看点”。更别提手写 CRUD 页面,调 CSS 调到怀疑人生,部署时还因为配置文件路径炸了锅。终于有一天,我受够了,怒开 VS Code,熬夜到凌晨,敲秃了键盘,搞出了这个开源后台系统——Art Design Pro。结果呢?同事试用后直呼“救命,这界面也太香了!”有个后端小哥甚至默默点了 star,说“终于不用看默认 Element UI 了”。今天,我就来跟大家聊聊这个项目,顺便吐槽一下开发路上的坑。准备好了吗?先点个赞暖暖场!痛点:后台开发的“四大天王”做过后台开发的,都懂那种崩溃:UI 辣眼睛:默认的 Element UI 蓝白配色,看久了像得了视觉疲劳症,产品经理嫌弃“太程序员风”。交互像便秘:按钮没反馈,页面切换卡顿,用户体验差到让客户想跑路。重复造轮子:用户管理、角色分配、数据表格,每次从零写,CSS 调到手抽筋,效率低到想哭。响应式灾难:PC 上好好的界面,手机上一打开直接崩,媒体查询写到凌晨。我在上一家公司就踩了这些坑,领导还要求“两天上线,美观一点”。于是我下定决心,搞一个开源后台系统,目标是“两小时搭建,UI 美到飞起,交互丝滑”,让程序员从 CRUD 和 CSS 地狱中解脱!Art Design Pro:你的后台救星废话不多说,Art Design Pro 是一个基于 Vue3 + TypeScript + Element Plus + Vite 的开源后台管理系统,专为不想被丑 UI 和重复代码折磨的程序员设计。以下是它的杀手锏:美到冒泡的 UI:抛弃 Element UI 的“程序员风”,采用精心调配的配色和动效,页面清爽,产品经理看了都想加鸡腿。丝滑的交互:每个按钮、每处切换都有反馈,操作起来像在“和界面谈恋爱”,再也不怕用户吐槽体验差。模块化神器:用户管理、权限控制、数据表格全内置,组件可自由定制,省下 80% 的重复代码量。全响应式适配:从 PC 到手机,界面自动适配,媒体查询?不存在的!拒绝过度封装:代码简洁透明,想改样式或加功能?直接上手,零学习成本。项目已在 GitHub 收获 1.2k star,被某初创公司用于内部管理系统,产品经理直呼“比商业软件还好看”。顺便吆喝一声,技术大厂机会机会,前后端测试捞人,待遇还可以~三步上手 Art Design Pro想试试 Art Design Pro?三步就能跑起来!克隆项目: git clone https://github.com/Daymychen/art-design-pro.git 安装依赖: cd art-design-pro pnpm install 启动项目: 复制代码 pnpm dev 访问 http://localhost:3006,就能看到美到冒泡的后台界面!来吐槽你的后台故事!你们用过哪些后台系统?遇到过啥奇葩 UI 或交互?评论区吐槽吧!顺手点个 star 支持下 Art Design Pro 呗~演示地址:www.lingchen.kim/art-design-…GitHub 地址:github.com/Daymychen/a…——转载自:琢磨先生TT
科叼
3 13 开源硬件平台
new Map 这么好用,你为什么不用?是不喜欢吗?
最近项目闲了,领导有事儿没事儿就进行代码评审。看完我的代码后,领导问我,你的代码怎么全是对Object 增删查改审,你怎么不用`new Map()`呢,我都审美疲劳了!我没用过,但我要装作我知道,于是我赶忙解释,对直接进行对象操作写的代码直观易懂啊!领导还真被我唬住了,哈哈。后来浅浅研究一下,没想到`new Map()`用起来确实更优雅!好用,爱用! 为什么要用 `new Map`?当我第一次认真用 `Map`,有点像发现了新大陆。`Map` 是 JavaScript 提供的一种键值对集合。在处理键值对时,它的这些优点真的令我上头:- 键可以是任何类型不像普通对象的键只能是字符串或 symbol,`Map` 的键可以是对象、数组、函数,甚至是 NaN!const map = new Map(); map.set({}, 'hello'); // 对象也能做 key(注意,你没有对象)! - 顺序可控,操作清晰`Map` 保留插入顺序,遍历也很优雅:for (const [key, value] of map) { console.log(key, value); } 比起 Object 还要 `Object.keys()` + 再去 index 取值,`Map` 简直是优雅代名词 💅- 一行就能「映射」数组const map = new Map(arr.map(item => [item.id, item.value])); 这是不是比用 reduce + object 拼键值对方便多了?为什么我们以前不用 Map?我相信有部分人一定和我一样,不用new Map完全是因为不了解这个东西,也不知道它的使用场景!没关系,我来帮你梳理一下。常用方法- 创建一个 Mapconst map = new Map(); 也可以通过数组初始化:const map = new Map([ ['name', 'Alice'], ['age', 25] ]); - `set(key, value)`添加或更新键值对。map.set('city', 'Beijing'); map.set(123, 'number key'); map.set({ id: 1 }, 'object key'); 返回值仍是该 `Map` 对象,可以链式调用:map.set('a', 1).set('b', 2); - `get(key)`获取对应键的值,找不到返回 `undefined`。map.get('city'); // "Beijing" map.get('unknown'); // undefined - `has(key)`判断是否存在某个键:map.has('city'); // true map.has('unknown'); // false - `delete(key)`删除指定键值对:map.delete('city'); // true map.has('city'); // false - `clear()`清空所有键值对:map.clear(); map.size; // 0 - `size`返回当前 `Map` 的元素数量(只读):const m = new Map(); m.set('x', 10); m.set('y', 20); console.log(m.size); // 2 顺便吆喝一声[机会][机会],技术大厂,前/后端/测试,待遇还可以~遍历方法- `keys()`返回一个**可迭代对象**,包含所有键:for (let key of map.keys()) { console.log(key); } - `values()`返回所有值:for (let value of map.values()) { console.log(value); } - `entries()`返回所有 `[key, value]` 对:for (let [key, value] of map.entries()) { console.log(key, value); } `Map` 本身也是可迭代对象,等价于 `entries()`:for (let [k, v] of map) { console.log(k, v); } - `forEach(callback[, thisArg])`与数组类似,支持 `forEach`:map.forEach((value, key) => { console.log(key, value); }); 转换为数组使用扩展运算符:const arr = [...map]; // [[key, value], ...] 仅键数组:const keys = [...map.keys()]; 值数组:const values = Array.from(map.values()); 实用场景- 去重,但保留顺序const map = new Map(); arr.forEach(item => map.set(item.id, item)); const result = [...map.values()]; - 根据对象某字段快速查找const userMap = new Map(users.map(u => [u.id, u])); const user = userMap.get(101); 是不是比用 `find()` 好用很多?- 用作缓存池const cache = new Map(); function fetchData(id) { if (cache.has(id)) return cache.get(id); const data = loadFromServer(id); cache.set(id, data); return data; } 总结不夸张地说,`Map` 就是更强大的对象升级版。所以嘛——你要是还没用 `new Map()`,赶紧优雅起来吧😉 ——转载自:快乐就是哈哈哈
科叼
4 5 开源硬件平台
__init__.py 是个啥,为什么深受大厂程序员偏爱?
👋 朋友们,今天我们来聊聊 Python 里一个低调却至关重要的文件 ——`__init__.py`。 说实话,这玩意儿刚开始学 Python 时,很多人(包括当年的我)都是一脸懵:“这啥?删了会咋样?”有些人可能听说过它是 “包的标志”,也有人觉得它 “没啥大用,可以忽略”,更有甚者以为它 “只是个装样子的文件”😂。今天,我们就来彻底搞清楚 `__init__.py` 到底是干啥的,以及它如何影响 Python 项目的结构和运行。 彩蛋惊喜:521 人生小满胜万全,[程序员脱单大作战][程序员脱单大作战] 🏗️ 先搞懂 Python 模块(module)在聊 `__init__.py` 之前,我们得先弄清楚 Python 里的 ** 模块 ** 和 ** 包 ** 这两个概念。📌 ** 模块(module)** :简单来说,就是一个 `.py` 文件,里面写了一些函数、类或者变量。 比如,有个叫 `math_tools.py` 的文件,里面有一堆数学工具函数,那它就是个模块。# math_tools.py def add(a, b): return a + b def subtract(a, b): return a - b 然后,我们可以在别的 Python 文件里这样用它:import math_tools print(math_tools.add(3, 5)) # 输出 8 这就是 ** 模块的基本用法 **,没啥难的,对吧?📦 Python 包(package)是啥?如果你写的模块越来越多,代码量越来越大,就得想办法组织它们。这时候,Python 里的 ** 包(package)** 就派上用场了。📌 ** 包(package)** :一个 ** 文件夹 **,里面包含多个模块(`.py` 文件)。在 **Python 3.3 之前 **,如果要让一个目录被识别为 Python 包,必须在里面创建 `__init__.py` 文件。** 但从 Python 3.3 开始,即使没有 `__init__.py`,Python 也能识别它是一个包(称为 “命名空间包”)。** 不过,大部分实际项目 ** 依然建议添加 `__init__.py`**,因为它可以:✅ 明确这个文件夹是一个包,避免某些工具(如打包工具)识别错误。 ✅ 允许在包初始化时执行特定代码,比如自动导入子模块。 ✅ 让导入行为更加可控,避免意外的命名冲突。比如,咱们有个 `math_utils` 目录,里面放了几个数学相关的模块:math_utils/ # 这个文件夹就是一个包 │── __init__.py │── basic.py │── advanced.py 其中,`basic.py` 和 `advanced.py` 分别是两个模块,而 `__init__.py` 可以用来 ** 自定义包的导入行为 **。顺便吆喝一声,技术大厂,前后端、测试 [捞人] 捞人],待遇还不错~🎭 那么 `__init__.py` 到底是干嘛的?虽然 `__init__.py` 不再是创建包的 ** 必需 ** 条件,但它依然是 Python 项目里一个重要的组件。它的主要作用有 ** 两个 **: 1️⃣ 明确标记目录为 Python 包如果 `__init__.py` 存在,Python 解析器就会知道: **“这个目录是个 Python 包,而不是普通文件夹。”**即使 Python 3.3+ 之后不强制要求 `__init__.py`,但加上它可以:✅ 避免 Python 解释器在某些情况下误认为这是普通目录。 ✅ 兼容旧版本 Python,让代码能在不同环境中运行得更稳定。 ✅ 让某些工具(如 `pytest`、`mypy`)更好地识别项目结构。 2️⃣ 让包能像模块一样被导入如果 `__init__.py` 里什么都不写,那它的作用只是个 “标志”。但如果我们在 `__init__.py` 里加点代码,它就能 ** 自定义包的导入行为 **。🌟 ** 示例 1:让包直接暴露子模块 **# math_utils/__init__.py from .basic import add, subtract from .advanced import power 这样,我们就可以直接 import 整个 `math_utils`,而不需要写 `.basic` 或 `.advanced` 了:import math_utils print(math_utils.add(2, 3)) # 输出 5 print(math_utils.power(2, 3)) # 假设 advanced 里有个 power 函数 等于说,`__init__.py` 让 ** 包变得像一个大模块 ** 一样,外部不需要知道里面的模块结构,直接用就行。 🌟示例 2:包初始化操作`__init__.py` 还能在包被导入时执行一些初始化操作,比如加载配置、设置日志等:# math_utils/__init__.py print("数学工具包加载成功!") # 只要 import 这个包,就会执行这行代码 🔥 `__init__.py` 还能干点啥?大厂的 Python 项目里,`__init__.py` 还经常被用来做这些事:✅ 1. ** 动态导入子模块 **在大型 Python 项目中,随着模块越来越多,手动维护 `__init__.py` 将变得特别复杂还容易出错,这时候动态导入子模块就成了香饽饽了。 假设我们不知道 `math_utils` 里具体有哪些模块,可以让 `__init__.py` 在导入时动态扫描并加载:# math_utils/__init__.py import os import importlib # 获取当前包的路径 package_path = os.path.dirname(__file__) # 遍历当前目录下的所有 .py 文件(不包括 __init__.py 本身) for module in os.listdir(package_path): if module.endswith(".py") and module != "__init__.py": module_name = module[:-3] # 去掉 .py 后缀 importlib.import_module(f"{__name__}.{module_name}") # 动态导入模块 ✨ 效果:** 这样,当你在别的地方写 `import mypackage`,所有 `mypackage` 里的 `.py` 文件都会自动加载,不用再手动 `import` 了!🎉✨没加动态导入要这么写:**import math_utils.basic print(math_utils.basic.add(1,2)) #如果直接 import math_utils 会报错AttributeError: module 'math_utils' has no attribute 'basic' **✨加了动态导入可以这么写:**import math_utils print(math_utils.basic.add(1,2)) ✅ 2. ** 控制对外暴露的模块 **有时候,我们不想让 ** 所有 ** 子模块都被自动导入,而是只暴露一部分给外部用。这时候可以用 `__all__` 来 ** 手动控制 ** 允许被 `from mypackage import *` 访问的模块。# math_utils/__init__.py import os import importlib package_path = os.path.dirname(__file__) __all__ = [] for module in os.listdir(package_path): if module.endswith(".py") and module != "__init__.py": module_name = module[:-3] __all__.append(module_name) # 只暴露在 __all__ 里的模块 importlib.import_module(f"{__name__}.{module_name}") 🌟 ** 效果 **:from math_utils import * print(basic) # 只有在 __all__ 里的模块能被导入 ✅ **3. 懒加载(Lazy Import)如果某些模块比较大,加载它们会影响性能,那可以用 ** 懒加载 **(lazy import)技术,在需要时才导入,而不是在 `import mypackage` 时一次性全加载。# math_utils/__init__.py import importlib def lazy_import(name): return importlib.import_module(f"{__name__}.{name}") module1 = lazy_import("basic") 🌟 ** 效果 **: 这样,`basic` 只有在第一次被使用时才会真正导入,提高了性能!💡 ✅ 4. ** 做版本控制 **`__init__.py` 还能给包加上版本号,让外部代码可以访问: # math_utils/__init__.py __version__ = "1.0.0" 然后,在别的地方可以这样用:import math_utils print(math_utils.__version__) # 输出 "1.0.0" ✅ 5. ** 隐藏内部实现 **有些模块是 “内部用” 的,不想让外部访问,怎么办?可以在 `__init__.py` 里手动控制 ** 对外暴露的内容 **:# math_utils/__init__.py from .basic import add, subtract __all__ = ["add", "subtract"] # advanced.py 里的东西就不会被直接 import 这样,外部只能用 `math_utils.add ()`,但 `math_utils.advanced` 就不让直接访问了。🎉 结尾 关于 `__init__.py`,咱们就聊到这儿!希望这篇文章能帮你彻底搞懂它的作用,今后写 Python 项目时能更自信地使用它。—— 转载自:花小姐的春天
科叼
3 11 硬创社
Jetbrains正式宣布免费,有点猛啊!
提到 Jetbrains 这家公司,相信搞开发的同学应该都不陌生。该公司盛产各种编程 IDE 和开发工具,虽然2000年才成立,到现在却已经发布了超 30 款世界顶级的编程软件,同时也收获了来自全球范围内开发者和用户的青睐。众所周知,在去年10月份的时候,Jetbrains 曾经搞过一个大动作,那就是:官宣 WebStorm 和 Rider 这两款强大的 IDE 对非商业用途全面免费![顺便吆喝一声,技术大厂跳板机会机会,前后端测试捞人,感兴趣可看,待遇还不错~]当时这个消息出来的时候,就曾在开发者圈子里引起了一阵轰动和讨论。而且我清楚地记得,在当时的评论区,还有小伙伴这样问道:“啥时候轮到 CLion 也免费呢?”这不,好消息再次来临了!!最近 Jetbrains 再度官宣:CLion 从现在开始,对非商业用途全面免费!众所周知,CLion 是由 JetBrains 设计开发的跨平台 C/C++ 集成开发环境,通过智能代码补全、深度代码分析和集成调试工具,为开发者提供高效、现代化的 C 语言和 C++ 开发体验。然而,CLion 一直以来的高昂授权费用也让不少初学者和开源爱好者为之望而却步。因此这回消息一出,又再次在开发者圈子里引起了一阵热烈的讨论,不少网友直呼 Jetbrains 这波格局打开了。看到这里,相信大家也挺好奇,那他们这里所说的 「非商业用途免费」具体指的是哪些情形呢?对此,Jetbrains 官方也给出了对应的说明,目前的非商业用途情形包括像:学习、自我教育、开源项目开发、内容创作、业余爱好开发等场景就可以免费使用这个 IDE 。所以现在无论是学生、Arduino 开发者,还是无惧 C 语言和 C++ 重重挑战的忠实爱好者,只要使用场景不涉及商业活动,都可以来免费使用 CLion 进行开发。说到这里,那具体的非商业用途免费版 CLion 怎么申请和使用呢?操作其实也非常简单。1、首先,去官网下载 CLion 安装包并安装。不过这里要注意的是,用户需要确保下载的 IDE 版本是支持非商业许可证的最新版本即可。2、启动运行 IDE 后,将会看到一个许可证对话框。在该对话框中,用户可以在其中选择 Non-commercial use(非商业用途)选项。3、登录自己的 JetBrains Account 或创建一个新的帐户。4、登录完成后,用户需要接受 Toolbox 非商业用途订阅协议。5、尽情享受在 IDE 中的开发。包括如果用户已经开始了试用期或使用付费许可证激活了自己的 IDE,也仍然可以改用非商业订阅,只需要转到帮助|注册里,并在打开的窗口中点击 Remove License(移除许可证)按钮,然后再选择 Non-commercial use(非商业用途)就行了。不过这里依然还有两个点需要格外注意。第一点,也是官方公告里明确所说的。如果用户选择使用 Non-commercial use 非商业用途的免费版,那软件是有可能会向 JetBrains 发送 IDE 遥测信息的,包括像:框架、产品中使用的文件模板、调用的操作,以及与产品功能的其他交互,但是官方提到不会包含个人数据。另外还有一点需要注意的是,虽说免费版本的 IDE 在功能上与付费版本并无二致,但在某些特定功能上可能存在一定的限制。例如,免费版本的 Code With Me 功能将仅限于 Community 版本。不过对于大多数非商业用途的开发者们来说,这些限制并不会对日常开发工作造成太大的影响。所以总而言之,JetBrains 推出的这些非商业用途免费使用政策,虽说有一些要求,但是总体来说还是极大地降低了 JetBrains IDE 的使用门槛。同时也会让更广泛的用户群体更容易获取并使用,从而鼓励更多的人投身于编程学习,参与到开源项目的建设,共同推动技术的进步与发展。文章的最后,我们也不妨再次大胆憧憬一下:既然目前的 WebStorm、Rider 以及 CLion 都已经开放了非商业用途的免费使用,那么接下来像: GoLand、IntelliJ IDEA 等的免费开放还会不会远呢?再次期待 Jetbrains 的下一步操作。——转载自:CodeSheep
科叼
1 6 开源硬件平台
什么鬼?两行代码就能适应任何屏幕?
你可能想不到,只用两行 CSS,就能让你的卡片、图片、内容块自动适应各种屏幕宽度,彻底摆脱复杂的媒体查询! 秘诀就是 CSS Grid 的 auto-fill 和 auto-fit。马上教你用!✨🧩 基础概念假设你有这样一个需求:一排展示很多卡片每个卡片最小宽度 200px,剩余空间平均分配屏幕变窄时自动换行只需在父元素加两行 CSS 就能实现: /* 父元素 */ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } /* 子元素 */ .item { height: 200px; background-color: rgb(141, 141, 255); border-radius: 10px; } 下面详细解释这行代码的意思: grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 这是 CSS Grid 布局里定义列宽的常用写法,逐个拆解如下:1. grid-template-columns作用:定义网格容器里有多少列,以及每列的宽度。2. repeat(auto-fit, ...)repeat 是个重复函数,表示后面的模式会被重复多次。auto-fit 是一个特殊值,意思是:自动根据容器宽度,能放下几个就放几个,每列都用后面的规则。容器宽度足够时,能多放就多放,放不下就自动换行。3. minmax(200px, 1fr)minmax 也是一个函数,意思是:每列最小200px,最大可以占1fr(剩余空间的平分)具体来说:当屏幕宽度很窄时,每列最小宽度是200px,再窄就会换行。当屏幕宽度变宽,卡片会自动拉伸,每列最大可以占据剩余空间的等分(1fr),让内容填满整行。4. 综合起来这行代码的意思就是:网格会自动生成多列,每列最小200px,最大可以平分一行的剩余空间。屏幕宽了就多显示几列,屏幕窄了就少显示几列,自动换行,自适应各种屏幕!不需要媒体查询,布局就能灵活响应。总结一句话:grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 让你的网格卡片最小200px,最大自动填满一行,自动适应任何屏幕,布局永远美观!这里还能填 auto-fill,和 auto-fit 有啥区别?顺便吆喝一声,民族企业机会,前、后端/测试缺人,待遇给的还可以哦~🥇 auto-fill 和 auto-fit 有啥区别?1. auto-fill🧱 尽可能多地填充列,即使没有内容也会“占位”会自动创建尽可能多的列轨道(包括空轨道),让网格尽量填满容器。适合需要“列对齐”或“固定网格数”的场景。2. auto-fit🧱 自动适应内容,能合并多余空列,不占位会自动“折叠”没有内容的轨道,让现有的内容尽量拉伸占满空间。适合希望内容自适应填满整行的场景。👀 直观对比假设容器宽度能容纳 10 个 200px 的卡片,但你只放了 5 个卡片:auto-fill 会保留 10 列宽度,5 个卡片在前五列,后面五列是“空轨道”。auto-fit 会折叠掉后面五列,让这 5 个卡片拉伸填满整行。👇 Demo 代码: auto-fill item1 item2 item3 item4 item5 auto-fit item1 item2 item3 item4 item5 .grid-fill { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 40px; } .grid-fit { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; } .grid-fill div { background: #08f700; } .grid-fit div { background: #f7b500; } .grid-fill div, .grid-fit div { padding: 24px; font-size: 18px; border-radius: 8px; text-align: center; } 兼容性caniuse.com/?search=aut…🎯 什么时候用 auto-fill,什么时候用 auto-fit?希望每行“有多少内容就撑多宽”,用 auto-fit 适合卡片式布局、相册、响应式按钮等。希望“固定列数/有占位”,用 auto-fill 比如表格、日历,或者你希望网格始终对齐,即使内容不满。📝 总结属性空轨道内容拉伸适用场景auto-fill保留否固定列数、占位网格auto-fit折叠是流式布局、拉伸填充 🌟 小结auto-fill 更像“占位”,auto-fit 更像“自适应”推荐大部分响应式卡片用 auto-fit善用 minmax 配合,让列宽自适应得更自然只需两行代码,你的页面就能优雅适配各种屏幕! 觉得有用就点赞收藏吧,更多前端干货持续更新中!🚀✨——转载自:前端九哥
科叼
3 6 开源硬件平台
MyBatis中的 10 个宝藏技巧!
前言说到 MyBatis,很多小伙伴都会用,但未必用得“惊艳”。实际上,这个轻量级的持久层框架还有很多隐藏的“宝藏技巧”。如果你能掌握这些技巧,不但能让开发更高效,还能避免掉入一些常见的“坑”。今天就从浅入深,分享 10 个让人眼前一亮的 MyBatis 开发技巧,每一个都配上具体的场景和代码示例,务求通俗易懂,希望对你会有所帮助。1. 灵活使用动态 SQL很多小伙伴在写 SQL 的时候,喜欢直接用拼接字符串的方式,比如: String sql = "SELECT * FROM user WHERE 1=1"; if (name != null) { sql += " AND name = '" + name + "'"; } 这种写法不仅麻烦,而且安全性很差(容易引发 SQL 注入)。MyBatis 的动态 SQL 是专门为解决这种问题设计的,你可以用 if、choose、foreach 等标签来动态构造 SQL。示例:动态条件查询 SELECT * FROM user WHERE 1=1 AND name = #{name}ANDage=#{age} 这个代码的好处是,SQL 逻辑清晰,不会因为某个参数为空就导致整个 SQL 报错。顺便吆喝一声,民族企业机会,前、后端/测试缺人,待遇给的还可以哦~2. 善用 resultMap 自定义结果映射有些小伙伴会遇到这样的问题:数据库表字段是下划线命名,但 Java 对象是驼峰命名。比如 user_name 对应 userName。如果直接用默认的 resultType,MyBatis 是无法自动映射的。这个时候,用 resultMap 就能完美解决。示例:自定义结果映射 SELECT id, user_name, age FROM user WHERE id = #{id} 有了 resultMap,再复杂的字段映射都可以轻松搞定。3. 利用 foreach 实现批量操作有些小伙伴可能会遇到这种需求:传入一个 ID 列表,查询所有匹配的用户信息。如果用拼接字符串的方式生成 IN 条件,不但代码丑,还容易踩坑。MyBatis 提供了 foreach 标签,可以优雅地处理这种场景。示例:批量查询 SELECT * FROM user WHERE id IN #{id} 传入的 idList 是一个 List 或数组,MyBatis 会自动帮你展开为 IN (1, 2, 3) 这样的格式,完全不用担心语法问题。4. MyBatis-Plus 的分页功能很多小伙伴在做分页的时候,习惯自己写 LIMIT 的 SQL,这样不仅麻烦,还容易出错。其实,用 MyBatis-Plus 的分页插件能省不少事。示例:MyBatis-Plus 分页功能 Page page = new Page[removed](1, 10); // 第 1 页,每页 10 条 IPage userPage = userMapper.selectPage(page, null); System.out.println("总记录数:" + userPage.getTotal()); System.out.println("当前页数据:" + userPage.getRecords()); 只需引入分页插件,就能轻松完成分页操作,简直不要太爽。5. 使用 @Mapper的接口代理有些小伙伴觉得 XML 文件太多太麻烦,其实 MyBatis 支持纯注解的开发模式,尤其是对于简单的 SQL,非常方便。示例:注解方式查询 @Mapper public interface UserMapper { @Select("SELECT * FROM user WHERE id = #{id}") User getUserById(int id); @Insert("INSERT INTO user(name, age) VALUES(#{name},#{age})") void addUser(User user); } 用这种方式,可以完全省掉 XML 配置,代码更加简洁。6. 二级缓存MyBatis 内置了一级缓存(SqlSession 范围内),但对于多次查询的场景,可以开启二级缓存来提升性能。示例:开启二级缓存 SELECT * FROM user WHERE id = #{id} 开启二级缓存后,同一个 Mapper 下的查询会自动命中缓存,大幅提高性能。总结MyBatis 的魅力在于简单、高效,但很多时候我们用得太“基础”,没有发挥它的全部潜力。希望这 些技巧能帮你更高效地使用 MyBatis,也让你的代码看起来更“惊艳”。如果觉得有帮助,记得收藏分享!最后说一句如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。求一键三连:点赞、转发、在看。——转载自:苏三说技术
科叼
4 4 开源硬件平台
技术 battle,我的就是错的吗?
问题这个周日一直被昨天与同事争论的一个问题所困扰(周六加了班),背景是上周的一个项目,TL 让我新建了一个 JAR 类型的工程,设计一个提供灰度功能的模块,经过清明的奋战赶工,也是如期的赶上了项目进度。我提供的灰度模块功能是这样的,最外层是业务所属的灰度层,提供每个业务定制的灰度功能,比如各个灰度间的串联,前置数据的查询等。内层为一个通用的单个灰度服务,灰度服务串联了 白名单、阻断、版本判断、AB 实验、梵高人群 五个环节,接口类提供了一个判断单个用户灰度的方法,主体流程都写在 abstract 抽象类里面,子类只需要实现一个 getConfig () 方法,提供本灰度需要的配置,例如 白名单配置、版本配置、AB 实验配置 等。当单个灰度接入时,只需要实现抽象类,返回灰度配置,定义好本灰度场景值即可。组内的一位同事,看了我设计的灰度流程后,反应比较激烈。同事的观点同事的观点是,我提供的判断单个灰度的流程,过于复杂冗余,不符合单一职责原则,abstract 类逻辑过多,其它同事接入时的学习成本太高。同事认为,不需要提供内层的单个灰度流程,而是提供自定义的灰度服务和几个工具类即可,即最外层的业务灰度,以及 白名单、阻断、版本判断、AB 实验、梵高判断 这 5 种工具,当使用者接入灰度时,新建一个业务灰度类,在业务灰度类内部调用工具串联起整个流程。同事举了一个例子,如果业务诉求是判断 1 个版本以及 3 个实验时,如果按照我的方式,需要写 3 个单个的灰度实现类,然后新建一个业务灰度类,再去调用这 3 个灰度实现类进行判断。如果按照他的方式,只需要新建一个业务灰度类,然后调用一个版本工具类判断,再调用 3 次实验判断,就可以完成业务方诉求。** 顺便推个机会,[民族企业]大厂,前、后端 / 测试缺人,待遇给的还可以哦~**我的理由我的观点是,所提供的通用单个灰度服务,并非不符合单一职责原则,单一职责原则虽然要求提供粒度小、功能单一的类,但是单一职责的目的是 可复用性、可维护性、可扩展性、可读性,虽然我所提供的单个灰度判断流程的可读性稍差,但是做到了很好的可复用、可维护、可扩展,因此与单一职责并没有冲突。而且使用者接入,在绝大多数灰度场景下,都不需要感知整体逻辑,只需要进行简单的抽象类实现,定义好版本、实验等配置即可,接入成本是非常低的。而且,同事所列举的例子,是属于比较少数的场景,绝大多数场景是提供【单个版本】+【单个实验】判断即可,如果按照我的流程,只需要提供单个灰度实现类,再新建一个业务灰度类,直接进行简单调用,这比在业务类中调用工具进行流程串联成本是更低的。而即使是同事所举的例子,接入成本也不高,虽然新建 3 个实现类,有类膨胀的趋势,但是这 3 个实现,本身就是 3 个单一的灰度,应该进行隔离。再者,因为通用的单一灰度框架进行了统一的监控打点、灰度降级等能力,因此从可观测可降级角度考虑,也是优于同事的方式的。结论这个问题目前并没有结论,同事准备下周一把问题抛给 TL,让 TL 进行决断,如果结论是同事的方式更好的话,他准备另写一套。不知大家怎么认为,是我的方式更好呢,还是同事的观点更为正确。 —— 转载自:起风了布布
科叼
0 2 技术沙龙
2025年了请使用更加优雅的Bean注入(@Resource过时了)
日常开发发现思考:大家看到如下代码,有发现什么问题呢?是不是很多@Resource,造成不仅是代码的整洁度,还是代码观感,其实都不是很好,我们常常说尽量消除冗余代码,增强复用,那么这里的注解我们是不是可以消除掉呢?[顺便推个机会]大厂摇人,前、后端/测试机会,偶尔有加班,加班有加班费,、薪酬待遇还不错。Spring官方更推荐我们使用构造器注入原先使用 @Autowired 注解官方会提示我们:删除此字段注入并使用构造函数注入 Remove this field injection and use constructor injection instead. 并且会以一个波浪线警告的形式出现,有代码洁癖的人会极度不舒适@Resource 这个注解不是Spring官方提供的,而是J2EE(Java 250 规范提案)提供的,它不会有波浪线警告两个注解最大区别就是:@Autowired根据 By Type 查找Bean,如果存在多个Bean,再根据 By Name 查找Bean@Resource根据By Name查找Bean,如果ByName找不到Bean,再根据By Type查找Bean其实,两种方式都是通过setter方式进行注入,终究不是Spring官方推荐的方式我们能不能使用构造器注入,么稳态啦!我们能不能优雅的构造器注入,么稳态啦!代码整洁优化前提引入Lombok依赖,这个依赖出现很多纷争,有些人推荐用,有些人不推荐用,可根据各公司实际情况使用使用 @RequiredArgsConstructor + final @NonNull field 实现构造器注入Bean是不是上面的代码不管是整洁度,还是代码观感都十分优雅了,而且我们不用在属性上加上@Resource 或者@Autowired,只需要像书写最终属性一样即可,而且我们还可以通过点击左边的小点,找到实现类。原理解读@RequiredArgsConstructor 这个属性是Lombok依赖提供的,作用域在类上,作用是生成所需要参数的构造函数,但是注意:字段必须是final修饰和具有@NonNull等约束的字段(这里的 final 和 @NonNull 满足其一即可)之前如果我们需要使用构造器注入,则需要手动书写构造器,而是对于后续更多Bean的注入,我们又需要重新在构造器中添加需要注入的Bean参数,略显繁琐,但是如今通过这种方法就可以省去了这个步骤,并且是Spring官方推荐的Bean注入方式。特别注意对于Lombok比较熟悉的人来说,肯定知道@AllArgsConstructor,这个注解可以生成该类下的全部属性的构造方法,那他们的区别是什么呢?@AllArgsConstructor:生成该类下全部属性的构造方法。@RequiredArgsConstructor:生成该类下被final修饰或者带有@NonNull的构造方法。使用@AllArgsConstructor之后,@Value就会不起作用可以使用@RequiredArgsConstructor代替——转载自:本当迷
科叼
0 3 开源硬件平台
为什么停止在小型项目中使用 TypeScript
我曾经是那种把 TypeScript 推到公司里每个项目中的前端开发者。感觉这真是个正确的操作——毕竟,静态类型让一切都变得更好了,不是吗?嗯,并非总是如此。多年来,我一直强迫自己在每个项目中都使用 TypeScript,现在我终于承认了一件事:对于小型项目来说,TypeScript 带来的麻烦远大于帮助。 如果我要快速构建一个 MVP、个人项目或一个简单的 API,我不再默认使用 TypeScript。原因如下。1. 前期准备工作不值得让我们面对现实吧——TypeScript 需要设置。配置tsconfig.json确保依赖项与 TypeScript 兼容安装和配置类型定义(@types/whatever)调整构建过程是的,我知道像 Vite、Next.js 或 Nuxt 这样的现代框架可以通过零配置模板简化设置。但是,当你从头开始或不使用完整框架时,这些配置仍然存在——对于快速 hack 或脚本来说,我宁愿避免这种摩擦。对于大型项目来说,这种设置确实值得。但对于一些小项目——比如一个快速 API 或一个周末的业余项目——我为什么要花 20 分钟来处理配置,而不是真正地编写代码呢?一个简单的 JavaScript 文件就可以工作:// index.js console.log("Hello, world!"); 有了 TypeScript,即使是这么基础的事情也需要额外的仪式:const message: string = "Hello, world!"; console.log(message); 让我们解决这个问题:不,您不需要string在这里明确注释 - TypeScript 可以很好地推断类型。这个例子对我来说有点象征意义。它表明,即使是最简单的脚本,在使用了 TypeScript 之后,也会变得愈发正式和冗长。在一个我只想打印一条消息或调用一个 API 的快速项目中,这层额外的代码层往往感觉像是阻力,而不是帮助。这是在设置构建过程 之前。[顺便推个机会]大厂摇人,前、后端/测试机会,可以吃零食,加班有加班费,稳定性较高,薪酬待遇还不错。2. TypeScript 会减缓开发速度JavaScript 最大的优势之一就是它的灵活性。想要快速完成一个概念验证?没问题。有了 TypeScript,这种灵活性就消失了。假设我正在尝试一个新的 API。在 JavaScript 中,我只需获取一些数据并继续:fetch("https://api.example.com/data") .then(res => res.json()) .then(data => console.log(data)) .catch(err => console.error(err)); 在 TypeScript 中?现在我需要定义类型:interface ApiResponse { id: number; name: string; email: string; } fetch("https://api.example.com/data") .then(res => res.json()) .then((data: ApiResponse) => console.log(data)) .catch(err => console.error(err)); 当然,TypeScript 允许你使用any或逐步引入类型。但这有点违背了使用 TypeScript 的初衷,对吧?我的意思是——当我处于开发模式时,我根本不想考虑类型。我想要快速的反馈,并且没有摩擦。当然,它更安全——但如果我只是随便玩玩,为什么我在知道这个 API 是否有用之前就编写了额外的代码?3. TypeScript 的优势在小型项目中并不那么有用我明白 TypeScript 有助于防止 bug。但是在小项目中,这真的很重要吗?大多数时候,TypeScript 在小项目中阻止的“错误”都是我会立即发现的。不好的例子:const age = "30"; console.log(age * 2); // NaN 好的,TypeScript 可以捕获这个问题。但这种 bug 会让我彻夜难眠吗?不会。如果我的整个应用程序只有 500 行代码,我不需要编译器来保护我——我可以直接阅读代码。4. 额外的构建步骤感觉没有必要使用 JavaScript,我可以立即运行我的脚本:node script.js 使用 TypeScript,我必须先编译它:tsc script.ts && node script.js 对于大型项目来说?没问题。但如果我写的是一个快速实用的脚本,这个额外的步骤会扼杀我的动力。是的,我知道可以使用它ts-node来避免手动编译,但它仍然会带来不必要的复杂性。5. 并非所有依赖项都能与 TypeScript 兼容是否曾经安装第三方包并立即遇到 TypeScript 错误?Property 'xyz' does not exist on type 'SomeModule'. 然后你检查了该包的 GitHub 仓库,发现它不支持 TypeScript。现在你有三个选择:查找 DefinitelyTyped 包(@types/xyz) (如果存在)。编写自己的类型定义(呃)。使用any并假装 TypeScript 不存在。如果我正在做一个大项目,我会花时间解决这个问题。但对于一个小应用程序来说,这只是另一个令人头疼的问题。当我仍然使用 TypeScript 时我并不是说 TypeScript 不好——我仍然将它用于正确的项目。✅大型应用(尤其是多人协作的应用)。✅需要长期维护的项目。✅代码库严重依赖模块间的严格契约。但对于:❌副项目❌快速脚本❌ MVP 和原型我坚持用 JavaScript。它更快、更简单,而且不用和编译器较劲, 也更有趣。TypeScript 是一种工具,而不是信仰有些开发者把 TypeScript 视为2025 年编写 JavaScript 的唯一方法。但事实并非如此。TypeScript 在合理的地方使用效果很好——但强制每个项目都使用它?这只会造成不必要的摩擦。如果你喜欢 TypeScript,那很好——在它对你有利的地方使用它。但是,如果你正在做一些小事,觉得 TypeScript 带来的麻烦比它的价值更大……也许确实如此。你的看法是什么?你现在还在用 TypeScript 做所有事情吗?还是已经开始选择自己的阵营了?欢迎在评论区留言讨论!——转载自作者:CF14年老兵
科叼
1 4 开源硬件平台
websocket和socket有什么区别?
WebSocket 和 Socket 的区别WebSocket 和 Socket 是两种不同的网络通信技术,它们在使用场景、协议、功能等方面有显著的差异。以下是它们之间的主要区别:1. 定义Socket:Socket 是一种网络通信的工具,可以实现不同计算机之间的数据交换。它是操作系统提供的 API,广泛应用于 TCP/IP 网络编程中。Socket 可以是流式(TCP)或数据报(UDP)类型的,用于低层次的网络通信。WebSocket:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务器和客户端之间实时地交换数据。WebSocket 是建立在 HTTP 协议之上的,主要用于 Web 应用程序,以实现实时数据传输。2. 协议层次Socket:Socket 是一种底层通信机制,通常与 TCP/IP 协议一起使用。它允许开发者通过编程语言直接访问网络接口。WebSocket:WebSocket 是一种应用层协议,建立在 HTTP 之上。在初始握手时,使用 HTTP 协议进行连接,之后切换到 WebSocket 协议进行数据传输。3. 连接方式Socket:Socket 通常需要手动管理连接的建立和关闭。通过调用相关的 API,开发者需要处理连接的状态,确保数据的可靠传输。WebSocket:WebSocket 的连接管理相对简单。建立连接后,不需要频繁地进行握手,可以保持持久连接,随时进行数据交换。[顺便推个机会]大厂摇人,前/后端、测试,可以吃零食,加班有加班费,稳定性加高,薪酬待遇还不错,全国多城市有机会哈。4. 数据传输模式Socket:Socket 可以实现单向或双向的数据传输,但通常需要在发送和接收之间进行明确的控制。WebSocket:WebSocket 支持全双工通信,客户端和服务器之间可以随时互相发送数据,无需等待响应。这使得实时通信变得更加高效。5. 适用场景Socket:Socket 常用于需要高性能、低延迟的场景,如游戏开发、文件传输、P2P 网络等。由于其底层特性,Socket 适合对网络性能有严格要求的应用。WebSocket:WebSocket 主要用于 Web 应用程序,如即时聊天、实时通知、在线游戏等。由于其易用性和高效性,WebSocket 特别适合需要实时更新和交互的前端应用。6. 数据格式Socket:Socket 发送的数据通常是二进制流或文本流,需要开发者自行定义数据格式和解析方式。WebSocket:WebSocket 支持多种数据格式,包括文本(如 JSON)和二进制(如 Blob、ArrayBuffer)。WebSocket 的数据传输格式非常灵活,易于与 JavaScript 进行交互。7. 性能Socket:Socket 对于大量并发连接的处理性能较高,但需要开发者进行优化和管理。WebSocket:WebSocket 在建立连接后可以保持长连接,减少了握手带来的延迟,适合高频率的数据交换场景。8. 安全性Socket:Socket 的安全性取决于使用的协议(如 TCP、UDP)和应用层的实现。开发者需要自行处理安全问题,如加密和身份验证。WebSocket:WebSocket 支持通过 WSS(WebSocket Secure)进行加密,提供更高层次的安全保障。它可以很好地与 HTTPS 集成,确保数据在传输过程中的安全性。9. 浏览器支持Socket:Socket 是底层的网络通信技术,通常不直接在浏览器中使用。Web 开发者需要通过后端语言(如 Node.js、Java、Python)来实现 Socket 通信。WebSocket:WebSocket 是专为 Web 应用设计的,所有现代浏览器均支持 WebSocket 协议,开发者可以直接在客户端使用 JavaScript API 进行通信。10. 工具和库Socket:使用 Socket 进行开发时,开发者通常需要使用底层网络编程库,如 BSD Sockets、Java Sockets、Python's socket 模块等。WebSocket:WebSocket 提供了简单的 API,开发者可以使用原生 JavaScript 或第三方库(如 Socket.IO)轻松实现 WebSocket 通信。结论总结来说,WebSocket 是一种为现代 Web 应用量身定制的协议,具有实时、双向通信的优势,而 Socket 是一种底层的网络通信机制,提供更灵活的使用方式。选择使用哪种技术取决于具体的应用场景和需求。对于需要实时交互的 Web 应用,WebSocket 是更合适的选择;而对于底层或高性能要求的网络通信,Socket 提供了更多的控制和灵活性。——转载自作者:Riesenzahn
科叼
0 5 硬创社
优秀的后端应该知道的易错点
编程社区给出了 2024 年编程语言流行度的指标, Java 排第三~1. 数据类型1.1 static修饰的变量大家在玩Java时有没发现,下面这样一个对象,我们即使没有给变量赋值,在创建它后这个变量依旧会有默认值。 class A { int a; } System.out.println(new A().a); 程序执行结果: 0 有时前端同学要求后端给个默认值0,我们甚至不用动手,Java编译器就把活给干完。这实际上是Java语言的一个特性,对于实例变量即成员变量,如果是基本数据类型都会有一个默认值。不同的基本类型默认值不同,我们看看以下各种基本类型的默认值。 int a; //0 short b; //0 long c; //0 float d; //0.0 double e; //0.0 boolean f; //false byte g; //0 char h; //空字符 1.2 自动类型提升(1)Java中的byte、short、char进行数学计算时都会提升为int类型,很容易忽略的基础知识,南哥慢慢道来。以下代码的运行正常吗? byte b1 = 1, b2 = 2, b3; b3 = b1 + b2; 答案在你意料之中,就是编译报错。 # 报错内容 java: 不兼容的类型: 从int转换到byte可能会有损失 既然byte、short、char进行数学计算时都会提升为int类型,那我们就需要在运行过程中把结果转换成byte类型。正确的做法如下。 b3 = (byte)(b1 + b2); (2)但假如byte变量是这样的写法,我们给b1和b2都加个final,很神奇,编译不会报错。 final byte b1 = 1, b2 = 2, b3; b3 = b1 + b2; 这种情况是一个特殊情况,Java编译器会为其进行特殊处理,我们称它为编译时常量表达式的求值。b1、b2、b3都是常量值,b3在编译阶段就会被编译器进行赋值,不会涉及到上面我们提到的数学计算提升为int类型,也就不会编译错误。(3)但如果是这种情况呢? final byte b1 = 1; byte b2 = 2, b3; b3 = b1 + b2; 以上两个byte变量,只有一个final修饰,也就是说对b3赋值运算不能在编译时进行,那这段代码依旧会报错,我们还是需要把结果转换为byte类型。正确做法如下。 java 代码解读 复制代码 b3 = (byte)(b1 + b2); 1.3 byte溢出byte类型的数据范围在-128 ~ 127,当这个值超过127会转变成 - 128。为什么呢? byte i = 127; System.out.println(++i); shell 程序执行结果: -128 byte类型的最大值127在二进制中表示为01111111,当我们对127的值增加1时,每位加1后都会产生进位,导致的结果就是所有的位都会翻转(从01111111变成10000000),而10000000十进制的表示就是-128。1.4 Bollean赋值业务开发编写最多就是条件语句了,特别在迭代年代比较旧的老项目,一套接一套的if语句。既然见识了那么多条件语句,那以下代码的执行结果是什么? Boolean flag = false; if (flag = true) { System.out.println("true"); } else { System.out.println("false"); } 在Java里,条件判断是有赋值的功能,try语句同样也有。此时falg在条件判断里被赋值了。 程序执行结果: true [需要看新机会的]顺便吆喝一句,技术大厂,待遇给的还可以,就是偶尔有加班(放心,加班有加班费) 前、后端/测试,多地缺人,感兴趣的可以来试试~2. 程序运算2.1 三元运算符三元运算符的坑,相信不少南友遇到过。。。我们来看看三元运算符是什么?Java中的三元运算符是一种简洁的条件表达式工具,其语法格式为:条件 ? 表达式1 : 表达式2。如果条件为真(true),则表达式的结果是表达式1;如果为假(false),则结果是表达式2。假如是这种情况呢,南哥问:o1最终的数据类型是什么? Object o1 = true ? new Integer(1) : new Double(2.0); 上面的代码行其实等同于这一行。 Object o1 = true ? new Double(1.0) : new Double(2.0); 三元运算符的一个非常关键的细节就是类型的统一化。Double类型的数据范围更大于Interger类型,所以Java编译器会对值类型进行类型提升,最终把Integer类型提升为Double类型。2.2 自增问题下面是南哥编写的两个i++自增的易错问题,面试考核经常出现在笔试题。(1)南哥第一问:以下代码执行的结果是什么? int i = 0; i = i++ + i; shell 程序执行结果: 1 (2)南哥第二问:以下代码执行的结果是什么? int i = 0; i = i++; System.out.println(i); shell 程序执行结果: 0 2.3 String对象我们创建一个String对象,JVM在背后实际上做了很多功夫,String对象在常量池、堆内存都有可能存在。我们具体问题来具体分析下。(1)以下代码段不包含引用类型,只是单纯的字面量拼接,所以只会创建一个对象存在于常量池中。 String s = "JavaProGuide" + "南哥" + 666; (2)以下代码段包含了引用类型,一共创建了3个对象,猜对了吗? String s = "Hello"; s = s + " world!" "Hello"、" world!"都属于字面量,所以它们都会被加入到Java字符串常量池中。而s + " world!"这么一个代码段涉及了引用类型,所以它在内存里创建了一个新的String对象,并不存在于常量池,而是存在于堆内存里。(3)以下代码段一共创建了两个对象,分别存在于常量池、堆内存。首先new对象会把该String对象放到堆内存里,而过程中会先检查常量池是否存在JavaProGuide String str = new String("JavaProGuide"); …… ——转载自作者:JavaSouth南哥
科叼
0 3 开源硬件平台
后端开发和你聊聊 JVM 如何优化
作者:京东零售京麦研发 马万全首先应该明确的是JVM调优不是常规手段,JVM的存在本身就是为了减轻开发对于内存管理的负担,当出现性能问题的时候第一时间考虑的是代码逻辑与设计方案,以及是否达到依赖中间件的瓶颈,最后才是针对JVM进行优化。1.JVM内存模型针对JAVA8的模型进行讨论,JVM的内存模型主要分为几个关键区域:堆、方法区、程序计数器、虚拟机栈和本地方法栈。堆内存进一步细分为年轻代、老年代,年轻代按其特性又分为E区,S1和S2区。关于内存模型的一些细节就不在这里讨论了接下来从内存模型简单流转来看一个对象的生命周期,对JVM的回收有一个概念,其中弱化堆栈和程序计数器1.首先我们写的.java文件通过java编译器javac编译成.class文件2.类被编译成.class文件后,通过类加载器(双亲委派模型)加载到JVM的元空间中3.当创建对象时,JVM在堆内存中为对象分配空间,通常首先在年轻代的E区(这里只讨论在堆上分配的情况)4.对象经历YGC后,如果存活移动到S区,多次存活后晋升到老年代5.当对象不再被引用下一次GC,垃圾收集器会回收对象并释放其占用的内存。[需要看新机会的]1、顺便吆喝一句,技术大厂,待遇给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地缺人原理对象创建会在年轻代的E区分配内存,当失去引用后,变成垃圾存在E区中,随着程序运行E区不断创建对象,就会逐步塞满,这时候E区中绝大部分都是失去引用的垃圾对象,和一小部分正在运行中的线程产生的存活对象。这时候会触发YGC(Young Gc)回收年轻代。然后把存活对象都放入第一个S区域中,也就是S0区域,接着垃圾回收器就会直接回收掉E区里全部垃圾对象,在整个这个垃圾回收的过程中全程会进入Stop the Wold状态,系统代码全部停止运行,不允许创建新的对象。YGC结束后,系统继续运行,下一次如果E区满了,就会再次触发YGC,把E区和S0区里的存活对象转移到S1区里去,然后直接清空掉E区和S0区中的垃圾对象1.2 、那么对象什么时候去老年代呢?1.2.1、对象的年龄躲过15次YGC之后的对象晋升到老年代,默认是15,这个值可以通过-XX:MaxTenuringThreshold设置这个值设置的随意调整会有什么问题?现在java项目普遍采用Spring框架管理对象的生命周期。Spring默认管理的对象都是单例的,这些对象是长期存活的应该直接放到老年代中,应该避免它们在年轻代中来回复制。调大晋升阀值会导致本该晋升的对象停留在年轻代中,造成频繁YGC。但是如果设置的过小会导致程序中稍微存在耗时的任务,就会导致大量对象晋升到老年代,导致老年代内存持续增长,不要盲目的调整晋升的阀值。1.2.2、动态对象年龄判断JVM都会检查S区中的对象,并记录下每个年龄段的对象总大小。如果某个年龄段及其之前所有年龄段的对象总大小超过了S区的一半,则从该年龄段开始的所有对象在下一次GC时都会被晋升到老年代。假设S区可以容纳100MB的数据。在进行一次YGC后,JVM统计出如下数据:•年龄1的对象总共占用了10MB。•年龄2的对象总共占用了20MB。•年龄3的对象总共占用了30MB。此时,年龄1至3的对象总共占用了60MB,超过了S区一半的容量(50MB)。根据动态对象年龄判断规则,所有年龄为3及以上的对象在下一次GC时都将被晋升到老年代,而不需要等到它们的年龄达到15。(注意:这里S区指的是S0或者S1的空间,而不是总的S,总的在这里是200MB)这个机制使得JVM能够根据实际情况动态调整对象的晋升策略,从而优化垃圾收集的性能。通过这种方式,JVM尽量保持S区空间的有效利用,同时减少因年轻代对象过多而导致的频繁GC。1.2.3.大对象直接进入老年代如果对象的大小超过了预设的阈值(可以通过-XX:PretenureSizeThreshold参数设置),这个对象会直接在老年代分配,因为大对象在年轻代中经常会导致空间分配不连续,从而提早触发GC,避免在E区及两个S区之间来回复制,减少垃圾收集时的开销。1.2.4.临时晋升在某些情况下,如果S区不足以容纳一次YGC后的存活对象,这些对象也会被直接晋升到老年代,即使它们的年龄没有达到晋升的年龄阈值。这是一种应对空间不足的临时措施。1.3老年代的GC触发时机一旦老年代对象过多,就可能会触发FGC(Full GC),FGC必然会带着Old GC,也就是针对老年代的GC 而且一般会跟着一次YGC,也会触发永久代的GC,但具体触发条件和行为还取决于使用的垃圾收集器,文章的最后会简单的介绍下垃圾收集器。•Serial Old/Parallel Old当老年代空间不足以分配新的对象时,会触发FGC,这包括清理整个堆空间,即年轻代和老年代。•CMS当老年代的使用达到某个阈值(默认情况下是68%)时,开始执行CMS收集过程,尝试清理老年代空间。如果在CMS运行期间老年代空间不足以分配新的对象,可能会触发一次Full GC。 启动CMS的阈值参数:-XX:CMSInitiatingOccupancyFraction=75,-XX:+UseCMSInitiatingOccupancyOnly• G1G1收集器将堆内存划分为多个区域(Region),包括年轻代和老年代区域。当老年代区域中的空间使用率达到一定比例(基于启发式方法或者显式配置的阈值)默认45%时,G1会计划并执行Mixed GC,这种GC包括选定的一些老年代区域和所有年轻代区域的垃圾收集。Mixed GC的阈值参数-XX:InitiatingHeapOccupancyPercent=40,-XX:MaxGCPauseMillis=2002.JVM优化调优目标:2.1JVM调优指标•低延迟(Low Latency) :GC停顿时间短。•高吞吐量(High Throughput) :单位时间内能处理更多的工作量。更多的是CPU资源来执行应用代码,而非垃圾回收或其他系统任务。•大内存(Large Heap) :支持更大的内存分配,可以存储更多的数据和对象。在处理大数据集或复杂应用时尤为重要,但大内存堆带来的挑战是GC会更加复杂和耗时。但是不同目标在实现是本身时有冲突的,为什么难以同时满足?•低延迟 vs. 高吞吐量:要想减少GC的停顿时间,就需要频繁地进行垃圾回收,或者采用更复杂的并发GC算法,这将消耗更多的CPU资源,从而降低应用的吞吐量。•低延迟 vs. 大内存:大内存堆意味着GC需要管理和回收的对象更多,这使得实现低延迟的GC变得更加困难,因为GC算法需要更多时间来标记和清理不再使用的对象。•高吞吐量 vs. 大内存:虽然大内存可以让应用存储更多数据,减少内存管理的开销,但是当进行全堆GC时,大内存堆的回收过程会占用大量CPU资源,从而降低了应用的吞吐量。2.2如何权衡在实际应用中,根据应用的需求和特性,开发者和运维工程师需要在这三个目标之间做出权衡:2.2.1Web应用和微服务 - 低延迟优先场景描述:对于用户交互密集的Web应用和微服务,快速响应是提供良好用户体验的关键。在这些场景中,低延迟比高吞吐量更为重要。推荐收集器:大内存应用推荐G1,内存偏小可以使用CMS,CMS曾经是低延迟应用的首选,因其并发回收特性而被广泛使用。不过由于CMS在JDK 9中被标记为废弃,并在后续版本中被移除可以使用极低延迟ZGC或Shenandoah。这两种收集器都设计为低延迟收集器,能够在大内存堆上提供几乎无停顿的垃圾回收,从而保证应用的响应速度,但是支持这两个回收器的JDK版本较高,在JDK8版本还是CMS和G1的天下。2.2.2 大数据处理和科学计算 - 高吞吐量优先场景描述:大数据处理和科学计算应用通常需要处理大量数据,对CPU资源的利用率要求极高。这类应用更注重于高吞吐量,以完成更多的数据处理任务,而不是每个任务的响应时间。推荐收集器:Parallel GC。这是一种以高吞吐量为目标设计的收集器,通过多线程并行回收垃圾,以最大化应用吞吐量,非常适合CPU资源充足的环境。2.2.3. 大型内存应用 - 大内存管理优先场景描述:对于需要管理大量内存的应用,例如内存数据库和某些缓存系统,有效地管理大内存成为首要考虑的因素。这类应用需要垃圾回收器能够高效地处理大量的堆内存,同时保持合理的响应时间和吞吐量。推荐收集器:G1 GC或ZGC。G1 GC通过将堆内存分割成多个区域来提高回收效率,适合大内存应用且提供了平衡的延迟和吞吐量。ZGC也适合大内存应用,提供极低的延迟,但可能需要对应用进行调优以实现最佳性能。总结:JVM优化没有拿过来直接用的方案,所有好的JVM优化方案都是在当前应用背景下的,还是开头那句话 JVM调优不是常规手段,如果没有发现问题尽量不主动优化JVM,但是一定要了解应用的JVM运行情况,这时候好的监控就显得格外重要。那么好的JVM应该是什么样的呢?简单的说就是尽量让每次YGC后的存活对象小于S区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少FGC的频率,避免频繁FGC对JVM性能的影响。了解了JVM优化的基本原理之后,实战就需要在日常中积累了,墨菲定律我觉得在这个场景很适用,不要相信线上的机器是稳定的,如果观察到监控有异常,过一会可能恢复了就不了了之,要敢于去排查问题,未知的总是令人恐惧的,在排查的过程中会加深自己对JVM的理解的同时,也会对应用更有信心。
科叼
0 2 开源硬件平台
当一个前端学了很久的神经网络...
前言最近在学习神经网络相关的知识,并做了一个简单的猫狗识别的神经网络,结果如图。虽然有点绷不住,但这其实是少数情况,整体的猫狗分类正确率已经来到 90% 了。本篇文章是给大家介绍一下我是如何利用前端如何做神经网络-猫狗训练的。步骤概览还是掏出之前那个步骤流程,我们只需要按照这个步骤就可以训练出自己的神经网络处理数据集定义模型神经网络层数每层节点数每层的激活函数编译模型训练模型使用模型最终的页面是这样的[removed]顺便吆喝一句,技术大厂,待遇给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地有位置找到数据集,本次使用的是这个 www.kaggle.com/datasets/li… 2000 个猫图,2000 个狗图,足够我们使用(其实我只用了其中 500 个,电脑跑太慢了)由于这些图片大小不一致,首先我们需要将其处理为大小一致。这一步可以使用 canvas 来做,我统一处理成了 128 * 128 像素大小。 const preprocessImage = (img: HTMLImageElement): HTMLCanvasElement => { const canvas = document.createElement("canvas"); canvas.width = 128; canvas.height = 128; const ctx = canvas.getContext("2d"); if (!ctx) return canvas; // 保持比例缩放并居中裁剪 const ratio = Math.min(128 / img.width, 128 / img.height); const newWidth = img.width * ratio; const newHeight = img.height * ratio; ctx.drawImage( img, (128 - newWidth) / 2, (128 - newHeight) / 2, newWidth, newHeight ); return canvas; }; 这里可能就有同学要问了:imooimoo,你怎么返回了 canvas,不应该返回它 getImageData 的数据点吗。我一开始也是这样想的,结果 ai 告诉我,tfjs 是可以直接读取 canvas 的,牛。tf.browser.fromPixels() // 可以接受 canvas 作为参数将其处理为 tfjs 可用的对象 // 加载单个图片并处理为 tfjs 对应格式 const loadImage = async (category: "cat" | "dog", index: number): Promise => { const imgPath = `src/pages/cat-dog/image/${category}/${category}.${index}.jpg`; const img = new Image(); img.src = imgPath; await new Promise((resolve, reject) => { img.onload = () => resolve(img); img.onerror = reject; }); return { path: imgPath, element: img, tensor: tf.browser.fromPixels(preprocessImage(img)).div(255), // 归一化 label: category === "cat" ? 0 : 1, }; }; // 加载全部图片 const loadDataset = async () => { const images: ImageData[] = []; for (const category of ["cat", "dog"]) { for (let i = 1000; i [removed] { const model = tf.sequential({ layers: [ // 最大池化层:降低特征图尺寸,增强特征鲁棒性 tf.layers.maxPooling2d({ inputShape: [128, 128, 3], // 输入形状 [高度, 宽度, 通道数] poolSize: 2, // 池化窗口尺寸 2x2 strides: 2, // 滑动步长:每次移动 n 像素,使输出尺寸减小到原先的 1/n }), // 卷积层:用于提取图像局部特征 tf.layers.conv2d({ filters: 32, // 卷积核数量,决定输出特征图的深度 kernelSize: 3, // 卷积核尺寸 3x3 activation: "relu", // 激活函数:修正线性单元,解决梯度消失问题 padding: "same", // 边缘填充方式:保持输出尺寸与输入相同 }), // 展平层:将多维特征图转换为一维向量 tf.layers.flatten(), // 全连接层(输出层):进行最终分类 tf.layers.dense({ units: 2, // 输出单元数:对应猫/狗两个类别 activation: "softmax", // 激活函数:将输出转换为概率分布 }), ], }); // 编译模型,参数基本写死这几个就对了 model.compile({ optimizer: "adam", loss: "categoricalCrossentropy", metrics: ["accuracy"], }); console.log("模型架构:"); model.summary(); return model; }; 这里实际上只需要额外注意两点:卷积层的激活函数 activation: "relu",这里理论上是个非线性激活函数就行。但是我个人更喜欢 relu,函数好记,速度和效果又不错。输出层的激活函数 activation: "softmax",由于我们做的是分类,最后必须是这个。训练模型训练模型可以说的就不多了,也就是提供一下你的模型、训练集就可以开始了。这里有俩参数可以注意下epochs: 训练轮次validationSplit: 验证集比例,用于测算训练好的模型准确程度并优化下一轮的模型 // 训练模型 const trainModel = async ( model: tf.Sequential, xData: tf.Tensor4D, yData: tf.Tensor2D ) => { setTrainingLogs([]); // 清空之前的训练日志 await model.fit(xData, yData, { epochs: 10, // 训练轮数 batchSize: 4, validationSplit: 0.4, callbacks: { onEpochEnd: (epoch, logs) => { if (!logs) return; setTrainingLogs((prev) => [ ...prev, { epoch: epoch + 1, loss: Number(logs.loss.toFixed(4)), accuracy: Number(logs.acc.toFixed(4)), }, ]); }, }, }); }; 整体页面基本就是这样了,稍微写一下页面,基本就完工了总结别慌,神经网络没那么可怕,核心步骤就那几步,冲冲冲。源码:github.com/imoo666/neu…——转载自作者:imoo
科叼
4 9 开源硬件平台
async/await 必须使用 try/catch 吗?
前言在 JavaScript 开发者的日常中,这样的对话时常发生:- 👨💻 新人:"为什么页面突然白屏了?"- 👨🔧 老人:"异步请求没做错误处理吧?"async/await 看似优雅的语法糖背后,隐藏着一个关键问题:错误处理策略的抉择。在 JavaScript 中使用 `async/await` 时,很多人会问: “必须使用 try/catch 吗?”其实答案并非绝对,而是取决于你如何设计错误处理策略和代码风格。接下来,我们将探讨 async/await 的错误处理机制、使用 try/catch 的优势,以及其他可选的错误处理方法。 async/await 的基本原理异步代码的进化史// 回调地狱时代 fetchData(url1, (data1) => { process(data1, (result1) => { fetchData(url2, (data2) => { // 更多嵌套... }) }) }) // Promise 时代 fetchData(url1) .then(process) .then(() => fetchData(url2)) .catch(handleError) // async/await 时代 async function workflow() { const data1 = await fetchData(url1) const result = await process(data1) return await fetchData(url2) } async/await 是基于 **Promise 的语法糖**,它使异步代码看起来**更像同步代码**,从而更易读、易写。一个 async 函数总是返回一个 Promise,你可以在该函数内部使用 await 来等待异步操作完成。如果在异步操作中出现错误(例如网络请求失败),该错误会使 Promise 进入 rejected 状态。async function fetchData() { const response = await fetch("https://api.example.com/data"); const data = await response.json(); return data; } [removed]顺便吆喝一句,技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费) 前、后端/测试,多地有空位,感兴趣的可以[试试] 个比喻,就好比**铁路信号系统**想象 async 函数是一列高速行驶的列车:- await 是轨道切换器:控制代码执行流向- 未捕获的错误如同脱轨事故:会沿着铁路网(调用栈)逆向传播- try/catch 是智能防护系统: - 自动触发紧急制动(错误捕获) - 启动备用轨道(错误恢复逻辑) - 向调度中心发送警报(错误日志)为了优雅地捕获 async/await 中出现的错误,通常我们会使用 try/catch 语句。这种方式**可以在同一个代码块中捕获抛出的错误**,使得错误处理逻辑更集中、直观。- 代码逻辑集中,错误处理与业务逻辑紧密结合。- 可以捕获多个 await 操作中抛出的错误。- 适合需要在出错时进行统一处理或恢复操作的场景。async function fetchData() { try { const response = await fetch("https://api.example.com/data"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error("Error fetching data:", error); // 根据需要,可以在此处处理错误,或者重新抛出以便上层捕获 throw error; } } 不使用 try/catch 的替代方案虽然 try/catch 是最直观的错误处理方式,但你也可以不在 async 函数内部使用它,而是**在调用该 async 函数时捕获错误**。在 Promise 链末尾添加 `.catch()`async function fetchData() { const response = await fetch("https://api.example.com/data"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 调用处使用 Promise.catch 捕获错误 fetchData() .then(data => { console.log("Data:", data); }) .catch(error => { console.error("Error fetching data:", error); }); 这种方式将错误处理逻辑移至函数调用方,适用于以下场景:- 当多个调用者希望以不同方式处理错误时。- 希望让 async 函数保持简洁,将错误处理交给全局统一的错误处理器(例如在 React 应用中可以使用 Error Boundary)。将 `await` 与 `catch` 结合async function fetchData() { const response = await fetch('https://api.example.com/data').catch(error => { console.error('Request failed:', error); return null; // 返回兜底值 }); if (!response) return; // 继续处理 response... } 全局错误监听(慎用,适合兜底)// 浏览器端全局监听 window.addEventListener('unhandledrejection', event => { event.preventDefault(); sendErrorLog({ type: 'UNHANDLED_REJECTION', error: event.reason, stack: event.reason.stack }); showErrorToast('系统异常,请联系管理员'); }); // Node.js 进程管理 process.on('unhandledRejection', (reason, promise) => { logger.fatal('未处理的 Promise 拒绝:', reason); process.exitCode = 1; }); 错误处理策略矩阵 决策树分析错误处理体系1. 基础层:80% 的异步操作使用 try/catch + 类型检查1. 中间层:15% 的通用错误使用全局拦截 + 日志上报1. 战略层:5% 的关键操作实现自动恢复机制 小结我的观点是:不强制要求,但强烈推荐- 不强制:如果不需要处理错误,可以不使用 `try/catch`,但未捕获的 Promise 拒绝(unhandled rejection)会导致程序崩溃(在 Node.js 或现代浏览器中)。- 推荐:90% 的场景下需要捕获错误,因此 `try/catch` 是最直接的错误处理方式。所有我个人观点:使用 async/await 尽量使用 try/catch。好的错误处理不是消灭错误,而是让系统具备优雅降级的能力。你的代码应该像优秀的飞行员——在遇到气流时,仍能保持平稳飞行。大家如有不同意见,还请评论区讨论,说出自己的见解。 ———转载自作者:雨夜寻晴天
科叼
0 0 开源硬件平台
Java 泛型中的通配符 T,E,K,V,?有去搞清楚吗?
前言不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。泛型有什么用?在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。 Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。类型安全:编译是检查类型是否匹配,避免了ClassCastexception的发生。 // 非泛型写法(存在类型转换风险) List list1 = new ArrayList(); list1.add("a"); Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException // 泛型写法(编译时检查类型) List list2 = new ArrayList[removed](); // list.add(1); // 编译报错 list2.add("a"); String str = list2.get(0); // 无需强制转换 消除代码强制类型转换:减少了一些类型转换操作。 // 非泛型写法 Map map1 = new HashMap(); map1.put("user", new User()); User user1 = (User) map1.get("user"); // 泛型写法 Map map2 = new HashMap[removed](); map2.put("user", new User()); // 自动转换 User user2 = map2.get("user"); 3.代码复用:可以支持多种数据类型,不要重复编写代码,例如:我们常用的统一响应结果类。 @Data @NoArgsConstructor @AllArgsConstructor public class Result { /** * 响应状态码 */ private int code; /** * 响应信息 */ private String message; /** * 响应数据 */ private T data; /** * 时间戳 */ private long timestamp; 其他代码省略... 增强可读性:通过类型参数就直接能看出要填入什么类型。 List list = new ArrayList[removed](); [removed]顺便吆喝一句,技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地有空位,感兴趣的可以戳试试~~泛型里的通配符我们在使用泛型的时候,经常会使用或者看见多种不同的通配符,常见的 T,E,K,V,?这几种,相信大家一定不陌生,但是真的问你他们有什么作用?有什么区别时,很多人应该是不能很好的介绍它们的,接下来我就来给大家介绍介绍。T,E,K,VT(Type) T表示任意类型参数,我们举个例子 pubile class A{ prvate T t; //其他省略... } //创建一个不带泛型参数的A A a = new A(); a.set(new B()); B b = (B) a.get();//需要进行强制类型转换 //创建一个带泛型参数的A A a = new A(); a.set(new B()); B b = a.get(); E(Element) E表示集合中的元素类型 List list = new ArrayList[removed](); K(Key) K表示映射的键的数据类型 Map map = new HashMap[removed](); V(Value) V表示映射的值的数据类型 Map map = new HashMap[removed](); 通配符 ?无界通配符 表示未知类型,接收任意类型 // 使用无界通配符处理任意类型的查询结果 public void logQueryResult(List resultList) { resultList.forEach(obj -> log.info("Result: {}", obj)); } 上界通配符 表示类型是T或者是子类 // 使用上界通配符读取缓存 public T getCache(String key, Class clazz) { Object value = redisTemplate.opsForValue().get(key); return clazz.cast(value); } 下界通配符 表示类型是T或者是父类 // 使用下界通配符写入缓存 public void setCache(String key, value) { redisTemplate.opsForValue().set(key, value); } 总结我们在很多时候只是单纯的会使用某些技术,但是对它们里面许许多多常见的都是一知半解的,只是会使用确实很重要,但是如果有时间,我们不妨好好的在对这些技术进行深入学习,不仅知其然,而且知其所以然,这样我们的技术才会不断提升进步。——转载自作者:镜花水月linyi
科叼
1 2 开源硬件平台
Java利用Deepseek进行项目代码审查
一、为什么需要AI代码审查?写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。 二、环境准备(5分钟搞定)1. 安装Deepseek插件(以VSCode为例):- 插件市场搜索"Deepseek Code Review"- 点击安装(就像安装手机APP一样简单)1. Java项目配置: com.deepseek code-analyzer 1.3.0 [removed]顺便吆喝一句,技术大厂,待遇之类的给的还可以!前、后端/测试,多地有空位,偶尔有加班(放心,加班有加班费),感兴趣的可以[试试][试试]~~三、真实案例:用户管理系统漏洞检测 原始问题代码:public class UserService { // 漏洞1:未处理空指针 public String getUserRole(String userId) { return UserDB.query(userId).getRole(); } // 漏洞2:资源未关闭 public void exportUsers() { FileOutputStream fos = new FileOutputStream("users.csv"); fos.write(getAllUsers().getBytes()); } // 漏洞3:SQL注入风险 public void deleteUser(String input) { Statement stmt = conn.createStatement(); stmt.execute("DELETE FROM users WHERE id = " + input); } } 使用Deepseek审查后:智能修复建议:1. 空指针防护 → 建议添加Optional处理1. 流资源 → 推荐try-with-resources语法1. SQL注入 → 提示改用PreparedStatement 修正后的代码:public class UserService { // 修复1:Optional处理空指针 public String getUserRole(String userId) { return Optional.ofNullable(UserDB.query(userId)) .map(User::getRole) .orElse("guest"); } // 修复2:自动资源管理 public void exportUsers() { try (FileOutputStream fos = new FileOutputStream("users.csv")) { fos.write(getAllUsers().getBytes()); } } // 修复3:预编译防注入 public void deleteUser(String input) { PreparedStatement pstmt = conn.prepareStatement( "DELETE FROM users WHERE id = ?"); pstmt.setString(1, input); pstmt.executeUpdate(); } } 四、实现原理揭秘Deepseek的代码审查就像"X光扫描仪",通过以下三步工作:1. **模式识别**:比对数千万个代码样本- 就像老师批改作业时发现常见错误1. **上下文理解**:分析代码的"人际关系"- 数据库连接有没有"成对出现"(打开/关闭)- 敏感操作有没有"保镖"(权限校验)1. **智能推理**:预测代码的"未来"- 这个变量走到这里会不会变成null?- 这个循环会不会变成"无限列车"?五、进阶使用技巧1. 自定义审查规则(配置文件示例):rules: security: sql_injection: error performance: loop_complexity: warning style: var_naming: info 2. 与CI/CD集成(GitHub Action示例):- name: Deepseek Code Review uses: deepseek-ai/code-review-action@v2 with: severity_level: warning fail_on: error 六、开发者常见疑问Q:AI会不会误判我的代码?A:就像导航偶尔会绕路,Deepseek给出的是"建议"而非"判决",最终决策权在你手中Q:处理历史遗留项目要多久?A:10万行代码项目约需3-5分钟,支持增量扫描七、效果对比数据指标人工审查Deepseek+人工平均耗时4小时30分钟漏洞发现率78%95%误报率5%12%知识库更新速度季度实时 ——转载自作者:Java技术小馆
科叼
2 6 立创开发板
URL地址末尾加不加”/“有什么区别
URL 结尾是否带 / 主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下:1. 基础概念URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。目录 vs. 资源:以 / 结尾的 URL 通常表示目录,例如: https://example.com/folder/ 不以 / 结尾的 URL 通常指向具体的资源(如文件),例如: https://example.com/file 2. 带 / 和不带 / 的具体区别(1)目录 vs. 资源https://example.com/folder/服务器通常会将其解析为 目录,并尝试返回该目录下的默认文件(如 index.html)。https://example.com/folder服务器可能会将其视为 文件,如果 folder 不是文件,而是目录,服务器可能会返回 301 重定向到 folder/。📌 示例:访问 https://example.com/blog/服务器可能返回 https://example.com/blog/index.html。访问 https://example.com/blog(如果 blog 是个目录)服务器可能重定向到 https://example.com/blog/,再返回 index.html。(2)相对路径解析URL 末尾是否有 / 会影响相对路径的解析。假设 HTML 页面包含以下 标签: 📌 示例:访问 https://example.com/folder/图片路径解析为 https://example.com/folder/image.png访问 https://example.com/folder图片路径解析为 https://example.com/image.png可能导致 404 错误,因为 image.png 在 folder/ 里,而浏览器错误地去 example.com/ 下查找。原因:以 / 结尾的 URL,浏览器会认为它是一个目录,相对路径会基于 folder/ 解析。不带 /,浏览器可能认为 folder 是文件,相对路径解析可能会出现错误。(3)SEO 影响搜索引擎对 https://example.com/folder/ 和 https://example.com/folder 可能会视为两个不同的页面,导致 重复内容问题,影响 SEO 排名。因此:网站通常会选择 一种形式 并用 301 重定向 规范化 URL。例如:https://example.com/folder 自动跳转 到 https://example.com/folder/。反之亦然。(4)API 请求对于 RESTful API,带 / 和不带 / 可能导致不同的行为:https://api.example.com/users可能返回所有用户数据。https://api.example.com/users/可能返回 404 或者产生不同的结果(取决于服务器实现)。一些 API 服务器对 / 非常敏感,因此最好遵循 API 文档的规范。[removed]顺便吆喝一句,技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地有空位,感兴趣的可以戳试试~~3. 总结URL形式 作用 影响https://example.com/folder/ 目录 通常返回 folder/ 下的默认文件,如 index.html,相对路径解析基于 folder/https://example.com/folder资源(或重定向) 可能被解析为文件,或者服务器重定向到 folder/,相对路径解析可能错误https://api.example.com/data/ API 路径 可能与 https://api.example.com/data 表现不同,具体由 API 设计决定如果你在开发网站,建议:统一 URL 规则,例如所有目录都加 / 或者所有请求都不加 /,然后用 301 重定向 确保一致性。测试 API 的行为,确认带 / 和不带 / 是否影响请求结果。——转载自作者:Chiyamin
科叼
0 0 开源硬件平台
从 DeepSeek 看25年前端的一个小趋势
最近DeepSeek R1爆火。有多火呢?连我爷爷都用上了,还研究起提示词工程来了。大模型不断发展对我们前端工程师有什么长远影响呢?本文聊聊25年前端会有的一个小趋势。模型进步的影响像DeepSeek R1这样的推理模型和一般语言模型(类似Claude Sonnet、GPT-4o、DeepSeek-V3)有什么区别呢?简单来说,推理模型的特点是:推理能力强,但速度慢、消耗高。他比较适合的场景比如:Meta Prompting(让推理模型生成或修改给一般语言模型用的提示词)路径规划等等这些应用场景主要利好AI Agent。再加上一般语言模型在生成效果、token上下文长度上持续提升。可以预见,类似Cursor Composer Agent这样的AI Agent在25年能力会持续提升,直到成为开发标配。这会给前端工程师带来什么进一步影响呢?[removed]技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地有空位,感兴趣的可以试试抽象得理解为应用压缩算法,什么意思呢?以Cursor Composer Agent举例:我们传入:描述应用状态的提示词描述应用结构的应用截图AI Agent帮我们生成应用代码。同样,也能反过来,让AI Agent根据应用代码帮我们生成描述应用的提示词。从左到右可以看作是解压算法,从右往左可以看作是压缩算法。就像图片的压缩算法存在失真,基于AI Agent抽象的应用压缩算法也存在失真,也就是生成的效果不理想。随着上文提到的AI Agent能力提高(背后是模型能力提高、工程化的完善),应用压缩算法的失真率会越来越低。这会带来什么进一步的影响呢?对开发的影响如果提示词(经过AI Agent)就能准确表达想要的代码效果,那会有越来越多原本需要用代码表达的东西被用提示词表达。比如,21st.dev的组件不是通过npm,而是通过提示词引入。相当于将引入组件的流程从:开发者 -> 代码变成了:开发者 -> 提示词 -> AI Agent -> 代码再比如,CopyCoder是一款上传应用截图,自动生成应用提示词的应用。当你上传应用截图后,他会为你生成多个提示词文件。其中.setup描述AI Agent需要执行的步骤,其他文件是描述应用实现细节的结构化提示词这个过程相当于根据应用截图,将应用压缩为提示词。很自然的,反过来我们就能用AI Agent将这段提示词重新解压为应用代码。这个过程在25年会越来越丝滑。这会造成的进一步影响是:越来越多前端开发场景会被提炼为标准化的提示词,比如:后台管理系统官网活动页前端开发的日常编码工作会越来越多被上述流程取代。你可能会说,当前AI生成的代码效果还不是很好。但请注意,我们谈的是趋势。当你日复一日做着同样的业务时,你的硅基对手正在每年大跨步进步。总结随着基础模型能力提高,以及工程化完善,AI Agent在25年会逐渐成为开发标配。作为应用开发者(而不是算法工程师),我们可以将AI Agent抽象得理解为应用压缩算法。随着时间推移,这套压缩算法的失真率会越来越低。届时,会有越来越多原本需要用代码表达的东西被用提示词表达。这对前端工程师来说,既是机遇也是挑战。——转载自作者:魔术师卡颂
科叼
0 2 开源硬件平台
这样的SQL太吓人了
很多小伙伴看到了能够快速发现问题,当 company_id 为 null 的时候,会导致全表更新。但是也有小伙伴不解,自己平时就是这么写的呀,也没什么问题,如果有问题,那么上面的 SQL 该怎么改呢?松哥来和大家简单聊几句。一 防止全表更新如果在生产环境中使用 UPDATE 语句更新表数据,此时如果忘记携带本应该添加的 WHERE 条件,那么后果不堪设想。那么怎么避免这个问题呢?二 sql_safe_updatessql_safe_updates 是 MySQL 数据库中的一个参数,它的作用是增强数据安全性,防止因误操作导致的数据丢失或破坏。具体来说,当 sql_safe_updates 设置为 ON(启用)时,MySQL 将阻止执行没有明确 WHERE 子句的 UPDATE 或 DELETE 语句。这意味着如果试图运行一个不包含 WHERE 条件来限定更新或删除范围的 DML 语句,MySQL 会抛出一个错误。而当 sql_safe_updates 设置为 OFF(禁用)时,MySQL 不会对此类无条件更新或删除操作进行特殊限制,允许它们按常规方式执行这个参数可以配置在会话级别或全局级别。在会话级别,可以通过执行 SET sql_safe_updates = 1; 命令来启用,这只对当前连接有效。在全局级别,可以通过 SET GLOBAL sql_safe_updates = 1; 命令或在 MySQL 配置文件中设置,这会影响服务器上所有新的会话,但是这个配置不会修改当前会话。启用 sql_safe_updates 参数可以减少因人为失误引发的重大数据事故,尤其适合开发环境和对数据完整性要求严格的生产环境。我们可以先执行 SHOW VARIABLES LIKE '%sql_safe_updates%'; 查看当前配置:然后执行 SET sql_safe_updates = 1; 去更新,更新之后再去查看配置,发现 sql_safe_updates 就已经开启了:这个时候,假设我们执行如下 SQL:UPDATE user set username='javaboy'; 就会报一个错误:需要注意的是,启用 sql_safe_updates 参数可能会影响现有应用程序的正常运行,特别是那些依赖于无条件更新或删除操作的程序,因此在生产环境中启用之前,必须确保所有相关的应用程序代码已经过严格审查和适配。看新机会的技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)前后端测试捞人,多地有空位,感兴趣的可以试试~三 SQL 插件MyBatis-Plus 提供了一个非法 SQL 拦截插件叫做 IllegalSQLInnerInterceptor。这是 MyBatis-Plus 框架中的一个安全控制插件,用于拦截和检查非法 SQL 语句。这个插件主要提供了四方面的功能:识别并拦截特定类型的 SQL 语句,如全表更新、删除等高风险操作。确保在执行查询时使用索引,以提高性能并避免全表扫描。防止未经授权的全表更新或删除操作,减少数据丢失风险。对包含 not、or 关键字或子查询的 SQL 语句进行额外检查,以防止逻辑错误或性能问题。插件用法也简单,配置一个 Bean 即可:@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加非法SQL拦截器 interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor()); return interceptor; } } 配置完成后,如果执行了不带 where 条件的 update 或者 delete 语句,就会报如下错误。但是!!!如果你的 SQL 后面有个 where 1=1,那么这样的 SQL 是不会被 IllegalSQLInnerInterceptor 插件识别并拦截的。四 、Code Review日常的 Code Review 也不可少,很多问题都是在 CR 的时候发现的。五、 问题解决除了上面提到的各种办法之外,对于本文一开始提出的问题,这个有问题的 SQL 还可以做哪些修改呢?欢迎小伙伴们评论区给出自己的答案~——转载自作者:江南一点雨
科叼
3 6 开源硬件平台
DeepSeek 还挺会找你Bug的
前言今天试试DeepSeek都能帮我识别到哪些bug,看看DeepSeek 实力如何。 插件我用的是MarsCode,模型选择 DeepSeek R1,今天看看都有哪些严重bug吧,这可比人工复查方便多了。DeepSeek Code Review 使用感受使用的是 MarsCode fix 功能,检查了两个项目,一个是线上运行的项目,一个是正在开发调试的项目。检测出最多的问题就是NPE,这个东西也是java程序员遇到过最多的一个错误。很多地方实际上从业务上就避免了NPE这个东西对于初级开发👦来说在调试过程中用处非常大🚀,还有就是我们在开发过程帮我检测XML还是不错的,我发现,对于中高级开发程序员🧓来说,xml 是最容易出现问题的,因为集成工具不能直接检测到一些语法问题。当然,AI给出的建议还是需要我们具备辨别的能力,下面看看我用AI 扫描出的一些问题,以及AI 给出的一些错误建议看新机会的技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)前后端测试捞人,多地有空位,感兴趣的可以试试~发现的问题1. 导出接口FileInputStream未正确关闭当然还有很多问题,其他问题从业务上应该是不会报错,这个inputStream 关闭方式确实 存在很大问题,和业务无关,纯技术问题。 public void exportData(Long exportRecordId) throws Exception { ExportRecord exportRecord = exportRecordService.getById(exportRecordId); try { ................... 查询数据................ IPage page = service.queryPage(req); String excelName = "本年记录" + System.currentTimeMillis() + ".xlsx"; String excelNameFilePath = ExportConstant.EXPORT_TEMP_FILE_PATH_STAR_CURRENT_APPLY + excelName; File file = new File(excelNameFilePath); if (file.exists()) { FileUtil.del(file); } else { FileUtil.touch(file); } .......................封装数据............. // 5.4、读写内容到excel。 EasyExcel.write(file).head(headerList).sheet().doWrite(rowsList); // 6、将excel文件上传文件服务器。 InputStream inputStream = new FileInputStream(file); // 上传到minio,upload 方法有关闭 inputStream.close(),没有处理IOException String uploadFilePath = MinioUtil.upload(inputStream, excelNameFilePath.substring(1)); // 7、最后删除临时文件。 FileUtil.del(file); }catch (Exception e){ e.printStackTrace(); log.error("导出失败",e); } } 2.XML 中的SQL问题最近不是在裁员么,昨天临时接手了一个半成品项目,发现SQL 在报错,于是就扫描了一下。果然没有测试的代码,用AI测试还是挺不错的。直接帮我把问题都扫描出来了AI主要修复点说明:连接条件错误:原c.district_industry_org_id = c.id改为c.district_industry_org_id = d.id,确保正确关联组织表多余条件语法:改为,移除无效的and判断错误排序字段:order by a.status改为a.assessment_result,保持与查询字段一致简化CDATA语法:统一使用更简洁的||连接符,移除冗余的CDATA声明3.错误的异常抛出 if (!allDeptsNotEmpty) { // 如果有任意一个deptId为空,抛出异常或返回错误信息 throw new JedisDataException("部门需要全部配置"); } AI修复说明如下: 错误的解决方案事务不完整提醒了我加事务,但是事务方法不能 用 private,并且我这个方法还是本类的 this 调用的like 查询优化建议,该语法在某些库不支持 and company like ('%'||#{req.company}||'%') AI优化建议: and company like concat('%', #{req.company}, '%') concat 在某些库不支持两个参数总结希望大家在开发过程中,经常使用AI插件 FIX的功能,能减少很多粗心大意 引起的bug,能提供我们自测 和 联调的效率。最后AI的优化建议,也有可能发生错误,所以我们得具备辨别的能力。——转载自作者:提前退休的java猿#畅聊专区#
科叼
2 5 开源硬件平台
GPT-o3超过99.9%的程序员,码农们何去何从?
最近OpenAI一连搞了十几天的新品直播,虽然热度不是特别高,但是也确实爆出了一些比较有意思的东西,比如GPT o3,据说编程能力已经超过 99.9%的程序员(某程序员竞赛的前200名:codeforces.com/ratings),我等码农是不是要哭晕在厕所了?程序员的出路在哪里?AI编程革命谈到这一轮的AI革命,很多人会类比工业革命。在工业革命初期,随着机器的普及应用,很多手工业者丢了生计,但也有一批手动业者主动进化,学会了机器的操作方法,在新的时代里依然抢手。所以最新的说法是:AI不会革所有人的命,AI革的是不会使用AI的人的命。这种说法是站的住脚的。就我个人而言,写代码已经离不开AI了,遇到不会写的代码就问AI已经是家常便饭,甚至有了什么思路之后,直接就让AI先写一个实现,感觉不好的话,再让AI重写,一般都能得出满意的结果。这比自己苦思冥想、到处去查资料要快上不少,甚至结果也往往更好。当然这里边还有一些前提条件,比如你要把自己的想法清晰的表达出来。就像人们对商品的需求是广泛且持续存在的,软件的需求也是广泛且持续存在的。使用AI可以提升软件程序的生产效率,如果你花1天时间可以写出100分的程序,那么使用AI,很可能只需要几分钟的时间就能写出至少80分的程序,这里边的生产力差距是很明显的,所以AI对于编程而言,自然也是一场革命。历史证明,不想被淘汰,只能积极的拥抱新的生产工具。学会使用AI编程,自然也成了程序员在AI时代的必备能力。AI编程的要点就像大家都写程序,有的人写的好,有的人写不出来。使用AI编程也是,有的人效率大幅提升,有的人生成的代码没法用,还耽误时间。为什么会这样?我个人的观点是:不得要领。编写代码的要领是:掌握各种编程规则,并在各种解决方案中做好权衡。很多同学以代码能跑起来为最终目标,不管性能,也不考虑维护、扩展,这样很难写出优秀的程序,自然也成不了高手。AI编程的要领是什么呢?讲清楚我们还是在编程,所以编程的要领还是要掌握,但是又增加了AI的方式,就需要结合AI的特性。这里边最主要的问题就是讲不清楚,程序员们往往知道怎么去实现,但是很难给别人讲明白。而使用生成式AI,一个很重要的工作是编写提示词,也就是把你想要的东西通过文字清晰明白的表达出来。举个简单的例子,你想要让AI设计一个商城的数据库,如果你只是简单的告诉它:帮我设计一个商城的数据库。它很大概率上是很难生成直接可用的代码的,因为它不知道你要使用什么数据库,也不知道你的商品和订单是怎么管理的,更不清楚你的订单量是什么样的规模,这些都会影响数据库的设计。AI大概率会给你了一个路边摊的方案,然后被你鄙视一番。要想让AI输出高质量的内容,我们必须把问题的上下文讲清楚,以AI能够理解的方式告诉它。在数据库的例子中,可能包括:你的商城都有哪些功能,商品品类如何组织,商品有哪些属性,每天的订单量如何,订单的流转过程如何,等等。有时候对于一个复杂的系统,一轮对话是不够的,可能需要多轮对话,比如先讲一下你的商城功能有哪些、业务规模如何,然后让AI给你一些数据库的选型建议;然后再基于你选择的数据库,通过各个部分的描述,让AI给出具体领域的设计。好模型工欲善其事,必先利其器。AI模型的性能也很重要,这里说的模型性能是AI模型生成结果的质量问题。了解过大语言模型的同学应该都听说过OpenAI,作为大语言模型的领导者,它的ChatGPT是顶好的,对于同样的问题,ChatGPT往往能理解的更为准确,也能给出质量更高的答案。对于编程来说,就是它输出的代码能正常跑起来,而很多模型给出的答案经常跑不起来,各种报错。根据我的测试体验,目前 GPT 4o 和 Claude 3.5 Sonnet 在编程方面的能力比较领先,国产的 通义灵码 也不错,但是有时给出的方案还是差强人意,有点路边摊的感觉,输出过多不太重要的东西,沟通效率上差一些,应该还是基础模型的能力不够强。当下,从效率方面看,使用更好的模型确实可以节约一些时间。程序员的未来​[removed]
科叼
6 11 开源硬件平台
async/await 你可能正在将异步写成同步
前言你是否察觉到自己随手写的异步函数,实际却是“同步”的效果!正文以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。第一版思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。import path from 'node:path' import fs from 'node:fs/promises' import { existsSync } from 'node:fs' async function findFiles(root) { if (!existsSync(root)) return const rootStat = await fs.stat(root) if (rootStat.isFile()) return [root] const result = [] const find = async (dir) => { const files = await fs.readdir(dir) for (let file of files) { file = path.resolve(dir, file) const stat = await fs.stat(file) if (stat.isFile()) { result.push(file) } else if (stat.isDirectory()) { await find(file) } } } await find(root) return result } 机智的你是否已经发现了问题?我们递归查询子目录的过程是不需要等待上一个结果的,但是第 20 行代码,只有查询完一个子目录之后才会查询下一个,显然让并发的异步,变成了顺序的“同步”执行。那我们去掉 20 行的 await 是不是就可以了,当然不行,这样的话 await find(root) 在没有完全遍历目录之前就会立刻返回,我们无法拿到正确的结果。思考一下,怎么修改它呢?......让我们看第二版代码。[removed]第二版import path from 'node:path' import fs from 'node:fs/promises' import { existsSync } from 'node:fs' async function findFiles(root) { if (!existsSync(root)) return const rootStat = await fs.stat(root) if (rootStat.isFile()) return [root] const result = [] const find = async (dir) => { const task = (await fs.readdir(dir)).map(async (file) => { file = path.resolve(dir, file) const stat = await fs.stat(file) if (stat.isFile()) { result.push(file) } else if (stat.isDirectory()) { await find(file) } }) return Promise.all(task) } await find(root) return result } 我们把每个子目录内容的查询作为独立的任务,扔给 Promise.all 执行,就是这个简单的改动,性能得到了质的提升,让我们看看测试,究竟能差多少。对比测试console.time('v1') const files1 = await findFiles1('D:\\Videos') console.timeEnd('v1') console.time('v2') const files2 = await findFiles2('D:\\Videos') console.timeEnd('v2') console.log(files1?.length, files2?.length) 版本二快了三倍不止,如果是并发的接口请求被不小心搞成了顺序执行,差距比这还要夸张。——转载自作者:justorez
科叼
0 3 开源硬件平台
我说数据分页用Limit,面试官直接让我回去等消息
最近我碰到了一个挺有趣的“小插曲”,大概是这样的:现在有一个社交应用,在聊天界面中,用户可以通过下滑页面来不断加载历史消息。我当时想不就一个分页,这么简单的需求怎么能难倒我这个练习时长两年半的SQL boy,我直接一个安一个limit上去直接就把这个问题解决了,写出来的SQL大概是这样的:select * from message order by create_time desc limit n_page*50 50; 看了两眼,好像没有什么问题,然后就跑去忙别的了。问题浮现等到了下午,我忙完了手上的其他需求,准备开始对这个功能进行一个简单的压测,然后就可以美美的把代码提交,去博德之门里扔几把骰子。但是随着压测的进行,我发现这个接口的响应时间波动比较剧烈,一开始我还没当回事,认为是数据库压力太大导致的性能下 降,但是紧接着我就发现了这些性能较差的接口都具有较大的n_page,同时普遍耗时都在其他接口的10倍以上这一下可就不能当做没看见了(悲伤痛哭)。算了,发现问题那就解决问题,于是经过我的一番查找,很快我就发现,原来这是全是深分页惹的祸!深分页问题那么什么是深分页问题呢? 在MySQL的limit中:limit 100,10,MySQL会根据查询条件去存储引擎层找到前110条记录,然后在server层丢弃前100条记录取最后10条 这样先扫描完再丢弃的记录相当于白找,深分页问题指的就是这种场景(当limit的偏移量过大时会导致性能开销)。 那么要如何解决这个问题呢,现在比较多的实践是使用游标分页。游标分页游标分页的思想说起来也简单,既然采用limit 100,10这样的分页方式会导致MySQL从头开始扫描,从而扫描到没用的前100条记录,那么如果我们能够直接从第101条开始扫描,扫描10条,那么这个问题不就解决了。那么应该怎么实现呢,简单,在扫描时加一个条件不就得了select * from message where create_time<上一次扫描到的最后一条记录的create_time order by create_time desc limit 10; 实际上游标翻页只是通过额外添加一个条件来改变MySQL扫描的起始位置。这也导致了游标翻页的使用必须要满足两个前提:需要一个列来记录上一次查询到的最大值,并且这个列要是有序的(比如我们上面用到的create_time)每一次的查询会依赖上一次查询的最大值,因此,分页查询需要时连续的,不能进行跳页(比如查完第一页然后跳过第二页直接查第三页)接下来让我们来做一个简单实验,验证一下游标分页的有效性。为了凸显效果,我们直接给表里整进去100万数据(这会花费很多时间,你可以自行减少记录数量)CREATE TABLE `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `empty_body` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT NOW(), PRIMARY KEY (`id`) )ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='测试表'; DROP PROCEDURE IF EXISTS my_insert; create procedure my_insert() BEGIN declare i int default 1; set autocommit = 0; while i [removed]1000000 limit 0,10; 可以看到,使用游标翻页的速度远远快于直接使用limit进行分页。被忽略的问题在上面我们证明了游标分页可以解决limit分页带来的性能问题,但是其实还有隐藏的,limit分页无法解决的问题:消息重复问题。让我们思考一下,如果我们在使用limit分页时,有新消息到来会怎么样。没错!由于limit分页是根据和最新记录的差值来计算记录位置的,所以如果有新消息到达,那么在读取下一页的时候,就有可能读取到和上一页重复的消息。但是由于游标翻页采用的是固定的游标,即使有新消息到达也不会读取到重复的消息。[removed]总结游标分页非常适合于深分页的场景,能够大大减少需要的记录数量,但是由于游标依赖于上一次查询,所以无法实现跳页,并且只有作为游标的列有序的情况下才能使用游标。而且这个解决方案可以很容易的迁移到项目中任何需要分页的地方,非常适合作为面试时的技术亮点。——转载自作者:单木
科叼
0 12 开源硬件平台
这样代码命名,总不会被同事蛐蛐了吧
1. 引言....又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是草头底下一个来回的回字么?”孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说,“对呀对呀!……回字有四样写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚用指甲蘸了酒,想在柜上写字,见我毫不热心,便又叹一口气,显出极惋惜的样子针对于同一个代码变量或者函数方法,张三可能认为可以叫 xxx,李四可能摇头说 不不不,得叫 yyyy ,好的命名让人如沐春风,原来是这个意思;坏的代码命名,同事可能会眉头紧锁,然后送你两斤熏鸡骨头让你炖汤比如隔壁小组新来的一个同事,对字符串命名就用 s,对于布尔值的命名就用 b,然后他的主管说他的变量名起的跟他人一样。如何做到信雅达的命名,让同事不会再背后蛐蛐,我是这样想的。[removed]2. 代码整洁之道2.1 团队规范“我在上家公司都是这样命名的,在这里我也要这样命名”小组里张三给 Service 起的名字叫 UserService 实现类是 UserServiceImpl;小组里李四给 Service 起的名字叫 CustomerService 实现类 CustomerServiceImpl你跳出来出来说,统统不对,接口需要区分对待 得叫 IUserService 和 ICustomerService但是组里成员都不习惯往接口类加个 I;或许这就是 E 人编码吧,不能写 I(我承认这个梗有点烂)双拳难敌四手,亲,这边建议你按照 UserService 和 CustomerService 起名这只是个简单的例子,还有就是你认为 4 就是 for,2 就是 to,如果小组内的成员表示认可你的想法,那你就尽管大胆的使用,但是小组成员要是没有这一点习惯,建议还是老老实实 for 和 to,毕竟你没有一票否决权诸如此类的还有 request -> req、response -> resp 等以下所有的代码命名建议都不能打破团队规范这一条大原则2.2 统一业务词汇在各行各业中,基于业务属性,我们都有一些专业术语,对于专业术语的命名往往在设计领域模型的时候已经确定下来,建议有一份业务词汇来规范团队同学命名,或者以数据库字段为准比如在保险行业中,我们有保费(premium)、保单(policy)、被保人(assured)等,针对于这些业务词汇,务必需要统一。被保人就是 assured 不是 Insured Person2.2 名副其实“语义一定要清晰,不然后续接手的人根本看不懂,我的这个函数名是用来对订单进行删除操作,然后进行 MQ 消息推送的,我准备给他起名为 deleteOrderByCustomerIdAndSendMqMessage”对,函数名很长很清晰,虽然我的屏幕很宽,但是针对于这样的命名,我觉得不可取,函数名和函数一样应该尽量短小,当你的命名极其长的时候你需要考虑简化你的命名,或者 考虑你的函数是否遵循到了单一职责。bad😭 deleteOrderByCustomerIdAndSendMqMessage(String userId) ​ good🤭 deleteOrder(String userId) sendMq() 我们在做阅读理解的时候,需要结合上下文来作答,同样,我们的命名需要让下一个做阅读理解的人感受到我们的上下文含义。在我们删除订单的时候,假设我们需要用到订单的 ID,那么我们的命名需要是 orderId = 123,而不是 id = 123bad😭 这个 id 指代的是什么,订单ID 还是用户 ID id = 123 good🤭 deleteOrder(String userId) orderId = 123 人靠衣装马靠鞍,变量类型需“平安”,我们在起名的时候需要对的起自己的名字bad😭 tm的喵,我以为是个 list String idList = "1,2,3" good🤭 List[removed] idList = ImmutabList.of("1", "2", "3") 默认我的同事的英文水平只有四级,我们变量命名的时候,尽量取一些大众化的词汇,可以四级词汇,切莫六级词汇bad😭 actiivityOrchestrater good🤭 活动策划人 actiivityPlanner 普通函数、方法命名以动词开头bad😭 messageSend good🤭 sendMessage 减少介词链接,考虑使用 形容词+名词productListForSpecialOffer -> specialOfferProductList productListForNewArrival -> newArrivalProductList productListFromHenan -> henanProductList productListWithGiftBox -> withGiftBoxProductList \ giftBoxedProductList productListWithoutGiftBox -> withoutGiftBoxProductList \ noGiftBoxProductList \ unGiftBoxedProductList 消除无意义的前后缀单词: userInfo、userData,info 和 data 的含义过于宽泛,没有实质性意义所以我们可以不用写。或者诸如在 UserService 类中,我们可以可以尝试将 selectUserList 更换为 selectList,因为我们调用的时候,上下文一定是 userService.selectList,阅读者是可以感受到我们的语义的userInfo -> user userService.selectUserList -> userService.selectList 做有意义的方法名的区分:在我刚入职的时候,有一个 OrderService 中,存在 4个方法,enableOrder、enableOrderV2、enableOrderV3、enableOrderV4,我问组里的同事,有什么区别,他们告诉我,现在各个外部服务用的不同,不知道有啥区别。所以为了避免给类似我这样的菜鸟产生歧义,建议在方法起名的时候做好区分,以免埋坑——转载自作者:isysc1
科叼
1 3 开源硬件平台
工作中 Spring Boot 五大实用小技巧,来看看你掌握了几个?
0. 引入Spring Boot 以其简化配置、快速开发和微服务支持等特点,成为了 Java 开发的首选框架。本文将结合我在实际工作中遇到的问题,分享五个高效的 Spring Boot 的技巧。希望这些技巧能对你有所帮助。1. Spring Boot 执行初始化逻辑1.1 背景项目的某次更新,数据库中的某张表新增了一个字段,且与业务有关联,需要对新建的字段根据对应的业务进行赋值操作。一种解决方案就是,更新前手动写 SQL 更新字段的值,但这样做的效率太低,而且每给不同环境更新一次,就需要手动执行一次,容易出错且效率低。另一种方案则是在项目启动时进行初始化操作,完成字段对应值的更新,这种方案效率更高且不容易出错。1.2 实现Spring Boot 提供了多种方案用于项目启动后执行初始化的逻辑。实现CommandLineRunner接口,重写run方法。@Slf4j @Component public class InitListen implements CommandLineRunner { @Overridepublic void run(String... args) {// 初始化相关逻辑...} } 实现ApplicationRunner接口,重写run方法。@Slf4j @Component public class InitListen implements ApplicationRunner { @Overridepublic void run(ApplicationArguments args) {// 初始化相关逻辑...} } 实现ApplicationListener接口@Slf4j @Configuration public class StartClientListener implements ApplicationListener[removed] { @Overridepublic void onApplicationEvent(ContextRefreshedEvent arg0) {// 初始化逻辑} } 针对于上述这个需求,如何实现仅更新一次字段的值?可在数据库字典表中设置一个更新标识字段,每次执行初始化逻辑之前,校验判断下字典中的这个值,确认是否已经更新,如果已经更新,就不需要再执行更新操作了。2. Spring Boot 动态控制数据源的加载2.1 背景期望通过在application.yml文件中,添加一个开关来控制是否加载数据库。2.2 实现启动类上添加注解 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }),代表禁止 Spring Boot 自动注入数据源。新建 DataSourceConfig配置类,用于初始化数据源。在DataSourceConfig配置类上添加条件注解 @ConditionalOnProperty(name = "spring.datasource.enabled", havingValue = "true",代表只有当 spring.datasource.enabled 为 true时,加载数据库,其余情况不加载数据库。仓库类 XxxRepository 的注入,需要使用注解 @Autowired(required = false)[removed]3. Spring Boot 根据不同环境加载配置文件3.1 背景实际开发工作中,针对同一个项目,可能会存在开发环境、测试环境、正式环境等,不同环境的配置内容可能会不一致,如:数据库、Redis等等。期望在项目在启动时能够针对不同的环境来加载不同的配置文件。3.2 实现Spring 提供 Profiles 特性,通过启动时设置指令-Dspring.profiles.active指定加载的配置文件,同一个配置文件中不同的配置使用---来区分。启动 jar 包时执行命令:java -jar test.jar -Dspring.profiles.active=dev -Dspring.profiles.active=dev代表激活 profiles 为 dev 的相关配置。##用---区分环境,不同环境获取不同配置---# 开发环境 spring: profiles: dev cloud: nacos: discovery: server-addr: 127.0.0.1:8848 # 命名空间为默认,所以不需要写命名空间 config: server-addr: ${spring.cloud.nacos.discovery.server-addr} extension-configs[0]: data-id: database-base.yaml group: DEFAULT_GROUP refresh: true extension-configs[1]: # 本地单机Redis data-id: redis-base-auth.yaml group: DEFAULT_GROUP refresh: true extension-configs[2]: data-id: master-base-auth.yaml group: DEFAULT_GROUP refresh: true --- #测试环境 spring: profiles: test cloud: nacos: discovery: server-addr: 192.168.0.111:8904 # 测试环境注册的命名空间 namespace: b80b921d-cd74-4f22-8025-333d9b3d0e1d config: server-addr: ${spring.cloud.nacos.discovery.server-addr} extension-configs[0]: data-id: database-base-test.yaml group: DEFAULT_GROUP refresh: true extension-configs[1]: data-id: redis-base-test.yaml group: DEFAULT_GROUP refresh: true extension-configs[2]: data-id: master-auth-test.yaml group: DEFAULT_GROUP refresh: true --- # 生产环境 spring: profiles: prod cloud: nacos: discovery: server-addr: 192.168.0.112:8848 config: server-addr: ${spring.cloud.nacos.discovery.server-addr} extension-configs[0]: # 生产环境 data-id: database-auth.yaml group: DEFAULT_GROUP refresh: true extension-configs[1]: # 生产环境 data-id: redis-base-auth.yaml group: DEFAULT_GROUP refresh: true extension-configs[2]: data-id: master-base-auth.yaml group: DEFAULT_GROUP refresh: true 也可以定义多个配置文件,如在application.yml中定义和环境无关的配置,而application-{profile}.yml则根据环境做不同区分,如在 application-dev.yml 中定义开发环境相关配置、application-test.yml 中定义测试环境相关配置。启动时指定环境命令同上,仍为:java -jar test.jar -Dspring.profiles.active=dev 参考资料zhuanlan.zhihu.com/p/646593227cloud.tencent.com/developer/a…——转载自作者:离开地球表面_99
科叼
0 5 开源硬件平台
接口不能对外暴露怎么办?
在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。面对这样的情况,我们该如何实现呢?1. 内外网接口微服务隔离将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。2. 网关 + redis 实现白名单机制在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。3. 方案三 网关 + AOP相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。[removed]具体实操下面就方案三,进行具体的代码演示。首先在网关侧,需要对进来的请求header添加外网标识符: from=public@Component public class AuthFilter implements GlobalFilter, Ordered {@Overridepublic Mono [removed] filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {return chain.filter( exchange.mutate().request( exchange.getRequest().mutate().header('id', '').header('from', 'public').build()).build()); } @Overridepublic int getOrder () {return 0;}} 接着,编写内外网访问权限判断的AOP和注解@Aspect @Component @Slf4j public class OnlyIntranetAccessAspect { @Pointcut ( '@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' ) public void onlyIntranetAccessOnClass () {} @Pointcut ( '@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' ) public void onlyIntranetAccessOnMethed () { } @Before ( value = 'onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()' ) public void before () { HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest (); String from = hsr.getHeader ( 'from' ); if ( !StringUtils.isEmpty( from ) && 'public'.equals ( from )) { log.error ( 'This api is only allowed invoked by intranet source' ); throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR); } } } @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface OnlyIntranetAccess { } 最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可@GetMapping ( '/role/add' ) @OnlyIntranetAccess public String onlyIntranetAccess() { return '该接口只允许内部服务调用'; } 4. 网关路径匹配 在DailyMart项目中我采用的是第四种:即在网关中进行路径匹配。 该方案中我们将内网访问的接口全部以前缀/pv开头,然后在网关过滤器中根据路径找到具体校验器,如果是/pv访问的路径则直接提示禁止外部访问。 使用网关路径匹配方案不仅可以应对内网接口的问题,还可以扩展到其他校验机制上。 譬如,有的接口需要通过access_token进行校验,有的接口需要校验api_key 和 api_secret,为了应对这种不同的校验场景,只需要再实现一个校验类即可,由不同的子类实现不同的校验逻辑,扩展非常方便。 如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。——转自作者:程序员蜗牛
科叼
0 0 开源硬件平台
十分钟学会WebSocket
WebSocket简介WebSocket是一种在客户端和服务器之间实现双向通信的网络协议。它通过在单个TCP连接上提供全双工通信功能,使得服务器可以主动向客户端推送数据,而不需要客户端发起请求。WebSocket与HTTP的区别与传统的HTTP协议相比,WebSocket具有以下几个显著的区别:双向通信:WebSocket支持客户端和服务器之间的实时双向通信,而HTTP协议是单向请求-响应模式。低延迟:由于WebSocket使用长连接,避免了HTTP的连接建立和断开过程,可以降低通信延迟。更少的数据传输:WebSocket头部信息相对较小,减少了数据传输的开销。跨域支持:WebSocket可以轻松跨域,而HTTP需要通过CORS等机制来实现。WebSocket的工作原理WebSocket的握手过程和HTTP有所不同。客户端通过发送特定的HTTP请求进行握手,服务器收到请求后进行验证,如果验证通过,则会建立WebSocket连接。建立连接后,客户端和服务器之间可以通过WebSocket发送和接收消息,可以使用文本、二进制数据等进行通信。WebSocket的应用场景WebSocket的实时双向通信特性使得它在许多应用场景中发挥重要作用,例如:即时聊天:WebSocket可以实现实时的聊天功能,用户可以发送和接收消息,实现快速、低延迟的聊天体验。实时数据更新:对于需要实时更新数据的应用,如股票行情、实时监控等,WebSocket可以将数据实时推送给客户端,确保数据的及时更新。在线游戏:在线游戏需要实时的双向通信,WebSocket可以提供稳定的通信通道,支持实时交互和多人游戏。WebSocket的使用以下是使用JavaScript与WebSocket建立连接的示例代码: var Socket = new WebSocket("url, [protocol]"); 以上代码中的第一个参数url, 指定连接的 URL。第二个参数protocol是可选的,指定了可接受的子协议。WebSocket 属性以下是 WebSocket 对象的属性。属性描述Socket.readyState只读属性readyState表示连接状态,可以是以下值:0-表示连接尚未建立。1-表示连接已建立,可以进行通信。2-表示连接正在进行关闭。3-表示连接已经关闭或者连接不能打开。Socket.bufferedAmount只读属性bufferedAmount已被send()放入正在队列中等待传输,但是还没有发出的UTF-8文本字节数。0-表示连接尚未建立。1-表示连接已建立,可以进行通信。2-表示连接正在进行关闭。3-表示连接已经关闭或者连接不能打开。WebSocket 事件以下是 WebSocket 对象的相关事件。事件事件处理程序描述openSocket.onopen连接建立时触发messageSocket.onmessage客户端接收服务端数据时触发errorSocket.onerror通信发生错误时触发closeSocket.onclose连接关闭时触发下面是相关示例代码: Socket.onopen = function() { //连接建立时触发 console.log("WebSocket连接已建立"); }; Socket.onmessage = function(event) { //客户端接收服务端数据时触发 var message = event.data; console.log("收到消息:" + message); }; Socket.onerror = function() { //通信发生错误时触发 console.log("WebSocket连接发生了错误"); }; Socket.onclose = function() { //连接关闭时触发 console.log("WebSocket连接已关闭"); }; WebSocket 方法以下是 WebSocket 对象的相关方法。方法描述Socket.send()使用连接发送数据Socket.close()关闭连接 //发送一条消息 Socket.send('你好') //关闭WebSocket连接 Socket.close() WebSocket 除了发送和接收文本消息外,还支持发送和接收二进制数据。对于发送二进制数据,可以使用 send() 方法传递一个 ArrayBuffer 或 Blob 对象,例如: const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint32(0, 1234); socket.send(buffer); 在接收二进制数据时,可以通过 event.data 获取到 ArrayBuffer 对象,然后进行处理。WebSocket的心跳机制WebSocket的心跳机制是一种用于保持WebSocket连接的稳定性和活跃性的方法。心跳机制的目的是定期发送小的探测消息,以确保连接仍然有效,如果连接断开或出现问题,可以及时发现并采取措施。下面是WebSocket心跳机制的详细步骤和相关代码示例:定义心跳间隔:为了定期发送心跳消息,你需要定义一个心跳间隔,通常以毫秒为单位。在示例中,我们将心跳间隔设置为30秒。 const heartbeatInterval = 30000; // 30秒 定义心跳消息:你需要定义用于发送心跳的消息内容。这通常是一个简单的字符串,如"heartbeat",但可以根据应用的需求自定义。 const heartbeatMessage = 'heartbeat'; 设置心跳定时器:一旦WebSocket连接打开,你可以使用setInterval函数设置一个定时器,以便每隔一段时间发送心跳消息。 let heartbeat; socket.addEventListener('open', () => { console.log('WebSocket连接已打开'); heartbeat = setInterval(() => { socket.send(heartbeatMessage); }, heartbeatInterval); }); 处理心跳消息:当你接收到来自服务器的消息时,你需要检查它是否是心跳消息。这可以通过比较接收到的消息内容和心跳消息的内容来实现。 socket.addEventListener('message', (event) => { const message = event.data; if (message === heartbeatMessage) { console.log('接收到心跳消息'); // 在这里可以执行一些处理心跳消息的操作 } else { console.log('接收到其他消息:', message); // 处理其他类型的消息 } }); 清除心跳定时器:当WebSocket连接关闭时,你应该清除之前设置的心跳定时器,以防止继续发送心跳消息。 socket.addEventListener('close', () => { console.log('WebSocket连接已关闭'); clearInterval(heartbeat); }); 通过这些步骤,你可以实现WebSocket的心跳机制,确保连接的持续稳定,以适应长时间的通信需求。如果连接断开或出现问题,你可以根据需要添加进一步的错误处理机制。WebSocket 的安全性和跨域问题如何处理?WebSocket 支持通过 wss:// 前缀建立加密的安全连接,使用 TLS/SSL 加密通信,确保数据的安全性。在使用加密连接时,服务器需要配置相应的证书。对于跨域问题,WebSocket 遵循同源策略,只能与同源的服务器建立连接。如果需要与不同域的服务器通信,可以使用 CORS(跨域资源共享)来进行跨域访问控制。有哪些好用的客户端WebSocket第三方库Socket.io-client:Socket.io 是一个流行的实时通信库,它提供了客户端 JavaScript 库,可用于在浏览器中与 Socket.io 服务器建立 WebSocket 连接。它支持自动重连、事件处理等功能,用于构建实时应用非常方便。ReconnectingWebSocket:ReconnectingWebSocket 是一个带有自动重连功能的 WebSocket 客户端库,可以很好地处理网络连接断开和重新连接的情况,适合用于浏览器端的 WebSocket 开发。SockJS-client:SockJS 提供了一个浏览器端的 JavaScript 客户端库,用于与 SockJS 服务器建立连接。它可以在不支持 WebSocket 的浏览器上自动降级到其他传输方式,具有良好的兼容性。RxJS WebSocketSubject:RxJS 是一个流式编程库,它提供了 WebSocketSubject 类,可以将 WebSocket 转换为可观察对象,方便进行响应式编程。autobahn.js:autobahn.js 是一个用于实现 WebSocket 和 WAMP(Web Application Messaging Protocol)的客户端库,在浏览器中可以方便地使用它来与 WAMP 路由进行通信。这些库都提供了良好的接口封装和功能特性,可以根据项目需求选择适合的库来进行浏览器端的 WebSocket 开发。​[removed]
科叼
0 7 开源硬件平台