Java Web + Vue 前后端分离跨域解决方案

这是一个完整的Java Web后端与Vue前端跨域通信的实现方案。以下是完整的代码和配置说明:

1. 完整的ApiFilter实现

复制代码
package org.example;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class ApiFilter implements Filter {
    private ConcurrentHashMap<String, AtomicInteger> requestCounts;
    private static final int RATE_LIMIT = 100; // 每分钟最多100次请求
    private static final long TIME_WINDOW = 60000; // 1分钟时间窗口
    private static final int SC_TOO_MANY_REQUESTS = 429;
    
    // CORS配置
    private static final String ALLOWED_ORIGINS = "http://localhost:8082";
    private static final String ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS, PATCH";
    private static final String ALLOWED_HEADERS = "Authorization, Content-Type, X-Requested-With, Accept, Origin";
    private static final String EXPOSE_HEADERS = "Location, Content-Disposition, Authorization";
    private static final boolean ALLOW_CREDENTIALS = true;
    private static final long MAX_AGE = 3600; // 1小时
    
    // 不需要Token验证的白名单路径
    private static final String[] WHITELIST_PATHS = {
        "/api/login",
        "/api/register",
        "/api/auth/login",
        "/api/auth/register",
        "/api/public/"
    };

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        requestCounts = new ConcurrentHashMap<>();
        // 启动清理线程,定期清理过期计数
        Thread cleaner = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(TIME_WINDOW);
                    requestCounts.clear();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
        cleaner.setDaemon(true);
        cleaner.start();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 1. 添加CORS响应头(必须在任何响应之前)
        addCorsHeaders(httpRequest, httpResponse);

        // 2. 处理OPTIONS预检请求
        if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
            httpResponse.setStatus(HttpServletResponse.SC_OK);
            return; // 直接返回,不继续处理
        }

        // 3. 获取请求路径并检查是否为白名单
        String requestPath = httpRequest.getRequestURI();
        String contextPath = httpRequest.getContextPath();
        
        // 移除上下文路径
        if (contextPath != null && !"/".equals(contextPath) && requestPath.startsWith(contextPath)) {
            requestPath = requestPath.substring(contextPath.length());
        }
        
        // 检查是否为白名单路径
        boolean isWhitelisted = isWhitelistedPath(requestPath);
        
        // 4. Token验证(白名单路径跳过)
        if (!isWhitelisted) {
            String token = httpRequest.getHeader("Authorization");
            if (!isValidToken(token)) {
                // 返回JSON格式的错误信息
                sendJsonErrorResponse(httpResponse, HttpServletResponse.SC_UNAUTHORIZED, 
                    "未授权访问,请先登录");
                return;
            }
        }

        // 5. 记录日志
        logRequest(httpRequest);

        // 6. 限流检查
        if (isRateLimited(httpRequest)) {
            sendJsonErrorResponse(httpResponse, SC_TOO_MANY_REQUESTS, 
                "请求频率过高,请稍后再试");
            return;
        }

        // 放行请求
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // 释放资源
    }

    /**
     * 添加CORS响应头
     */
    private void addCorsHeaders(HttpServletRequest request, HttpServletResponse response) {
        String origin = request.getHeader("Origin");
        
        // 允许指定源的跨域请求
        if (origin != null && (origin.contains("localhost") || origin.contains("127.0.0.1"))) {
            response.setHeader("Access-Control-Allow-Origin", origin);
        } else {
            // 生产环境应该配置具体的域名
            response.setHeader("Access-Control-Allow-Origin", ALLOWED_ORIGINS);
        }
        
        response.setHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
        response.setHeader("Access-Control-Allow-Headers", ALLOWED_HEADERS);
        response.setHeader("Access-Control-Expose-Headers", EXPOSE_HEADERS);
        response.setHeader("Access-Control-Max-Age", String.valueOf(MAX_AGE));
        
        if (ALLOW_CREDENTIALS) {
            response.setHeader("Access-Control-Allow-Credentials", "true");
        }
        
        // 对于预检请求,添加额外的头
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setHeader("Access-Control-Allow-Headers", 
                "Authorization, Content-Type, X-Requested-With, Accept, Origin, " +
                "Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With");
        }
    }

    /**
     * 检查是否为白名单路径
     */
    private boolean isWhitelistedPath(String path) {
        for (String whitelistPath : WHITELIST_PATHS) {
            if (path.equals(whitelistPath) || path.startsWith(whitelistPath)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Token验证逻辑
     */
    private boolean isValidToken(String token) {
        if (token == null || token.trim().isEmpty()) {
            return false;
        }
        
        // 移除Bearer前缀(如果存在)
        if (token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        
        // 这里应该实现真正的Token验证逻辑
        // 例如:验证JWT Token的有效性、过期时间等
        // 暂时返回true,实际项目中需要根据业务逻辑实现
        return validateJwtToken(token);
    }
    
    /**
     * 验证JWT Token(示例)
     */
    private boolean validateJwtToken(String token) {
        // TODO: 实现JWT Token验证逻辑
        // 这里应该是实际的Token验证,例如:
        // 1. 解析JWT
        // 2. 验证签名
        // 3. 检查过期时间
        // 4. 验证用户信息
        // 暂时返回true
        return true;
    }

    /**
     * 发送JSON格式的错误响应
     */
    private void sendJsonErrorResponse(HttpServletResponse response, int statusCode, String message) 
            throws IOException {
        response.setStatus(statusCode);
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        
        String jsonResponse = String.format(
            "{\"success\": false, \"code\": %d, \"message\": \"%s\", \"timestamp\": %d}",
            statusCode, message, System.currentTimeMillis()
        );
        
        response.getWriter().write(jsonResponse);
        response.getWriter().flush();
    }

    private void logRequest(HttpServletRequest request) {
        String clientIP = getClientIP(request);
        String userAgent = request.getHeader("User-Agent");
        String method = request.getMethod();
        String uri = request.getRequestURI();
        String queryString = request.getQueryString();
        
        String logMessage = String.format(
            "API请求: %s %s%s | IP: %s | User-Agent: %s",
            method, 
            uri,
            queryString != null ? "?" + queryString : "",
            clientIP,
            userAgent != null ? userAgent : "Unknown"
        );
        
        System.out.println(logMessage);
    }

    private boolean isRateLimited(HttpServletRequest request) {
        String clientIP = getClientIP(request);
        AtomicInteger count = requestCounts.computeIfAbsent(clientIP, k -> new AtomicInteger(0));
        int currentCount = count.incrementAndGet();

        // 简单限流:每分钟最多RATE_LIMIT次请求
        boolean limited = currentCount > RATE_LIMIT;
        if (limited) {
            System.out.println("限流客户端: " + clientIP + ", 当前计数: " + currentCount);
        }
        return limited;
    }

    private String getClientIP(HttpServletRequest request) {
        String xfHeader = request.getHeader("X-Forwarded-For");
        if (xfHeader == null) {
            return request.getRemoteAddr();
        }
        // X-Forwarded-For可能包含多个IP,取第一个
        return xfHeader.split(",")[0].trim();
    }
}

2. Vue前端配置

2.1 Axios全局配置

复制代码
// src/utils/axios.js
import axios from 'axios';
import { Message } from 'element-ui'; // 如果你使用Element UI

// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API || 'http://localhost:8081',
  timeout: 10000, // 请求超时时间
  withCredentials: true, // 允许携带cookie
});

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 从本地存储获取token
    const token = localStorage.getItem('access_token') || 
                  sessionStorage.getItem('access_token');
    
    // 如果token存在,将其添加到请求头
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    
    // 设置Content-Type
    if (!config.headers['Content-Type']) {
      config.headers['Content-Type'] = 'application/json';
    }
    
    return config;
  },
  error => {
    console.error('请求错误:', error);
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data;
    
    // 如果返回的code不是200,则判断为错误
    if (res.code && res.code !== 200) {
      Message({
        message: res.message || '请求失败',
        type: 'error',
        duration: 5 * 1000
      });
      
      // 401: 未登录
      if (res.code === 401) {
        // 清除token并跳转到登录页
        localStorage.removeItem('access_token');
        sessionStorage.removeItem('access_token');
        window.location.href = '/login';
      }
      
      // 429: 请求频率过高
      if (res.code === 429) {
        Message({
          message: '操作过于频繁,请稍后再试',
          type: 'warning',
          duration: 3 * 1000
        });
      }
      
      return Promise.reject(new Error(res.message || 'Error'));
    } else {
      return res;
    }
  },
  error => {
    console.error('响应错误:', error);
    
    // 处理HTTP错误
    if (error.response) {
      switch (error.response.status) {
        case 401:
          Message({
            message: '登录已过期,请重新登录',
            type: 'error',
            duration: 5 * 1000
          });
          localStorage.removeItem('access_token');
          sessionStorage.removeItem('access_token');
          window.location.href = '/login';
          break;
        case 403:
          Message({
            message: '没有权限访问',
            type: 'error',
            duration: 5 * 1000
          });
          break;
        case 404:
          Message({
            message: '请求的资源不存在',
            type: 'error',
            duration: 5 * 1000
          });
          break;
        case 429:
          Message({
            message: '请求频率过高,请稍后再试',
            type: 'warning',
            duration: 5 * 1000
          });
          break;
        case 500:
          Message({
            message: '服务器内部错误',
            type: 'error',
            duration: 5 * 1000
          });
          break;
        default:
          Message({
            message: error.response.data?.message || '请求失败',
            type: 'error',
            duration: 5 * 1000
          });
      }
    } else if (error.request) {
      Message({
        message: '网络连接失败,请检查网络设置',
        type: 'error',
        duration: 5 * 1000
      });
    } else {
      Message({
        message: error.message || '请求失败',
        type: 'error',
        duration: 5 * 1000
      });
    }
    
    return Promise.reject(error);
  }
);

export default service;

2.2 Vue环境配置

复制代码
// vue.config.js
module.exports = {
  devServer: {
    port: 8082, // 前端开发服务器端口
    proxy: {
      '/api': {
        target: 'http://localhost:8081', // 后端地址
        changeOrigin: true, // 允许跨域
        ws: true, // 代理websockets
        pathRewrite: {
          '^/api': '' // 重写路径
        }
      }
    }
  },
  // 其他配置...
};

2.3 Vue登录组件示例

复制代码
<template>
  <div class="login-container">
    <el-form 
      ref="loginForm" 
      :model="loginForm" 
      :rules="loginRules" 
      class="login-form"
      label-position="left"
    >
      <h3 class="title">系统登录</h3>
      
      <el-form-item prop="username">
        <el-input
          v-model="loginForm.username"
          type="text"
          auto-complete="off"
          placeholder="请输入用户名"
          prefix-icon="el-icon-user"
        />
      </el-form-item>
      
      <el-form-item prop="password">
        <el-input
          v-model="loginForm.password"
          type="password"
          auto-complete="off"
          placeholder="请输入密码"
          prefix-icon="el-icon-lock"
          @keyup.enter.native="handleLogin"
        />
      </el-form-item>
      
      <el-form-item>
        <el-checkbox v-model="rememberMe">记住密码</el-checkbox>
      </el-form-item>
      
      <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          type="primary"
          style="width:100%;"
          @click.native.prevent="handleLogin"
        >
          登录
        </el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import { login } from '@/api/user';
import { setToken } from '@/utils/auth';

export default {
  name: 'Login',
  data() {
    return {
      loginForm: {
        username: '',
        password: ''
      },
      loginRules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' }
        ]
      },
      rememberMe: false,
      loading: false
    };
  },
  mounted() {
    // 从本地存储获取记住的密码
    const rememberedUsername = localStorage.getItem('remembered_username');
    if (rememberedUsername) {
      this.loginForm.username = rememberedUsername;
      this.rememberMe = true;
    }
  },
  methods: {
    handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true;
          
          // 调用登录接口
          login({
            username: this.loginForm.username,
            password: this.loginForm.password
          })
            .then(response => {
              // 登录成功
              const token = response.data.token;
              
              // 保存token
              if (this.rememberMe) {
                // 长期存储
                localStorage.setItem('access_token', token);
                localStorage.setItem('remembered_username', this.loginForm.username);
              } else {
                // 会话存储
                sessionStorage.setItem('access_token', token);
                localStorage.removeItem('remembered_username');
              }
              
              // 设置axios默认header
              this.$axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
              
              // 跳转到首页
              this.$router.push('/dashboard');
            })
            .catch(error => {
              console.error('登录失败:', error);
              this.$message.error(error.message || '登录失败');
            })
            .finally(() => {
              this.loading = false;
            });
        } else {
          console.log('表单验证失败');
          return false;
        }
      });
    }
  }
};
</script>

<style scoped>
.login-container {
  height: 100vh;
  background-color: #2d3a4b;
  display: flex;
  justify-content: center;
  align-items: center;
}

.login-form {
  width: 400px;
  padding: 35px;
  background: #fff;
  border-radius: 6px;
  box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}

.title {
  margin: 0 auto 40px;
  text-align: center;
  color: #707070;
  font-size: 26px;
  font-weight: 700;
}
</style>

2.4 API接口封装

复制代码
// src/api/user.js
import request from '@/utils/axios';

export function login(data) {
  return request({
    url: '/api/login',
    method: 'post',
    data
  });
}

export function logout() {
  return request({
    url: '/api/logout',
    method: 'post'
  });
}

export function getUserInfo() {
  return request({
    url: '/api/user/info',
    method: 'get'
  });
}

export function register(data) {
  return request({
    url: '/api/register',
    method: 'post',
    data
  });
}

3. 后端登录API实现(JSON版本)

复制代码
package org.example;

import com.google.gson.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.stream.Collectors;

@WebServlet("/api/login")
public class LoginApiServlet extends HttpServlet {
    private UserDAO userDAO;
    private Gson gson = new Gson();
    
    @Override
    public void init() {
        userDAO = new UserDAO();
    }
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        // 设置响应头
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        
        // 设置CORS头
        response.setHeader("Access-Control-Allow-Origin", "http://localhost:8082");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        
        PrintWriter out = response.getWriter();
        JsonObject jsonResponse = new JsonObject();
        
        try {
            // 读取请求体中的JSON数据
            String requestBody = request.getReader().lines()
                .collect(Collectors.joining(System.lineSeparator()));
            
            // 解析JSON
            JsonObject jsonObject = gson.fromJson(requestBody, JsonObject.class);
            
            if (jsonObject == null) {
                jsonResponse.addProperty("success", false);
                jsonResponse.addProperty("message", "请求格式错误");
                jsonResponse.addProperty("code", 400);
                response.setStatus(400);
                out.print(gson.toJson(jsonResponse));
                return;
            }
            
            // 获取用户名和密码
            String username = jsonObject.has("username") ? 
                jsonObject.get("username").getAsString() : null;
            String password = jsonObject.has("password") ? 
                jsonObject.get("password").getAsString() : null;
            
            // 验证参数
            if (username == null || username.trim().isEmpty() ||
                    password == null || password.trim().isEmpty()) {
                jsonResponse.addProperty("success", false);
                jsonResponse.addProperty("message", "用户名和密码不能为空");
                jsonResponse.addProperty("code", 400);
                response.setStatus(400);
                out.print(gson.toJson(jsonResponse));
                return;
            }
            
            // 创建用户对象进行验证
            User user = new User();
            user.setUsername(username);
            user.setPassword(password);
            
            // 验证用户凭据
            if (userDAO.validate(user)) {
                // 生成Token(实际应该使用JWT)
                String token = generateToken(username);
                
                // 获取用户信息
                User fullUser = userDAO.selectUserByUsername(username);
                
                // 构建响应数据
                jsonResponse.addProperty("success", true);
                jsonResponse.addProperty("message", "登录成功");
                jsonResponse.addProperty("code", 200);
                jsonResponse.addProperty("token", token);
                
                JsonObject userObj = new JsonObject();
                userObj.addProperty("id", fullUser.getId());
                userObj.addProperty("username", fullUser.getUsername());
                userObj.addProperty("email", fullUser.getEmail());
                
                jsonResponse.add("data", userObj);
                
                response.setStatus(200);
                out.print(gson.toJson(jsonResponse));
            } else {
                jsonResponse.addProperty("success", false);
                jsonResponse.addProperty("message", "用户名或密码错误");
                jsonResponse.addProperty("code", 401);
                response.setStatus(401);
                out.print(gson.toJson(jsonResponse));
            }
            
        } catch (JsonSyntaxException e) {
            jsonResponse.addProperty("success", false);
            jsonResponse.addProperty("message", "JSON格式错误");
            jsonResponse.addProperty("code", 400);
            response.setStatus(400);
            out.print(gson.toJson(jsonResponse));
        } catch (Exception e) {
            e.printStackTrace();
            jsonResponse.addProperty("success", false);
            jsonResponse.addProperty("message", "服务器内部错误");
            jsonResponse.addProperty("code", 500);
            response.setStatus(500);
            out.print(gson.toJson(jsonResponse));
        } finally {
            out.flush();
            out.close();
        }
    }
    
    @Override
    protected void doOptions(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 处理OPTIONS预检请求
        response.setHeader("Access-Control-Allow-Origin", "http://localhost:8082");
        response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setStatus(HttpServletResponse.SC_OK);
    }
    
    private String generateToken(String username) {
        // 实际应该生成JWT token
        // 这里只是示例,应该使用更安全的方式
        return "jwt_token_" + username + "_" + System.currentTimeMillis();
    }
}

4. 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>CharacterEncodingFilter</filter-name>
        <filter-class>org.example.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <!-- API过滤器(处理CORS、限流、Token验证) -->
    <filter>
        <filter-name>ApiFilter</filter-name>
        <filter-class>org.example.ApiFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ApiFilter</filter-name>
        <url-pattern>/api/*</url-pattern>
    </filter-mapping>
    
    <!-- API Servlet -->
    <servlet>
        <servlet-name>LoginApiServlet</servlet-name>
        <servlet-class>org.example.LoginApiServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>LoginApiServlet</servlet-name>
        <url-pattern>/api/login</url-pattern>
    </servlet-mapping>
    
    <!-- 其他Servlet配置... -->
    
    <!-- 欢迎页面 -->
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
    
</web-app>

5. 字符编码过滤器

复制代码
package org.example;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class CharacterEncodingFilter implements Filter {
    
    private String encoding = "UTF-8";
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String encodingParam = filterConfig.getInitParameter("encoding");
        if (encodingParam != null) {
            encoding = encodingParam;
        }
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        request.setCharacterEncoding(encoding);
        response.setCharacterEncoding(encoding);
        response.setContentType("text/html;charset=" + encoding);
        chain.doFilter(request, response);
    }
    
    @Override
    public void destroy() {
        // 清理资源
    }
}

6. 关键问题解决

6.1 跨域问题

  1. 预检请求(OPTIONS)处理:过滤器需要正确处理OPTIONS请求

  2. CORS头设置:必须在响应中正确设置CORS头

  3. 凭证(Credentials) :如果前端需要发送Cookie,必须设置Access-Control-Allow-Credentials: true

6.2 认证流程

  1. 登录接口:不需要Token验证,返回Token

  2. 其他接口:需要携带Token验证

  3. Token存储:前端将Token存储在localStorage或sessionStorage中

6.3 限流策略

  1. 基于IP限流:防止恶意请求

  2. 白名单例外:登录等接口不限流

  3. 错误响应:返回429状态码和友好提示

7. 测试步骤

  1. 启动后端:在8081端口启动Java Web应用

  2. 启动前端:在8082端口启动Vue应用

  3. 登录测试 :前端调用/api/login接口

  4. Token验证:登录成功后,后续请求自动携带Token

  5. 跨域测试:确保前端可以正常访问后端API

这个方案实现了完整的Java Web + Vue前后端分离跨域通信,包含了安全认证、限流、错误处理等功能。

相关推荐
艺杯羹2 小时前
Thymeleaf模板引擎:让Spring Boot页面开发更简单高效
java·spring boot·后端·thymeleadf
小尧嵌入式2 小时前
Linux进程线程与进程间通信
linux·运维·服务器·c语言·开发语言·数据结构·microsoft
烂不烂问厨房2 小时前
前端自适应布局之等比例缩放
开发语言·前端·javascript
小鸡吃米…2 小时前
Python - 发送电子邮件
开发语言·python
SmoothSailingT2 小时前
C/C++——结构体(Struct)
开发语言·c++·结构体·struct
大佬,救命!!!2 小时前
python对应sql操作
开发语言·python·sql·学习笔记·学习方法
shoubepatien2 小时前
JavaWeb_Maven
java·maven
逸风尊者2 小时前
开发可掌握的知识:推荐系统
java·后端·算法
IT方大同2 小时前
C语言选择控制结构
c语言·开发语言