17.7. 使应用程序可执行
完成 Red Hat Build of OptaPlanner Spring Boot timetable 项目后,将所有内容打包成由标准 Java main ()
方法驱动的单个可执行 JAR 文件。
先决条件
- 您有一个已完成的 OptaPlanner Spring Boot timetable 项目。
流程
使用以下内容创建
TimeTableSpringBootApp.java
类:package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TimeTableSpringBootApp { public static void main(String[] args) { SpringApplication.run(TimeTableSpringBootApp.class, args); } }
-
将由 Spring Initializr 创建的
src/main/java/com/example/DemoApplication.java
类替换为TimeTableSpringBootApp.java
类。 -
将
TimeTableSpringBootApp.java
类作为常规 Java 应用程序的主类运行。
17.7.1. 尝试 timetable 应用程序
启动 Red Hat Build of OptaPlanner Spring Boot timetable 应用程序后,您可以使用您想要的任何 REST 客户端测试 REST 服务。本例使用 Linux curl
命令发送 POST 请求。
先决条件
- OptaPlanner Spring Boot timetable 应用程序正在运行。
流程
使用以下命令:
$ curl -i -X POST http://localhost:8080/timeTable/solve -H "Content-Type:application/json" -d '{"timeslotList":[{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"}],"roomList":[{"name":"Room A"},{"name":"Room B"}],"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade"},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade"},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade"},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade"}]}'
大约 5 秒后,终止会花费在 application.properties
中定义的时间,服务会返回类似以下示例的输出:
HTTP/1.1 200 Content-Type: application/json ... {"timeslotList":...,"roomList":...,"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room A"}},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room A"}},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room B"}},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room B"}}],"score":"0hard/0soft"}
请注意,应用程序会将所有四个课程分配给两个时间插槽之一,另一个是两个房间。另请注意,它符合所有硬约束。例如,M. Curie 的两个课程位于不同的时间插槽中。
在服务器端,info
日志显示 OptaPlanner 在 5 秒内执行的操作:
... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). ... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4). ... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398). ... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE).
17.7.2. 测试应用
良好的应用程序包括测试覆盖。这个示例测试 Timetable Red Hat Build of OptaPlanner Spring Boot 应用程序。它使用 JUnit 测试来生成测试数据集并将其发送到 TimeTableController
以解决。
流程
使用以下内容创建 src/test/java/com/example/solver/TimeTableControllerTest.java
类:
package com.example.solver; import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import com.example.domain.Lesson; import com.example.domain.Room; import com.example.domain.TimeTable; import com.example.domain.Timeslot; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest(properties = { "optaplanner.solver.termination.spent-limit=1h", // Effectively disable this termination in favor of the best-score-limit "optaplanner.solver.termination.best-score-limit=0hard/*soft"}) public class TimeTableControllerTest { @Autowired private TimeTableController timeTableController; @Test @Timeout(600_000) public void solve() { TimeTable problem = generateProblem(); TimeTable solution = timeTableController.solve(problem); assertFalse(solution.getLessonList().isEmpty()); for (Lesson lesson : solution.getLessonList()) { assertNotNull(lesson.getTimeslot()); assertNotNull(lesson.getRoom()); } assertTrue(solution.getScore().isFeasible()); } private TimeTable generateProblem() { List<Timeslot> timeslotList = new ArrayList<>(); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade")); lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade")); lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade")); lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } }
此测试会验证在解决后,所有课程都分配给一个时间插槽和房间。它还会验证是否发现一种可行的解决方案(无硬限制)。
通常,该方案在 200 毫秒内找到可行的解决方案。请注意,@SpringBootTest
注释 的属性
如何覆盖 solver 终止,以便在可行的解决方案(0hard8:0:1::soft
)时立即终止。这可避免硬编码代码,因为单元测试可能会在任意硬件上运行。这种方法可确保测试运行时间足够长,以找到可行的解决方案,即使在速度较慢的系统上也是如此。但是,即使在快速系统中,它不会比严格必须运行 milli 秒。
17.7.3. 日志记录
完成 Red Hat Build of OptaPlanner Spring Boot timetable 应用程序后,您可以使用日志信息微调 ConstraintProvider
中的限制。查看 info
日志文件中的分数计算速度,以评估对您约束的更改的影响。以调试模式运行应用程序,以显示应用程序所采用的每个步骤,或使用 trace logging 来记录每个步骤和每个移动。
流程
- 运行 timetable 应用以固定时间,例如五分钟。
查看日志文件中的分数计算速度,如下例所示:
... Solving ended: ..., score calculation speed (29455/sec), ...
-
更改约束,再次运行 planning 应用程序以相同的时间,并检查日志文件中记录的分数计算速度。
以 debug 模式运行应用程序以记录每个步骤:
-
要从命令行运行调试模式,请使用
-D
系统属性。 要更改
application.properties
文件中的日志记录,请在该文件中添加以下行:logging.level.org.optaplanner=debug
以下示例显示了
日志文件中的
debug 模式的输出:... Solving started: time spent (67), best score (-20init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). ... CH step (0), time spent (128), score (-18init/0hard/0soft), selected move count (15), picked move ([Math(101) {null -> Room A}, Math(101) {null -> MONDAY 08:30}]). ... CH step (1), time spent (145), score (-16init/0hard/0soft), selected move count (15), picked move ([Physics(102) {null -> Room A}, Physics(102) {null -> MONDAY 09:30}]). ...
-
要从命令行运行调试模式,请使用
-
使用
trace
logging 显示每个步骤和每个步骤。