JS+Springboot做一个交互Demo

背景:老大做了一个 SDK,包含字符加解密、文件加解密,要求能从前端访问,并且能演示的 Demo。

思路:html 写页面,js 发送请求,freemarker 做简单的参数交互,JAVA 后端处理。

一、项目依赖

● java17

● springboot 3.1.2

● 模板技术:spring-boot-starter-freemarker 2.3.32

● 工具包:commons-io 2.16.0

● 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">
    <parent>
        <groupId>com.xxx.xx.x</groupId>
        <artifactId>xx-x</artifactId>
        <version>3.0-SNAPSHOT</version>
    </parent>

    <artifactId>x-sdk-service</artifactId>
    <modelVersion>4.0.0</modelVersion>
    <description>sdk 项目服务</description>
    <packaging>jar</packaging>
    <name>sdk-service</name>

    <properties>
        <jdk.version>17</jdk.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.1.2</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>

        <!-- SpringBoot 拦截器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- Spring框架基本的核心工具 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.8</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>logback-classic</artifactId>
                    <groupId>ch.qos.logback</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 前端渲染视图技术 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.16.1</version>
        </dependency>
    </dependencies>

  
</project>

二、前端代码

使用的模板技术,JS + Freemark。"${baseUrl}"指向的是后端配置的IP地址,可改为静态的,但是那样html页面就写死了。文件放在项目:resource/templates下,无则需要创建。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>演示页</title>
</head>
<script>

    // 访问地址
    const baseUrl = "${baseUrl}"

    /**
     * 加密
     */
    function getEnc() {
        // 处理传入数据
        let data = document.getElementById("encText").value;
        data = btoa(data);
        // 组装请求
        const oReq = new XMLHttpRequest();
        const url = baseUrl + "/enc?data=" + data;
        console.log("request " + url)
        oReq.open("GET", url, true);
        // 响应类型
        oReq.responseType = "text";
        oReq.onprogress = function (result) {
            console.log(result)
            // 失败处理
            if (fail(result)) {
                return;
            }
            // 处理响应
            const data = result.currentTarget.response;
            let parse = JSON.parse(data);
            const text = parse.data;
            document.getElementById("decText").value = String(text);
        };
        oReq.send();
    }

    /**
     * 解密
     */
    function getDec() {
        let data = document.getElementById("decText").value;
        const oReq = new XMLHttpRequest();
        data = btoa(data);
        const url = baseUrl + "/dec?data=" + data;
        console.log("request " + url)
        oReq.open("GET", url, true);
        // 响应类型
        oReq.responseType = "text";
        oReq.onprogress = function (result) {
            console.log(result)
            if (fail(result)) {
                return;
            }
            const data = result.currentTarget.response;
            let parse = JSON.parse(data);
            const text = parse.data;
            document.getElementById("encText").value = String(atob(text));
        };
        oReq.send();
    }

    /**
     * 文件加密
     */
    function getEncFile() {
        const fileInput = document.getElementById('encFile');
        const file = fileInput.files[0];
        let formData = new FormData();
        formData.append("file", file)
        const oReq = new XMLHttpRequest();
        const url = baseUrl + "/encFile";
        console.log("request " + url)
        oReq.open("POST", url, true);
        // 响应类型
        oReq.onprogress = function (result) {
            console.log(result)
            if (fail(result)) {
                return;
            }
            const data = result.currentTarget.response;
            let parse = JSON.parse(data);
            const fileName = parse.data;
            // 下载文件
            downloadFile(baseUrl + "/downLoadFile/" + fileName, fileName)
            // 原组件置空
            fileInput.value = '';
        };
        oReq.send(formData);
    }

    /**
     * 文件解密
     */
    function getDecFile() {
        const fileInput = document.getElementById('decFile');
        const file = fileInput.files[0];
        let formData = new FormData();
        formData.append("file", file)
        const oReq = new XMLHttpRequest();
        const url = baseUrl + "/decFile";
        console.log("request " + url)
        oReq.open("POST", url, true);
        // 响应类型
        oReq.onload = function (result) {
            console.log(result)
            if (fail(result)) {
                console.log("encFile:" + result)
                return;
            }
            const data = result.currentTarget.response;
            let parse = JSON.parse(data);
            const fileName = parse.data;
            downloadFile(baseUrl + "/downLoadFile/" + fileName, fileName)
            fileInput.value = '';
        };
        oReq.send(formData);
    }

    /**
     * 下载文件
     * @param url
     * @param fileName
     */
    function downloadFile(url, fileName) {
        console.log("downloadFile:" + url)
        console.log("downloadFile fileName:" + fileName)
        fetch(url)
            .then(response => response.blob())
            .then(blob => {
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = fileName;
                link.target = "_blank"; // 可选,如果希望在新窗口中下载文件,请取消注释此行
                link.click();
            });
    }

    /**
     * 请求是否失败
     * @param result 请求
     * @returns {boolean} true 失败
     */
    function fail(result) {
        const data = result.currentTarget.response;
        let parse = JSON.parse(data);
        if (200 !== parse.code) {
            alert("请求失败:" + parse.msg)
            return true;
        }
        return false;
    }

</script>
<body style="text-align: center;margin-top: auto">
<h1>加密工具演示页</h1>
<!-- 文本加密 -->
<div style="margin-top: 48px">
    <h3>文本加密</h3>
    <div>
        <input id="encText" type="text" style="width: 200px;"/>
        <button onclick="getEnc();" style="margin-left: 12px">加密</button>
    </div>
    <div style="margin-top: 12px">
        <input id="decText" type="text" style="width: 200px;"/>
        <button onclick="getDec();" style="margin-left: 12px">解密</button>
    </div>
</div>
<!-- 文件加密 -->
<div style="margin-top: 48px;">
    <h3>文件加解密</h3>
    <div style="margin-top: 24px">
        文件加密:
        <input id="encFile" type="file" onchange="getEncFile()"/>
    </div>
    <div style="margin-top: 24px;">
        文件解密:
        <input id="decFile" type="file" onchange="getDecFile()"/>
    </div>
</div>
</body>
</html>

三、后端配置

application配置文件

yaml 复制代码
server:
  port: 12321

tmv:
  baseUrl: http://localhost:12321
  baseDir: ./config/


# application.yml
spring:
  servlet:
    multipart:
      max-file-size: 10000MB
      max-request-size: 10000MB
  #配置freemarker
  freemarker:
    #指定HttpServletRequest的属性是否可以覆盖controller的model的同名项
    allow-request-override: false
    #req访问request
    request-context-attribute: req
    #后缀名freemarker默认后缀为.ftl,当然你也可以改成自己习惯的.html
    suffix: .ftl
    #设置响应的内容类型
    content-type: text/html;charset=utf-8
    #是否允许mvc使用freemarker
    enabled: true
    #是否开启template caching
    cache: false
    #设定模板的加载路径,多个以逗号分隔,默认: ["classpath:/templates/"]
    template-loader-path: classpath:/templates/
    #设定Template的编码
    charset: UTF-8
    # 设置静态文件路径,js,css等
  mvc:
    static-path-pattern: /static/**

四、后端代码

java 复制代码
@RestController
@RequestMapping
@Slf4j
public class EncryptController {

    @Value("${tmv.baseUrl:http://localhost:12321}")
    String baseUrl;
    @Value("${tmv.baseDir:D:\\temp\\temp\\}")
    String baseDir;

    private static KmService kmService;

    /**
     * 初始化 SDK
     */
    @PostConstruct
    public void init() {
        try {
            log.info("load kmService start, baseDir: {}", baseDir);
            kmService = KmService.getInstance(baseDir);
            log.info("load kmService success");
        } catch (Exception e) {
            log.error("SDK加载失败,请检查");
            log.error("load kmService error", e);
        }
    }

    @RequestMapping("/")
    public ModelAndView index() {
        // 跳转到index.ftl 模板
        ModelAndView mv = new ModelAndView("index");
        // 添加属性
        mv.addObject("baseUrl", baseUrl);
        return mv;
    }

    @GetMapping("enc")
    public TResult enc(@RequestParam String data) {
        log.info("enc request: {}", data);
        String result;
        try {
            result = kmService.enc(data, null);
        } catch (Exception e) {
            log.error("enc error", e);
            result = e.getMessage();
        }
        log.info("enc resp: {}", result);
        return TResult.success(result);
    }

    @GetMapping("dec")
    public TResult dec(@RequestParam String data) {
        log.info("dec : {}", data);
        // 需要转码
        byte[] bytes = Base64.decodeBase64(data);
        log.info("dec : {}", data);
        String result;
        try {
            result = kmService.dec(new String(bytes), null);
        } catch (Exception e) {
            log.error("dec error", e);
            result = e.getMessage();
        }
        log.info("dec resp: {}", result);
        return TResult.success(result);
    }

    @PostMapping("encFile")
    public TResult encFile(@RequestParam MultipartFile file) {
        log.info("fileName request: {}", file.getOriginalFilename());
        long size = file.getSize();
        log.info("file size: {} bit {} kb", size, size / 1000);
        String result;
        try {
            // 创建临时文件
            File tempFile = File.createTempFile(System.currentTimeMillis() + "", "");
            byte[] bytes = file.getBytes();
            FileUtils.copyInputStreamToFile(new ByteArrayInputStream(bytes), tempFile);
            File destFile = new File(baseDir + UUIDGenerator.getUUID() + ".enc.txt", "");
            // 加密
            boolean flag = kmService.fileEncrypt(tempFile.getAbsolutePath(), destFile.getAbsolutePath());
            if (flag) {
                result = destFile.getName();
            } else {
                throw new RuntimeException("处理失败,请检查输入信息是否有误");
            }
        } catch (Exception e) {
            log.error("encFile error", e);
            return TResult.fail(e.getMessage());
        }
        log.info("enc resp: {}", result);
        return TResult.success(result);
    }

    @PostMapping("decFile")
    public TResult decFile(@RequestParam MultipartFile file) {
        log.info("decFile fileName request: {}", file.getOriginalFilename());
        long size = file.getSize();
        log.info("file size: {} bit {} kb", size, size / 1000);
        String result;
        try {
            // 创建临时文件
            File tempFile = File.createTempFile(System.currentTimeMillis() + "", "");
            byte[] bytes = file.getBytes();
            FileUtils.copyInputStreamToFile(new ByteArrayInputStream(bytes), tempFile);
            File destFile = new File(baseDir + UUIDGenerator.getUUID() + ".dec.txt", "");
            // 加密
            boolean flag = kmService.fileDecrypt(tempFile.getAbsolutePath(), destFile.getAbsolutePath());
            if (flag) {
                result = destFile.getName();
            } else {
                throw new RuntimeException("处理失败,请检查输入信息是否有误");
            }
        } catch (Exception e) {
            log.error("decFile error", e);
            return TResult.fail(e.getMessage());
        }
        log.info("enc resp: {}", result);
        return TResult.success(result);
    }

    @GetMapping("downLoadFile/{fileName}")
    public void downLoadFile(HttpServletResponse response, @PathVariable("fileName") String fileName) throws IOException {
        log.info("downLoadFile fileName request: {}", fileName);
        String filePath = baseDir + fileName;
        log.info("filePath :{}", filePath);
        File file = new File(filePath);
        try {
            copy2respond(response, file);
        } catch (Exception e) {
            log.error("decFile error", e);
        }
    }

    /**
     * 文件转下载流
     *
     * @param response 响应
     * @param file     文件
     * @throws IOException 异常
     */
    private static void copy2respond(HttpServletResponse response, File file) throws IOException {
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());

        FileInputStream fileInputStream = new FileInputStream(file);
        OutputStream out = response.getOutputStream();

        byte[] buffer = new byte[4096];
        int length;
        while ((length = fileInputStream.read(buffer)) > 0) {
            out.write(buffer, 0, length);
        }
        out.flush();
        fileInputStream.close();
    }

}

响应对象

java 复制代码
@Data
public class TResult implements Serializable {

    private static final long serialVersionUID = 1L;

    @Schema(description = "响应码")
    @Getter
    private int code;

    @Schema(description = "响应消息")
    @Getter
    private String msg;

    @Schema(description = "响应数据")
    @Getter
    private Object data;

    public static TResult success(Object data){
        TResult tResult = new TResult();
        tResult.setCode(200);
        tResult.setMsg("OK");
        tResult.setData(data);
        return tResult;
    }

    public static TResult fail(String messag){
        TResult tResult = new TResult();
        tResult.setCode(500);
        tResult.setMsg(messag);
        tResult.setData(null);
        return tResult;
    }
}

五、页面效果

最后,作为一个Javaer,会点前端是OK的。但,技不外露,最好不要随便接前端需求。还好最终老大放了一条生路,否则笔者大概率要学习深入理解Html + css + JavaScript了。

(因为笔者觉得只是两个输入框所以自告奋勇的接下来,谁知道后面又要加远程访问、文件加解密、UI优化、全局异常捕获、人性化提示...差点翻车)

相关推荐
Monly214 分钟前
JS:JSON操作
前端·javascript·json
YesPMP2526 分钟前
短剧小程序,打造专属短剧观看平台
小程序·app·html5·平台·短剧·影视
觉醒法师1 小时前
HarmonyOS开发 - 本地持久化之实现LocalStorage支持多实例
前端·javascript·华为·typescript·harmonyos
w风雨无阻w2 小时前
Vue3 学习笔记(十一)Vue生命周期
javascript·vue.js·前端框架·vue3
武昌库里写JAVA2 小时前
【MySql】-0.1、Unbunt20.04二进制方式安装Mysql5.7和8.0
spring boot·spring·毕业设计·layui·课程设计
清清ww2 小时前
【vue】13.深入理解递归组件
前端·javascript·vue.js
清清ww2 小时前
【vue】09.computer和watch的使用
前端·javascript·vue.js
你不讲 wood2 小时前
使用 Axios 上传大文件分片上传
开发语言·前端·javascript·node.js·html·html5
lexusv8ls600h3 小时前
微服务设计模式 - 重试模式(Retry Pattern)
java·spring boot·微服务
清灵xmf4 小时前
UI 组件的二次封装
前端·javascript·vue.js·element-plus