【Java EE】Spring MVC 综合案例与架构设计
·
一、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;
}
}
注意:
- 代码要尽量减少嵌套,增加可读性
- 判断两个字符串是否相等的时候要把常量写在前面,变量写在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]]](https://i-blog.csdnimg.cn/direct/a1afc2a158f54869af855d7f3e44f713.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,适用于传统前后端不分离项目。
- View 视图 前端页面HTML,负责展示数据、接收用户输入,和用户直接交互。
- Controller 控制器 请求分发器,接收浏览器请求,调用Model处理数据,处理完成后跳转指定视图。
- Model 模型 负责业务逻辑处理、数据增删改查,包含业务数据与业务规则。
MVC完整执行流程
- 用户浏览器发起请求,访问View页面;
- 请求转发至Controller控制器;
- Controller调用对应Model处理业务、操作数据;
- Model处理完成返回结果给Controller;
- Controller携带数据跳转至指定View;
- 浏览器渲染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 项目分层优势
- 职责单一,代码清晰 每一层只负责一类工作,查找bug时可快速定位层级:页面报错找Controller,业务计算错误找Service,数据查询异常找Dao。
- 代码复用,消除重复 复杂业务逻辑统一写在Service,多个Controller接口可复用同一个Service方法,避免重复代码。
- 低耦合,易扩展替换 如果后续更换数据库(MySQL换Oracle),仅修改Dao层代码,上层Service、Controller完全不需要改动; 如果业务规则变更,只修改Service,不影响数据层与接口层。
- 团队多人协作开发 前端开发只对接Controller接口;后端开发A负责写Controller,开发B负责业务Service,开发C负责数据库Dao,互不干扰。
- 降低后期维护成本 项目迭代新增功能时,分层结构清晰,新增代码只需要对应补充到对应层级,不会出现代码堆砌混乱。
三、企业Java后端统一命名规范
3.1 各类标识符命名规则
1. 包名
- 全部小写英文,点分隔,每个分段仅一个语义单词;
- 示例:
com.example.demo.controller、com.example.demo.service; - 禁止大写、下划线、多个单词拼接。
2. 类名:大驼峰
所有单词首字母大写:
- 控制层:
UserController、BookController; - 业务层:
UserService、MessageService; - 数据层:
UserDao、BookDao; - 实体类:
BookInfo、MessageInfo; 特殊后缀DO/DTO/VO/BO同样使用大驼峰。
3. 方法名、成员变量、局部变量:小驼峰
首字母小写,后续单词首字母大写:
- 方法:
getBookList()、publishMessage()、login(); - 变量:
loginUser、bookList、messageInfo。
3.2 其他命名格式
- 蛇形命名(下划线分隔 user_name) 仅用于数据库表名、字段名,Java代码中禁止使用;
- 串形命名(短横线 user-name) 仅用于URL接口路径,Java代码禁止使用。
更多推荐

所有评论(0)