JavaEE项目总结(1)

一、在vue项目中安装axios

由于需要使用axios框架进行异步请求,所以需要在vue项目中安装axios框架。在官方下载速度较慢,所以选择更换镜像源(我使用的是华为云镜像)

在项目终端中输入npm config set registry http://mirrors.cloud.tencent.com/npm/

更换后通过在终端输入npm config get registry检查当前镜像源

二、前端向后端发送请求

用户在浏览器进行操作,而这些操作最开始到达的是前端。这时候需要前端向后端发出响应。

我们以一个vue项目的登录功能为例,当用户输入了账户密码并点击登录是,前端需要拿到账户密码的数据并向后端发送

html 复制代码
<!-- 一个.vue文件是一个组件,可以理解为一个页面,但是和页面不同 
  内容都写在一个template标签中,
  template标签必须有一个根标签
-->
<template>
	 <div class="login_container">
	     <!-- 登录盒子-->
	     <div class="login_box">
	          <!-- 头像盒子-->
	          <div class="img_box">
	                <img src="./assets/logo.png" />
	          </div>
			  
			<!-- 表单 -->
			<div style="margin-top: 100px; padding-right: 50px;">
				
				<el-form ref="form" label-width="80px">
					<!-- 第一栏 -->
					<el-form-item label="账号">
						 <el-input v-model="account"></el-input>
					</el-form-item>
					<!-- 第二栏 -->
					<el-form-item label="密码" show-password>
						 <el-input v-model="password"></el-input>
					</el-form-item>
					<!-- 按钮 -->
					 <el-form-item>
						<el-button type="primary"  @click="save()">登录</el-button>
						<el-button>取消</el-button>
					  </el-form-item>
					  
				</el-form>  
			</div>
				
		
	     </div>
	  </div>
</template>

<script>
/* 导出组件,并为组件定义数据,函数,生命周期函数 */
 export default{
	 data(){
		 return{
			 account:'admin',
			 password:'111'
		 }
	 },
	 methods:{
		 save(){
			 if(this.account.length==0){
				 this.$message({
				           message: '账号不得为空',
				           type: 'warning'
				         });
						 return;
			 }
			 if(this.password.length==0){
				 this.$message({
				           message: '密码不得为空',
				           type: 'warning'
				         });
						 return;		
			 }
			 // 向后端交互"/login","account="+this.account+"&password="+this.password
			 this.$http.post("login","account="+this.account+"&password="+this.password).then((resp)=>{
			 				/* 后端响应的结果 */
			 				
			 })
			 	
		 }
	 }
 }
</script>

<style>
  .login_container{
    height: 100vh;
    margin: 0px;
    padding: 0px;
	background-image: url(assets/bg.png);
  }

    .login_box{
      width: 450px;
      height: 350px;
      background-color: #fff;
      border-radius: 10px;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%,-50%);
	  opacity: 0.95;
    }

    .img_box{
       width: 130px;
       height: 130px;
       position: absolute;
       left: 50%;
       transform: translate(-50%,-50%);
       background-color: #fff;
       border-radius: 50%;
       padding: 5px;
       border: 1px solid #eee;
    }
    
    .img_box img{
         width: 100%;
         height: 100%;
         border-radius: 50%;
         background-color: #eee;
     }
</style>

三、后端请求的处理

后端处理主要分成三个步骤,简单来说就是"接化发"。

接是接收来自前端的请求,化是在后端进行处理,发是对前端进行响应。这三个步骤也是后端的基本作用

登录servlet

java 复制代码
package com.wbc.dorm.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wbc.dorm.dao.LoginDao;
import com.wbc.dorm.model.Admin;
import com.wbc.dorm.model.Result;
import com.wbc.dorm.util.JWTUtil;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/*登录处理servlet*/
@WebServlet(urlPatterns = "/login",name = "login", loadOnStartup = 1)
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        /*登录请求的账号密码*/
        String account = req.getParameter("account");
        String password = req.getParameter("password");
        //System.out.println(account);
        //System.out.println(password);
        Result result = null;
        PrintWriter printWriter = resp.getWriter();
        /*调用dao层查询账号密码是否正确*/

        try {
            LoginDao loginDao = new LoginDao();
            Admin admin = loginDao.login(account, password);
            //System.out.println(admin.toString());
            if (admin!=null){
                //将admin放入标准化响应模型
                result = new Result(200, "登陆成功", admin);
            }
            else {
                result = new Result(201, "账号或密码错误", null);

            }
        } catch (Exception e) {
            e.printStackTrace();
            result = new Result(500, "系统忙", null);
        }
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonString = objectMapper.writeValueAsString(result);
        printWriter.print(jsonString);
    }
}

管理员模型

java 复制代码
package com.wbc.dorm.model;

public class Admin {
    private int id;
    private String account;
    private String password;
   
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "Admin{" +
                "id=" + id +
                ", account='" + account + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

与数据库交互的dao

java 复制代码
package com.wbc.dorm.dao;

import com.wbc.dorm.model.Admin;
import com.wbc.dorm.model.Admin;

import java.sql.*;

public class LoginDao {
    public Admin login(String username, String password) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        String url ="jdbc:mysql://127.0.0.1:3306/dormdb?serverTimezone=Asia/Shanghai";//定义连接sql所需的url
        String users ="root";//用户名
        String passwords ="Wbc11280";//密码
        //建立连接
        Connection connection = DriverManager.getConnection(url,users,passwords);//建立连接
        //预编译
        PreparedStatement preparedStatement =connection.prepareStatement("select id,account from admin where account = ? and password = ?");
        try{
            //传入数据
            preparedStatement.setObject(1, username);
            preparedStatement.setObject(2, password);
            //查询操作
            ResultSet resultSet = preparedStatement.executeQuery();//将查询结构封装到ResultSet类型的对象中 需要将数据封装到指定类型的对象中
            Admin admin = null;
            while (resultSet.next()) {
                admin = new Admin();
                admin.setId(resultSet.getInt("id"));
                admin.setAccount(resultSet.getString("account"));
//                System.out.println(admin.toString());
            }
            return admin;
            //return null;
        }
        finally {
            if(preparedStatement != null){
                preparedStatement.close();
            }
            if(connection != null){
                connection.close();
            }
        }
    }
}

四、前端接收响应并做处理

当后端程序向前端响应后,前端需要接收响应并作出响应的处理

javascript 复制代码
// 向后端交互"/login","account="+this.account+"&password="+this.password
			 this.$http.post("login","account="+this.account+"&password="+this.password).then((resp)=>{
			 				/* 后端响应的结果 */
			 				if(resp.data.code==200){
								sessionStorage.setItem("account",resp.data.data.account);//存储到绘画空间,浏览器内部存储
					
			 					this.$router.push("/main");
			 				}
			 				else if(resp.data.code==201){
			 					this.$message({message:resp.data.message,type:'warning'})
			 				}
			 				else if(resp.data.code==500){
			 					this.$message({message:resp.data.message,type:'warning'})
			 				}
			 })

根据后端传来的标准化响应模型对象中的状态码,来做出对用户的响应,如登陆成功跳转到主页面,登陆失败根据状态码给出相应提示

说明:

sessionStorage.setItem(key, value)可以将浏览器中的数据存储到浏览器中的会话空间,只要窗口打开,数据就可以一直使用,直到界面关闭或者通过sessionStorage.clear()方法手动清楚。会话空间的数据可以通过sessionStorage.getItem(key)方法取出,从而在整个项目中共享数据。

五、路由导航守卫

当我们有了登录验证功能之后,肯定不希望用户通过路由导航(直接输入网址)的方式越过登录直接进入到主页面。如果在每次跳转之后都进行判断则相当的麻烦

html 复制代码
<template>
	<div>
		<el-container>
		  <el-header style="text-align: right; font-size: 12px">
			  <div class="header-title">后台管理系统</div>
						<el-dropdown>
						  <i class="el-icon-setting" style="margin-right: 15px"></i>
						  <el-dropdown-menu slot="dropdown" >
							<el-dropdown-item>主页</el-dropdown-item>
							<el-dropdown-item>修改密码</el-dropdown-item>
							<el-dropdown-item><span @click="logout()">安全退出</span></el-dropdown-item>
						  </el-dropdown-menu>
						</el-dropdown>
						<span>{{account}}</span>
		    </el-header>
			
		  <el-container>
		     <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
		        <el-menu :default-openeds="['1', '3']" router>
		          <el-submenu index="1">
					  <template slot="title"><i class="el-icon-message"></i>操作菜单</template>
		            <el-menu-item-group>
		              <el-menu-item index="/majorlist">专业管理</el-menu-item>
		              <el-menu-item index="/studentlist">学生管理</el-menu-item>
					  <el-menu-item index="1-3">学院管理</el-menu-item>
		            </el-menu-item-group>
		          </el-submenu>
		        </el-menu>
		      </el-aside>
		    <el-main>
				<router-view></router-view>
			</el-main>
		  </el-container>
		  
		</el-container>
	</div>
</template>

<script>
	export default{
		
		data(){
			return{
				account:"",
				
			}
		},
		methods:{
			logout(){
				this.$confirm('您确定要退出么?', '提示', {
				          confirmButtonText: '确定',
				          cancelButtonText: '取消',
				          type: 'warning'
				        }).then(() => {
				          this.$message({
				            type: 'success',
				            message: '退出成功成功!'
				          });
						  sessionStorage.clear();
						  this.$router.push("/login")
				        })
			}
		},
		mounted(){
			this.account=window.sessionStorage.getItem("account");//将account存入路由器内部
			console.log(this.account);
			if(account==null){
				 this.$router.push("/login")
			}
		}
	}
</script>

<style>
  .el-header {
    background-color: #00a7fa;
    color: #333;
    line-height: 60px;
  }

  .header-title{
     width: 300px;
     float: left;
     text-align: left;
     font-size: 20px;
     color: white;
  }
  
  .el-main{
	  background-color: aliceblue;
	  height: 100vh;
  }
</style>

需要在每一次跳转中都进行一次判断。

而我们所使用的axios框架为我们提出了更加方便的解决方法------路由导航守卫。我们可以通过在路由中配置路由导航守卫,在每一次跳转时自动执行代码,进行判断是否已经登录

在/router/index.js文件中定义路由组件代码下方加入如下代码

javascript 复制代码
//配置路由导航守卫
//每当进行一次组件路由时,就会触发导航守卫
rout.beforeEach((to,from,next)=>{
	if(to.path=='/login'){//如果用户访问的登录页,直接放行
		return next();//继续访问
	}	
	else{
	var account = window.sessionStorage.getItem("account");//获取路由器中存储的管理员信息
	if(account==null){//如果为空说明没有登录
		return next("/login");//跳转到登陆页面
	}
	else{//已经登陆
		next();//继续访问
	}
 }
})

如此我们就快要省去每次跳转判断的代码

html 复制代码
<template>
	<div>
		<el-container>
		  <el-header style="text-align: right; font-size: 12px">
			  <div class="header-title">后台管理系统</div>
						<el-dropdown>
						  <i class="el-icon-setting" style="margin-right: 15px"></i>
						  <el-dropdown-menu slot="dropdown" >
							<el-dropdown-item>主页</el-dropdown-item>
							<el-dropdown-item>修改密码</el-dropdown-item>
							<el-dropdown-item><span @click="logout()">安全退出</span></el-dropdown-item>
						  </el-dropdown-menu>
						</el-dropdown>
						<span>{{account}}</span>
		    </el-header>
			
		  <el-container>
		     <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
		        <el-menu :default-openeds="['1', '3']" router>
		          <el-submenu index="1">
					  <template slot="title"><i class="el-icon-message"></i>操作菜单</template>
		            <el-menu-item-group>
		              <el-menu-item index="/majorlist">专业管理</el-menu-item>
		              <el-menu-item index="/studentlist">学生管理</el-menu-item>
					  <el-menu-item index="1-3">学院管理</el-menu-item>
		            </el-menu-item-group>
		          </el-submenu>
		        </el-menu>
		      </el-aside>
		    <el-main>
				<router-view></router-view>
			</el-main>
		  </el-container>
		  
		</el-container>
	</div>
</template>

<script>
	export default{
		
		data(){
			return{
				account:"",
				
			}
		},
		methods:{
			logout(){
				this.$confirm('您确定要退出么?', '提示', {
				          confirmButtonText: '确定',
				          cancelButtonText: '取消',
				          type: 'warning'
				        }).then(() => {
				          this.$message({
				            type: 'success',
				            message: '退出成功成功!'
				          });
						  sessionStorage.clear();
						  this.$router.push("/login")
				        })
			}
		},
		mounted(){
			this.account=window.sessionStorage.getItem("account");//将account存入路由器内部
			console.log(this.account);
			/* if(account==null){
				 this.$router.push("/login")
			} */
		}
	}
</script>

<style>
  .el-header {
    background-color: #00a7fa;
    color: #333;
    line-height: 60px;
  }

  .header-title{
     width: 300px;
     float: left;
     text-align: left;
     font-size: 20px;
     color: white;
  }
  
  .el-main{
	  background-color: aliceblue;
	  height: 100vh;
  }
</style>

六、web前后端之间的会话跟踪

当我们在登陆成功后需要进行其他操作时,前端会像后端发送http请求。但http请求是无状态的,因此后端不知道是谁在进行操作。对会话进行跟踪 就是为了解决这样的问题。

会话跟踪是Web程序中常用的技术,用来跟踪用户的整个会话过程。 给客户端们颁发一个通行证,每人一个,无论谁访问都必须携带自己通行证。 这样服务器就能从通行证上确认客户身份了。

我们大致可以将会话跟踪描述为下图的过程

当我们在

具体实现

(1)在登陆成功后,在后端为用户生成一个token字符串

token令牌,可以理解为身份证号,是该用户唯一标识字符串,通过这个字符串进行前后端验证。

我们可以通过jwt组件为管理员生成token令牌

说明:

token令牌分为三部分:声明、载荷、签证

声明包含着生成类型和加密算法的基本信息,载荷包含着用户信息。二者通过base64转码生成,此过程可逆,不包含加密,因此不建议在载荷中加入用户关键信息。

签证结合前两部分以及密钥,加密生成,故密钥十分重要

我们通过maven添加依赖

XML 复制代码
<dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.2</version>
        </dependency>

并在服务器中注册(web.xml)

XML 复制代码
 <!--验证token过滤器-->
    <filter>
        <filter-name>tokenfilter</filter-name>
        <filter-class>com.wbc.dorm.filter.AdminTokenFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>tokenfilter</filter-name>
        <!--请求地址中有api地址的Servlet进入过滤器-->
        <url-pattern>/api/*</url-pattern>
    </filter-mapping>

由于登录界面不需要生成token,所以通过在其他配置信息的地址前添加/api来区分

将jwt的util稍作修改直接拿来用

java 复制代码
package com.wbc.dorm.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.wbc.dorm.model.Admin;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT工具类
 */
public class JWTUtil {

    /**
     * 根据用户id,账号生成token
     * @param admin
     * @return
     */
    public static String getToken(Admin admin) {
        String token = "";
        try {
            //过期时间 为1970.1.1 0:0:0 至 过期时间  当前的毫秒值 + 有效时间
            Date expireDate = new Date(new Date().getTime() + 10000*1000);
            //秘钥及加密算法
            Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
            //设置头部信息
            Map<String,Object> header = new HashMap<>();
            header.put("typ","JWT");
            header.put("alg","HS256");
            //携带id,账号信息,生成签名
            token = JWT.create()
                    .withHeader(header)//设置头部
                    .withClaim("id",admin.getId())//设置载荷
                    .withClaim("account",admin.getAccount())//设置载荷
                    .withExpiresAt(expireDate)//设置token有效时间
                    .sign(algorithm);//设置密钥
        }catch (Exception e){
            e.printStackTrace();
            return  null;
        }
        return token;
    }

    /**
     * 验证token是否有效
     * @param token
     * @return
     */
    public static boolean verify(String token){
        try {
            //验签
            Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {//当传过来的token如果有问题,抛出异常
            return false;
        }
    }

    /**
     * 获得token 中playload部分数据,按需使用
     * @param token
     * @return
     */
    public static DecodedJWT getTokenInfo(String token){
        return JWT.require(Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE")).build().verify(token);
    }

}

添加Admin内的成员变量adminToken,并增加相应的get、set方法。

java 复制代码
package com.wbc.dorm.model;

public class Admin {
    private int id;
    private String account;
    private String password;
    private String adminToken;

    public String getAdminToken() {
        return adminToken;
    }

    public void setAdminToken(String adminToken) {
        this.adminToken = adminToken;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "Admin{" +
                "id=" + id +
                ", account='" + account + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

并在登录servlet中添加相应代码以生成token

java 复制代码
package com.wbc.dorm.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wbc.dorm.dao.LoginDao;
import com.wbc.dorm.model.Admin;
import com.wbc.dorm.model.Result;
import com.wbc.dorm.util.JWTUtil;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/*登录处理servlet*/
@WebServlet(urlPatterns = "/login",name = "login", loadOnStartup = 1)
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        /*登录请求的账号密码*/
        String account = req.getParameter("account");
        String password = req.getParameter("password");
        //System.out.println(account);
        //System.out.println(password);
        Result result = null;
        PrintWriter printWriter = resp.getWriter();
        /*调用dao层查询账号密码是否正确*/

        try {
            LoginDao loginDao = new LoginDao();
            Admin admin = loginDao.login(account, password);
            //System.out.println(admin.toString());
            if (admin!=null){

                //登陆成功后,为admin生成一个token字符串
                //使用jwt组件为管理员生成公私密钥
                String adminToken = JWTUtil.getToken(admin);
                //将adminToken放入admin
                admin.setAdminToken(adminToken);
                //将admin放入标准化响应模型
                result = new Result(200, "登陆成功", admin);
            }
            else {
                result = new Result(201, "账号或密码错误", null);

            }
        } catch (Exception e) {
            e.printStackTrace();
            result = new Result(500, "系统忙", null);
        }
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonString = objectMapper.writeValueAsString(result);
        printWriter.print(jsonString);
    }
}

(2)在浏览器中存储token,每次发送请求时将身份码一同发出

在登录时,接收后端相应处,添加接收身份码的代码,并将其添加到会话空间

javascript 复制代码
sessionStorage.setItem("adminToken",resp.data.data.adminToken);

在我们操作发送请求时,如果在发送请求时,通过字符串拼接将身份码一同发于后端,实在过于麻烦,幸好axios为我们提供了请求拦截的功能

在main.js中添加如下代码,可以将身份码添加到请求头中,直接发送

javascript 复制代码
//axios 请求拦截
axios.interceptors.request.use(config =>{
	 //为请求头对象,添加Token验证的token字段
	config.headers.adminToken = window.sessionStorage.getItem('adminToken');
	 return config;
 });

(3)在后端创建一个过滤器,用于检测token的正确性,正确则退出过滤器继续响应,错误则直接向前端响应并给出状态码和提示信息

java 复制代码
package com.wbc.dorm.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wbc.dorm.model.Result;
import com.wbc.dorm.util.JWTUtil;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;

public class AdminTokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //父类向子类转换(强制类型转换)
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String adminToken = request.getHeader("adminToken");
        //验证token
        Boolean res= JWTUtil.verify(adminToken);
        System.out.println(res);
        //处理验证结果
        if(res){
            filterChain.doFilter(request, servletResponse);//离开过滤器继续向下执行
        }
        else{
            //向前端进行响应
            Result result = new Result(401,"token验证失败,请重新登录",null);
            PrintWriter printWriter = new PrintWriter(servletResponse.getWriter());
            printWriter.write(new ObjectMapper().writeValueAsString(result));
        }


    }
}

在前端中main.js中添加响应拦截器(类似于javaEE中的过滤器),用于拦截后端的身份码错误响应

javascript 复制代码
 // 添加响应拦截器
axios.interceptors.response.use((resp) =>{
	//正常响应拦截
	if(resp.data.code==500){
		ElementUI.Message({message:resp.data.message,type:"error"});
		return;
	 }
	 if(resp.data.code==401){
		 ElementUI.Message({message:resp.data.message,type:"error"});
		 sessionStorage.clear();
		router.replace("/login");
	 }
	 return resp;
 });
相关推荐
wearegogog1231 天前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
molaifeng1 天前
Go 语言如何实现高性能网络 I/O:Netpoller 模型揭秘
开发语言·网络·golang
韩师学子--小倪1 天前
fastjson与gson的toString差异
java·json
Drawing stars1 天前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
崇山峻岭之间1 天前
Matlab学习记录33
开发语言·学习·matlab
品克缤1 天前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
Evand J1 天前
【2026课题推荐】DOA定位——MUSIC算法进行多传感器协同目标定位。附MATLAB例程运行结果
开发语言·算法·matlab
super_lzb1 天前
【包教包会系列】springboot将依赖jar打到指定位置
maven·springboot·springboot打包·maven打包·项目依赖打包
nbsaas-boot1 天前
SQL Server 存储过程开发规范(公司内部模板)
java·服务器·数据库
小二·1 天前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus