【spring mvc】全局处理请求体和响应体

目录

说明

由于项目中需要进行加密传输数据提高项目安全性,前端调用接口时,请求体中的数据全部加密成密文传输到后端接口,后端接口接收之后解密回明文。本例中使用的RSA非对称加解密方式处理。

如果后端接口参数使用字符串String 接收密文参数,则需要在每个接口方法中进行解密操做,代码会产生冗余。

如下伪代码:

java 复制代码
@Slf4j
@RestController
@RequestMapping(value = "user")
public class TUserController {
    /**
    * 说明: 添加
    * @author   zhangxiaosan
    * @create   2024/12/3
    * @param
    * @return
    */
    @PostMapping(value = "add")
    public String add(@RequestBody String user){
    	// user 参数为前端传递过来的 密文
    	// 使用RSA 解密方法,传入密文和密钥解密得到明文
    	String data = RSA.decryption(user,key); // 此部分为冗余代码。
		
		// todo 业务操做逻辑
				
        return user.toString();
    }
}

现在,有个需求是,接口方法中正常使用实体类来作为参数类型接收。前端依旧是传递加密后的密文。后端接收到密文之后,解密得到明文后,将明文数据转成接口参数所需要的数据类型。

业务处理完成之后,将返回的数据再进行加密传给前端。前端解密后才能得到明文展示 。

实现效果

逻辑图

实现步骤

创建公共处理的请求和响应的类

此类主要是公共处理api接收前端请求的密文参数,解密。处理方法返回数据的加密。

java 复制代码
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import www.three.common.response.Response;
import www.three.common.security.rsa.RSAUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Objects;

/**
 * 说明:
 * 对请求体和响应体进行加密解密
 * 此类用于全局处理请求和响应的功能类
 * RequestBodyAdvice 接口
 * RequestBodyAdvice 接口用于在请求的请求体被反序列化(即转化为 Java 对象)之前对其进行处理。你可以使用它来解密请求体中的数据。
 * <p>
 * ResponseBodyAdvice<Response> 接口
 * ResponseBodyAdvice 接口用于在响应体被序列化(即将 Java 对象转化为响应内容)之前对其进行处理。你可以使用它来加密响应体中的数据。Response 为自定义的响应格式实体类。
 *
 * @author 张小三
 * @create 2024-12-04 10:00
 * @verson 1.0.0
 */
@Slf4j
@ControllerAdvice
public class EncryptRequestResponseBodyAdvice implements ResponseBodyAdvice<Response>, RequestBodyAdvice {
	// 前端 RSA算法的私钥,用于解密前端传递过来的密文。前端需要于此私钥对应的公钥进行加密数据。
    private static final String privateKey = "";// 此处填写为前端的RSA私钥
	
	// 后端Api RSA 算法的公钥,用于加密响应体的数据。前端需要使用此公钥对应的私钥才能进行数据解密。
    private static final String publicKey = ""; // 此处填写为后端api的RSA公钥

    /**
     * 此方法判断当前处理器是否支持给定的方法参数、目标类型和HTTP消息转换器类型
     * @param methodParameter 方法参数
     * @param targetType      目标类型
     * @param converterType   HTTP消息转换器类型
     * @return 总是返回false,表示不支持
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;//返回 true 表示我们想要处理所有请求的请求体
    }

    /**
     * 在读取请求体之前调用此方法进行预处理
     * @param inputMessage  输入消息
     * @param parameter     方法参数
     * @param targetType    目标类型
     * @param converterType HTTP消息转换器类型
     * @return 返回null,表示不需要进行特殊处理
     * @throws IOException 如果处理过程中发生I/O错误
     */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return new HttpInputMessage() {
            @Override
            public InputStream getBody() throws IOException {
            	// 读取响应体的流
                InputStream inputStream = inputMessage.getBody();
                byte[] bytes = new byte[inputStream.available()];
                inputStream.read(bytes);
                // 转成字符串
                String data = JSON.parseObject(bytes).getString("data");
                log.info("RequestBody 密文:" + data);
                // 使用RSA工具方法解密
                data = RSAUtil.decryption(data,privateKey);
                log.info("RequestBody 明文:" + data);
                return new ByteArrayInputStream(data.getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }
        };
    }

    /**
     * 在读取请求体之后调用此方法进行后处理
     *
     * @param body          请求体内容
     * @param inputMessage  输入消息
     * @param parameter     方法参数
     * @param targetType    目标类型
     * @param converterType HTTP消息转换器类型
     * @return 返回null,表示不需要进行特殊处理
     */
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    /**
     * 当请求体为空时调用此方法进行处理
     *
     * @param body          请求体内容(此时为空)
     * @param inputMessage  输入消息
     * @param parameter     方法参数
     * @param targetType    目标类型
     * @param converterType HTTP消息转换器类型
     * @return 返回null,表示不需要进行特殊处理
     */
    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    /**
     * 此方法判断当前处理器是否支持给定的返回类型和HTTP消息转换器类型
     * @param returnType    返回类型
     * @param converterType HTTP消息转换器类型
     * @return 总是返回false,表示不支持
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    /**
     * 在写入响应体之前调用此方法进行预处理
     * 本实现中不需要任何预处理,因此返回null
     *
     * @param body                  响应体内容
     * @param returnType            返回类型
     * @param selectedContentType   选定的内容类型
     * @param selectedConverterType 选定的HTTP消息转换器类型
     * @param request               HTTP请求
     * @param response              HTTP响应
     * @return 返回null,表示不需要进行特殊处理
     */
    @Override
    public Response beforeBodyWrite(Response body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (Objects.nonNull(body) && body.getData() != null) {
            String data = JSON.toJSONString(body.getData());
            log.info("ResponseBody 明文:" + data);
            // 用RSA工具将返回的数据进行加密
            data = RSAUtil.encryption(data,publicKey);
            log.info("ResponseBody 密文:" + data);
            body.setData(data);
        }
        return body;
    }
}

api接口

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import www.three.common.response.Response;
import www.three.components.log.annotations.Log;
import www.three.system.user.model.po.TUser;

/**
 * 说明:
 *
 * @author 张小三
 * @create 2024-12-03 17:37
 * @verson 1.0.0
 */
@Slf4j
@RestController
@RequestMapping(value = "user")
public class TUserController {
    /**
    * 说明: 添加
    * @author   zhangxiaosan
    * @create   2024/12/3
    * @param
    * @return
    */
    @Log
    @PostMapping(value = "add")
    public Response<?> add(@RequestBody TUser user){
        log.info("添加用户:{}",user.toString());
        return Response.success(user);
    }
}

测试

前端请求

使用idea的 HTTP Client 工具 请求案例:

json 复制代码
POST http://127.0.0.1:8081/three-user/user/add
Content-Type: application/json

{"data":"6944323931323669415876424a653867445233636247553464552b3256466c2b3631586639314675584a6b5068394a6f6b54457235464c4743666e496b5342574a6256564e484b4c49667a6d0d0a6c55696d74444b4778363858465945516f34756d4b6938714f494b732f5157742b6a3235543256737733356c7677465976474463613043454e636376726257502f2f6f3337686c6d6b6e46680d0a38367237467663634c6950586648657135534b31736d6e372b6637667658454d386a79454e794c346b44546c444f367050476e594c793041366d65324a4a6136387251536a616963325a51680d0a6756446369386a743346755a47565377574147332f76306739472f57614e4b3330372b4455467377677536536e79534e71703530635637576a5a38533732654477744d4365587945653638550d0a4976704d4c6e6a303465446c754f506474626f33493578693479724265446a6f356f46557a513d3d"}

响应结果

json 复制代码
{
  "code": 0,
  "message": "成功",
  "data": "4b637168342f4a7537637956784c7368637a527068314772783139515555506e336e7049474469524a654754464a7242327873545766574a666c346d66427268414b754264306a45454766640d0a5a764a76426e644d77526664613632635a48596a72562b546447597366653667796f6c68616648536f786f6737464575765342686b706473624b4d78574b645145637257655a62615a6e4c670d0a6d79566f467a697a7a5468445153744264627a6d304b42524d54654b6f41347746376d6137427878774c66537159526856484261486f305578734b686f4d78763638434342763959653249320d0a4f557073423545784b354e5a724465416663535a502f4b34516455464d4f33655a335552505067674f714f6d42755a4351654858586c3366334441704448444f5a6b644561354b436e702f330d0a636d6e2f38347771586c62373038344d316479546b4e466778696358592b62555864567a51773d3d",
  "count": null
}

扩展

Response响应格式实体

java 复制代码
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import lombok.Data;

/**
 * 说明:
 *
 * @author 张小三
 * @create 2024-07-12 16:12
 * @verson 1.0.0
 */
@Data
public class Response<T> {
    /**
     * 状态码
     * 其他值表示失败
     */
    private Integer code;

    /**
     * 提示信息
     */
    private String message;

    /**
     * 响应数据
     */
    private T data;

    /**
     * 数据总量(layui 列表使用)
     */
    private Integer count;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Object getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    public Response(Integer code, String message, T data, Integer count) {
        this.code = code;
        this.message = message;
        this.data = data;
        this.count = count;
    }

    public static <T> Response<T> success(){
        return response(ResponseCode.successCode,ResponseCode.successMessage,null,null);
    }

    public static <T> Response<T> success(T data){
        return response(ResponseCode.successCode,ResponseCode.successMessage,data,null);
    }

    public static <T> Response<T> success(T data, String message){
        return response(ResponseCode.successCode,message,data,null);
    }

    public static <T> Response<T> fail(){
        return response(ResponseCode.failCode,ResponseCode.failMessage,null,null);
    }

    public static <T> Response<T> fail(String message){
        return response(ResponseCode.failCode,message,null,null);
    }


    private static <T> Response<T> response(Integer code,String message,T data,Integer count){
        return new Response(code,message,data,count);
    }

    @Override
    public String toString() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code",code);
        jsonObject.put("message",message);
        jsonObject.put("count",count);
        jsonObject.put("data",data);
        return JSON.toJSONString(jsonObject, JSONWriter.Feature.WriteNulls);
    }

}

ResponseCode 响应状态码

java 复制代码
import java.io.Serializable;

/**
 * 说明:
 *  响应状态码
 * @author 张小三
 * @create 2024-07-12 14:19
 * @verson 1.0.0
 */
public class ResponseCode implements Serializable {
    /**
    * 说明: 成功状态码
    * @author   zhangxiaosan
    * @create   2024/7/12
    * @param
    * @return
    */
    public static final Integer successCode = 0;

    /**
    * 说明: 成功默认信息
    * @author   zhangxiaosan
    * @create   2024/7/12
    * @param
    * @return
    */
    public static final String successMessage = "成功";

    /**
     * 说明: 失败状态码
     * @author   zhangxiaosan
     * @create   2024/7/12
     * @param
     * @return
     */
    public static final Integer failCode = -1;

    /**
     * 说明: 失败默认信息
     * @author   zhangxiaosan
     * @create   2024/7/12
     * @param
     * @return
     */
    public static final String failMessage = "失败";

}

RSA工具类

java 复制代码
import com.alibaba.fastjson2.JSONObject;
import www.three.common.security.hex.HexUtil;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 说明:
 *  rsa 工具类
 * @author 张小三
 * @create 2024-12-03 11:52
 * @verson 1.0.0
 */
public class RSAUtil {
    private static String keySize =  "2048";
    /**
     * 创建公钥和私钥
     *
     * @return
     * @throws RSAException
     */
    public static Map<String, String> createKeys() throws RSAException {
        KeyPairGenerator keygen;
        try {
            keygen = KeyPairGenerator.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            throw new RSAException("RSA初始化密钥出现错误,算法异常");
        }
        SecureRandom secrand = new SecureRandom();
        //初始化随机产生器
        secrand.setSeed("jst".getBytes());
        //初始化密钥生成器
        if (Objects.isNull(keySize)){
            keySize = "2048";
        }
        keygen.initialize(Integer.valueOf(keySize), secrand);
        KeyPair keyPair = keygen.genKeyPair();
        //获取公钥并转成base64编码
        byte[] pub_key = keyPair.getPublic().getEncoded();
        String publicKeyStr = Base64.getEncoder().encodeToString(pub_key);
        //获取私钥并转成base64编码
        byte[] pri_key = keyPair.getPrivate().getEncoded();
        String privateKeyStr = Base64.getEncoder().encodeToString(pri_key);

        // 公钥和私钥转成16进制
        publicKeyStr = HexUtil.encode(publicKeyStr);
        privateKeyStr = HexUtil.encode(privateKeyStr);

        //创建一个Map返回结果
        Map<String, String> keyPairMap = new HashMap<>();
        keyPairMap.put("publicKey", publicKeyStr);
        keyPairMap.put("privateKey", privateKeyStr);
        return keyPairMap;
    }


    /**
     * RSA 加密
     *
     * @param data 加密内容
     * @param key  密钥
     * @return
     */
    public static  String encryption(String data,String key) throws RSAException {
        // 16进制密钥还原
        key = HexUtil.decode(key);
        byte[] publicKeyByte = Base64.getMimeDecoder().decode(key);
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyByte);
        String res = null;
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
            cipher.init(Cipher.ENCRYPT_MODE,publicKey);
            byte[] bytes = cipher.doFinal(data.getBytes());
            res = Base64.getMimeEncoder().encodeToString(bytes);
            res = HexUtil.encode(res); // 密文转成16进制
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException |
                 IllegalBlockSizeException | BadPaddingException e) {
            throw new RSAException("RSA出现错误,算法异常:"+e.getMessage());
        }
        return res ;
    }

    /**
     * RSA 解密
     *
     * @param data 密文
     * @param key 密钥
     * @return
     */
    public  static String decryption(String data ,String key) throws RSAException {
        // 16进制密钥还原
        key = HexUtil.decode(key);
        byte[] privateKeyByte = Base64.getMimeDecoder().decode(key);
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyByte);
        String res = null;
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
            cipher.init(Cipher.DECRYPT_MODE,privateKey);
            // 密文还原
            data = HexUtil.decode(data);
            byte[] decode = Base64.getMimeDecoder().decode(data);
            byte[] bytes = cipher.doFinal(decode);
            res = new String(bytes);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
            throw new RSAException("RSA出现错误,算法异常:"+e.getMessage());
        }
        return res ;
    }

    /**
     * RSA 签名
     *
     * @param data 待签名的数据
     * @param privateKey 私钥
     * @return 签名后的字符串
     * @throws RSAException 如果签名过程中出现异常
     */
    public static String sign(String data, String privateKey) throws RSAException {
        // 16进制密钥还原
        privateKey = HexUtil.decode(privateKey);
        byte[] privateKeyByte = Base64.getMimeDecoder().decode(privateKey);
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyByte);
        String signature = null;
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PrivateKey privateKeyObj = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
            Signature signer = Signature.getInstance("SHA256withRSA");
            signer.initSign(privateKeyObj);
            signer.update(data.getBytes());
            byte[] signedData = signer.sign();
            signature = Base64.getMimeEncoder().encodeToString(signedData);
            signature = HexUtil.encode(signature); // 签名转成16进制
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) {
            throw new RSAException("RSA签名出现错误,算法异常:" + e.getMessage());
        }
        return signature;
    }

    /**
     * RSA 验签
     *
     * @param data 原始数据
     * @param publicKey 公钥
     * @param signature 签名
     * @return 验签结果
     * @throws RSAException 如果验签过程中出现异常
     */
    public static boolean verify(String data, String publicKey, String signature) throws RSAException {
        // 16进制密钥还原
        publicKey = HexUtil.decode(publicKey);
        byte[] publicKeyByte = Base64.getMimeDecoder().decode(publicKey);
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyByte);
        boolean isVerify = false;
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKeyObj = keyFactory.generatePublic(x509EncodedKeySpec);
            Signature signer = Signature.getInstance("SHA256withRSA");
            signer.initVerify(publicKeyObj);
            signer.update(data.getBytes());
            // 签名还原
            signature = HexUtil.decode(signature);
            byte[] decodedSignature = Base64.getMimeDecoder().decode(signature);
            isVerify = signer.verify(decodedSignature);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) {
            throw new RSAException("RSA验签出现错误,算法异常:" + e.getMessage());
        }
        return isVerify;
    }
}

RequestBodyAdvice 介绍

用于在请求体被反序列化之前对其进行处理,典型场景是解密请求体。用于拦截请求体(Request Body)的处理,在请求体被反序列化为 Java 对象之前对其进行处理。这个接口允许开发者在请求进入控制器之前做一些预处理,比如解密或格式转换等。

RequestBodyAdvice 接口提供了 5 个方法,具体如下:

  1. boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType)

    用于判断当前请求是否需要处理,返回 true 表示该请求体需要进行处理。你可以根据 methodParameter、targetType 或 converterType 来做特定的控制。

  2. Object handleBodyBeforeRead(MethodParameter methodParameter, Type targetType, HttpInputMessage inputMessage, Class<? extends HttpMessageConverter<?>> converterType) throws IOException

    在请求体反序列化之前调用,可以对请求体进行解密、转换或者其他处理。返回值将是处理后的请求体内容,或者可以直接读取 inputMessage 数据并进行修改。

  3. Object handleBodyAfterRead(Object body, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType, HttpInputMessage inputMessage)

    请求体被成功反序列化后调用。在此方法中你可以进一步处理数据,比如验证或转换成其他格式。

  4. boolean handleBodyAfterWrite(Object body, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType, HttpOutputMessage outputMessage) throws IOException

    在响应体被序列化后调用,允许你对响应体进行最后的修改,比如加密、转换等。返回值决定了是否继续执行后续处理。

  5. boolean handleBodyBeforeWrite(Object body, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType, HttpOutputMessage outputMessage) throws IOException

    在响应体被写入前调用。你可以在这里对响应体数据进行修改、加密等处理。

使用场景

解密请求体: 在请求到达控制器之前,你可能需要对请求体进行解密操作(例如,使用某种加密算法加密请求体数据)。

日志记录: 记录请求的请求体内容,或者修改请求体中的数据。

格式转换: 例如,将请求体从 JSON 转换成 XML,或者做其他格式的处理。

ResponseBodyAdvice 介绍

用于在响应体被序列化并发送之前对其进行处理,典型场景是加密响应体。当控制器方法执行完毕并返回数据后,Spring 会通过 ResponseBodyAdvice 对响应体进行处理。它可以用于对响应数据进行加密、修改、过滤等操作。

ResponseBodyAdvice 接口提供了 2 个方法,具体如下:

  1. boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType)
    用于判断是否对当前的响应体执行处理。你可以根据返回类型、converterType 等条件判断是否处理。
  2. Object handleBodyBeforeWrite(Object body, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType, HttpOutputMessage outputMessage) throws IOException
    在响应体被序列化并写入 HTTP 响应之前调用。你可以在这里对响应体进行加密或修改等操作。

使用场景

加密响应体: 例如,在返回给客户端的数据需要加密时,你可以在 ResponseBodyAdvice 中对响应体进行加密。

日志记录: 记录响应体内容,或者根据需要修改返回的数据。

修改响应体格式: 可以根据需要改变响应的格式,比如将返回的 JSON 数据转换成其他格式。

相关推荐
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 分钟前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
李小白661 小时前
Spring MVC(上)
java·spring·mvc
m0_748235241 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
Lojarro3 小时前
【Spring】Spring框架之-AOP
java·mysql·spring
zjw_rp4 小时前
Spring-AOP
java·后端·spring·spring-aop
王ASC7 小时前
SpringMVC的URL组成,以及URI中对/斜杠的处理,解决IllegalStateException: Ambiguous mapping
java·mvc·springboot·web
撒呼呼7 小时前
# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)
数据库·spring boot·spring·mvc·springboot
天使day8 小时前
SpringMVC
java·spring·java-ee
壹佰大多9 小时前
【spring-cloud-gateway总结】
java·spring·gateway
CodeChampion9 小时前
60.基于SSM的个人网站的设计与实现(项目 + 论文)
java·vue.js·mysql·spring·elementui·node.js·mybatis