Spring Boot 整合視圖層技術

這一節我們主要學習如何整合視圖層技術:

在之前的案例中,我們都是通過 @RestController 來處理請求,所以返回的內容為json對象。那么如果需要渲染html頁面的時候,要如何實現呢?

Spring Boot推薦使用模板引擎

模板引擎實現偽html 達到seo優化 使動態頁面靜態化

在動態html上實現Spring Boot依然可以完美勝任,并且提供了多種模板引擎的默認配置支持,所以在推薦的模板引擎下,我們可以很快的上手開發動態網站

Spring Boot提供了默認配置的模板引擎主要有以下幾種:

Thymeleaf

FreeMarker

Velocity

Groovy

Mustache

Spring Boot建議使用這些模板引擎,避免使用jsp。

Jsp

創建項目

創建 war 項目,編寫pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.springboot</groupId>
    <artifactId>springboot-view</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>springboot-view Maven Webapp</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Web 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Boot不推薦使用 jsp,所以需要手動導入 jstl 和 jasper 依賴 -->
        <!-- jstl -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
        <!-- jasper -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

視圖解析

resources/application.properties

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

實體類

User.java

package com.springboot.pojo;

import java.io.Serializable;

public class User implements Serializable {

    private Integer id;
    private String username;
    private Integer age;

    public User() {
    }

    public User(Integer id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '/'' +
                ", age=" + age +
                '}';
    }
    
}

控制層

UserController.java

package com.springboot.controller;

import com.springboot.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.ArrayList;
import java.util.List;

@Controller
public class UserController {

    @RequestMapping("/showUser")
    public String showUser(Model model) {
        List<User> list = new ArrayList<>();
        list.add(new User(1, "張三", 18));
        list.add(new User(2, "李四", 20));
        list.add(new User(3, "王五", 22));
        model.addAttribute("list", list);
        // 跳轉視圖
        return "userList";
    }

}

視圖層

userList.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>用戶展示</title>
</head>
<body>
    <table border="1" cellspacing="0" align="center" width="50%">
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Age</th>
        </tr>
        <c:forEach items="${list}" var="user">
            <tr>
                <td>${user.id}</td>
                <td>${user.username}</td>
                <td>${user.age}</td>
            </tr>
        </c:forEach>
    </table>
</body>
</html>

啟動類

App.java

package com.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}

結果

Spring Boot 整合視圖層技術

Freemarker

創建項目

創建 war 項目,編寫pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.springboot</groupId>
    <artifactId>springboot-view</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>springboot-view Maven Webapp</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <dependencies>
        <!-- web 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- freemarker 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
    </dependencies>
</project>

視圖解析器

該文件內容不一定非得需要,可以不添加,不添加使用默認值。

resources/application.properties

# FREEMARKER (FreeMarkerAutoConfiguration)
spring.freemarker.allow-request-override=false
spring.freemarker.allow-session-override=false
spring.freemarker.cache=true
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.enabled=true
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.expose-spring-macro-helpers=true
spring.freemarker.prefer-file-system-access=true
spring.freemarker.suffix=.ftl
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.settings.template_update_delay=0
spring.freemarker.settings.default_encoding=UTF-8
spring.freemarker.settings.classic_compatible=true
spring.freemarker.order=1

實體類

User.java

package com.springboot.pojo;

import java.io.Serializable;

public class User implements Serializable {

    private Integer id;
    private String username;
    private Integer age;

    public User() {
    }

    public User(Integer id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '/'' +
                ", age=" + age +
                '}';
    }
    
}

控制層

UserController.java

package com.springboot.controller;

import com.springboot.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.ArrayList;
import java.util.List;

@Controller
public class UserController {

    @RequestMapping("/showUser")
    public String showUser(Model model) {
        List<User> list = new ArrayList<>();
        list.add(new User(1, "張三", 18));
        list.add(new User(2, "李四", 20));
        list.add(new User(3, "王五", 22));
        model.addAttribute("list", list);
        // 跳轉視圖
        return "userList";
    }

}

視圖層

Spring Boot要求模板形式的視圖層技術的文件必須要放到 src/main/resources 目錄下的 templates 目錄。

該目錄內的模板文件名稱必須是 ftl 的后綴結尾。

userList.ftl

<!DOCTYPE html>
<html>
<head>
    <title>用戶展示</title>
    <meta charset="UTF-8"></meta>
</head>
<body>
    <table border="1" cellspacing="0" align="center" width="50%">
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Age</th>
        </tr>
        <#list list as user >
            <tr>
                <td>${user.id}</td>
                <td>${user.username}</td>
                <td>${user.age}</td>
            </tr>
        </#list>
    </table>
</body>
</html>

啟動類

App.java

package com.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}

結果

Spring Boot 整合視圖層技術

Thymeleaf (重點講解)

入門案例

創建項目

創建 war 項目,編寫pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.springboot</groupId>
    <artifactId>springboot-view</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>springboot-view Maven Webapp</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <dependencies>
        <!-- web 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- thymeleaf 組件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
</project>

控制層

ThymeleafController.java

package com.springboot.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ThymeleafController {

    @RequestMapping("/show")
    public String showMsg(Model model) {
        model.addAttribute("msg", "Thymeleaf 入門案例");
        return "msg";
    }

}

視圖層

Spring Boot要求模板形式的視圖層技術的文件必須要放到 src/main/resources 目錄下的 templates 目錄。

msg.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf</title>
</head>
<body>
    <span th:text="Thymeleaf"></span>
    <hr/>
    <span th:text="${msg}"></span>
</body>
</html>

啟動類

App.java

package com.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}

運行結果出現異常(如果是 Spring Boot 1.X.X 版本才會出現此異常):

org.xml.sax.SAXParseException: 元素類型 "meta" 必須由匹配的結束標記 "</meta>" 終止。

異常處理

如果是 Spring Boot 1.X.X 版本需要以下操作,本案例是 Spring Boot 2.1.6 版本,所以不需要更改。

方式一:編寫風格嚴謹的HTML代碼

<meta charset="UTF-8"/>

方式二:更換Thymeleaf的jar包版本

thymeleaf.jar:更新為 3.0 以上的版本

Spring Boot 整合視圖層技術

<properties>
    <thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
</properties>

運行結果

Spring Boot 整合視圖層技術

Thymeleaf 語法詳解

Thymeleaf 內置對象語法:

  • 調用內置對象一定要用 #
  • 大部分的內置對象都以 s 結尾 stringsnumbersdates

準備數據

實體類

User.java

package com.springboot.pojo;

import java.io.Serializable;

public class User implements Serializable {

    private Integer id;
    private String username;
    private Integer age;

    public User() {
    }

    public User(Integer id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '/'' +
                ", age=" + age +
                '}';
    }
    
}

控制層

ThymeleafController.java

package com.springboot.controller;

import com.springboot.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;

@Controller
public class ThymeleafController {

    @RequestMapping("/show")
    public String showMsg(Model model,
                          HttpServletRequest request,
                          HttpServletResponse response) {

        // 字符串
        model.addAttribute("msg", "Thymeleaf 入門案例");
        // 日期時間
        model.addAttribute("myDate", new Date());
        // 條件判斷if
        model.addAttribute("sex", 1);
        // 條件判斷switch
        model.addAttribute("id", 1);
        // 對象
        model.addAttribute("user", new User(1, "張三", 20));

        // 迭代遍歷list
        List<User> userList = new ArrayList<>();
        userList.add(new User(1, "張三", 20));
        userList.add(new User(2, "李四", 22));
        userList.add(new User(3, "王五", 24));
        model.addAttribute("userList", userList);

        // 迭代遍歷map
        Map<String, User> userMap = new HashMap<>();
        userMap.put("u1", new User(1, "張三", 20));
        userMap.put("u2", new User(2, "李四", 22));
        userMap.put("u3", new User(3, "王五", 24));
        model.addAttribute("userMap", userMap);

        // 域對象操作
        request.setAttribute("req", "HttpServletRequest");
        request.getSession().setAttribute("sess", "HttpSession");
        request.getSession().getServletContext().setAttribute("app", "Application");
        return "msg";

    }

    /**
     * URL表達式-相對路徑
     * @return
     */
    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    /**
     * URL表達式-普通傳參
     * @param id
     * @param username
     * @return
     */
    @RequestMapping("/user")
    public String user(Integer id, String username) {
        System.out.println("id:" + id + " username:" + username);
        return "user";
    }

    /**
     * URL表達式-restful傳參
     * @param id
     * @param username
     * @return
     */
    @RequestMapping("/person/{id}/{username}")
    public String person(@PathVariable Integer id, @PathVariable String username) {
        System.out.println("id:" + id + " username:" + username);
        return "person";
    }

}

視圖層

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf</title>
</head>
<body>
    index
</body>
</html>

user.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf</title>
</head>
<body>
    user
</body>
</html>

person.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf</title>
</head>
<body>
    person
</body>
</html>

字符串

變量輸出

th:text :在頁面中輸入值

<span th:text="Thymeleaf"></span>

th:value :可以將一個值設置至input標簽的value中

<input type="text" th:value="${msg}" />

字符串操作

${#strings.isEmpty(key)} :判斷字符串是否為空

<span th:text="${#strings.isEmpty(msg)}"></span>

${#strings.contains(msg, 'T')} :判斷字符串是否包含子串

<span th:text="${#strings.contains(msg, 'T')}"></span>

${#strings.startsWith(msg, 'T')} :判斷字符串是否以子串開頭

<span th:text="${#strings.startsWith(msg, 'T')}"></span>

${#strings.endsWith(msg, 'T')} :判斷字符串是否以子串結尾

<span th:text="${#strings.endsWith(msg, 'T')}"></span>

${#strings.length(msg)} :返回字符串的長度

<span th:text="${#strings.length(msg)}"></span>

${#strings.indexOf(msg, 'T')} :查找子串的位置,并返回該子串的下標,如果沒找到則返回-1

<span th:text="${#strings.indexOf(msg, 'T')}"></span>

${#strings.substring(msg, 5)} :截取子串,從指定下標開始截止到末尾結束

${#strings.substring(msg, 5, 12)} :截取子串,從指定下標開始截止到指定下標結束

<span th:text="${#strings.substring(msg, 5)}"></span>
<span th:text="${#strings.substring(msg, 5, 12)}"></span>

${#strings.toUpperCase(msg)} :將字符串轉大寫

<span th:text="${#strings.toUpperCase(msg)}"></span>

${#strings.toLowerCase(msg)} :將字符串轉小寫

<span th:text="${#strings.toLowerCase(msg)}"></span>

日期時間

${#dates.format(key)} :格式化日期,以瀏覽器默認語言為格式化標準

<span th:text="${#dates.format(myDate)}"></span>

${#dates.format(key,'yyy/MM/dd')} :自定義格式日期轉換

<span th:text="${#dates.format(myDate, 'yyyy年MM月dd日 HH:mm:ss')}"></span>

${#dates.year(myDate)} :獲取年份,還可以獲取月份、日、時、分、秒

<span th:text="${#dates.year(myDate)}"></span>年
<span th:text="${#dates.month(myDate)}"></span>月
<span th:text="${#dates.day(myDate)}"></span>日
<span th:text="${#dates.hour(myDate)}"></span>時
<span th:text="${#dates.minute(myDate)}"></span>分
<span th:text="${#dates.second(myDate)}"></span>秒

條件判斷

th:if :單選擇

性別:<span th:if="${sex} == 1">男</span>
     <span th:if="${sex} == 2">女</span>

th:switch :多選擇(如果要實現 if else if else 判斷表達式,在 Thymeleaf 要使用 th:switch 代替)

編號:<span th:switch="${id}">
        <span th:case="1">1 張三</span>
        <span th:case="2">2 李四</span>
        <span th:case="3">3 王五</span>
    </span>

對象

<input th:value="${user.username}"/>
<span th:text="${user.age}"></span>

迭代遍歷

th:each :迭代遍歷

<table border="1" cellspacing="0">
    <tr>
        <th>ID</th>
        <th>NAME</th>
        <th>AGE</th>
    </tr>
    <tr th:each="user : ${userList}">
        <td th:text="${user.id}"></td>
        <td th:text="${user.username}"></td>
        <td th:text="${user.age}"></td>
    </tr>
</table>

th:each :狀態變量屬性

  • index:當前迭代器的索引 從 0 開始
  • count:當前迭代對象的計數 從 1 開始
  • size:被迭代對象的長度
  • even/odd:布爾值,當前循環是否是偶數/奇數 從 0 開始
  • first:布爾值,當前循環的是否是第一條,如果是返回 true 否則返回 false
  • last:布爾值,當前循環的是否是最后一條,如果是則返回 true 否則返回 false
<table border="1" cellspacing="0">
    <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Age</th>
        <th>Index</th>
        <th>Count</th>
        <th>Size</th>
        <th>Even</th>
        <th>Odd</th>
        <th>First</th>
        <th>Last</th>
    </tr>
    <tr th:each="user, state : ${userList}">
        <td th:text="${user.id}"></td>
        <td th:text="${user.username}"></td>
        <td th:text="${user.age}"></td>
        <td th:text="${state.index}"></td>
        <td th:text="${state.count}"></td>
        <td th:text="${state.size}"></td>
        <td th:text="${state.even}"></td>
        <td th:text="${state.odd}"></td>
        <td th:text="${state.first}"></td>
        <td th:text="${state.last}"></td>
    </tr>
</table>

Spring Boot 整合視圖層技術

th:each :迭代 Map

<table border="1" cellspacing="0">
    <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Age</th>
    </tr>
    <tr th:each="user : ${userMap}">
        <td th:text="${user}"></td>
    </tr>
</table>
<table border="1" cellspacing="0">
    <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Age</th>
    </tr>
    <tr th:each="user : ${userMap}">
        <td th:each="entry : ${user}" th:text="${entry.value.id}" ></td>
        <td th:each="entry : ${user}" th:text="${entry.value.username}"></td>
        <td th:each="entry : ${user}" th:text="${entry.value.age}"></td>
    </tr>
</table>

Spring Boot 整合視圖層技術

域對象操作

${#httpServletRequest.getAttribute(key)} :HttpServletRequest

Request:<span th:text="${#httpServletRequest.getAttribute('req')}"></span>

${session.key} :HttpSession

Session:<span th:text="${session.sess}"></span>

${application.key} :ServletContext

Application:<span th:text="${application.app}"></span>

URL表達式

基本語法

URL表達式的基本語法: @{}

th:href :絕對路徑

<a th:href="@{http://www.baidu.com}">絕對路徑</a>

th:href :相對路徑,相對于當前項目的根路徑

<a th:href="@{/index}">相對于當前項目的根路徑</a>

th:href :相對路徑, 相對于服務器的根路徑

<a th:href="@{~/project/resourcename}">相對于服務器的根路徑</a>

參數傳遞

<a th:href="@{/user(id=1, username=zhagnsan)}">相對路徑-普通傳參</a>
<a th:href="@{/person/1/zhangsan}">相對路徑-restful傳參</a>
<a th:href="@{/person/{id}/{name}(id=2, name=lisi)}">相對路徑-restful傳參</a>

??本章節到這里就結束了,喜歡的話就點贊 加轉發:revolving_hearts:吧,我們下章節再見:wave:。

原文 

https://segmentfault.com/a/1190000020604378

本站部分文章源于互聯網,本著傳播知識、有益學習和研究的目的進行的轉載,為網友免費提供。如有著作權人或出版方提出異議,本站將立即刪除。如果您對文章轉載有任何疑問請告之我們,以便我們及時糾正。

PS:推薦一個微信公眾號: askHarries 或者qq群:474807195,里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

轉載請注明原文出處:Harries Blog? » Spring Boot 整合視圖層技術

贊 (0)
分享到:更多 ()

評論 0

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
手机彩票计划软件超稳