【SpringMVC笔记】 - 6 - RESTFul编程风格

【SpringMVC 笔记】 - 6 - RESTFul 编程风格

一、RESTFul 核心概念

1.1 什么是 RESTFul

RESTFul(Representational State Transfer,表述性状态转移)是一种 WEB 服务接口的设计风格,而非强制标准。它定义了一组约束条件和规范,遵循该风格的接口具备简洁、易理解、易扩展、安全可靠的特性,是目前前后端交互、接口设计的主流范式。

核心思想:通过「URI + HTTP 请求方式」来描述并操作服务器端的资源 ,而非通过 URL 携带操作语义(如传统的getUserByIddeleteUser)。

1.2 RESTFul 的核心约束

约束维度 具体规范
URL 格式 基于资源命名(名词),而非操作(动词);使用层级结构,通过/分隔;避免后缀(.html/.json)
HTTP 请求方式 GET(查询)、POST(新增)、PUT(全量修改)、DELETE(删除)、PATCH(局部修改)
数据格式 推荐 JSON/XML,前后端约定统一;响应体仅返回数据,状态通过 HTTP 状态码表达
HTTP 状态码 200(成功)、201(创建成功)、400(参数错误)、404(资源不存在)、500(服务端错误)等

1.3 RESTFul vs 传统 URL 风格

操作 传统 URL(基于动作) RESTFul URL(基于资源) HTTP 请求方式
根据 ID 查询 /getUserById?id=1 /user/1 GET
查询所有 /getAllUser /user GET
新增用户 /addUser /user POST
修改用户 /modifyUser /user PUT
删除用户 /deleteUserById?id=1 /user/1 DELETE

核心差异:传统 URL 是 "动作导向",RESTFul 是 "资源导向",通过请求方式区分对资源的操作。

二、SpringMVC 实现 RESTFul 的核心技术

2.1 核心注解与组件

技术点 作用
@RequestMapping 映射 URL 和请求方式(可指定method = RequestMethod.GET/POST/PUT/DELETE
@GetMapping/@PostMapping/@PutMapping/@DeleteMapping @RequestMapping的简化版,分别对应 GET/POST/PUT/DELETE 请求
@PathVariable 从 URL 路径中提取参数(如/user/{id}中的id
HiddenHttpMethodFilter SpringMVC 提供的过滤器,将 POST 请求转换为 PUT/DELETE 请求(解决浏览器不支持 PUT/DELETE 的问题)

2.2 HiddenHttpMethodFilter 原理

浏览器原生仅支持 GET/POST 请求,PUT/DELETE 请求需通过以下方式模拟:

  1. 前端表单提交方式为POST
  2. 表单中添加隐藏域:<input type="hidden" name="_method" value="PUT/DELETE"/>
  3. 后端配置HiddenHttpMethodFilter过滤器,该过滤器会读取_method参数,将 POST 请求转换为对应 PUT/DELETE 请求。

关键注意事项

HiddenHttpMethodFilter依赖request.getParameter("_method"),因此需保证字符编码过滤器(CharacterEncodingFilter)在其之前执行(否则会导致_method参数乱码)。

三、SpringMVC 实现 RESTFul 实战(演员管理系统)

3.1 环境搭建

3.1.1 依赖配置(Maven)
xml 复制代码
<dependencies>
    <!-- SpringMVC核心依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>6.1.5</version>
    </dependency>
    <!-- Servlet API -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
    </dependency>
    <!-- Thymeleaf(视图渲染) -->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring6</artifactId>
        <version>3.1.2.RELEASE</version>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.5.3</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- 编译插件(解决参数名映射问题) -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.12.1</version>
            <configuration>
                <source>21</source>
                <target>21</target>
                <compilerArgs>
                    <arg>-parameters</arg>
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>
3.1.2 web.xml 配置(核心过滤器 + 前端控制器)
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
    <!-- 1. 字符编码过滤器(必须在HiddenHttpMethodFilter之前) -->
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceRequestEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 2. HiddenHttpMethodFilter:转换POST为PUT/DELETE -->
    <filter>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 3. SpringMVC前端控制器 -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!-- 服务器启动时初始化 -->
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
3.1.3 springmvc.xml 配置(核心配置)
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/context 
        https://www.springframework.org/schema/context/spring-context.xsd 
        http://www.springframework.org/schema/mvc 
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 1. 组件扫描(Controller、Dao、Service等) -->
    <context:component-scan base-package="com.zzz"/>

    <!-- 2. Thymeleaf视图解析器 -->
    <bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
        <property name="characterEncoding" value="UTF-8"/>
        <property name="order" value="1"/>
        <property name="templateEngine">
            <bean class="org.thymeleaf.spring6.SpringTemplateEngine">
                <property name="templateResolver">
                    <bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
                        <!-- 视图前缀:HTML文件存放路径 -->
                        <property name="prefix" value="/WEB-INF/templates/"/>
                        <!-- 视图后缀 -->
                        <property name="suffix" value=".html"/>
                        <property name="templateMode" value="HTML"/>
                        <property name="characterEncoding" value="UTF-8"/>
                    </bean>
                </property>
            </bean>
        </property>
    </bean>

    <!-- 3. 视图控制器(直接映射URL到视图,无需Controller) -->
    <mvc:view-controller path="/" view-name="index"/>
    <mvc:view-controller path="/actormgt" view-name="actor_index"/>
    <mvc:view-controller path="/toAdd" view-name="actor_insert"/>

    <!-- 4. 静态资源放行(CSS/JS/图片等) -->
    <mvc:default-servlet-handler/>

    <!-- 5. 开启注解驱动(支持@GetMapping/@PostMapping等) -->
    <mvc:annotation-driven/>
</beans>

3.2 核心业务代码实现

3.2.1 实体类(Actor)
java 复制代码
package com.zzz.pojo;

/**
 * 演员实体类(对应RESTFul中的"资源")
 */
public class Actor {
    private Long id;         // 主键ID
    private String username; // 用户名
    private String sex;      // 性别
    private String email;    // 邮箱

    // 无参构造(SpringMVC参数绑定必需)
    public Actor() {}

    // 有参构造
    public Actor(Long id, String username, String sex, String email) {
        this.id = id;
        this.username = username;
        this.sex = sex;
        this.email = email;
    }

    // Getter/Setter(SpringMVC参数绑定必需)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getSex() { return sex; }
    public void setSex(String sex) { this.sex = sex; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    @Override
    public String toString() {
        return "Actor{id = " + id + ", username = " + username + ", sex = " + sex + ", email = " + email + "}";
    }
}
3.2.2 数据访问层(ActorDao)

模拟内存数据库,实现 CRUD:

java 复制代码
package com.zzz.dao;

import com.zzz.pojo.Actor;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 演员数据访问层(@Repository标识为Spring组件)
 */
@Repository
public class ActorDao {
    // 模拟数据库表:存储演员数据
    private static List<Actor> actors = new ArrayList<>();

    // 初始化测试数据
    static {
        actors.add(new Actor(1001L, "张三", "男", "123@qq.com"));
        actors.add(new Actor(1002L, "李四", "女", "456@qq.com"));
        actors.add(new Actor(1003L, "王五", "男", "789@qq.com"));
        actors.add(new Actor(1004L, "赵六", "女", "012@qq.com"));
        actors.add(new Actor(1005L, "孙七", "男", "345@qq.com"));
    }

    /**
     * 查询所有演员(GET /actor)
     */
    public List<Actor> getAll() {
        return new ArrayList<>(actors); // 返回副本,避免外部修改
    }

    /**
     * 新增演员(POST /actor)
     */
    public void insert(Actor actor) {
        // 生成自增ID:基于现有最大ID+1
        Long maxId = actors.stream()
                .map(Actor::getId)
                .max(Long::compareTo)
                .orElse(1000L);
        actor.setId(maxId + 1);
        actors.add(actor);
    }

    /**
     * 根据ID查询演员(GET /actor/{id})
     */
    public Actor getById(Long id) {
        return actors.stream()
                .filter(actor -> actor.getId().equals(id))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("演员ID不存在:" + id));
    }

    /**
     * 修改演员(PUT /actor)
     */
    public void update(Actor actor) {
        // 查找并替换原有数据
        for (int i = 0; i < actors.size(); i++) {
            if (actors.get(i).getId().equals(actor.getId())) {
                actors.set(i, actor);
                return;
            }
        }
        throw new RuntimeException("修改失败:演员ID不存在");
    }

    /**
     * 删除演员(DELETE /actor/{id})
     */
    public void deleteById(Long id) {
        boolean removed = actors.removeIf(actor -> actor.getId().equals(id));
        if (!removed) {
            throw new RuntimeException("删除失败:演员ID不存在");
        }
    }
}
3.2.3 控制器(ActorController)

核心:通过 RESTFul 风格映射请求,处理 CRUD:

java 复制代码
package com.zzz.controller;

import com.zzz.dao.ActorDao;
import com.zzz.pojo.Actor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 演员控制器:RESTFul风格接口实现
 */
@Controller
public class ActorController {
    // 自动注入Dao层(Spring依赖注入)
    @Autowired
    private ActorDao actorDao;

    /**
     * 1. 查询所有演员
     * 请求方式:GET
     * 请求URL:/actor
     */
    @GetMapping("/actor")
    public String getAllActors(Model model) {
        List<Actor> actors = actorDao.getAll();
        // 将数据存入Request域(供Thymeleaf渲染)
        model.addAttribute("actors", actors);
        // 跳转至演员列表页面
        return "actor_list";
    }

    /**
     * 2. 新增演员
     * 请求方式:POST
     * 请求URL:/actor
     */
    @PostMapping("/actor")
    public String saveActor(Actor actor) {
        actorDao.insert(actor);
        // 重定向:避免表单重复提交,刷新后回到列表页
        return "redirect:/actor";
    }

    /**
     * 3. 根据ID查询演员(跳转修改页面)
     * 请求方式:GET
     * 请求URL:/actor/{id}
     */
    @GetMapping("/actor/{id}")
    public String getActorById(@PathVariable("id") Long id, Model model) {
        Actor actor = actorDao.getById(id);
        model.addAttribute("actor", actor);
        // 跳转至修改页面
        return "actor_update";
    }

    /**
     * 4. 修改演员
     * 请求方式:PUT
     * 请求URL:/actor
     */
    @PutMapping("/actor")
    public String updateActor(Actor actor) {
        actorDao.update(actor);
        // 重定向至列表页
        return "redirect:/actor";
    }

    /**
     * 5. 删除演员
     * 请求方式:DELETE
     * 请求URL:/actor/{id}
     */
    @DeleteMapping("/actor/{id}")
    public String deleteActor(@PathVariable("id") Long id) {
        actorDao.deleteById(id);
        // 重定向至列表页
        return "redirect:/actor";
    }
}

3.3 前端页面实现(Thymeleaf)

3.3.1 演员列表页(actor_list.html)

核心:展示数据、提供新增 / 修改 / 删除入口,模拟 DELETE 请求:

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>演员列表</title>
    <link rel="stylesheet" th:href="@{/static/css/actor.css}" type="text/css"/>
</head>
<body>
<div class="header">
    <h1>演员列表</h1>
</div>

<!-- 新增按钮 -->
<div class="add-button-wrapper">
    <a class="add-button" th:href="@{/toAdd}">新增演员</a>
</div>

<!-- 演员表格 -->
<table>
    <thead>
    <tr>
        <th>编号</th>
        <th>用户名</th>
        <th>性别</th>
        <th>邮箱</th>
        <th>操作</th>
    </tr>
    </thead>
    <tbody>
    <!-- 遍历演员列表:th:each -->
    <tr th:each="actor : ${actors}">
        <td th:text="${actor.id}"></td>
        <td th:text="${actor.username}"></td>
        <td th:text="${actor.sex}"></td>
        <td th:text="${actor.email}"></td>
        <td>
            <!-- 修改:GET /actor/{id} -->
            <a th:href="@{'/actor/' + ${actor.id}}">修改</a>
            &nbsp;
            <!-- 删除:模拟DELETE请求 -->
            <a th:href="@{'/actor/' + ${actor.id}}" onclick="del(event)">删除</a>
        </td>
    </tr>
    </tbody>
</table>

<!-- 隐藏表单:用于提交DELETE请求 -->
<div style="display: none">
    <form id="delForm" method="post">
        <input type="hidden" name="_method" value="delete">
    </form>
</div>

<script>
    // 删除确认+表单提交
    function del(event) {
        // 1. 获取删除表单
        let delForm = document.getElementById("delForm");
        // 2. 设置表单action为当前删除链接的URL
        delForm.action = event.target.href;
        // 3. 确认删除
        if (window.confirm("确定删除该演员吗?")) {
            delForm.submit();
        }
        // 4. 阻止超链接默认跳转
        event.preventDefault();
    }
</script>
</body>
</html>
3.3.2 新增演员页(actor_insert.html)

核心:POST 提交表单,新增资源:

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>新增演员</title>
    <link rel="stylesheet" th:href="@{/static/css/actor_insert.css}" />
</head>
<body>
<div class="header">
    <h1>新增演员</h1>
</div>

<!-- 表单提交:POST /actor -->
<form th:action="@{/actor}"  method="post">
    <div class="form-group">
        <label>用户名:</label>
        <input type="text" name="username" required />
    </div>

    <div class="form-group">
        <label>性别:</label>
        <select name="sex" required>
            <option value="">-- 请选择 --</option>
            <option value="男">男</option>
            <option value="女">女</option>
        </select>
    </div>

    <div class="form-group">
        <label>邮箱:</label>
        <input type="email" name="email" required />
    </div>

    <div class="form-group">
        <button type="submit">保存</button>
        <a th:href="@{/actor}" style="margin-left: 10px;">返回列表</a>
    </div>
</form>

</body>
</html>
3.3.3 修改演员页(actor_update.html)

核心:模拟 PUT 请求,提交修改数据:

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>修改用户</title>
    <link rel="stylesheet" th:href="@{/static/css/actor_update.css}" type="text/css"></link>
</head>
<body>
<h1>修改用户</h1>
<!-- 表单提交:POST + _method=put -->
<form th:action="@{/actor}" method="post">
    <!-- 隐藏域:指定请求方式为PUT -->
    <input type="hidden" name="_method" value="put">
    <!-- 隐藏域:提交演员ID(修改必需) -->
    <input type="hidden" name="id" th:value="${actor.id}">
    
    <label>用户名:</label>
    <input type="text" name="username" th:value="${actor.username}" required>

    <label>性别:</label>
    <select name="sex" required>
        <option value="">-- 请选择 --</option>
        <!-- th:field:自动选中当前值 -->
        <option value="男" th:field="${actor.sex}">男</option>
        <option value="女" th:field="${actor.sex}">女</option>
    </select>

    <label>邮箱:</label>
    <input type="email" name="email" th:value="${actor.email}" required>

    <button type="submit">修改</button>
</form>
</body>
</html>
3.3.4 样式文件(actor.css/actor_insert.css/actor_update.css)

核心:统一页面样式,支持响应式布局(移动端适配),关键特性:

actor.css:

css 复制代码
/* actor.css - 统一样式 */

/* 重置与基础设置 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: "Microsoft YaHei", Arial, sans-serif;
    background-color: #f5f7fa;
    color: #333;
    line-height: 1.6;
    padding: 20px;
}

/* 头部样式 */
.header {
    text-align: center;
    margin-bottom: 20px;
}

.header h1 {
    font-size: 28px;
    color: #2c3e50;
    margin-bottom: 10px;
}

/* 导航菜单(ul) */
ul {
    list-style-type: none;
    background-color: #34495e;
    padding: 0;
    margin-bottom: 20px;
    border-radius: 4px;
    display: inline-block;
    width: 100%;
    text-align: center;
}

ul li {
    display: inline-block;
}

ul li a {
    display: block;
    color: white;
    text-decoration: none;
    padding: 12px 20px;
    transition: background-color 0.3s;
}

ul li a:hover,
ul li a.active {
    background-color: #2c3e50;
    font-weight: bold;
}

/* 新增按钮 */
.add-button-wrapper {
    text-align: right;
    margin-bottom: 20px;
}

.add-button {
    display: inline-block;
    padding: 8px 16px;
    background-color: #27ae60;
    color: white;
    text-decoration: none;
    border-radius: 4px;
    font-weight: bold;
    transition: background-color 0.3s;
}

.add-button:hover {
    background-color: #219653;
}

/* 表格样式 */
table {
    width: 100%;
    border-collapse: collapse;
    background-color: white;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
    border-radius: 6px;
    overflow: hidden;
}

th, td {
    padding: 12px 15px;
    text-align: left;
    border-bottom: 1px solid #e0e0e0;
}

th {
    background-color: #3498db;
    color: white;
    font-weight: bold;
}

tr:nth-child(even) {
    background-color: #f9fbfd;
}

tr:hover {
    background-color: #edf7ff;
}

/* 操作列链接 */
td a {
    text-decoration: none;
    color: #e74c3c;
    margin-right: 8px;
    font-size: 14px;
}

td a:hover {
    text-decoration: underline;
}

/* 响应式优化(可选) */
@media (max-width: 600px) {
    .add-button-wrapper {
        text-align: center;
    }

    table, thead, tbody, th, td, tr {
        display: block;
    }

    td {
        position: relative;
        padding-left: 50% !important;
        border: none;
        padding-top: 8px;
        padding-bottom: 8px;
    }

    td:before {
        content: attr(data-label);
        position: absolute;
        left: 10px;
        width: 45%;
        font-weight: bold;
        color: #555;
    }

    th {
        display: none;
    }

}

actor_insert.css:

css 复制代码
/* 基础重置 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
    background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
    color: #333;
    line-height: 1.6;
    padding: 30px 20px;
    min-height: 100vh;
}

/* 头部标题 */
.header {
    text-align: center;
    margin-bottom: 30px;
}

.header h1 {
    font-size: 28px;
    color: #2c3e50;
    font-weight: 600;
    letter-spacing: 1px;
}

/* 表单容器 */
form {
    max-width: 560px;
    margin: 0 auto;
    background: white;
    padding: 32px;
    border-radius: 12px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
}

/* 表单项 */
.form-group {
    margin-bottom: 24px;
}

.form-group label {
    display: block;
    margin-bottom: 8px;
    font-weight: 600;
    color: #2d3748;
    font-size: 15px;
}

.form-group input,
.form-group select {
    width: 100%;
    padding: 12px 14px;
    font-size: 15px;
    border: 1px solid #cbd5e0;
    border-radius: 8px;
    background-color: #fafafa;
    transition: all 0.3s ease;
}

.form-group input:focus,
.form-group select:focus {
    outline: none;
    border-color: #3182ce;
    background-color: #ffffff;
    box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.15);
}

/* 按钮区域 */
.form-group button,
.form-group a {
    display: inline-block;
    padding: 11px 24px;
    font-size: 16px;
    font-weight: 600;
    border-radius: 8px;
    text-decoration: none;
    cursor: pointer;
    transition: all 0.25s ease;
}

.form-group button {
    background-color: #3182ce;
    color: white;
    border: none;
    margin-right: 12px;
}

.form-group button:hover {
    background-color: #2c5aa0;
    transform: translateY(-1px);
    box-shadow: 0 4px 8px rgba(49, 130, 206, 0.3);
}

.form-group a {
    background-color: #718096;
    color: white;
}

.form-group a:hover {
    background-color: #4a5568;
    transform: translateY(-1px);
    box-shadow: 0 4px 8px rgba(113, 128, 150, 0.3);
}

/* 响应式优化 */
@media (max-width: 600px) {
    body {
        padding: 20px 15px;
    }

    form {
        padding: 24px 20px;
    }

    .header h1 {
        font-size: 24px;
    }

    .form-group input,
    .form-group select {
        font-size: 16px; /* 防止 iOS 自动缩放 */
    }

    .form-group button,
    .form-group a {
        width: 100%;
        text-align: center;
        margin-bottom: 10px;
    }

    .form-group button:last-child,
    .form-group a:last-child {
        margin-bottom: 0;
    }
}

actor_update.css:

css 复制代码
/* actor_update.css - 专用于"修改用户"页面 */

/* 基础重置 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
    background: linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%);
    color: #333;
    line-height: 1.6;
    padding: 30px 20px;
    min-height: 100vh;
}

/* 页面标题 */
h1 {
    text-align: center;
    font-size: 28px;
    color: #2d3748;
    font-weight: 600;
    margin-bottom: 30px;
    letter-spacing: 1px;
}

/* 表单容器 */
form {
    max-width: 560px;
    margin: 0 auto;
    background: white;
    padding: 32px;
    border-radius: 12px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}

/* 表单项 */
.form-group,
form > div:not([class]) {
    /* 兼容未加 class 的 label+input 组合 */
    margin-bottom: 24px;
}

label {
    display: block;
    margin-bottom: 8px;
    font-weight: 600;
    color: #2d3748;
    font-size: 15px;
}

input[type="text"],
input[type="email"],
select {
    width: 100%;
    padding: 12px 14px;
    font-size: 15px;
    border: 1px solid #cbd5e0;
    border-radius: 8px;
    background-color: #fafafa;
    transition: all 0.3s ease;
}

input[type="text"]:focus,
input[type="email"]:focus,
select:focus {
    outline: none;
    border-color: #3182ce;
    background-color: #ffffff;
    box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.15);
}

/* 隐藏域不显示 */
input[type="hidden"] {
    display: none;
}

/* 提交按钮 */
button[type="submit"] {
    display: block;
    width: 100%;
    padding: 12px;
    font-size: 16px;
    font-weight: 600;
    color: white;
    background-color: #3182ce;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.25s ease;
}

button[type="submit"]:hover {
    background-color: #2c5aa0;
    transform: translateY(-1px);
    box-shadow: 0 4px 10px rgba(49, 130, 206, 0.35);
}

/* 响应式优化 */
@media (max-width: 600px) {
    body {
        padding: 20px 15px;
    }

    h1 {
        font-size: 24px;
    }

    form {
        padding: 24px 20px;
    }

    input[type="text"],
    input[type="email"],
    select {
        font-size: 16px; /* 防止 iOS 自动缩放 */
    }

    button[type="submit"] {
        padding: 14px;
    }
}

四、总结

RESTFul 是一种 "资源导向" 的接口设计风格,核心是通过URI + HTTP请求方式描述资源操作。在 SpringMVC 中,通过@GetMapping/@PostMapping/@PutMapping/@DeleteMapping@PathVariableHiddenHttpMethodFilter等技术可快速实现 RESTFul 接口。

本实战以 "演员管理系统" 为例,完整实现了 RESTFul 风格的 CRUD,涵盖了环境搭建、核心代码、前端页面、样式适配等全流程,为后续学习分布式、微服务接口设计奠定基础。

相关推荐
yhole2 小时前
spring security 超详细使用教程(接入springboot、前后端分离)
java·spring boot·spring
zjjsctcdl2 小时前
SpringBoot3.3.0集成Knife4j4.5.0实战
java
常利兵2 小时前
Spring Boot 搭建邮件发送系统:开启你的邮件自动化之旅
spring boot·后端·自动化
彭于晏Yan2 小时前
Spring Boot 集成邮件服务实现发送邮件功能
java·spring boot·后端
浮尘笔记2 小时前
Java Snowy 框架生产环境安全部署全流程(服务器篇)
java·运维·服务器·开发语言·后端
宸津-代码粉碎机2 小时前
Spring Boot 4.0虚拟线程实战续更预告:高阶技巧、监控排查与分布式场景落地指南
java·大数据·spring boot·分布式·后端·python
Rsun045512 小时前
6、Java 适配器模式从入门到实战
java·开发语言·适配器模式
MaCa .BaKa2 小时前
52-考研备考服务平台系统-考研系统
java·spring boot·mysql·考研·tomcat·maven·mybatis
JZC_xiaozhong2 小时前
2026技术深潜:解构Spring Boot与Spring Framework架构,透视KPaaS集成平台底层逻辑
大数据·spring boot·spring·架构·数据集成与应用集成·异构系统集成·应用对接