【SpringMVC 笔记】 - 6 - RESTFul 编程风格
一、RESTFul 核心概念
1.1 什么是 RESTFul
RESTFul(Representational State Transfer,表述性状态转移)是一种 WEB 服务接口的设计风格,而非强制标准。它定义了一组约束条件和规范,遵循该风格的接口具备简洁、易理解、易扩展、安全可靠的特性,是目前前后端交互、接口设计的主流范式。
核心思想:通过「URI + HTTP 请求方式」来描述并操作服务器端的资源 ,而非通过 URL 携带操作语义(如传统的getUserById、deleteUser)。
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 请求需通过以下方式模拟:
- 前端表单提交方式为
POST; - 表单中添加隐藏域:
<input type="hidden" name="_method" value="PUT/DELETE"/>; - 后端配置
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>
<!-- 删除:模拟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、@PathVariable、HiddenHttpMethodFilter等技术可快速实现 RESTFul 接口。
本实战以 "演员管理系统" 为例,完整实现了 RESTFul 风格的 CRUD,涵盖了环境搭建、核心代码、前端页面、样式适配等全流程,为后续学习分布式、微服务接口设计奠定基础。