基于SpringMVC的加法器设计(附完整代码)
基于SpringMVC的加法器设计(附完整代码)
本项目实现了一个具有毛玻璃视觉风格的动态加法器,支持前后端双重校验、粒子动态背景、星空流星效果,并成功部署到阿里云ECS。本文将详细介绍SpringMVC框架下的完整开发过程、核心代码以及部署调试经验。
在线演示(实验期间有效):http://118.31.44.31:8080/task10/calculator
一、实验目的
理解MVC设计模式,掌握SpringMVC框架的基本应用,掌握SpringMVC框架技术在JSP项目开发中的应用。
二、实验内容与设计思想
基于SpringMVC框架编写实现一个简单的加法器,要求任意输入两个数字,输出它们的和。要求可以判断输入的非空、非数字等情况并给出合理的提示,界面友好。
设计思想:采用MVC分层架构,将数据模型(CalculatorBean)、控制器(CalculatorController)、校验工具(InputValidator)与视图(calculator.jsp)清晰分离。前端使用JavaScript进行实时校验,后端再次校验保证数据合法性。视觉上采用动态渐变背景、粒子网络、星空流星及毛玻璃卡片,提升用户体验。
三、实验环境
- 操作系统:Microsoft Windows 10/11
- 编程环境:JDK 1.8、Tomcat 9.0、Eclipse Web Developer
- 依赖管理:Maven
- 部署环境:阿里云ECS(Windows Server)
四、项目结构与核心代码
📁 项目目录结构(点击展开)
task10
│
├── pom.xml
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── task10
│ │ ├── bean
│ │ │ └── CalculatorBean.java
│ │ └── controller
│ │ ├── CalculatorController.java
│ │ └── InputValidator.java
│ └── webapp
│ ├── css
│ │ ├── background.css
│ │ ├── calculator.css
│ │ ├── glass.css
│ │ └── layout.css
│ ├── js
│ │ ├── calculator.js
│ │ ├── particle.js
│ │ ├── ring.js
│ │ └── star.js
│ └── WEB-INF
│ ├── jsp
│ │ └── calculator.jsp
│ ├── springmvc-servlet.xml
│ └── web.xml
4.1 Maven配置(pom.xml)
📄 pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.task10</groupId>
<artifactId>task10</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.3.39</spring.version>
</properties>
<dependencies>
<!-- Spring Web MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- JSP API -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<!-- JSTL -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<build>
<finalName>task10</finalName>
</build>
</project>
4.2 Bean层(CalculatorBean.java)
📄 CalculatorBean.java
package com.task10.bean;
import java.io.Serializable;
public class CalculatorBean implements Serializable {
private static final long serialVersionUID = 1L;
private double num1;
private double num2;
private double sum;
public CalculatorBean() {}
public double getNum1() { return num1; }
public void setNum1(double num1) { this.num1 = num1; }
public double getNum2() { return num2; }
public void setNum2(double num2) { this.num2 = num2; }
public double getSum() { return sum; }
public void setSum(double sum) { this.sum = sum; }
}
4.3 Controller层(CalculatorController.java)
📄 CalculatorController.java
package com.task10.controller;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.task10.bean.CalculatorBean;
@Controller
public class CalculatorController {
@RequestMapping("/")
public String index() {
return "calculator";
}
@RequestMapping("/calculator")
public String calculator(HttpServletRequest request, Model model) {
String num1 = request.getParameter("num1");
String num2 = request.getParameter("num2");
if (num1 == null && num2 == null) {
return "calculator";
}
Map<String, Object> validationResult = InputValidator.validate(num1, num2);
boolean isValid = (Boolean) validationResult.get("isValid");
List<String> errorList = (List<String>) validationResult.get("errors");
if (!isValid) {
model.addAttribute("errorMessages", errorList);
model.addAttribute("num1", num1);
model.addAttribute("num2", num2);
return "calculator";
}
double number1 = Double.parseDouble(num1);
double number2 = Double.parseDouble(num2);
double sum = number1 + number2;
CalculatorBean bean = new CalculatorBean();
bean.setNum1(number1);
bean.setNum2(number2);
bean.setSum(sum);
model.addAttribute("calculator", bean);
return "calculator";
}
}
4.4 输入验证器(InputValidator.java)
📄 InputValidator.java
package com.task10.controller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class InputValidator {
public static Map<String, Object> validate(String num1, String num2) {
Map<String, Object> result = new HashMap<>();
List<String> errors = new ArrayList<>();
if (num1 == null || num1.trim().isEmpty()) {
errors.add("第一个数字不能为空");
} else {
try {
Double.parseDouble(num1);
} catch (NumberFormatException e) {
errors.add("第一个输入不是合法数字");
}
}
if (num2 == null || num2.trim().isEmpty()) {
errors.add("第二个数字不能为空");
} else {
try {
Double.parseDouble(num2);
} catch (NumberFormatException e) {
errors.add("第二个输入不是合法数字");
}
}
result.put("isValid", errors.isEmpty());
result.put("errors", errors);
return result;
}
}
4.5 SpringMVC配置文件(springmvc-servlet.xml)
📄 springmvc-servlet.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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven />
<context:component-scan base-package="com.task10.controller" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<mvc:resources mapping="/css/**" location="/css/" />
<mvc:resources mapping="/js/**" location="/js/" />
</beans>
4.6 Web部署描述文件(web.xml)
📄 web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<filter>
<filter-name>encodingFilter</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>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
</web-app>
4.7 JSP视图(calculator.jsp)
注意:博客园不会执行JSP代码,以下代码块可安全显示。
📄 calculator.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Dynamic Calculator - Glassmorphism</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/layout.css">
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/background.css">
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/glass.css">
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/calculator.css">
</head>
<body>
<canvas id="particleCanvas"></canvas>
<canvas id="ringCanvas"></canvas>
<div id="calculatorContainer" class="glass-container">
<div class="title">✨ 动态加法器</div>
<form id="calcForm" method="post" action="${pageContext.request.contextPath}/calculator">
<div class="input-area">
<input type="text" id="num1" name="num1" placeholder="数字1" value="${num1}">
<span class="symbol">+</span>
<input type="text" id="num2" name="num2" placeholder="数字2" value="${num2}">
</div>
<c:if test="${not empty errorMessages}">
<div class="error-list">
<c:forEach var="err" items="${errorMessages}">
<div class="error-msg">⚠ ${err}</div>
</c:forEach>
</div>
</c:if>
<div class="error-list" id="jsErrors"></div>
<button type="submit" id="calculateBtn">开始计算</button>
</form>
<div class="result-panel">
<div class="result-label">计算结果</div>
<div id="sumResult">
<c:if test="${not empty calculator}">
${calculator.sum}
</c:if>
</div>
</div>
</div>
<script src="${pageContext.request.contextPath}/js/particle.js"></script>
<script src="${pageContext.request.contextPath}/js/ring.js"></script>
<script src="${pageContext.request.contextPath}/js/star.js"></script>
<script src="${pageContext.request.contextPath}/js/calculator.js"></script>
</body>
</html>
4.8 CSS样式
🎨 layout.css(全屏布局与Canvas层级)
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
background-color: #0a0f1a;
}
canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -10;
}
#calculatorContainer {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 100;
}
🎨 glass.css(毛玻璃容器与浮动动画)
.glass-container {
width: 420px;
padding: 30px 20px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 24px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 0 30px rgba(0, 255, 255, 0.2), 0 0 60px rgba(0, 120, 255, 0.1);
text-align: center;
color: white;
animation: floatGlass 6s ease-in-out infinite;
}
@keyframes floatGlass {
0% { transform: translate(-50%, -50%); }
50% { transform: translate(-50%, -55%); }
100% { transform: translate(-50%, -50%); }
}
.glass-container .title {
font-size: 2rem;
font-weight: 300;
margin-bottom: 25px;
text-shadow: 0 0 20px rgba(255, 255, 255, 0.6);
}
.glass-container button {
background: rgba(255, 255, 255, 0.15);
color: white;
font-size: 1.1rem;
border: none;
border-radius: 30px;
padding: 12px 30px;
cursor: pointer;
margin-top: 20px;
transition: all 0.3s ease;
}
.glass-container button:hover {
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 0 20px rgba(0, 255, 255, 0.5);
transform: scale(1.05);
}
🎨 calculator.css(输入框、错误提示、结果区域)
.input-area {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 15px;
}
.symbol {
font-size: 2rem;
color: rgba(255, 255, 255, 0.9);
}
.glass-container input[type="text"] {
width: 120px;
height: 45px;
padding: 5px 10px;
font-size: 1.2rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(0, 0, 0, 0.3);
color: white;
text-align: center;
transition: 0.3s;
outline: none;
}
.glass-container input[type="text"]:focus {
border-color: #00e5ff;
box-shadow: 0 0 8px rgba(0, 229, 255, 0.6);
}
.error-msg {
color: #ff7b7b;
font-size: 0.95rem;
margin: 5px 0;
background: rgba(255, 80, 80, 0.15);
padding: 6px 12px;
border-radius: 8px;
display: inline-block;
}
.result-panel {
margin-top: 25px;
}
.result-label {
font-size: 0.9rem;
opacity: 0.8;
margin-bottom: 5px;
}
#sumResult {
font-size: 2rem;
font-weight: bold;
color: #00ffff;
text-shadow: 0 0 10px rgba(0, 255, 255, 0.7);
}
🎨 background.css(动态渐变背景)
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: "Microsoft YaHei", sans-serif;
background: linear-gradient(-45deg, #08121f, #102b48, #15294a, #24163d, #0d304f, #11253c);
background-size: 700% 700%;
animation: backgroundMove 50s ease infinite;
z-index: -30;
position: fixed;
top: 0;
left: 0;
}
@keyframes backgroundMove {
0% { background-position: 0% 50%; }
25% { background-position: 50% 100%; }
50% { background-position: 100% 50%; }
75% { background-position: 50% 0%; }
100% { background-position: 0% 50%; }
}
#particleCanvas, #ringCanvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -20;
pointer-events: none;
}
#starCanvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -25;
pointer-events: none;
}
4.9 JavaScript交互脚本
📜 calculator.js(前端校验与提交拦截)
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('calcForm');
const num1Input = document.getElementById('num1');
const num2Input = document.getElementById('num2');
const jsErrorsContainer = document.getElementById('jsErrors');
function validateSingle(value, fieldName) {
if (value.trim() === '') {
return fieldName + ' 不能为空';
}
if (!/^-?\d+(\.\d+)?$/.test(value.trim())) {
return fieldName + ' 必须为有效数字';
}
return null;
}
function collectErrors() {
const errors = [];
const err1 = validateSingle(num1Input.value, '数字1');
if (err1) errors.push(err1);
const err2 = validateSingle(num2Input.value, '数字2');
if (err2) errors.push(err2);
return errors;
}
function showErrors(errors) {
jsErrorsContainer.innerHTML = '';
if (errors.length === 0) return;
errors.forEach(function (msg) {
const div = document.createElement('div');
div.className = 'error-msg';
div.textContent = '⚠ ' + msg;
jsErrorsContainer.appendChild(div);
});
}
function refreshErrors() {
const errors = collectErrors();
showErrors(errors);
}
num1Input.addEventListener('blur', refreshErrors);
num2Input.addEventListener('blur', refreshErrors);
form.addEventListener('submit', function (e) {
const errors = collectErrors();
if (errors.length > 0) {
e.preventDefault();
showErrors(errors);
} else {
showErrors([]);
}
});
});
📜 particle.js(动态粒子背景 + 鼠标排斥 + 涟漪)
window.onload = function () {
const canvas = document.getElementById("particleCanvas");
if (!canvas) return;
const ctx = canvas.getContext("2d");
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener("resize", resize);
const mouse = { x: -1000, y: -1000, radius: 160 };
window.addEventListener("mousemove", e => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
const ripples = [];
window.addEventListener("click", e => {
ripples.push({ x: e.clientX, y: e.clientY, r: 0, alpha: 1 });
});
class Particle {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * 2 + 1;
this.speedX = (Math.random() - 0.5) * 0.18;
this.speedY = (Math.random() - 0.5) * 0.18;
this.color = `hsla(${Math.floor(Math.random() * 360)}, 80%, 80%, 0.85)`;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
if (this.x <= 0 || this.x >= canvas.width) this.speedX *= -1;
if (this.y <= 0 || this.y >= canvas.height) this.speedY *= -1;
const dx = this.x - mouse.x;
const dy = this.y - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < mouse.radius) {
this.x += dx / dist * 2.2;
this.y += dy / dist * 2.2;
}
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
}
const particles = [];
for (let i = 0; i < 180; i++) {
particles.push(new Particle());
}
function connect() {
for (let a = 0; a < particles.length; a++) {
for (let b = a + 1; b < particles.length; b++) {
const dx = particles[a].x - particles[b].x;
const dy = particles[a].y - particles[b].y;
const dist = dx * dx + dy * dy;
if (dist < 14000) {
ctx.beginPath();
ctx.moveTo(particles[a].x, particles[a].y);
ctx.lineTo(particles[b].x, particles[b].y);
ctx.strokeStyle = `rgba(255,255,255,${(1 - dist / 14000) * 0.15})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
}
}
}
let hue = 0;
function drawLight() {
hue += 0.08;
const g = ctx.createRadialGradient(canvas.width / 2, canvas.height / 2, 50, canvas.width / 2, canvas.height / 2, canvas.width);
g.addColorStop(0, `hsla(${hue},60%,35%,0.08)`);
g.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawRipples() {
for (let i = ripples.length - 1; i >= 0; i--) {
const w = ripples[i];
ctx.beginPath();
ctx.arc(w.x, w.y, w.r, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255,255,255,${w.alpha})`;
ctx.lineWidth = 2;
ctx.stroke();
w.r += 3;
w.alpha -= 0.015;
if (w.alpha <= 0) ripples.splice(i, 1);
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawLight();
particles.forEach(p => { p.update(); p.draw(); });
connect();
drawRipples();
requestAnimationFrame(animate);
}
animate();
};
📜 ring.js(独立水波纹效果)
(function () {
const canvas = document.getElementById('ringCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
const waves = [];
document.addEventListener('click', function (e) {
waves.push({
x: e.clientX,
y: e.clientY,
radius: 0,
alpha: 1
});
});
function drawWaves() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = waves.length - 1; i >= 0; i--) {
const w = waves[i];
ctx.beginPath();
ctx.arc(w.x, w.y, w.radius, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255, 255, 255, ${w.alpha})`;
ctx.lineWidth = 2;
ctx.stroke();
w.radius += 3;
w.alpha -= 0.015;
if (w.alpha <= 0) waves.splice(i, 1);
}
requestAnimationFrame(drawWaves);
}
drawWaves();
})();
📜 star.js(星空闪烁与随机流星)
(function () {
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = 0;
canvas.style.pointerEvents = 'none';
const ctx = canvas.getContext('2d');
let stars = [];
let meteors = [];
const STAR_COUNT = 150;
const METEOR_CHANCE = 0.005;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
initStars();
}
function initStars() {
stars = [];
for (let i = 0; i < STAR_COUNT; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 1.5 + 0.5,
alpha: Math.random(),
delta: Math.random() * 0.02 + 0.01
});
}
}
function drawStars() {
for (let star of stars) {
star.alpha += star.delta;
if (star.alpha > 1 || star.alpha < 0.1) star.delta *= -1;
ctx.beginPath();
ctx.arc(star.x, star.y, star.radius, 0, 2 * Math.PI);
ctx.fillStyle = `rgba(255,255,255,${star.alpha})`;
ctx.fill();
}
}
function spawnMeteor() {
if (Math.random() < METEOR_CHANCE) {
meteors.push({
x: Math.random() * canvas.width,
y: -10,
len: Math.random() * 80 + 50,
speed: Math.random() * 5 + 3,
angle: Math.random() * 0.3 + 0.2
});
}
}
function drawMeteors() {
for (let i = meteors.length - 1; i >= 0; i--) {
let m = meteors[i];
ctx.beginPath();
ctx.moveTo(m.x, m.y);
ctx.lineTo(m.x - m.len * Math.cos(m.angle), m.y + m.len * Math.sin(m.angle));
ctx.strokeStyle = 'rgba(255,255,255,0.8)';
ctx.lineWidth = 1.5;
ctx.stroke();
m.x += m.speed * Math.cos(m.angle);
m.y += m.speed * Math.sin(m.angle);
if (m.x > canvas.width || m.y > canvas.height) meteors.splice(i, 1);
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawStars();
spawnMeteor();
drawMeteors();
requestAnimationFrame(animate);
}
window.addEventListener('resize', resize);
resize();
animate();
})();
五、部署与调试过程
5.1 本地Tomcat部署
- 在Eclipse中导入Maven项目,配置Tomcat 9.0运行时。
- 右键项目 → Run As → Run on Server,选择Tomcat 9.0启动。
- 访问
http://localhost:8080/task10/calculator验证功能。
5.2 阿里云ECS部署
- 执行
mvn clean package生成target/task10.war。 - 通过WinSCP(SFTP)或远程桌面复制WAR包到服务器的Tomcat
webapps目录。 - 启动Tomcat:
startup.bat(Windows)或./startup.sh(Linux)。 - 访问公网地址
http://<服务器IP>:8080/task10/calculator测试。
5.3 典型问题与解决方案
| 问题 | 原因 | 解决方法 |
|---|---|---|
| Eclipse XML校验报错(cvc-elt.1.a) | IDE禁止下载XSD资源 | 关闭Eclipse的XML验证(不影响运行) |
| 玻璃面板未居中 | CSS动画覆盖了transform水平偏移 | 修改动画关键帧,每帧都包含 translate(-50%, y) |
| 表单提交无结果 | JSP缺少<form>标签,按钮未设type="submit" |
添加表单,设置按钮类型,完善模型回显 |
| Canvas遮挡按钮 | Canvas默认拦截鼠标事件 | 设置 pointer-events: none,提高容器z-index |
| 8080端口被占用 | 其他进程占用 | `netstat -ano |
| 远程部署时SCP不可用 | Windows未安装OpenSSH客户端 | 改用WinSCP或远程桌面直接复制 |
| FTP被动模式端口被拦截 | 安全组未放行被动端口范围 | 放弃FTP,使用远程桌面复制文件 |
5.4 最终效果
- 毛玻璃卡片始终居中,带有上下浮动动画。
- 输入两个有效数字(支持整数、小数、负数),点击“开始计算”即时显示和。
- 空值或非数字输入时,前端(失焦时)和后端分别给出明确错误提示。
- 动态粒子背景、鼠标排斥、粒子间连线、点击涟漪、流星闪烁等视觉效果全部正常。
5.5 演示图片
以下为本地及云端测试过程中的关键界面截图,展示了加法器的不同状态和视觉效果。
图1:加法器初始页面状态
页面加载后,毛玻璃卡片居中于动态渐变背景之上,输入框为空,结果区域无数据显示,粒子背景与星空效果自动运行。

图2:正常计算(8 + 12 = 20)
在两个输入框中分别输入“8”和“12”,点击“开始计算”后,结果区域正确显示“20”,无任何错误提示。

图3:空值校验提示
当某个输入框为空时(例如数字1为空,数字2为“12”),前端失焦时立即显示“数字1 不能为空”;提交时后端同样拦截并展示错误列表。

图4:非数字校验提示
输入非数字字符(如“abc”)时,前端正则校验及后端 abc 均会捕获异常,并给出“必须为有效数字”的明确错误信息。

图5:云端部署正常计算(123 + 456 = 579)
项目部署至阿里云ECS后,通过公网IP访问,计算功能与本地表现一致,输入“123”和“456”得到正确和“579”。

图6:云端全错误提示(两个输入均非法)
同时提交两个非法输入(例如空值和字母),错误列表会显示全部错误信息,确保用户一次性获知所有问题。

说明:以上图片来自实验过程中的真实截图。
六、实验小结
本次实验基于SpringMVC框架实现了一个功能完整、界面现代的加法器,重点掌握了以下内容:
- MVC分层设计:Controller负责请求处理与数据传递,Bean封装数据,Validator独立校验逻辑,JSP负责视图渲染。
- 前后端双重校验:前端提升用户体验,后端保证数据安全,两者配合缺一不可。
- SpringMVC配置:组件扫描、视图解析器、静态资源映射、编码过滤器。
- 动态视觉增强:CSS毛玻璃效果、Canvas粒子系统、星空流星动画,提升了项目的趣味性和展示性。
- 云服务器部署:解决了端口占用、文件传输、防火墙配置等实际问题,成功公网发布。
通过小组四人协作(方致沅架构后端、李润哲视觉样式、王鑫杰前端脚本、康奕彬部署运维),我们不仅完成了实验要求,还额外实现了丰富的动态背景和交互效果,为后续课程设计积累了可复用经验。
七、附录
- 《Web编程技术》(第二版),余元辉主编,清华大学出版社,2024.8
- 《JSP设计》(第三版),Hans Bergsten 著,林琪、朱涛江译,O‘Reilly Media, 2013

浙公网安备 33010602011771号