基于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部署

  1. 在Eclipse中导入Maven项目,配置Tomcat 9.0运行时。
  2. 右键项目 → Run As → Run on Server,选择Tomcat 9.0启动。
  3. 访问 http://localhost:8080/task10/calculator 验证功能。

5.2 阿里云ECS部署

  1. 执行 mvn clean package 生成 target/task10.war
  2. 通过WinSCP(SFTP)或远程桌面复制WAR包到服务器的Tomcat webapps目录。
  3. 启动Tomcat:startup.bat(Windows)或 ./startup.sh(Linux)。
  4. 访问公网地址 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:加法器初始页面状态
页面加载后,毛玻璃卡片居中于动态渐变背景之上,输入框为空,结果区域无数据显示,粒子背景与星空效果自动运行。

图片1


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

图片2


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

图片3


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

图片4


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

图片5


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

图片6

说明:以上图片来自实验过程中的真实截图。

六、实验小结

本次实验基于SpringMVC框架实现了一个功能完整、界面现代的加法器,重点掌握了以下内容:

  • MVC分层设计:Controller负责请求处理与数据传递,Bean封装数据,Validator独立校验逻辑,JSP负责视图渲染。
  • 前后端双重校验:前端提升用户体验,后端保证数据安全,两者配合缺一不可。
  • SpringMVC配置:组件扫描、视图解析器、静态资源映射、编码过滤器。
  • 动态视觉增强:CSS毛玻璃效果、Canvas粒子系统、星空流星动画,提升了项目的趣味性和展示性。
  • 云服务器部署:解决了端口占用、文件传输、防火墙配置等实际问题,成功公网发布。

通过小组四人协作(方致沅架构后端、李润哲视觉样式、王鑫杰前端脚本、康奕彬部署运维),我们不仅完成了实验要求,还额外实现了丰富的动态背景和交互效果,为后续课程设计积累了可复用经验。

七、附录

  • 《Web编程技术》(第二版),余元辉主编,清华大学出版社,2024.8
  • 《JSP设计》(第三版),Hans Bergsten 著,林琪、朱涛江译,O‘Reilly Media, 2013
posted @ 2026-06-12 14:42  fangzhiyuan  阅读(10)  评论(0)    收藏  举报