news 2026/9/24 21:08:16

抽象类与抽象方法:Java面向对象设计中的骨架与扩展点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
抽象类与抽象方法:Java面向对象设计中的骨架与扩展点

你知道吗,在面向对象编程里,abstract这个关键字,可能是最容易被人“跳过”的一个。初学的时候,很多人都觉得抽象类没啥用,不如接口灵活,不如普通类实在。但工作几年后回头看,抽象类恰恰是设计优雅代码的基石之一。它不像接口那样追求“契约的纯粹”,也不像普通类那样追求“实现的完整”,它卡在中间,承担着“定义骨架、沉淀公共逻辑”的重任。

这篇聊的就是抽象类和抽象方法。我会用大量实际的代码场景,把抽象类的设计意图、使用场景、易踩的坑,以及与普通类、接口的区别一次讲透。如果你是刚学会继承和多态,或者写了不少代码但还没搞懂抽象类到底该用在哪儿,这篇文章应该能帮上忙。

1. 为什么需要抽象类:从“不完整”的设计说起

先想一个问题:你在设计一个系统的时候,为什么需要“不完整”的类?

1.1 父类里那些“没法写”的方法

假设你现在要做一个文件解析工具,支持 CSV、JSON、XML 三种格式。三种格式的解析流程其实大同小异:打开文件、读取内容、按格式解析、返回结果。你很容易想到用继承来复用代码,于是写了一个父类FileParser

public class FileParser { public List<Record> parseFile(String path) { String content = readFile(path); List<Record> records = parseContent(content); return records; } protected String readFile(String path) { // 读取文件内容的通用逻辑 return null; } protected List<Record> parseContent(String content) { // 每种格式的解析逻辑完全不同,这里该怎么写? return null; } }

问题来了:parseContent方法在父类里根本没法写。CSV 要按逗号分割,JSON 要按对象层级递归解析,XML 要处理标签嵌套。三种逻辑完全不同,父类写任何实现都是错的。

如果直接让父类方法返回null或者抛异常,调用的时候就要小心翼翼,万一某个子类忘记重写这个方法,程序就会在运行时莫名其妙地拿到空数据。更麻烦的是,你根本无法在编译阶段发现“某个子类漏了实现”。

1.2 抽象类给出的解法:定义规则,留出空白

抽象类的做法就很直接:既然父类这个方法没法写,那就干脆不写实现,只声明“这里有一个方法”,然后让所有子类必须自己实现。

public abstract class FileParser { public List<Record> parseFile(String path) { String content = readFile(path); return parseContent(content); } protected String readFile(String path) { // 通用逻辑:打开文件、读取全部内容 return null; } protected abstract List<Record> parseContent(String content); }

这里parseContent就是抽象方法,它没有方法体。而FileParser因为包含了抽象方法,也必须声明为抽象类。

这样设计带来的好处很明显:

  • 编译期强制约束:任何继承FileParser的类,如果不实现parseContent,编译器直接报错。漏实现的问题在写代码的那一刻就暴露了,而不是等到运行期。
  • 公共逻辑只写一次readFileparseFile的执行流程在父类里固定好了,子类只需要关注“如何把内容解析成记录”,其他都不用管。
  • 扩展新格式成本极低:以后要支持 YAML 格式,新写一个类继承FileParser,实现parseContent就完事了,不用动任何已有代码。

这套思路其实就是经典设计模式中的“模板方法模式”。抽象类把算法骨架固定住,把可变的部分留给子类去填充。后面我会专门演示一个完整的例子。

2. 抽象类和普通类的本质区别:不是“能不能 new”那么肤浅

很多人对抽象类的第一印象就是“不能实例化”,这个说法没错,但远远不够。抽象类和普通类的区别,核心不在语法,而在设计意图。

2.1 语法层面的硬性差异

先看最基础的规则,我用一张表整理出来:

对比维度普通类抽象类
实例化可以直接 new不能直接 new,只能由子类实例化
抽象方法不能包含可以包含,也可以不包含
普通方法可以包含可以包含
成员变量可以包含可以包含
构造方法可以包含可以包含(用于子类初始化父类属性)
继承关键字extendsextends
能否被 final 修饰可以不能(被 final 修饰的类不能被继承,抽象类恰恰是为了被继承)

这里有个容易忽略的点:抽象类可以没有抽象方法。虽然少见,但这是合法的。比如你写一个BaseController,里面全是统一的日志记录、参数校验等通用方法,不给任何抽象方法,只是不希望别人直接 new 它,而是强制继承后使用。这种情况用抽象类也是合理的,相当于告诉别人“这个类你千万别直接用,我的设计意图就是让你继承它”。

反过来还有一个规则:只要类里有一个抽象方法,这个类就必须声明为抽象类。哪怕只有一个抽象方法,也不能漏掉abstract修饰符。这个编译器会强制检查,不用记,报错了就知道。

2.2 设计意图的核心差别

普通类的设计意图是“我已经足够完整了,你可以直接用,想要扩展的话也可以继承”。而抽象类的设计意图是“我定义好了框架和流程,但不完整,你必须继承我,把缺失的部分补上之后才能用”。

举个例子,普通类就像一台组装好的电脑,插上电就能开机。抽象类更像一台准系统,主板、电源、机箱都有了,但 CPU 和内存留给你自己装。你说准系统算不算一台电脑?算,但你不装 CPU 就用不了。抽象类也一样,它承载了大部分设计,但不实现全部方法,子类不补全就用不了。

另外要注意,抽象类里可以有final方法。final方法在子类中不能被重写,这也是一个很实用的控制手段。回到文件解析的例子,parseFile方法的执行流程(读取文件 -> 解析内容 -> 返回结果)通常是固定的,你希望所有子类都遵循这个流程,不想让某个子类重写后把整个流程改乱。那就可以把parseFile声明为final

public abstract class FileParser { public final List<Record> parseFile(String path) { String content = readFile(path); return parseContent(content); } protected abstract List<Record> parseContent(String content); }

这样就把流程的控制权收回到父类手中,子类只负责“解析内容”这一个步骤,不能篡改整体流程。这就是抽象类在“约束扩展”上的一个典型用法。

2.3 抽象类里的构造方法:一个好玩但重要的细节

抽象类不能 new,但可以有构造方法。这个很多人一开始想不明白,不能实例化,要构造方法干什么?

答案是:子类实例化的时候,会先调用父类的构造方法,把父类的成员变量初始化好。抽象类的构造方法不是给自己用的,是给子类用的。

public abstract class AbstractLogger { protected String prefix; public AbstractLogger(String prefix) { this.prefix = prefix; } public void log(String message) { System.out.println(prefix + ": " + message); } } public class FileLogger extends AbstractLogger { public FileLogger() { super("[FILE]"); } }

FileLogger创建实例的时候,super("[FILE]")就是在调用AbstractLogger的构造方法。父类的prefix字段被初始化后,log方法才能正常使用。所以抽象类的构造方法虽然不能直接调用,但它在继承链中扮演着“初始化基座”的角色。

如果你想让抽象类的构造逻辑更灵活,也可以在抽象类的构造方法里调用抽象方法。这算是一个进阶技巧,但有个大坑,我们在第 4 节专门讲。

3. 抽象方法和接口的区别:什么时候用哪个

和“抽象类 vs 普通类”相比,“抽象类 vs 接口”才是真正让很多人纠结的地方。尤其是 Java 8 之后接口也可以有默认方法和静态方法了,两者的边界越来越模糊。

3.1 核心区别对比

先说结论:抽象类是“是什么”的关系,接口是“能做什么”的关系。类继承抽象类,是在说“我是你的一种”;类实现接口,是在说“我拥有了你定义的能力”。

对比维度抽象类接口
继承方式extends,单继承implements,可多实现
成员变量可以是普通成员变量只能是 public static final 常量
构造方法有,供子类调用没有
方法实现可以有普通方法(已实现)方法默认无实现,Java 8+ 可有 default/static 方法
访问修饰符可以用 public/private/protectedJDK9+ 可以有 private 方法,但通常都是 public
设计意图共性代码复用、模板流程能力契约、行为规范

这里的单继承和多实现是最关键的差异。Java 只允许一个类继承一个父类,所以抽象类在继承体系里占据的是一条主干道;而接口可以同时实现多个,相当于在主干道之外,类的身上还可以挂很多功能标签。

3.2 选择依据:看你的代码意图

我个人的经验是,做选择时不要纠结语法差异,先问自己三个问题:

第一,子类和父类之间有没有明确的“主从关系”?比如Dog继承Animal,狗就是一种动物,这种“is-a”关系用抽象类非常自然。如果只是需要某个能力,比如会飞、会游泳,那用接口更合适,因为鸟和昆虫都能飞,但它们不是同一种东西。

第二,需不需要在父类里写公共的实现逻辑?比如多个子类都要用同一个模板方法,或者都要共享一套成员变量,那就用抽象类。接口的 default 方法虽然也能提供实现,但没法持有实例变量,状态管理能力很弱。

第三,这个“能力”会不会被完全无关的类复用?比如“可比较”这个能力,Integer能用,String能用,LocalDate也能用,它们之间毫无继承关系。这种就要用接口,因为接口是给全世界用的,抽象类只能给同一棵继承树上的类用。

3.3 模板方法模式和策略模式的取向

其实抽象类和接口在很多场景下对应的是两种不同风格的设计模式。

抽象类天然适合“模板方法模式”:父类把步骤定好,子类填充细节。前面文件解析的例子就是典型。再比如支付流程:下单、对账、调第三方接口、更新订单状态,整体流程不变,但每家支付渠道的对接细节不同。抽象类是很自然的方案。

接口天然适合“策略模式”:调用方只知道接口,不关心具体实现。比如一个排序算法接口,可能有快排、归并、堆排多种实现,调用方可以在运行期动态切换策略。这种情况下接口更灵活,因为接口不要求实现类之间有血缘关系。

我见过不少团队直接把模板方法模式用接口写,接口里放一个 default 方法,把整个流程写死,然后让各个实现类去重写某个抽象方法。也不是不能用,但可读性比抽象类差很多。抽象类的好处是,它把“哪部分是公共的、哪部分必须由子类提供”表达得非常直观,读代码的人一眼就能看出设计意图。

4. 实操案例:用抽象类重构一个日志处理器

语法讲再多,不如代码走一遍。这里我用一个稍复杂的例子,完整演示抽象类的实际落地。不是那种“张三继承人类”的玩具代码,而是有点真实感的业务场景。

4.1 从需求出发

假设你要设计一个日志处理器,需要支持两种输出方式:控制台输出和文件输出。两种方式有一些公共能力:格式化日志级别、添加时间戳、敏感信息脱敏。但最终写入的地方不同,控制台直接打印,文件要追加到磁盘。

4.2 第一版:用抽象类实现

public abstract class LogHandler { private final String appName; public LogHandler(String appName) { this.appName = appName; } public final void log(LogLevel level, String message) { String formatted = format(level, message); String safeMessage = desensitize(formatted); write(safeMessage); } private String format(LogLevel level, String message) { return "[" + appName + "] [" + level + "] " + message; } private String desensitize(String message) { // 把手机号中间四位替换为 * return message.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); } protected abstract void write(String message); }

来看看这个设计里抽象类做的事情:

  • log方法用final锁死,保证整个日志处理流程固定:格式化 -> 脱敏 -> 写入。子类没法破坏这个顺序。
  • formatdesensitize是私有方法,属于公共逻辑,子类看不见也不用关心。
  • write是抽象方法,写入动作是变化的,控制台和文件方式不同,所以留给子类实现。

然后写两个子类:

public class ConsoleLogHandler extends LogHandler { public ConsoleLogHandler(String appName) { super(appName); } @Override protected void write(String message) { System.out.println(message); } } public class FileLogHandler extends LogHandler { private final Path path; public FileLogHandler(String appName, Path path) { super(appName); this.path = path; } @Override protected void write(String message) { try { Files.write(path, (message + System.lineSeparator()).getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.APPEND); } catch (IOException e) { throw new RuntimeException("Failed to write log to file", e); } } }

使用的时候完全面向抽象类编程:

LogHandler handler = new ConsoleLogHandler("order-service"); handler.log(LogLevel.INFO, "用户 13812345678 创建了订单 #1001");

输出结果类似:

[order-service] [INFO] 用户 138****5678 创建了订单 #1001

而如果修改成new FileLogHandler(...),同一个调用方式,日志就写进了文件。调用方根本不需要关心日志写到哪里去了,它只知道自己在调用一个LogHandlerlog方法。这就是面向抽象编程的威力。

4.3 试想不用抽象类的版本

如果不用抽象类,直接用一个普通类 + 一个枚举/参数来区分输出方式:

public class LogHandler { private final OutputType outputType; public void writeToConsole(String message) { System.out.println(message); } public void writeToFile(String message) { // 写文件逻辑 } }

这种写法有两个隐患。第一,每次新增一种输出方式(比如发到消息队列),都得改这个类,违背开闭原则。第二,如果某些输出方式有特殊需求,比如网络发送需要异步,控制台发送需要同步,这个类会变得越来越臃肿,最后变成谁都改不动的大泥球。

用抽象类 + 子类的方式,新增一个MqLogHandler只需要新写一个类,老代码一行不用动。这就是抽象类在可维护性上的价值。

4.4 避坑提示:不要在抽象类构造方法里调用抽象方法

这一节压个重点。前面我提到过抽象类可以有构造方法,但有个很隐蔽的坑,在构造方法里调用抽象方法。

public abstract class AbstractService { public AbstractService() { init(); } protected abstract void init(); } public class UserService extends AbstractService { private String name; @Override protected void init() { name = "张三"; } public void printName() { System.out.println(name); } }

看起来没什么问题,UserService初始化时调用了init方法给name赋值。但实际上,Java 的初始化顺序是:子类构造方法先调用父类构造方法,父类构造方法执行完之后,子类的成员变量才初始化。也就是说,在父类构造方法执行init()的那一刻,UserServicename字段还是null,赋值根本不起作用。

我在真实项目中就踩过这个坑。当初写一个框架的抽象基类,想在构造时让子类注册一些信息,写成了类似上面的代码,结果线上服务一启动,一堆空指针异常。排查了一晚上,最后发现是初始化顺序的问题。

建议:不要在抽象类的构造方法中调用任何抽象方法。如果确实需要子类参与初始化,可以用“延迟初始化”或者提供 protected 方法让子类自行调用。

5. 抽象类使用场景:哪里才是它的主场

抛开纯语法,从实际项目经验看,抽象类最常出现在这几类场景。

5.1 框架和基类设计

凡是写“基础能力层”的代码,抽象类几乎是标配。DAO 层基类、Service 层基类、MQ 消费者基类,这些类把通用的生命周期管理、异常处理、日志埋点全部封装好,只留一个或几个抽象方法给业务子类实现。

我写过不少这类基类,最大的体会是:抽象类能够把“规范”固化在代码结构里。团队里新来的同事要做一个新的消息处理器,只需要继承你写好的AbstractMessageHandler,编译器就会引导他实现process方法。他不需要翻文档、看 wiki,光看继承关系就知道自己该干什么。

5.2 模板方法模式的载体

模板方法模式是抽象类最常见的形态。支付回调处理、数据导入导出、审批流处理,这些“流程固定、步骤可变”的业务场景,抽象类的优势非常明显。

流程固定意味着主干不能改,步骤可变意味着细节必须定制。抽象类用final方法锁流程,用抽象方法留扩展点,天生的匹配这种需求。相比之下,如果这些场景用接口,那你很难避免在接口的 default 方法里写一大串不适合所有人用的默认流程。

5.3 领域建模中的基类抽象

在做领域驱动设计时,往往会有一些核心领域对象的公共特征。比如订单系统里,所有的订单状态变化都要记录审计日志。与其在每一类订单里重复写审计逻辑,不如写一个抽象的AbstractOrder基类,把审计逻辑封装好,然后让NormalOrderGiftOrderRefundOrder去继承。

这种场景下,抽象类承载的不仅有代码复用,还有领域规则的一致性约束。所有订单的审计行为都在基类里统一实现,子类无法跳过,这就保证了整个订单子域的行为是统一的。

5.4 什么时候该放弃抽象类

抽象类也不是银弹。以下几种情况,我建议你考虑接口或者组合:

  • 你的类已经继承了别的类:Java 单继承,如果Dog已经继承了Pet,再想继承一个抽象类Nameable就不可能了。这时候如果“可命名”是一个能力,用接口。
  • 行为可以被任意类复用:接口是多实现的,抽象类是单继承的,接口的复用范围远大于抽象类。
  • 你只是想声明一个能力规范:比如ComparableRunnable,没有任何实现逻辑,也不需要共享状态,好好的接口不用,非搞抽象类,就没必要了。

记住这句话:抽象类优先考虑的是“代码复用 + 流程控制”,接口优先考虑的是“能力声明 + 行为契约”,两者的出发点不同,适用场景自然也不同。

6. 常见问题和排查技巧实录

最后整理一些我自己带新人时经常被问到的问题,以及真实项目里遇到过的坑。

6.1 抽象类真的不能实例化吗?如何证明?

不能直接new,但有个常见说法是“抽象类可以通过匿名内部类实例化”。严格来说这不叫实例化抽象类,而是创建了一个继承抽象类并实现了所有抽象方法的匿名子类对象。

LogHandler handler = new LogHandler("test") { @Override protected void write(String message) { System.out.println(message); } };

这段代码能编译,是因为你实际上创建了一个匿名类,它继承了LogHandler并提供了write的实现。抽象类的所有抽象方法都被实现后,这个匿名子类就不再抽象,所以可以创建对象。在单元测试的时候,这个技巧很常用,尤其是当你只关心某个公共方法的逻辑,不想专门建一个测试子类的时候。

6.2 抽象类里的抽象方法和空方法体的普通方法有什么区别?

这是一个特别容易混淆的问题。看这两段代码:

public abstract class A { public abstract void doSomething(); } public abstract class B { public void doSomething() { // 空实现,什么都不做 } }

A中的doSomething没有方法体,任何子类必须实现它,否则子类也必须声明为抽象类。B中的doSomething有方法体,虽然是空的,但它是“一个什么都不做的默认实现”。子类可以不重写它,直接继承这个空实现。

区别在于意图:抽象方法表达的是“我不打算提供任何默认行为,你必须自己来”;空方法体表达的是“我提供一个默认行为,这个行为的默认值就是什么都不做”。

在很多框架设计中,空实现和抽象方法会配合使用。比如监听器接口,里面声明了一堆回调方法,但如果你用一个抽象类作为适配器,把所有方法先空实现一遍,继承者只需要重写自己关心的那一个方法,代码会干净很多。AWT 和 Swing 时代的WindowAdapter就是这么干的。

6.3 一个子类能继承两个抽象类吗?

不能。Java 的类是单继承,一个类最多只能有一个直接父类。不管这个父类是普通类还是抽象类,规则一样。

如果你遇到需要复用两个抽象类里的逻辑,那就得想办法折中。一种思路是用接口补充能力,一种思路是调整继承层级让抽象类套抽象类,还有一种思路是用组合。

组合通常是更推荐的方案。比如你既想要AbstractLogHandler的模板流程,又想要MetricsCollector的监控采集逻辑,与其让一个新类同时继承它们(不可能),不如在新类里持有MetricsCollector的实例,在关键节点手动调用采集方法。组合替代继承,在处理多个抽象类冲突时特别好用。

6.4 抽象类和接口同时存在时,应该怎么设计?

在真实代码中,抽象类和接口不是互斥的,而是经常同时出现。一个常见的模式是:接口定义契约,抽象类提供默认实现。

public interface MessageHandler { void handle(Message message); boolean supports(MessageType type); } public abstract class AbstractMessageHandler implements MessageHandler { private final MessageType supportedType; public AbstractMessageHandler(MessageType supportedType) { this.supportedType = supportedType; } @Override public boolean supports(MessageType type) { return this.supportedType == type; } @Override public void handle(Message message) { if (!supports(message.getType())) { throw new IllegalArgumentException("Unsupported message type: " + message.getType()); } process(message); } protected abstract void process(Message message); }

接口MessageHandler告诉世界“这是一类可以处理消息的组件”,抽象类AbstractMessageHandler则把公共的supports判断和handle模板流程实现掉,只留给子类一个process。这样设计,既享受了接口的多实现灵活性,又拿到了抽象类的代码复用,是很成熟的工程实践。

6.5 常见抽象类使用误区速查表

误区问题正确做法
抽象类里放了太多具体业务逻辑子类被强绑了不该有的行为抽象类只放公共逻辑,业务私有逻辑放子类
为了“复用一个方法”强行使用抽象类歪曲了继承的语义优先考虑组合而不是强行继承
抽象类层级太深(A -> B -> C -> D)难以维护,语义容易混乱三层以内,超过就考虑拆解或组合
把抽象类当工具类用抽象类不能被实例化,不适合放静态工具方法工具方法放类里的 static 方法或者独立的工具类
用接口写满 default 方法复现抽象类的效果可读性差,语义模糊模板流程用抽象类,能力规范用接口

写在最后

抽象类这个东西,理解起来不难,难的是在不同场景下做出合适的设计选择。我个人在实际项目里的体会是,判断一个类该不该设计成抽象类,别只看“能不能复用”,要问“这里有没有一个明确的继承骨架”。

如果子类和父类之间的关系是清晰的“是一种”,并且父类确实承载了公共流程和状态,那抽象类几乎是不可替代的选择。如果只是想让某些类具备某些能力,接口永远优先。

最后再分享一个小技巧。你可以在 IDE 里给抽象类加一个注释模板,写清楚“这个抽象类的抽象方法分别代表什么扩展点,使用该抽象类时必须实现哪些方法”。一行注释的事,却能让后来接手的同事省下大量猜代码的时间。抽象类的价值一半在设计,另一半其实在沟通。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 21:07:44

工业设备进出口供电转换:电压频率断电问题一次讲透

做设备进出口这些年&#xff0c;我见过太多设备漂洋过海到了现场&#xff0c;外观完好、手续齐全&#xff0c;结果一上电就出问题——要么电机转速不对&#xff0c;要么变频器直接报故障&#xff0c;严重的直接把开关电源烧了。问题几乎都出在供电上&#xff1a;进口入华的设备…

作者头像 李华
网站建设 2026/9/24 21:07:39

工业设备进出口必看:三相电电压频率断电转换指南

这个问题我在一线见过太多次了。国产设备出口到北美、日本&#xff0c;或者进口德国的机器拉到国内工厂&#xff0c;很多都不是“通电就转”这么简单。三相电的电压等级、频率、接地系统甚至断电策略&#xff0c;每一项不对&#xff0c;轻则设备保护性停机让你查半天&#xff0…

作者头像 李华
网站建设 2026/9/24 21:06:50

级联码与交织:RS+卷积码为何四十年不过时

简介&#xff1a;面向无线通信、卫星通信等信道编码场景&#xff0c;这套MATLAB项目实现了RS码与卷积码的级联编码&#xff0c;并引入交织技术以增强抗突发错误能力。资源重点解决级联码的构造、交织与解交织以及AWGN信道下的仿真验证问题&#xff0c;适合通信工程、电子信息类…

作者头像 李华
网站建设 2026/9/24 21:06:46

开源商业化落地指南:从模式选型到全球共生

1. 开源商业化&#xff1a;从“理想国”到“生意场”的必然之路 每年到了 COSCon&#xff08;中国开源年会&#xff09;临近的时候&#xff0c;开源圈子里总会有一种特殊的氛围——老朋友们终于能在线下见面了&#xff0c;新项目终于有机会被更多人看到了&#xff0c;而那些一年…

作者头像 李华
网站建设 2026/9/24 21:06:17

自进化Agent离真正的RSI有多远?从反馈闭环谈起

最近跟几个做AI应用的朋友闲聊&#xff0c;一个话题反复被抛出来——你们做的Agent&#xff0c;真的会自己进化吗&#xff1f;有个朋友调侃说&#xff1a;“我做的Agent能自己改prompt&#xff0c;这算不算自进化&#xff1f;”另一个接了句&#xff1a;“改prompt算什么&#…

作者头像 李华
网站建设 2026/9/24 21:05:09

校园二手教材拍卖系统:Java+微信小程序全栈开发与并发出价实战

简介&#xff1a;这是一套面向高校计算机相关专业学生的微信小程序校园二手教材与书籍拍卖系统&#xff0c;适合用作毕业设计、课程设计或期末大作业。项目采用小程序前端搭配SSM/SpringBoot后台框架&#xff0c;开发环境为IDEA与微信开发者工具&#xff0c;数据库使用MySQL 5.…

作者头像 李华