详细描述ThreadPoolExecutor的各个参数的含义,介绍一个任务提交到线程池后的执行流程。
| ublic ThreadPoolExecutor(int corePoolSize, |
| int maximumPoolSize, |
| long keepAliveTime, |
| TimeUnit unit, |
| BlockingQueue<Runnable> workQueue, |
| ThreadFactory threadFactory, |
| RejectedExecutionHandler handler) { |
| if (corePoolSize < 0 || |
| maximumPoolSize <= 0 || |
| maximumPoolSize < corePoolSize || |
| keepAliveTime < 0) |
| throw new IllegalArgumentException(); |
| if (workQueue == null || threadFactory == null || handler == null) |
| throw new NullPointerException(); |
| this.corePoolSize = corePoolSize; |
| this.maximumPoolSize = maximumPoolSize; |
| this.workQueue = workQueue; |
| this.keepAliveTime = unit.toNanos(keepAliveTime); |
| this.threadFactory = threadFactory; |
| this.handler = handler; |
| } |
| |
| 1. corePoolSize: 核心线程池大小 |
| 2. maximumPoolSize: 线程池最大大小 |
| 3. keepAliveTime: 空闲线程的保活时间 |
| 4. unit: 保活时间单位 |
| 5. workQueue: 任务执行队列 |
| 6. ThreadFactory: 创建线程的工厂 |
| - 1. new SingleThreadExector(); |
| - 2. new SingleThreadScheduledExecutor(): |
| - 3. new FixedThreadPool(); |
| - 4. new ScheduledThreadPool(); |
| - 5. new CachedThreadPool(); |
| - 6. new WorkStealingPool(); |
| - 7. ThreadPoolExecutor(); |
| 7. Handler: 拒绝策略 |
| - AbortPolicy:丢弃并抛异常 |
| - DiscardPolicy:丢弃不抛异常 |
| - DiscardOldestPolicy: 丢弃最早的任务 |
| - CallerRunnerPolicy: 让执行线程池的线程执行 |
| |
| 工作过程: |
| 1. 提交任务到线程池中 |
| 2. 判断当前运行的线程数是否小于核心线程数 |
| 3. 小于则通过双重检查看线程池的状态是否正常 |
| 4. 线程池状态正常之后判断任务是否放入任务队列中 |
| 5. 如果放入任务队列则创建线程但不执行任务 |
请简要说明Servlet中的生命周期
Servlet 通过调用 init () 方法进行初始化。 (只调用依次)
Servlet 调用 service() 方法来处理客户端的请求。
Servlet 通过调用 destroy() 方法终止(结束)。(只调用一次)
开启两个线程A、B,打印1到10,线程A打印奇数(1、3、5、7、9),线程B打印偶数(2、4、6、8、10)
请编写代码实现单例模式 ,类名为Singleton
| 1.饿汉模式 |
| public class HungrySingleton{ |
| private static final HungrySingleton instance = new HungrySingleton(); |
| |
| private HungrySingleton(){} |
| |
| public static HungrySingleton getInstance(){ |
| return instance; |
| } |
| } |
| 2.懒汉模式 ,请注意线程安全问题 |
| public class LazySingleton{ |
| private volatile static LazySingleton instance; |
| |
| private LazySingleton(){} |
| |
| public static LazySingleton getInstance(){ |
| if(instance == null){ |
| synchronized(LazySingleton.class){ |
| if(instance == null){ |
| instance = new LazySingleton(); |
| } |
| } |
| } |
| return instance; |
| } |
| } |
| 3.使用静态内部类 |
| public class StaticInnerClassSingleton{ |
| |
| |
| private StaticInnerClassSingleton(){} |
| |
| |
| public static StaticInnerClassSingleton getInstance(){ |
| return InnerClass.instance; |
| } |
| |
| class InnerClass{ |
| private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton(); |
| } |
| } |
写一个Map转换成JavaBean的工具类方法,实现如下mapToObject方法(使用Java反射,不允许使用第三方类库)
| |
| |
| |
| public static <T> T mapToObject(Map<String, Object> map, Class<T> beanClass) throws IllegalAccessException { |
| if (null == map){ |
| return null; |
| } |
| try { |
| T object = beanClass.newInstance(); |
| Field[] fields = object.getClass().getDeclaredFields(); |
| if (null != fields && fields.length > 0){ |
| for (Field field : fields){ |
| |
| int mod = field.getModifiers(); |
| if (Modifier.isStatic(mod) || Modifier.isFinal(mod)){ |
| System.out.println("==========="); |
| continue; |
| } |
| field.setAccessible(true); |
| field.set(object, map.get(field.getName())); |
| } |
| } |
| return object; |
| } catch (InstantiationException e) { |
| e.printStackTrace(); |
| } |
| return null; |
| } |
数据库操作是我们经常使用的一个技能, 请你完成一个简单的用户密码验证过程 ,给定的条件如下:
CREATE TABLE `users` (
`uid` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(32) NOT NULL COMMENT '用户账号',
`password` varchar(64) NOT NULL COMMENT '用户混淆密码',
PRIMARY KEY (`uid`),
UNIQUE KEY `u_user_name` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
JDBC构建流程:
- 注册驱动程序
- 根据DBUrl、username、password获得数据库连接
- 构建会话
- 执行sql语句,获得结果集
- 打开结果集,处理结果
- 关闭资源
完善以下方法
| private static final String DB_URL= "jdbc:mysql://localhost:8080/testdb?characterEncoding=utf8"; |
| private static final String DB_USERNAME = "root"; |
| private static final String DB_PASSWORD = "somepass"; |
| |
| |
| |
| private Connection getConnectionInstance() throws Exception{ |
| |
| Class.forName("com.mysql.jdbc.Driver"); |
| |
| Connection instance = DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD); |
| return instance; |
| } |
| |
| |
| |
| public boolean verifyPassword(String username, String password) throws Exception{ |
| |
| boolean flag = false; |
| try( |
| Connection connection = getConnectionInstance(); |
| |
| String sql = "SELECT password FROM users WHERE username = ?"; |
| |
| PreparedStatement pstat = connection.prepareStatement(sql); |
| pstat.setObject(1, username); |
| |
| ResultSet rs = pstat.executeQuery(); |
| ){ |
| |
| while(rs.next()){ |
| if(password.equals(rs.getString("password")){ |
| flag = true; |
| } |
| } |
| }catch(Exception e){ |
| e.printStackTrace(); |
| } |
| return flag; |
| } |
介绍HashMap的数据结构、扩容机制,HashMap与Hashtable的区别,是否是线程安全的,并介绍ConcurrentHashMap的实现机制。
HashMap
1)数据结构:数组+链表(红黑树),数组用于存储内容,链表(红黑树)用于解决hash冲突。如果链表长度大于阈值8,但是当前数组长度小于树化阈值64,则进行数组扩容操作;如果数组长度大于树化阈值64,则进行链表树化操作,将单向链表转化为红黑树结构。
2)扩容机制:如果不指定容量,则初始容量默认为16。如果指定容量,则初始容量设置为大于指定容量的最小2的幂数。当当前容量大于容量*负载因子(默认为0.75)时进行扩容操作,扩容为原容量的2倍。
HashMap与HashTable的区别
1)数据结构区别:HashMap为数组+链表(红黑树),HashTable为数组+链表,HashTable没有树化操作。
2)扩容机制区别:未指定容量情况下,HashMap容量默认16,每次扩容为2n(n:原容量)。HashTable容量默认为11,每次扩容为2n+1(n:原容量)。指定容量情况下,HashMap将保证容量为2的幂数,HashTable将直接使用指定容量。
3)数据插入方式的区别:当发生hash冲突时,HashMap使用尾插法插入链表,HashTable使用头插法插入链表。
4)线程安全区别:HashMap是非线程安全的,HashTable因为使用synchronized修饰方法,所以HashTable是线程安全的。
ConcurrentHashMap的实现机制
1)ConcurrentHashMap通过synchronized关键字和CAS操作实现线程安全,若插入的槽没有数据,使用CAS操作执行插入操作,若插入的槽有数据,通过synchronized锁住链表的头节点,从而实现效率与线程安全的平衡。
介绍数据库连接池的实现方式。如何从连接池中获取连接、将连接放回连接池?使用连接池的优势是什么?列举一下自己用过的连接池。
什么是死锁?JAVA程序中什么情况下回出现死锁?如何避免出现死锁?
死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅会发生在线程之间,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。
基本上死锁的发生是因为:
互斥条件,类似Java中Monitor都是独占的,要么是我用,要么是你用。
互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其它线程抢占。
循环依赖关系,两个或者多个个体之间出现了锁的链条环。
免死锁的思路和方法。
1、如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁。
2、如果必须使用多个锁,尽量设计好锁的获取顺序。
3、使用带超时的方法,为程序带来更多可控性
分布式锁有几种实现方式,并介绍每种方式的优缺点。
| 分布式锁解决并发的三种实现方式 |
| |
| 在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同 一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。所以针对分布式锁的实现目前有多种方案: |
| 分布式锁一般有三种实现方式: |
| 1、 数据库锁 |
| 2、基于Redis的分布式锁 |
| 3、基于ZooKeeper的分布式锁 |
什么是TCP粘包拆包?为什么会出现粘包拆包?如何在应用层面解决此问题?
果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题。
1、TCP是基于字节流的,虽然应用层和传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;
2、在TCP的首部没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。
解决
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
请大致描述一下BIO,AIO和NIO的区别?
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO:异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
在JAVA语法中加载类的的方式有哪些?
1. 使用new 静态加载
2. 调用某个 ClassLoader 实例的 loadClass()
3. 调用 Class.forName()
建立三个线程A、B、C,A线程打印10次字母A,B线程打印10次字母B,C线程打印10次字母C,但是要求三个线程同时运行,并且实现交替打印,即按照ABCABCABC的顺序打印。
| class Task3{ |
| private volatile boolean firstDone; |
| private volatile boolean secondDone; |
| private volatile boolean thirdDone = true; |
| volatile int sum =0; |
| void first(){ |
| while (true){ |
| while (!thirdDone){ |
| |
| } |
| sum++; |
| System.out.println(Thread.currentThread().getName()+" : A 计数:"+sum); |
| thirdDone=false; |
| firstDone=true; |
| if(sum==28)return; |
| } |
| } |
| void second(){ |
| while (true){ |
| while (!firstDone){ |
| |
| } |
| sum++; |
| System.out.println(Thread.currentThread().getName()+" : B 计数:"+sum); |
| secondDone=true; |
| firstDone=false; |
| if(sum==29)return; |
| } |
| } |
| void third(){ |
| while (true){ |
| while (!secondDone){ |
| |
| } |
| sum++; |
| System.out.println(Thread.currentThread().getName()+" : C 计数:"+sum); |
| System.out.println("==================================================="); |
| secondDone=false; |
| thirdDone=true; |
| if(sum==30)return; |
| } |
| } |
| } |
请列举5个spring框架中的注解,并说明注解的用法以及使用场景

给定一组自然数,数字的值有可能会大于2^64 ,要求计算出所有数字的和
| package top.handsomecoder; |
| |
| |
| import java.math.BigDecimal; |
| import java.util.ArrayList; |
| |
| public class Solution { |
| |
| |
| |
| |
| |
| public String sum (ArrayList<String> numbers) { |
| BigDecimal initN = new BigDecimal("0"); |
| for (String str : |
| numbers) { |
| BigDecimal bd1 = new BigDecimal(str); |
| initN=initN.add(bd1); |
| |
| } |
| return initN.toString(); |
| } |
| } |
给定一个int 数字,要求计算出int数字对应的二进制中1的个数
| |
| public int count(int n){ |
| int count = 0; |
| while(n != 0){ |
| count += n & 1; |
| n >>>= 1; |
| } |
| return count; |
| } |
| |
| |
| public int count(int n){ |
| int count = 0; |
| while(n != 0){ |
| n = n & (n-1); |
| count++; |
| } |
| return count; |
| } |
| 原理:每次-1会将这个数改变得到一个新的数:右边第一个1右边的0变为1,然后右边第一个的1变0。 |
| 将这个新得到数和原本的数相与就能将右边第一个1变0\ |
| 例子: n = 100100 |
| 则 n - 1 = 100011 |
| 二者相与得到 100000 |
| 然后 count++ 成功统计到一个1 |
| 直到全为 0 停止 |
根据产品策略某本书可以设置包月到期时间,需要计算指定时间到包月到期时间还有多少分钟,不足60S的不计入。
| package com.dume.server.face; |
| |
| import javax.xml.crypto.Data; |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.Scanner; |
| |
| |
| |
| |
| |
| |
| public class ExpireDate { |
| |
| public static void main(String[] args) { |
| System.out.println("请输入产品到期时间,格式为“yyyy-MM-dd HH:mm:ss” :"); |
| SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| Scanner scanner = new Scanner(System.in); |
| String string = scanner.nextLine(); |
| |
| Date end = new Date(); |
| try{ |
| end = simpleDateFormat.parse(string); |
| }catch(Exception e){ |
| System.out.println("时间格式错误!"); |
| } |
| |
| long endTime = end.getTime(); |
| long nowTime = System.currentTimeMillis(); |
| |
| if(endTime<nowTime){ |
| System.out.println("到期时间不能在此刻之前!"); |
| }else{ |
| System.out.println("到期时间剩余" +(endTime-nowTime)/(60*1000)+"分钟"); |
| } |
| } |
| } |
map是一种开发过程中经常使用的k-v数据结构,有个map保存了书名和书字数的关系,编写代码对map里面的书按照字数进行升序排序
| public static Map<String, Integer> sortMap(Map<String, Integer> map) { |
| TreeMap<Integer, List<String>> treeMap = new TreeMap<>(); |
| map.entrySet().forEach(entry -> { |
| List<String> indexList = treeMap.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()); |
| indexList.add(entry.getKey()); |
| }); |
| Map<String, Integer> result = new ListOrderedMap(); |
| treeMap.entrySet().forEach(entry -> { |
| entry.getValue().forEach(key -> result.put(key, map.get(key))); |
| }); |
| return result; |
| } |
| |
| public static Map<String, Integer> sortMap2(Map<String, Integer> map) { |
| Map result = new ListOrderedMap(); |
| map.entrySet().stream(). |
| sorted(Map.Entry.comparingByValue()). |
| forEachOrdered(entry -> result.put(entry.getKey(), entry.getValue())); |
| |
| return result; |
| } |
允许对内容严格操作,如用户发表了 作者大大666666,为你点赞 ,经过过滤后也可以为作者大大,为你点赞 ,将666666过滤掉了。
| public String filterContent (String content) { |
| |
| return content.replaceAll("[1-9]\\d{5,9}",""); |
| } |
质数(又称素数),是指在大于1的自然数中,除了1和它本身外,不能被其他自然数整除(除0以外)的数称之为素数(质数)。请写个程序判断输入的数字是否是质数,如果是素数请输出:true,不是请输出false
| package leecode; |
| |
| public class IsPrimeDemo { |
| public static boolean isPrime(int n){ |
| if (n < 1){ |
| return false; |
| } |
| int i = 2; |
| int end = (int) Math.sqrt(n); |
| while (i <= end ){ |
| if (n % i == 0){ |
| return false; |
| } |
| ++i; |
| } |
| return true; |
| } |
| |
| public static void main(String[] args) { |
| int n = 7; |
| System.out.println(isPrime(n)); |
| } |
| } |
有 n 个台阶,你一次能走 1 个或者 2 个台阶,那么请问,走完这 n 个台阶共有几种方式?
| public static void main(String[] args) { |
| Scanner sc = new Scanner(System.in); |
| System.out.println("请输入台阶数:"); |
| int num = sc.nextInt(); |
| Integer i = judge(num); |
| if (i==0){ |
| System.out.println("有点正事吧,台阶要大于0"); |
| }else { |
| System.out.println(num+"个台阶共"+i+"种方式能走完"); |
| } |
| } |
| public static Integer judge(int num){ |
| while (num>0){ |
| if (num<=2){ |
| return num; |
| }else{ |
| return judge(num - 1) + judge(num - 2); |
| } |
| } |
| return 0; |
| } |
给定一个字符串,返回这个字符串中有多少个回文子串。
两个相同的回文子串出现在不同的位置,认为是2个回文子串。
a、aa、aaa、aba、aabaa、abcba均认为是回文子串。
| public static int getHuiWenNumber(String str){ |
| int number=0; |
| for(int i=0;i<str.length();i++){ |
| for(int left=i,right =i;left>=0 &&right<str.length();left--,right++){ |
| if(str.charAt(left)!=str.charAt(right)){ |
| break; |
| } |
| number++; |
| } |
| } |
| return number; |
| } |
给定一个递增排序的数组,查找某个数字是否在数组中,如果在数组中,则返回该数字在数组中第一次出现的位置(从0开始);如果不在数组中,返回-1 。
不需要考虑给定的数组不是递增的情况。
务必使用二分查找的方式。
| int binarySearch(int* arr, int arrLen, int a) { |
| |
| int left = 0; |
| int right = arrLen-1; |
| int mid = 0; |
| while(left<=right) |
| { |
| mid = (left+right)/2; |
| if(arr[mid]==a) |
| { |
| if(arr[mid-1]!=a) |
| return mid; |
| else |
| return mid-1; |
| } |
| else if(arr[mid]>a) |
| right = mid - 1; |
| else |
| left = mid + 1; |
| } |
| return -1; |
| } |
求出一个正整数转换成二进制后的数字“1”的个数。
例:数字23转为二进制为 10111,其中1的个数为4
| public static int binaryTo(int num) { int sum = 0; while (num > 0) { |
| sum += num % 2; |
| num = num / 2; |
| } return sum; |
| } |
去除字符串中的重复字符,对于出现超过2次(包含2次)的字符,只保留第一个。
例:输入abcbdde,输出abcde。
| import java.util.*; |
| |
| |
| public class Solution { |
| |
| |
| |
| |
| |
| public String removeDuplicatedChars (String str) { |
| |
| boolean[] isExistChar = new boolean[26]; |
| boolean[] isExistNum = new boolean[10]; |
| char[] chars = str.toCharArray(); |
| StringBuilder sb = new StringBuilder(); |
| for (char c : chars) { |
| |
| if(c >= 'a' && c <= 'z'){ |
| if(!isExistChar[c - 'a']){ |
| sb.append(c); |
| isExistChar[c - 'a'] = true; |
| } |
| } |
| |
| if(c >= '0' && c <= '9'){ |
| if(!isExistNum[c - '0']){ |
| sb.append(c); |
| isExistNum[c - '0'] = true; |
| } |
| } |
| } |
| return sb.toString(); |
| } |
| } |
给定一个整型数组,移除数组的某个元素使其剩下的元素乘积最大,如果数组出现相同的元素 ,请输出第一次出现的元素
| public static String removeMin(List<Integer> list) { |
| int min = list.get(0); |
| HashSet<Integer> set = new HashSet<>(); |
| int num=0,index=0; |
| for (int i = 0; i < list.size(); i++) { |
| if (!set.add(list.get(i))) { |
| num=list.get(i); |
| } |
| if (list.get(i) - min < 0) { |
| min = list.get(i); |
| index=i; |
| } |
| } |
| return "移除的元素为"+min+",下标是"+index+",相同元素第一次出现的是"+num; |
| } |
在字符串中找到第一个不重复的字符。
例:对于字符串“hellohehe”,第一个不重复的字符是“o”。
如果每个字符都有重复,则抛出运行时异常。
| public char findFirstNonRepeatChar (String str) throws Exception { |
| |
| Map<Character, Integer> map = new LinkedHashMap<>(); |
| char[] chars = str.toCharArray(); |
| for (char c : chars) { |
| if(!map.containsKey(c)){ |
| map.put(c, 1); |
| }else{ |
| map.put(c, map.get(c) + 1); |
| } |
| } |
| Set<Map.Entry<Character, Integer>> entries = map.entrySet(); |
| for (Map.Entry<Character, Integer> entry : entries) { |
| if(entry.getValue() == 1){ |
| return entry.getKey(); |
| } |
| } |
| throw new Exception("每个字符都有重复"); |
| } |
在和外部公司联调HTTP接口时,对方要求调用的接口需要计算token,给到的规则如下:
1) 所有的参数值必须经过urlencode,编码为utf-8;
2) 对编码后数据按照key值进行字典升序排序;
3)将所有的参数按照排序的顺序拼接成字符串 ,格式如下: k1=v1&k2=v2&k3=v3;
4) 将第三步的算出的值计算md5值,md5加密的值小写
请你编写一段方法计算token值
| public String getToken(Map<String, String> params) { |
| List<String> ss = params.keySet().stream().sorted().map( |
| k -> { |
| String v = params.get(k); |
| String kv = null; |
| try { |
| kv = k + "=" + URLEncoder.encode(v, "utf-8"); |
| } catch (Exception e) { |
| |
| } |
| return kv; |
| } |
| ).collect(Collectors.toList()); |
| String token = null; |
| try { |
| token = new String(MessageDigest.getInstance("md5").digest(String.join("&", ss).getBytes())); |
| } catch (Exception e) { |
| } |
| return token; |
| } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具