跳转至

Spring Boot与Web开发

Spring Boot为Web开发提供了强大的支持,包括RESTful API开发和传统的Web页面开发。本章将重点介绍RESTful开发,同时也会涵盖Thymeleaf模板引擎的使用。

Web开发概述

RESTful vs Thymeleaf

RESTful API开发: - 专注于数据交换 - 前后端分离架构 - 返回JSON/XML数据 - 适合现代单页应用(SPA)和移动应用

Thymeleaf模板开发: - 服务端渲染 - 传统的MVC模式 - 返回HTML页面 - 适合传统Web应用

RESTful API开发

什么是RESTful

REST(Representational State Transfer)是一种架构风格,具有以下特点:

  1. 资源(Resource):URI标识资源
  2. 表示(Representation):JSON、XML等格式
  3. 状态转移(State Transfer):通过HTTP方法操作资源
  4. 无状态(Stateless):每个请求包含所有必要信息

HTTP方法与CRUD操作

HTTP方法 操作 描述 示例URL
GET Read 获取资源 GET /users/1
POST Create 创建资源 POST /users
PUT Update 更新整个资源 PUT /users/1
PATCH Update 部分更新资源 PATCH /users/1
DELETE Delete 删除资源 DELETE /users/1

创建RESTful控制器

基础用户控制器

@RestController
@RequestMapping("/api/v1/users")
@Validated
@Slf4j
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    // 获取所有用户
    @GetMapping
    public ResponseEntity<List<UserDTO>> getAllUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy) {

        log.info("Fetching users: page={}, size={}, sortBy={}", page, size, sortBy);

        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
        Page<UserDTO> users = userService.findAll(pageable);

        return ResponseEntity.ok()
                .header("X-Total-Count", String.valueOf(users.getTotalElements()))
                .body(users.getContent());
    }

    // 根据ID获取用户
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable @Min(1) Long id) {
        log.info("Fetching user with id: {}", id);

        UserDTO user = userService.findById(id);
        return ResponseEntity.ok(user);
    }

    // 创建用户
    @PostMapping
    public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
        log.info("Creating user: {}", request.getUsername());

        UserDTO createdUser = userService.create(request);

        return ResponseEntity.status(HttpStatus.CREATED)
                .location(URI.create("/api/v1/users/" + createdUser.getId()))
                .body(createdUser);
    }

    // 更新用户
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> updateUser(
            @PathVariable @Min(1) Long id,
            @Valid @RequestBody UpdateUserRequest request) {

        log.info("Updating user with id: {}", id);

        UserDTO updatedUser = userService.update(id, request);
        return ResponseEntity.ok(updatedUser);
    }

    // 部分更新用户
    @PatchMapping("/{id}")
    public ResponseEntity<UserDTO> patchUser(
            @PathVariable @Min(1) Long id,
            @RequestBody Map<String, Object> updates) {

        log.info("Partially updating user with id: {}", id);

        UserDTO updatedUser = userService.patch(id, updates);
        return ResponseEntity.ok(updatedUser);
    }

    // 删除用户
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable @Min(1) Long id) {
        log.info("Deleting user with id: {}", id);

        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

DTO和请求对象

UserDTO

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    private Long id;
    private String username;
    private String email;
    private String fullName;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private boolean active;
}

CreateUserRequest

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20字符之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String username;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotBlank(message = "密码不能为空")
    @Size(min = 8, max = 20, message = "密码长度必须在8-20字符之间")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{8,}$", 
             message = "密码必须包含至少一个大写字母、一个小写字母和一个数字")
    private String password;

    @NotBlank(message = "全名不能为空")
    @Size(max = 50, message = "全名长度不能超过50字符")
    private String fullName;
}

UpdateUserRequest

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateUserRequest {

    @Email(message = "邮箱格式不正确")
    private String email;

    @Size(max = 50, message = "全名长度不能超过50字符")
    private String fullName;

    private Boolean active;
}

响应格式标准化

统一响应格式

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    private long timestamp;
    private String path;

    public static <T> ApiResponse<T> success(T data) {
        return ApiResponse.<T>builder()
                .success(true)
                .message("操作成功")
                .data(data)
                .timestamp(System.currentTimeMillis())
                .build();
    }

    public static <T> ApiResponse<T> success(T data, String message) {
        return ApiResponse.<T>builder()
                .success(true)
                .message(message)
                .data(data)
                .timestamp(System.currentTimeMillis())
                .build();
    }

    public static <T> ApiResponse<T> error(String message) {
        return ApiResponse.<T>builder()
                .success(false)
                .message(message)
                .timestamp(System.currentTimeMillis())
                .build();
    }
}

响应包装器

@RestControllerAdvice
public class ResponseWrapper implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return !returnType.getDeclaringClass().isAnnotationPresent(NoResponseWrapper.class) &&
               !returnType.hasMethodAnnotation(NoResponseWrapper.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {

        if (body instanceof ApiResponse) {
            return body;
        }

        return ApiResponse.success(body);
    }
}

// 标记注解,用于跳过响应包装
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoResponseWrapper {
}

内容协商

Spring Boot支持多种内容格式的自动协商:

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    // 支持JSON和XML格式
    @GetMapping(value = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        Product product = productService.findById(id);
        return ResponseEntity.ok(product);
    }

    // 根据Accept头返回不同格式
    @GetMapping
    public ResponseEntity<List<Product>> getProducts(HttpServletRequest request) {
        List<Product> products = productService.findAll();

        String acceptHeader = request.getHeader("Accept");
        if (acceptHeader != null && acceptHeader.contains("application/xml")) {
            // 返回XML格式
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_XML)
                    .body(products);
        } else {
            // 默认返回JSON格式
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(products);
        }
    }
}

跨域配置(CORS)

全局CORS配置

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        // 允许的源
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));

        // 允许的HTTP方法
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));

        // 允许的头部
        configuration.setAllowedHeaders(Arrays.asList("*"));

        // 允许发送凭证
        configuration.setAllowCredentials(true);

        // 预检请求的缓存时间
        configuration.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", configuration);

        return source;
    }
}

注解方式配置CORS

@RestController
@RequestMapping("/api/v1/users")
@CrossOrigin(origins = {"http://localhost:3000", "https://mydomain.com"}, 
             methods = {RequestMethod.GET, RequestMethod.POST},
             maxAge = 3600)
public class UserController {
    // 控制器方法
}

API文档集成

集成Swagger/OpenAPI

添加依赖:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.7.0</version>
</dependency>

配置OpenAPI:

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("用户管理API")
                        .version("1.0.0")
                        .description("用户管理系统的RESTful API文档")
                        .contact(new Contact()
                                .name("开发团队")
                                .email("dev@example.com")
                                .url("https://example.com"))
                        .license(new License()
                                .name("Apache 2.0")
                                .url("http://www.apache.org/licenses/LICENSE-2.0.html")))
                .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
                .components(new Components()
                        .addSecuritySchemes("bearerAuth", new SecurityScheme()
                                .type(SecurityScheme.Type.HTTP)
                                .scheme("bearer")
                                .bearerFormat("JWT")));
    }
}

在控制器中添加API文档注解:

@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "用户管理", description = "用户相关的API接口")
public class UserController {

    @GetMapping("/{id}")
    @Operation(summary = "根据ID获取用户", description = "通过用户ID获取用户详细信息")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功获取用户信息",
                    content = @Content(schema = @Schema(implementation = UserDTO.class))),
            @ApiResponse(responseCode = "404", description = "用户不存在"),
            @ApiResponse(responseCode = "500", description = "服务器内部错误")
    })
    public ResponseEntity<UserDTO> getUserById(
            @Parameter(description = "用户ID", required = true) @PathVariable Long id) {
        UserDTO user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
}

Thymeleaf模板引擎

Thymeleaf简介

Thymeleaf是一个现代的服务端Java模板引擎,具有以下特点:

  1. 自然模板:Thymeleaf模板本身就是有效的HTML
  2. 丰富的表达式:支持变量表达式、消息表达式等
  3. Spring集成:与Spring Boot无缝集成
  4. 国际化支持:内置国际化功能

添加Thymeleaf依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Thymeleaf配置

spring:
  thymeleaf:
    # 模板位置
    prefix: classpath:/templates/
    # 模板后缀
    suffix: .html
    # 模板编码
    encoding: UTF-8
    # 内容类型
    servlet:
      content-type: text/html
    # 开发时禁用缓存
    cache: false
    # 模板模式
    mode: HTML

创建Web控制器

@Controller
@RequestMapping("/web")
@Slf4j
public class WebController {

    private final UserService userService;

    public WebController(UserService userService) {
        this.userService = userService;
    }

    // 首页
    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("title", "欢迎使用用户管理系统");
        model.addAttribute("message", "这是一个基于Spring Boot和Thymeleaf的Web应用");
        return "index";
    }

    // 用户列表页面
    @GetMapping("/users")
    public String userList(Model model,
                          @RequestParam(defaultValue = "0") int page,
                          @RequestParam(defaultValue = "10") int size) {

        Pageable pageable = PageRequest.of(page, size);
        Page<UserDTO> users = userService.findAll(pageable);

        model.addAttribute("users", users.getContent());
        model.addAttribute("currentPage", page);
        model.addAttribute("totalPages", users.getTotalPages());
        model.addAttribute("totalElements", users.getTotalElements());

        return "users/list";
    }

    // 用户详情页面
    @GetMapping("/users/{id}")
    public String userDetail(@PathVariable Long id, Model model) {
        try {
            UserDTO user = userService.findById(id);
            model.addAttribute("user", user);
            return "users/detail";
        } catch (UserNotFoundException e) {
            model.addAttribute("error", "用户不存在");
            return "error/404";
        }
    }

    // 显示创建用户表单
    @GetMapping("/users/new")
    public String showCreateUserForm(Model model) {
        model.addAttribute("user", new CreateUserRequest());
        return "users/create";
    }

    // 处理创建用户表单提交
    @PostMapping("/users")
    public String createUser(@Valid @ModelAttribute("user") CreateUserRequest request,
                           BindingResult bindingResult,
                           Model model,
                           RedirectAttributes redirectAttributes) {

        if (bindingResult.hasErrors()) {
            return "users/create";
        }

        try {
            UserDTO createdUser = userService.create(request);
            redirectAttributes.addFlashAttribute("message", "用户创建成功");
            return "redirect:/web/users/" + createdUser.getId();
        } catch (Exception e) {
            model.addAttribute("error", "创建用户失败:" + e.getMessage());
            return "users/create";
        }
    }

    // 显示编辑用户表单
    @GetMapping("/users/{id}/edit")
    public String showEditUserForm(@PathVariable Long id, Model model) {
        try {
            UserDTO user = userService.findById(id);
            model.addAttribute("user", user);
            return "users/edit";
        } catch (UserNotFoundException e) {
            model.addAttribute("error", "用户不存在");
            return "error/404";
        }
    }

    // 处理编辑用户表单提交
    @PostMapping("/users/{id}")
    public String updateUser(@PathVariable Long id,
                           @Valid @ModelAttribute("user") UpdateUserRequest request,
                           BindingResult bindingResult,
                           Model model,
                           RedirectAttributes redirectAttributes) {

        if (bindingResult.hasErrors()) {
            UserDTO user = userService.findById(id);
            model.addAttribute("user", user);
            return "users/edit";
        }

        try {
            userService.update(id, request);
            redirectAttributes.addFlashAttribute("message", "用户更新成功");
            return "redirect:/web/users/" + id;
        } catch (Exception e) {
            model.addAttribute("error", "更新用户失败:" + e.getMessage());
            return "users/edit";
        }
    }

    // 删除用户
    @PostMapping("/users/{id}/delete")
    public String deleteUser(@PathVariable Long id, RedirectAttributes redirectAttributes) {
        try {
            userService.delete(id);
            redirectAttributes.addFlashAttribute("message", "用户删除成功");
        } catch (Exception e) {
            redirectAttributes.addFlashAttribute("error", "删除用户失败:" + e.getMessage());
        }
        return "redirect:/web/users";
    }
}

Thymeleaf模板示例

布局模板(layout.html)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${title} ?: '用户管理系统'">用户管理系统</title>

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- 自定义CSS -->
    <link th:href="@{/css/style.css}" rel="stylesheet">
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container">
            <a class="navbar-brand" th:href="@{/web/}">用户管理系统</a>

            <div class="navbar-nav">
                <a class="nav-link" th:href="@{/web/}">首页</a>
                <a class="nav-link" th:href="@{/web/users}">用户列表</a>
                <a class="nav-link" th:href="@{/web/users/new}">创建用户</a>
            </div>
        </div>
    </nav>

    <!-- 主要内容 -->
    <div class="container mt-4">
        <!-- 消息提示 -->
        <div th:if="${message}" class="alert alert-success alert-dismissible fade show" role="alert">
            <span th:text="${message}"></span>
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>

        <div th:if="${error}" class="alert alert-danger alert-dismissible fade show" role="alert">
            <span th:text="${error}"></span>
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>

        <!-- 页面内容 -->
        <div th:replace="${content}">
            页面内容将在这里显示
        </div>
    </div>

    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

用户列表页面(users/list.html)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户列表</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="row">
            <div class="col-md-12">
                <div class="d-flex justify-content-between align-items-center mb-3">
                    <h2>用户列表</h2>
                    <a th:href="@{/web/users/new}" class="btn btn-primary">
                        <i class="fas fa-plus"></i> 创建用户
                    </a>
                </div>

                <!-- 用户表格 -->
                <div class="card">
                    <div class="card-body">
                        <table class="table table-striped">
                            <thead>
                                <tr>
                                    <th>ID</th>
                                    <th>用户名</th>
                                    <th>邮箱</th>
                                    <th>全名</th>
                                    <th>状态</th>
                                    <th>创建时间</th>
                                    <th>操作</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr th:each="user : ${users}">
                                    <td th:text="${user.id}">1</td>
                                    <td th:text="${user.username}">admin</td>
                                    <td th:text="${user.email}">admin@example.com</td>
                                    <td th:text="${user.fullName}">管理员</td>
                                    <td>
                                        <span th:if="${user.active}" class="badge bg-success">激活</span>
                                        <span th:unless="${user.active}" class="badge bg-secondary">禁用</span>
                                    </td>
                                    <td th:text="${#temporals.format(user.createdAt, 'yyyy-MM-dd HH:mm')}">2023-12-01 10:30</td>
                                    <td>
                                        <a th:href="@{/web/users/{id}(id=${user.id})}" class="btn btn-sm btn-outline-primary">查看</a>
                                        <a th:href="@{/web/users/{id}/edit(id=${user.id})}" class="btn btn-sm btn-outline-warning">编辑</a>
                                        <form th:action="@{/web/users/{id}/delete(id=${user.id})}" method="post" style="display: inline;">
                                            <button type="submit" class="btn btn-sm btn-outline-danger" 
                                                    onclick="return confirm('确定要删除这个用户吗?')">删除</button>
                                        </form>
                                    </td>
                                </tr>
                                <tr th:if="${#lists.isEmpty(users)}">
                                    <td colspan="7" class="text-center">暂无用户数据</td>
                                </tr>
                            </tbody>
                        </table>

                        <!-- 分页 -->
                        <nav th:if="${totalPages > 1}">
                            <ul class="pagination justify-content-center">
                                <li class="page-item" th:classappend="${currentPage == 0} ? 'disabled'">
                                    <a class="page-link" th:href="@{/web/users(page=${currentPage - 1})}">上一页</a>
                                </li>

                                <li th:each="pageNum : ${#numbers.sequence(0, totalPages - 1)}"
                                    class="page-item" th:classappend="${pageNum == currentPage} ? 'active'">
                                    <a class="page-link" th:href="@{/web/users(page=${pageNum})}" th:text="${pageNum + 1}">1</a>
                                </li>

                                <li class="page-item" th:classappend="${currentPage == totalPages - 1} ? 'disabled'">
                                    <a class="page-link" th:href="@{/web/users(page=${currentPage + 1})}">下一页</a>
                                </li>
                            </ul>
                        </nav>

                        <div class="text-center mt-3">
                            <small class="text-muted"><span th:text="${totalElements}">0</span> 条记录,
                                第 <span th:text="${currentPage + 1}">1</span> 页,
                                共 <span th:text="${totalPages}">1</span></small>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

用户表单页面(users/create.html)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>创建用户</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="row justify-content-center">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h4>创建用户</h4>
                    </div>
                    <div class="card-body">
                        <form th:action="@{/web/users}" th:object="${user}" method="post">
                            <!-- 用户名 -->
                            <div class="mb-3">
                                <label for="username" class="form-label">用户名 *</label>
                                <input type="text" class="form-control" 
                                       th:field="*{username}" 
                                       th:classappend="${#fields.hasErrors('username')} ? 'is-invalid'" 
                                       id="username" placeholder="请输入用户名">
                                <div class="invalid-feedback" th:if="${#fields.hasErrors('username')}" 
                                     th:errors="*{username}">用户名错误信息</div>
                            </div>

                            <!-- 邮箱 -->
                            <div class="mb-3">
                                <label for="email" class="form-label">邮箱 *</label>
                                <input type="email" class="form-control" 
                                       th:field="*{email}" 
                                       th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'" 
                                       id="email" placeholder="请输入邮箱">
                                <div class="invalid-feedback" th:if="${#fields.hasErrors('email')}" 
                                     th:errors="*{email}">邮箱错误信息</div>
                            </div>

                            <!-- 密码 -->
                            <div class="mb-3">
                                <label for="password" class="form-label">密码 *</label>
                                <input type="password" class="form-control" 
                                       th:field="*{password}" 
                                       th:classappend="${#fields.hasErrors('password')} ? 'is-invalid'" 
                                       id="password" placeholder="请输入密码">
                                <div class="invalid-feedback" th:if="${#fields.hasErrors('password')}" 
                                     th:errors="*{password}">密码错误信息</div>
                                <div class="form-text">密码必须包含至少一个大写字母、一个小写字母和一个数字,长度8-20字符</div>
                            </div>

                            <!-- 全名 -->
                            <div class="mb-3">
                                <label for="fullName" class="form-label">全名 *</label>
                                <input type="text" class="form-control" 
                                       th:field="*{fullName}" 
                                       th:classappend="${#fields.hasErrors('fullName')} ? 'is-invalid'" 
                                       id="fullName" placeholder="请输入全名">
                                <div class="invalid-feedback" th:if="${#fields.hasErrors('fullName')}" 
                                     th:errors="*{fullName}">全名错误信息</div>
                            </div>

                            <!-- 提交按钮 -->
                            <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                                <a th:href="@{/web/users}" class="btn btn-secondary">取消</a>
                                <button type="submit" class="btn btn-primary">创建用户</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

静态资源处理

静态资源配置

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 自定义静态资源路径
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .setCachePeriod(3600); // 缓存1小时

        // 文件上传路径
        registry.addResourceHandler("/uploads/**")
                .addResourceLocations("file:./uploads/")
                .setCachePeriod(0); // 不缓存
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

文件上传处理

@RestController
@RequestMapping("/api/v1/files")
public class FileController {

    @Value("${app.upload.dir:./uploads}")
    private String uploadDir;

    @PostMapping("/upload")
    public ResponseEntity<Map<String, String>> uploadFile(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }

        try {
            // 创建上传目录
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }

            // 生成唯一文件名
            String originalFilename = file.getOriginalFilename();
            String extension = FilenameUtils.getExtension(originalFilename);
            String newFilename = UUID.randomUUID().toString() + "." + extension;

            // 保存文件
            Path filePath = uploadPath.resolve(newFilename);
            Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);

            // 返回文件信息
            Map<String, String> response = new HashMap<>();
            response.put("filename", newFilename);
            response.put("originalName", originalFilename);
            response.put("url", "/uploads/" + newFilename);
            response.put("size", String.valueOf(file.getSize()));

            return ResponseEntity.ok(response);

        } catch (IOException e) {
            throw new RuntimeException("文件上传失败", e);
        }
    }
}

小结

Spring Boot的Web开发支持包括:

RESTful API开发

  1. 完整的HTTP方法支持:GET、POST、PUT、PATCH、DELETE
  2. 数据绑定和验证:自动参数绑定和JSR-303验证
  3. 内容协商:支持JSON、XML等多种格式
  4. CORS支持:跨域资源共享配置
  5. API文档:Swagger/OpenAPI集成

Thymeleaf模板开发

  1. 自然模板:可以直接在浏览器中查看
  2. 丰富的表达式:变量、条件、循环等
  3. 表单处理:表单绑定和验证错误显示
  4. 国际化支持:多语言支持
  5. 布局系统:模板继承和片段复用

通过本章的学习,您可以根据项目需求选择合适的Web开发方式: - 前后端分离项目选择RESTful API - 传统Web应用选择Thymeleaf模板 - 或者两者结合使用,既提供API又提供Web界面