OptaPlanner入门以及实战教学

在日常开发中,我们经常会遇到这类"棘手"的优化问题:学校要排课程表,得满足"一个老师不能同时上两节课""一个房间不能同时用""学生不喜欢连续上同一门课"等一堆限制;企业要排员工排班,得兼顾员工技能、休息时间和业务需求。这些问题看似简单,实则属于NP-Hard问题------没有万能的快速解法,但只要找到"相对最优解"就能满足业务需求。

OptaPlanner就是为解决这类问题而生的开源Java约束求解器。它内部封装了各种AI优化算法,我们不用深入研究算法细节,只需通过API定义问题和约束,就能让它自动找到最优解决方案。本文就从"是什么、核心概念、算法原理"三个入门要点,再到"课程表规划"完整实战,带你快速掌握OptaPlanner。

一、什么是OptaPlanner?

OptaPlanner是轻量级、可嵌入的Java开源规划引擎(AI约束求解器),核心作用是:在满足一系列约束条件的前提下,对复杂问题找到"相对最优解"。

用大白话解释:就像你请了一个"智能规划助手",你把所有限制条件(比如"老师A周三下午不能上课""房间B只能上理科课")和优化目标(比如"尽量让老师连堂""减少学生换教室的次数")告诉它,它就会在合理时间内给出一份最符合要求的安排,不用你手动一点点试错。

它支持解决的典型场景:

  • 资源分配类:员工排班、车辆路径规划、云资源调度;
  • 日程安排类:课程表规划、会议安排、设备维护调度;
  • 组合优化类:工厂作业调度、装箱问题(比如快递装箱最大化利用空间)。

二、核心概念:用"课程表"例子讲明白

OptaPlanner有几个必须理解的核心概念,直接套"课程表规划"的例子,一看就懂(就像G1的Region用"内存分区"理解一样):

2.1 问题事实(Problem Fact)

求解问题时"固定不变"的基础数据,是规划的前提。就像搭积木的"固定零件",不能改。

课程表场景示例:

  • 时间段(Timeslot):比如"周一08:30-09:30""周二14:30-15:30",这些是提前确定好的,求解时不会新增或修改;
  • 房间(Room):比如"房间A""房间B",数量和属性固定;
  • 老师(Teacher):比如"张老师(教语文)""李老师(教数学)",技能和可用时间固定;
  • 班级(StudentGroup):比如"1班""2班",学生数量固定。

2.2 规划实体(Planning Entity)

需要被"优化安排"的对象,是求解过程中核心操作的载体。就像积木里"需要摆放的零件"。

课程表场景示例:课程(Lesson)。我们要给每节课安排"哪个时间段上""在哪个房间上",所以课程就是需要被规划的实体。

2.3 规划变量(Planning Variable)

规划实体上"可以动态修改"的属性,也是找到最优解的关键。就像零件上"可以调整的摆放位置"。

课程表场景示例:课程的时间段(timeslot)房间(room)。这两个属性是可变的,调整它们就能得到不同的课程表安排。

2.4 约束(Constraint)

规划问题的"规则和限制",决定了哪些安排是合理的、哪些是不合理的。OptaPlanner把约束分为两类,和我们日常规划逻辑完全一致:

  • 硬约束(Hard Constraint):必须严格遵守,一旦违反,这个安排就是"不可行的"。比如"一个房间同一时间只能上一节课""一位老师同一时间只能教一节课"------违反了就会出现"冲突",这样的课程表根本没法用;
  • 软约束(Soft Constraint):不是必须遵守,但遵守了会让安排更优。比如"老师更喜欢连堂上课(减少课间换教室的麻烦)""学生不喜欢连续上同一门课""老师尽量在同一个房间上课"------违反了也能用,但体验不好。

2.5 分数(Score)

用来衡量"某个安排好不好"的标准,直接由约束决定。OptaPlanner用"惩罚(违反约束)"和"奖励(符合软约束)"来计算分数:

  • 违反硬约束:会受到"硬惩罚",分数直接降低(比如扣1000分),硬分数为负的安排是不可行的;
  • 违反软约束:会受到"软惩罚"(比如扣1分);符合软约束:会得到"软奖励"(比如加1分);
  • 最终分数越高,安排越优。

举个例子:某课程表违反了"房间冲突"硬约束,硬分数扣1000分(硬分数=-1000);但满足了"老师连堂"软约束,软分数加5分(软分数=5)。这个安排是不可行的,因为硬分数为负。

三、核心算法原理:用"生活化例子"讲透

OptaPlanner内部封装了多种优化算法,不用我们手动实现,理解它们的核心逻辑就能更好地适配场景。这些算法就像"不同的找最优解的思路",下面用大白话+例子解释最常用的4种:

3.1 禁忌搜索(Tabu Search):不重复走"老路"

核心逻辑:找到一个较好的解后,记录下来"不能再走回头路"(比如不能再回到上一个差的安排),避免陷入"局部最优解"。

生活化例子:你要找从家到公司的最优路线(最短时间+最少红绿灯)。第一次试了路线A,觉得不错;接下来不会再重复走路线A,而是尝试路线B、C......如果找到比A更好的路线D,就把D作为新的"基准",再围绕D尝试其他路线。这样就能避免一直停留在"还不错"的路线A,找到更优的路线D。

OptaPlanner中的作用:比如排课程表时,不会反复切换到之前已经验证过的"有冲突的安排",而是持续探索新的、更优的安排。

3.2 模拟退火(Simulated Annealing):允许"暂时不完美",避免错过最优解

核心逻辑:模拟物理中"金属退火"的过程------一开始温度高,分子运动剧烈,允许较大的变化(即使暂时变差);随着温度降低,分子运动平缓,不再接受变差的变化,最终稳定在最优状态。

生活化例子:你找最优路线时,一开始可以尝试"绕点远路"(即使暂时变慢),因为可能绕远路后能找到更短的后续路线;慢慢的,就不再尝试绕远路,只在当前好路线的附近微调。比如一开始你试了路线A(20分钟),然后尝试路线B(25分钟,变差了),但继续走路线B的延伸路线C,发现只要18分钟(更优);到后期,就不会再尝试超过20分钟的路线了。

OptaPlanner中的作用:避免一开始就陷入"局部最优解"。比如排课程表时,一开始可能会接受"有一个软约束违反"的安排,但后续可能会在这个安排的基础上,找到完全符合软约束的最优解。

3.3 遗传算法(Genetic Algorithm):优胜劣汰,"进化"出最优解

核心逻辑:模拟生物进化的"自然选择、交叉、变异"过程。把每个安排当作一个"个体",多个个体组成"种群";通过选择(保留优秀个体)、交叉(组合两个优秀个体的优点生成新个体)、变异(随机微调个体),让种群不断"进化",最终得到最优个体。

生活化例子:你有10个初步的课程表安排(种群),其中3个安排冲突少、符合软约束多(优秀个体);把这3个优秀安排的"优点"组合起来(比如A的上午安排+B的下午安排),生成新的安排;再随机微调新安排的某个课程时间(变异);重复这个过程,最终会得到比原来更优的课程表。

OptaPlanner中的作用:适合大规模优化问题(比如几百个课程、几十个房间的规划),能通过"进化"快速筛选出最优解。

3.4 局部搜索(Local Search):在当前解附近"微调"找更优解

核心逻辑:从一个初始解开始,在它的"邻域"(比如微调一个课程的时间或房间)内寻找更好的解;找到后,以新解为基准继续微调,直到找不到更好的解为止。

生活化例子:你找到一个20分钟的上班路线,然后微调路线(比如换一个红绿灯路口),发现时间变成19分钟;再以19分钟的路线为基准,微调另一个路口,发现18分钟;直到微调后时间不再减少,就确定18分钟是当前最优路线。

OptaPlanner中的作用:适合中小规模问题,求解速度快,能快速找到接近最优的解。

提示:OptaPlanner会自动选择或组合这些算法(默认是"混合算法"),我们不用手动指定,只需配置求解时间(比如"求解3分钟")即可。

四、实战:从零搭建最优课程表

下面以"学校课程表规划"为场景,完整实现从"定义问题"到"运行求解"的全流程。技术栈:Java + Maven + OptaPlanner,最终实现一份无硬约束冲突、尽量符合软约束的课程表。

4.1 第一步:明确需求(约束定义)

先把课程表的约束明确下来,后续代码都围绕这些约束编写:

硬约束(必须遵守):
  1. 一个房间在同一时间段只能上一节课(无房间冲突);
  2. 一位老师在同一时间段只能教一节课(无老师冲突);
  3. 一个班级在同一时间段只能上一节课(无班级冲突)。
软约束(尽量遵守):
  1. 老师尽量在同一个房间上所有课(减少换教室麻烦);
  2. 老师尽量连堂上课(减少课间空隙);
  3. 学生尽量不连续上同一门课(提升学习体验)。

4.2 第二步:环境搭建(Maven依赖)

创建Maven项目,在pom.xml中添加OptaPlanner依赖(最新版本可参考官方文档):

复制代码
<dependency>
    <groupId>org.optaplanner</groupId>
    <artifactId>optaplanner-core</artifactId>
    <version>9.44.0.Final</version>
</dependency>

<!-- 用于JSON序列化结果(可选,方便展示) -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>

4.3 第三步:建立模型(实体类定义)

根据核心概念,定义4个实体类:问题事实(Timeslot、Room、Teacher、StudentGroup)、规划实体(Lesson)、解决方案(TimeTable)。

3.1 问题事实类:Timeslot(时间段)

固定的时间段,求解时不修改:

复制代码
// 时间段(问题事实)
public class Timeslot {
    // 唯一标识
    private Long id;
    // 星期(如"MON")
    private String dayOfWeek;
    // 开始时间(如"08:30")
    private String startTime;
    // 结束时间(如"09:30")
    private String endTime;

    // 构造器、getter、setter、toString
    public Timeslot(Long id, String dayOfWeek, String startTime, String endTime) {
        this.id = id;
        this.dayOfWeek = dayOfWeek;
        this.startTime = startTime;
        this.endTime = endTime;
    }

    // getter和setter省略(IDE自动生成)
    @Override
    public String toString() {
        return dayOfWeek + " " + startTime + "-" + endTime;
    }
}
3.2 问题事实类:Room(房间)

固定的房间,求解时不修改:

复制代码
// 房间(问题事实)
public class Room {
    private Long id;
    private String name; // 如"房间A"

    // 构造器、getter、setter、toString
    public Room(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getter和setter省略
    @Override
    public String toString() {
        return name;
    }
}
3.3 问题事实类:Teacher(老师)和StudentGroup(班级)

简单定义,存储基本信息:

复制代码
// 老师(问题事实)
public class Teacher {
    private Long id;
    private String name; // 如"张老师"

    public Teacher(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getter和setter省略
    @Override
    public String toString() {
        return name;
    }
}

// 班级(问题事实)
public class StudentGroup {
    private Long id;
    private String name; // 如"1班"

    public StudentGroup(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getter和setter省略
    @Override
    public String toString() {
        return name;
    }
}
3.4 规划实体类:Lesson(课程)

需要优化的实体,标注@PlanningEntity;规划变量(timeslot、room)标注@PlanningVariable:

复制代码
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.variable.PlanningVariable;

// 课程(规划实体):需要被安排时间段和房间
@PlanningEntity
public class Lesson {
    private Long id;
    private String subject; // 课程名称(如"语文")
    private Teacher teacher; // 授课老师(固定,问题事实)
    private StudentGroup studentGroup; // 授课班级(固定,问题事实)

    // 规划变量1:时间段(需要OptaPlanner分配)
    @PlanningVariable(allowsUnassigned = true) // allowsUnassigned=true:允许初始未分配
    private Timeslot timeslot;

    // 规划变量2:房间(需要OptaPlanner分配)
    @PlanningVariable(allowsUnassigned = true)
    private Room room;

    // 构造器(无参构造器必须有,OptaPlanner反射需要)
    public Lesson() {}

    // 带参构造器(初始化固定属性)
    public Lesson(Long id, String subject, Teacher teacher, StudentGroup studentGroup) {
        this.id = id;
        this.subject = subject;
        this.teacher = teacher;
        this.studentGroup = studentGroup;
    }

    // getter和setter省略
    @Override
    public String toString() {
        return subject + "(" + teacher + "-" + studentGroup + ") " + timeslot + "-" + room;
    }
}
3.5 解决方案类:TimeTable(课程表)

封装所有问题数据和规划结果,标注@PlanningSolution;分数标注@PlanningScore:

复制代码
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.score.Score;
import org.optaplanner.core.api.domain.score.buildin.hardsoft.HardSoftScore;

import java.util.List;

// 课程表(解决方案):包含所有问题数据和规划结果
@PlanningSolution
public class TimeTable {
    // 问题事实集合:所有可用时间段(OptaPlanner会自动识别为规划变量的可选值)
    @ProblemFactCollectionProperty
    private List<Timeslot> timeslotList;

    // 问题事实集合:所有可用房间
    @ProblemFactCollectionProperty
    private List<Room> roomList;

    // 规划实体集合:所有需要安排的课程
    @PlanningEntityCollectionProperty
    private List<Lesson> lessonList;

    // 分数:OptaPlanner计算的结果(硬分数+软分数)
    @PlanningScore
    private HardSoftScore score;

    // 无参构造器必须有
    public TimeTable() {}

    // 带参构造器(初始化问题事实和待规划课程)
    public TimeTable(List<Timeslot> timeslotList, List<Room> roomList, List<Lesson> lessonList) {
        this.timeslotList = timeslotList;
        this.roomList = roomList;
        this.lessonList = lessonList;
    }

    // getter和setter省略
}

4.4 第四步:定义约束(分数计算逻辑)

实现ConstraintProvider接口,定义所有硬约束和软约束的计算逻辑。OptaPlanner通过这些逻辑自动计算每个安排的分数。

复制代码
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaplanner.core.api.score.stream.Joiners;

// 课程表约束提供者:定义所有约束和分数计算规则
public class TimeTableConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                // 硬约束:1. 房间冲突(同一房间同一时间段不能有两节课)
                roomConflict(constraintFactory),
                // 硬约束:2. 老师冲突(同一老师同一时间段不能上两节课)
                teacherConflict(constraintFactory),
                // 硬约束:3. 班级冲突(同一班级同一时间段不能上两节课)
                studentGroupConflict(constraintFactory),

                // 软约束:1. 老师尽量在同一房间上课(违反则惩罚)
                teacherRoomStability(constraintFactory),
                // 软约束:2. 老师尽量连堂上课(符合则奖励)
                teacherTimeEfficiency(constraintFactory),
                // 软约束:3. 学生尽量不连续上同一门课(违反则惩罚)
                studentGroupSubjectVariety(constraintFactory)
        };
    }

    // 硬约束1:房间冲突
    private Constraint roomConflict(ConstraintFactory constraintFactory) {
        // 逻辑:找到两个不同的课程,它们的时间段相同且房间相同 → 违反硬约束,扣1个硬分数
        return constraintFactory
                .forEachUniquePair(Lesson.class, // 选择两个不同的课程
                        Joiners.equal(Lesson::getTimeslot), // 条件1:时间段相同
                        Joiners.equal(Lesson::getRoom) // 条件2:房间相同
                )
                .penalize("Room conflict", HardSoftScore.ONE_HARD); // 惩罚:扣1硬分
    }

    // 硬约束2:老师冲突
    private Constraint teacherConflict(ConstraintFactory constraintFactory) {
        return constraintFactory
                .forEachUniquePair(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getTeacher) // 条件:同一老师
                )
                .penalize("Teacher conflict", HardSoftScore.ONE_HARD);
    }

    // 硬约束3:班级冲突
    private Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
        return constraintFactory
                .forEachUniquePair(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getStudentGroup) // 条件:同一班级
                )
                .penalize("StudentGroup conflict", HardSoftScore.ONE_HARD);
    }

    // 软约束1:老师尽量在同一房间上课
    private Constraint teacherRoomStability(ConstraintFactory constraintFactory) {
        // 逻辑:对同一老师的所有课程,统计每个课程的房间与"老师常用房间"是否一致 → 不一致则扣1软分
        return constraintFactory
                .forEach(Lesson.class)
                .groupBy(Lesson::getTeacher, Lesson::getRoom, // 按老师+房间分组
                        ConstraintCollectors.count()) // 统计每个老师在每个房间的课程数量
                .filter((teacher, room, count) -> count <= 1) // 只关注课程数量≤1的房间(即不常用房间)
                .penalize("Teacher room stability", HardSoftScore.ONE_SOFT,
                        (teacher, room, count) -> count); // 惩罚:每个不常用房间的课程扣1软分
    }

    // 软约束2:老师尽量连堂上课
    private Constraint teacherTimeEfficiency(ConstraintFactory constraintFactory) {
        // 逻辑:找到同一老师的两节课,它们的时间段连续(如08:30-09:30和09:30-10:30) → 奖励1软分
        return constraintFactory
                .forEach(Lesson.class)
                .join(Lesson.class, // 关联同一老师的另一节课
                        Joiners.equal(Lesson::getTeacher),
                        // 条件:两节课的时间段连续(这里简化判断:前一节课的结束时间=后一节课的开始时间)
                        Joiners.equal(lesson -> lesson.getTimeslot().getEndTime(),
                                Lesson::getStartTime)
                )
                .reward("Teacher time efficiency", HardSoftScore.ONE_SOFT); // 奖励:加1软分
    }

    // 软约束3:学生尽量不连续上同一门课
    private Constraint studentGroupSubjectVariety(ConstraintFactory constraintFactory) {
        // 逻辑:找到同一班级的两节课,它们的课程相同且时间段连续 → 惩罚1软分
        return constraintFactory
                .forEach(Lesson.class)
                .join(Lesson.class,
                        Joiners.equal(Lesson::getStudentGroup),
                        Joiners.equal(Lesson::getSubject), // 条件:同一门课
                        Joiners.equal(lesson -> lesson.getTimeslot().getEndTime(),
                                Lesson::getStartTime) // 条件:时间段连续
                )
                .penalize("StudentGroup subject variety", HardSoftScore.ONE_SOFT);
    }
}

4.5 第五步:配置求解器

创建求解器配置文件(src/main/resources/optaplanner/lessonSchedulingSolverConfig.xml),指定约束提供者、求解时间等:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<solver xmlns="https://www.optaplanner.org/xsd/solver"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd"&gt;

    <!-- 解决方案类 -->
    <solutionClass>com.example.optaplanner.TimeTable</solutionClass&gt;
    <!-- 规划实体类 -->
    <entityClass>com.example.optaplanner.Lesson&lt;/entityClass&gt;

    <!-- 约束提供者(分数计算逻辑) -->
    <scoreDirectorFactory>
        <constraintProviderClass>com.example.optaplanner.TimeTableConstraintProvider</constraintProviderClass>
    &lt;/scoreDirectorFactory&gt;

    <!-- 求解终止条件:最多求解3分钟(可根据需求调整) -->
    <termination>
        <secondsSpentLimit>180</secondsSpentLimit>
    </termination>

</solver>

4.6 第六步:编写主程序,运行求解

初始化问题数据(时间段、房间、老师、班级、课程),创建求解器,运行并输出结果:

复制代码
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import java.util.Arrays;
import java.util.List;

public class LessonSchedulingApp {
    public static void main(String[] args) {
        // 1. 初始化问题事实:时间段(周一至周二的5个时间段)
        List<Timeslot> timeslotList = Arrays.asList(
                new Timeslot(1L, "MON", "08:30", "09:30"),
                new Timeslot(2L, "MON", "09:30", "10:30"),
                new Timeslot(3L, "MON", "10:30", "11:30"),
                new Timeslot(4L, "TUE", "08:30", "09:30"),
                new Timeslot(5L, "TUE", "09:30", "10:30")
        );

        // 2. 初始化问题事实:房间(3个房间)
        List<Room> roomList = Arrays.asList(
                new Room(1L, "房间A"),
                new Room(2L, "房间B"),
                new Room(3L, "房间C")
        );

        // 3. 初始化问题事实:老师和班级
        Teacher zhangTeacher = new Teacher(1L, "张老师"); // 语文
        Teacher liTeacher = new Teacher(2L, "李老师");   // 数学
        Teacher zhongTeacher = new Teacher(3L, "钟老师"); // 英语

        StudentGroup class1 = new StudentGroup(1L, "1班");
        StudentGroup class2 = new StudentGroup(2L, "2班");
        StudentGroup class3 = new StudentGroup(3L, "3班");

        // 4. 初始化待规划课程(未分配时间段和房间)
        List<Lesson> lessonList = Arrays.asList(
                new Lesson(1L, "语文", zhangTeacher, class1),
                new Lesson(2L, "语文", zhangTeacher, class2),
                new Lesson(3L, "数学", liTeacher, class1),
                new Lesson(4L, "数学", liTeacher, class3),
                new Lesson(5L, "英语", zhongTeacher, class2),
                new Lesson(6L, "英语", zhongTeacher, class3)
        );

        // 5. 构建解决方案(初始状态:课程未分配时间段和房间)
        TimeTable unsolvedTimeTable = new TimeTable(timeslotList, roomList, lessonList);

        // 6. 创建求解器(加载配置文件)
        SolverFactory<TimeTable> solverFactory = SolverFactory.createFromXmlResource(
                "optaplanner/lessonSchedulingSolverConfig.xml");
        Solver<TimeTable> solver = solverFactory.buildSolver();

        // 7. 运行求解器,得到最优解
        TimeTable solvedTimeTable = solver.solve(unsolvedTimeTable);

        // 8. 输出结果
        System.out.println("求解完成!最终分数:" + solvedTimeTable.getScore());
        System.out.println("最优课程表安排:");
        solvedTimeTable.getLessonList().forEach(lesson -> 
                System.out.println(lesson.toString()));
    }
}

4.7 第七步:运行结果与解读

运行主程序后,输出结果类似如下(无硬约束冲突,软分数较高):

复制代码
求解完成!最终分数:0hard/3soft
最优课程表安排:
语文(张老师-1班) MON 08:30-09:30-房间A
语文(张老师-2班) MON 09:30-10:30-房间A
数学(李老师-1班) MON 09:30-10:30-房间B
数学(李老师-3班) TUE 08:30-09:30-房间B
英语(钟老师-2班) MON 10:30-11:30-房间A
英语(钟老师-3班) TUE 09:30-10:30-房间A

结果解读:

  • 分数:0hard/3soft → 硬分数为0(无硬约束冲突,安排可行),软分数为3(符合3个软约束,比如张老师连堂且在同一房间上课);
  • 安排合理:比如张老师的两节课连堂(MON 08:30和09:30)且都在房间A,符合软约束;无房间、老师、班级冲突,符合硬约束。
相关推荐
JavaGuide21 小时前
利用元旦假期,我开源了一个大模型智能面试平台+知识库!
前端·后端
大猪宝宝学AI21 小时前
【AI Infra】BF-PP:广度优先流水线并行
人工智能·性能优化·大模型·模型训练
eason_fan1 天前
前端性能优化利器:LitePage 轻量级全页设计解析
前端·性能优化·前端工程化
世洋Blog1 天前
AStar算法基础学习总结
算法·面试·c#·astar·寻路
橙子家1 天前
Serilog 日志库简单实践(四)消息队列 Sinks(.net8)
后端
Victor3561 天前
Hibernate(21)Hibernate的映射文件是什么?
后端
pe7er1 天前
如何阅读英文文档
java·前端·后端
pe7er1 天前
IDEA 实用小技巧(自用)
后端
Victor3561 天前
Hibernate(22)Hibernate的注解配置是什么?
后端