一、Spring MVC 综合案例

1. 案例一:加法计算器

接口定义

  • 功能:两数相加。
  • 路径:calc/sum
  • 参数:
参数名称 类型 是否必须
num1 Integer
num2 Integer
  • 响应数据:
    • contentType:text/html
    • 响应内容:计算结果:ans

代码实现

  • 前端静态页面calc.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>加法计算器</title>
</head>
<body>
    <h1>简易计算器</h1>
    <!-- 表单提交地址 /calc/sum,请求方式post -->
    <form action="calc/sum" method="post">
        数字1:<input type="text" name="num1"><br>
        数字2:<input type="text" name="num2"><br>
        <input type="submit" value="点击相加">
    </form>
</body>
</html>

访问地址:http://127.0.0.1:8080/calc.html

  • 后端接口
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/calc")
public class CalcController {
    @RequestMapping("/sum")
    public String sum(Integer num1, Integer num2){
        Integer total = num1 + num2;
        return "<h1>计算结果:" + total + "</h1>";
    }
}

2. 案例二:用户登录系统

接口定义

1. 检验用户信息
  • 功能:检验账号密码是否正确。
  • 路径:/user/login
  • 请求方式:POST
  • 参数:
参数名称 类型 是否必须
username String
password String
  • 响应数据:
    • contentType:text/html
    • 响应内容:
      • true //验证成功
      • false //验证失败
2. 查询登录用户
  • 功能:查询当前登录的用户
  • 请求路径:/user/getLoginUser
  • 请求方式:GET
  • 请求参数:无
  • 响应数据:
    • contentType:text/html
    • 响应内容:admin

后端

import jakarta.servlet.http.HttpSession;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class LoginController {
    // 登录校验接口
    @RequestMapping("/login")
    public boolean login(String userName, String password, HttpSession session){
        // 判断账号密码是否为空
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
            return false;
        }
        // 模拟数据库账号密码
        if("zhangsan".equals(userName) && "123456".equals(password)){
            // 登录成功,存入session
            session.setAttribute("loginUser", userName);
            return true;
        }
        return false;
    }

    // 获取当前登录用户
    @RequestMapping("/getLoginUser")
    public String getLoginUser(HttpSession session){
        // getSession(false):无会话不创建新session
        String user = (String) session.getAttribute("loginUser");
        return user == null ? "" : user;
    }
}

注意:

  1. 代码要尽量减少嵌套,增加可读性
  2. 判断两个字符串是否相等的时候要把常量写在前面,变量写在equal()里,防止空指针异常:"admin".equals(username)

前端

采用AJAX

AJAX ( Asynchronous JavaScript And XML) 异步 JavaScript 和 XML
在不重新加载整个页面的情况下,与服务器交换数据、局部更新网页内容。

  • login.html
  
<body>  
  <h1>用户登录</h1>  
  用户名:<input name="username" type="text" id="username"><br>  
  密码:<input name="password" type="password" id="password"><br>  
  <input type="button" value="登录" onclick="login()">  
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>  
  <script>    function login() {  
      $.ajax({  
        type: "post",  
        url: "user/login",  
        data: {  
          username: $("#username").val(),  
          password: $("#password").val()  
        },  
        //http请求成功  
        success: function(result){  
          if(result){  
            //密码正确,跳转  
            location.href = "index.html";  
          }else{  
            //密码错误  
            alert("账户或密码错误");  
          }        }  
      });  
    }  
  
  </script>  
</body>
  • index.html
<body>  
    登录人: <span id="loginUser"></span>  
  
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>  
    <script>        $.ajax({  
            type:"get",  
            url: "/user/getLoginUser",  
            success:function (username){  
                $("#loginUser").text(username);  
            }  
        })  
    </script>  
</body>

3. 案例三:留言板

Lombok

引入依赖
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
常用注解

Lombok主要作用是自动生成get、set、toString等方法,生命周期只在源码阶段。

注解 作用
@Data 综合注解:自动生成getter、setter、toString、equals、hashCode、无参构造
@Getter/@Setter 单独生成get/set方法
@NoArgsConstructor 无参构造方法
@AllArgsConstructor 全参构造方法
@NonNull 属性非空校验

注意:

  • @Data = @Getter+@Setter+toString()+equals()+hashCode()
  • @Data生成的内容非常多,可以使用下面的注解生成具体的函数
  • @Getter/@Setter既是类注解,也是属性注解。放在类上表示对所有属性生成getter和setter方法;放在属性上,表示单独对这个属性生成getter和setter

![[Pasted image 20260714172016.png]]

接口设计

1. 获取全部留言
  • 请求方式:GET
  • 请求路径:/message/getList
  • 响应数据:
    • Content-Type: application/json
    • 类型:JSON
2. 发送留言
  • 请求方式:POST
  • 请求路径:/message/publish
  • 响应数据:JSON格式
    • 相应内容:
      • ok:0 //发布失败
      • ok:1 //发布成功

后端

@RestController  
@RequestMapping("/message")  
public class MessageController {  
    private List<MessageInfo> messageList = new ArrayList<>() ;  
  
    //获取全部留言  
    @RequestMapping("/getList" )  
    public List<MessageInfo> getList(){  
        return messageList;  
    }  
    //发布留言  
    @RequestMapping(value = "/publish",produces = "application/json")  
    public String publish(@RequestBody MessageInfo messageInfo){  
        if(!StringUtils.hasLength(messageInfo.getFrom())||  
                !StringUtils.hasLength(messageInfo.getTo())||  
                !StringUtils.hasLength(messageInfo.getMessage())){  
            return "{\"ok\":0}";  
        }        messageList.add(messageInfo);  
        return "{\"ok\":1}";  
    }  
}

前端

<!DOCTYPE html>  
<html lang="en">  
  
<head>  
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1.0">  
    <title>留言板</title>  
    <style>        .container {  
            width: 350px;  
            height: 300px;  
            margin: 0 auto;  
            /* border: 1px black solid; */  
            text-align: center;  
        }  
  
        .grey {  
            color: grey;  
        }  
  
        .container .row {  
            width: 350px;  
            height: 40px;  
  
            display: flex;  
            justify-content: space-between;  
            align-items: center;  
        }  
  
        .container .row input {  
            width: 260px;  
            height: 30px;  
        }  
  
        #submit {  
            width: 350px;  
            height: 40px;  
            background-color: orange;  
            color: white;  
            border: none;  
            margin: 10px;  
            border-radius: 5px;  
            font-size: 20px;  
        }  
    </style>  
</head>  
  
<body>  
    <div class="container">  
        <h1>留言板</h1>  
        <p class="grey">输入后点击提交, 会将信息显示下方空白处</p>  
        <div class="row">  
            <span>谁:</span> <input type="text" name="" id="from">  
        </div>  
        <div class="row">  
            <span>对谁:</span> <input type="text" name="" id="to">  
        </div>  
        <div class="row">  
            <span>说什么:</span> <input type="text" name="" id="say">  
        </div>  
        <input type="button" value="提交" id="submit" onclick="submit()">  
        <!-- <div>A 对 B 说: hello</div> -->  
    </div>  
  
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>  
    <script>        load();  
        function load(){  
            $.ajax({  
                type: "get",  
                url: "/message/getList",  
                success: function (messages){  
                    if(messages!=null && messages.length>0){  
                        var item;  
                        for(var m of messages){  
                            item += "<div>"+m.from +"对" + m.to + "说:" + m.message+"</div>";  
                        }  
                        $(".container").append(item)  
                    }  
                }  
            })  
        }  
        function submit(){  
            //1. 获取留言的内容  
            var from = $('#from').val();  
            var to = $('#to').val();  
            var say = $('#say').val();  
            if (from== '' || to == '' || say == '') {  
                return;  
            }  
            var data = {  
                from: from,  
                to: to,  
                message: say  
            }  
            $.ajax({  
                url: "message/publish",  
                type: "POST",  
                data: JSON.stringify(data),  
                contentType: "application/json",  
                success: function (res) {  
                    if(res.ok==1){  
                        // 留言成功  
                        //2. 构造节点  
                        var divE = "<div>"+from +"对" + to + "说:" + say+"</div>";  
                        //3. 把节点添加到页面上  
                        $(".container").append(divE);  
  
                        //4. 清空输入框的值  
                        $('#from').val("");  
                        $('#to').val("");  
                        $('#say').val("");  
                    }else{  
                        alert("留言失败");  
                    }                }  
            })  
  
                    }  
        </script>  
</body>  
  
</html>

4. 案例四:图书管理系统

接口定义

1. 登录接口
  • 请求方式:post
  • 请求路径:/user/login
  • 请求参数:name = admin&password = 12345
  • 响应数据:
    • true //密码正确
    • false //密码错误
2. 图书列表展示
  • 请求方式:get
  • 请求路径:/book/getList
  • 请求参数:无
  • 响应数据:图书列表
    • Content-Type: application/json
    • 类型:JSON

后端

  • 图书实体 BookInfo
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;

@Data
public class BookInfo {
    private Integer id;
    private String bookName;
    private String author;
    private Integer count;
    private BigDecimal price;
    private String publish;
    // 状态 1可借阅 2不可借阅
    private Integer status;
    // 页面展示中文状态,不持久化数据库
    private String statusCN;
    private Date createTime;
    private Date updateTime;
}
  • UserController
@RequestMapping("/user")  
@RestController  
  
public class UserController {  
    @RequestMapping("/login")  
    public Boolean login(String username, String password, HttpSession session){  
        if(!StringUtils.hasLength(username)||!StringUtils.hasLength(password))  
            return false;  
        if("admin".equals(username) && "12345".equals(password)){  
            session.setAttribute("username",username);  
            return true;  
        }        return false;  
    }  
}
  • BookController
@RequestMapping("/user")  
@RestController  
  
public class UserController {  
    @RequestMapping("/login")  
    public Boolean login(String username, String password, HttpSession session){  
        if(!StringUtils.hasLength(username)||!StringUtils.hasLength(password))  
            return false;  
        if("admin".equals(username) && "12345".equals(password)){  
            session.setAttribute("username",username);  
            return true;  
        }        return false;  
    }  
}

前端

  • 登录界面
<!DOCTYPE html>  
<html lang="en">  
  
<head>  
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1.0">  
    <title>Document</title>  
    <link rel="stylesheet" href="css/bootstrap.min.css">  
    <link rel="stylesheet" href="css/login.css">  
    <script type="text/javascript" src="js/jquery.min.js"></script>  
</head>  
  
<body>  
    <div class="container-login">  
        <div class="container-pic">  
            <img src="pic/computer.png" width="350px">  
        </div>  
        <div class="login-dialog">  
            <h3>登陆</h3>  
            <div class="row">  
                <span>用户名</span>  
                <input type="text" name="userName" id="userName" class="form-control">  
            </div>  
            <div class="row">  
                <span>密码</span>  
                <input type="password" name="password" id="password" class="form-control">  
            </div>  
            <div class="row">  
                <button type="button" class="btn btn-info btn-lg" onclick="login()">登录</button>  
            </div>  
        </div>  
    </div>  
    <script src="js/jquery.min.js"></script>  
    <script>        function login() {  
            $.ajax({  
                type:"post",  
                url:"user/login",  
                data: {  
                    username: $("#userName").val(),  
                    password: $("#password").val()  
                },  
                success: function (result) {  
                    if(result){  
                        //正确,跳转  
                        location.href = "book_list.html";  
                    }else{  
                        alert("用户名或密码错误");  
                    }                }  
  
            })  
        }    </script>  
</body>  
  
</html>
  • 图书列表
<!DOCTYPE html>  
<html lang="en">  
  
<head>  
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1.0">  
    <title>图书列表展示</title>  
    <link rel="stylesheet" href="css/bootstrap.min.css">  
  
    <link rel="stylesheet" href="css/list.css">  
    <script type="text/javascript" src="js/jquery.min.js"></script>  
    <script type="text/javascript" src="js/bootstrap.min.js"></script>  
    <script src="js/jq-paginator.js"></script>  
  
</head>  
  
<body>  
    <div class="bookContainer">  
        <h2>图书列表展示</h2>  
        <div class="navbar-justify-between">  
            <div>  
                <button class="btn btn-outline-info" type="button" onclick="location.href='book_add.html'">添加图书</button>  
                <button class="btn btn-outline-info" type="button" onclick="batchDelete()">批量删除</button>  
            </div>  
        </div>  
  
        <table>            <thead>  
                <tr>  
                    <td>选择</td>  
                    <td class="width100">图书ID</td>  
                    <td>书名</td>  
                    <td>作者</td>  
                    <td>数量</td>  
                    <td>定价</td>  
                    <td>出版社</td>  
                    <td>状态</td>  
                    <td class="width200">操作</td>  
                </tr>  
            </thead>  
            <tbody>                        </tbody>  
        </table>  
  
        <div class="demo">  
            <ul id="pageContainer" class="pagination justify-content-center"></ul>  
        </div>  
        <script>  
            getBookList();  
            function getBookList() {  
                $.ajax({  
                    type:"get",  
                    url: "book/getList",  
                    success: function (books) {  
                        //拼接html  
                        var html = "";  
                        for(var book of books){  
                            html+="<tr>"+  
                                "<td><input type='checkbox' name='selectBook' value='"+book.bookId+"' id = ‘selectBook’  class='book-select'></td>"+  
                                "<td>"+book.bookId+"</td>"+  
                                "<td>"+book.bookName+"</td>"+  
                                "<td>"+book.author+"</td>"+  
                                "<td>"+book.num+"</td>"+  
                                "<td>"+book.price+"</td>"+  
                                "<td>"+book.publish+"</td>"+  
                                "<td>"+(book.status===1?"可借阅":"已借出")+"</td>"+  
                                "<td class='width200'>"+  
                                "<div class='op'>"+  
                                "<a href='book_update.html?bookId="+book.bookId+"'>修改</a>"+  
                                "<a href='javascript:void(0)' onclick='deleteBook("+book.bookId+")'>删除</a>"+  
                                "</div>"+  
                                "</td>"+  
                                "</tr>";  
                        }                        $("tbody").html(html);  
                    }  
  
                });  
            }  
    //翻页信息  
            $("#pageContainer").jqPaginator({  
                totalCounts: 100, //总记录数  
                pageSize: 10,    //每页的个数  
                visiblePages: 5, //可视页数  
                currentPage: 1,  //当前页码  
                first: '<li class="page-item"><a class="page-link">首页</a></li>',  
                prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',  
                next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',  
                last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',  
                page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',  
                //页面初始化和页码点击时都会执行  
                onPageChange: function (page, type) {  
                    console.log("第"+page+"页, 类型:"+type);  
                }  
            });  
            function deleteBook(id) {  
                var isDelete = confirm("确认删除?");  
                if (isDelete) {  
                    //删除图书  
                    alert("删除成功");  
                }  
            }  
            function batchDelete() {  
                var isDelete = confirm("确认批量删除?");  
                if (isDelete) {  
                    //获取复选框的id  
                    var ids = [];  
                    $("input:checkbox[name='selectBook']:checked").each(function () {  
                        ids.push($(this).val());  
                    });  
                    console.log(ids);  
                    alert("批量删除成功");  
                }  
            }  
  
        </script>  
    </div>  
</body>  
  
</html>

原始代码存在的缺陷

所有代码全部写在Controller中:模拟数据、状态转换业务逻辑、数据存储耦合在一起。

  • 后续新增修改图书功能,Controller代码会极度臃肿;
  • 业务逻辑无法复用,其他接口需要状态转换需要重复复制代码;
  • 数据库替换时,Controller需要大面积修改; 由此引出三层架构分层思想,解决代码耦合问题。

二、三层架构 & MVC 模式

2.1 什么是MVC设计模式

MVC是软件工程通用架构模式,拆分三大模块:Model、View、Controller,适用于传统前后端不分离项目。

  1. View 视图 前端页面HTML,负责展示数据、接收用户输入,和用户直接交互。
  2. Controller 控制器 请求分发器,接收浏览器请求,调用Model处理数据,处理完成后跳转指定视图。
  3. Model 模型 负责业务逻辑处理、数据增删改查,包含业务数据与业务规则。

MVC完整执行流程

  1. 用户浏览器发起请求,访问View页面;
  2. 请求转发至Controller控制器;
  3. Controller调用对应Model处理业务、操作数据;
  4. Model处理完成返回结果给Controller;
  5. Controller携带数据跳转至指定View;
  6. 浏览器渲染View页面展示数据。

2.2 三层架构

当前互联网主流开发模式为前后端分离,后端只提供JSON接口,不再渲染页面View。针对纯后端接口,衍生出标准三层架构:Controller控制层、Service业务层、Dao数据访问层

三层分层职责

分层 包路径规范 核心职责 操作
Controller 控制层(表现层) com.xxx.controller 1.接收HTTP请求
2.接收前端参数
3.简单参数校验
4.调用Service业务层
5.封装响应数据返回前端
不写复杂业务逻辑、不直接操作数据库
Service 业务逻辑层 com.xxx.service 1.业务逻辑处理
2.数据组装、状态转换
3.事务控制
4.调用Dao层获取数据
不接收HTTP请求、不处理前端参数、不直接操作数据库
Dao 数据访问层 com.xxx.dao 仅负责数据库CRUD操作,提供原始数据 无任何业务逻辑,只做数据存取

配套模块:model实体包,存放数据库映射实体类(BookInfo、UserInfo、MessageInfo),用于数据传输。

2.3 MVC 和 三层架构的联系与区别

1. 视角不同

  • MVC:全栈视角,兼顾前端页面+后端逻辑,传统SSM页面渲染项目使用;
  • 三层架构:纯后端接口视角,剥离View视图,只处理请求、业务、数据,适配前后端分离开发。

2. 层级对应关系

  • MVC中的 View + Controller = 三层架构的 Controller(表现层);
  • MVC中的 Model = 三层架构 Service业务层 + Dao持久层 + model实体类。

3. 相同目标

二者设计思想统一:高内聚、低耦合

  • 高内聚:同一层级内部功能紧密相关,只做一类事情;
  • 低耦合:层级之间依赖弱,修改一层代码不会大面积影响其他层。

2.4 图书案例分层代码重构

分层包结构

com.example.demo
├── controller 控制层
│   └── BookController.java
├── service 业务层
│   └── BookService.java
├── dao 数据访问层
│   └── BookDao.java
└── model 实体类
    └── BookInfo.java

Dao层(只提供模拟原始数据)

package com.example.demo.dao;
import com.example.demo.model.BookInfo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class BookDao {
    // 仅提供原始数据,无任何业务处理
    public List<BookInfo> mockData(){
        List<BookInfo> books = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            BookInfo book = new BookInfo();
            book.setId(i);
            book.setBookName("书籍"+i);
            book.setAuthor("作者"+i);
            book.setCount(i*5+3);
            book.setPrice(new BigDecimal(new Random().nextInt(100)));
            book.setPublish("出版社"+i);
            book.setStatus(1);
            books.add(book);
        }
        return books;
    }
}

Service层(处理业务逻辑)

package com.example.demo.service;
import com.example.demo.dao.BookDao;
import com.example.demo.model.BookInfo;
import java.util.List;

public class BookService {
    // 依赖Dao获取原始数据
    private BookDao bookDao = new BookDao();

    public List<BookInfo> getBookList(){
        // 获取原始数据
        List<BookInfo> books = bookDao.mockData();
        // 业务逻辑:数字状态转换中文
        for (BookInfo book : books) {
            if(book.getStatus() == 1){
                book.setStatusCN("可借阅");
            }else{
                book.setStatusCN("不可借阅");
            }
        }
        return books;
    }
}

Controller层

package com.example.demo.controller;
import com.example.demo.model.BookInfo;
import com.example.demo.service.BookService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequestMapping("/book")
public class BookController {
    // 调用业务层
    private BookService bookService = new BookService();

    @RequestMapping("/getList")
    public List<BookInfo> getList(){
        // 仅转发调用,无业务、无数据查询
        return bookService.getBookList();
    }
}

2.5 项目分层优势

  1. 职责单一,代码清晰 每一层只负责一类工作,查找bug时可快速定位层级:页面报错找Controller,业务计算错误找Service,数据查询异常找Dao。
  2. 代码复用,消除重复 复杂业务逻辑统一写在Service,多个Controller接口可复用同一个Service方法,避免重复代码。
  3. 低耦合,易扩展替换 如果后续更换数据库(MySQL换Oracle),仅修改Dao层代码,上层Service、Controller完全不需要改动; 如果业务规则变更,只修改Service,不影响数据层与接口层。
  4. 团队多人协作开发 前端开发只对接Controller接口;后端开发A负责写Controller,开发B负责业务Service,开发C负责数据库Dao,互不干扰。
  5. 降低后期维护成本 项目迭代新增功能时,分层结构清晰,新增代码只需要对应补充到对应层级,不会出现代码堆砌混乱。

三、企业Java后端统一命名规范

3.1 各类标识符命名规则

1. 包名

  • 全部小写英文,点分隔,每个分段仅一个语义单词;
  • 示例:com.example.demo.controllercom.example.demo.service
  • 禁止大写、下划线、多个单词拼接。

2. 类名:大驼峰

所有单词首字母大写:

  • 控制层:UserControllerBookController
  • 业务层:UserServiceMessageService
  • 数据层:UserDaoBookDao
  • 实体类:BookInfoMessageInfo; 特殊后缀DO/DTO/VO/BO同样使用大驼峰。

3. 方法名、成员变量、局部变量:小驼峰

首字母小写,后续单词首字母大写:

  • 方法:getBookList()publishMessage()login()
  • 变量:loginUserbookListmessageInfo

3.2 其他命名格式

  1. 蛇形命名(下划线分隔 user_name) 仅用于数据库表名、字段名,Java代码中禁止使用;
  2. 串形命名(短横线 user-name) 仅用于URL接口路径,Java代码禁止使用。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐