SpringBoot统⼀功能处理

前言🍭

❤️❤️❤️SSM专栏更新中,各位大佬觉得写得不错,支持一下,感谢了!❤️❤️❤️

Spring + Spring MVC + MyBatis_冷兮雪的博客-CSDN博客

本章是讲Spring Boot 统⼀功能处理模块,也是 AOP 的实战环节,要实现的目标有以下 3 个:

  1. 使用拦截器实现用户登录权限的统一验证;
  2. 统⼀数据格式返回;
  3. 统⼀异常处理。

一、用户登录权限效验🍭

1、最初用户登录验证🍉

用户登录权限的发展从之前每个方法中自己验证用户登录权限,到现在统⼀的用户登录验证处理,它是⼀个逐渐完善和逐渐优化的过程。

@RestController
@RequestMapping("/user")
public class UserController {/*** 某⽅法 1*/@RequestMapping("/m1")public Object method(HttpServletRequest request) {// 有 session 就获取,没有不会创建HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userinfo") != null) {// 说明已经登录,业务处理return true;} else {// 未登录return false;}}/*** 某⽅法 2*/@RequestMapping("/m2")public Object method2(HttpServletRequest request) {// 有 session 就获取,没有不会创建HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userinfo") != null) {// 说明已经登录,业务处理return true;} else {// 未登录return false;}}// 其他⽅法。。。
}

从上述代码可以看出,每个方法中都有相同的用户登录验证权限,它的缺点是:

  1. 每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也⼀样要传参调用和在方法中进行判断。
  2. 添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成本和维护成本。
  3. 这些用户登录验证的方法和接下来要实现的业务几何没有任何关联,但每个方法中都要写⼀遍。 所以提供⼀个公共的 AOP 方法来进行统⼀的用户登录权限验证迫在眉睫。

2、Spring AOP 用户统⼀登录验证的问题🍉

说到统⼀的用户登录验证,我们想到的第⼀个实现方案是 Spring AOP 前置通知或环绕通知来实现,具体实现代码如下:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Component
public class UserAspect {// 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法@Pointcut("execution(* com.example.demo.controller..*.*(..))")public void pointcut() {}// 前置⽅法@Before("pointcut()")public void doBefore() {}// 环绕⽅法@Around("pointcut()")public Object doAround(ProceedingJoinPoint joinPoint) {Object obj = null;System.out.println("Around ⽅法开始执⾏");try {// 执⾏拦截⽅法obj = joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}System.out.println("Around 方法结束执行");return obj;}
}

如果要在以上 Spring AOP 的切面中实现用户登录权限效验的功能,有以下两个问题:

  1. 定义拦截的规则(表达式)非常难。(我们要对⼀部分方法进行拦截,而另⼀部分方法不拦截,如注册方法和登录方法是不拦截的,这样 的话排除方法的规则很难定义,甚至没办法定义)。
  2. 在切面类中拿到 HttpSession 比较难

那这样如何解决呢?

Spring 拦截器🍉

对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:

  1. 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理方法。
  2. 将自定义拦截器加入WebMvcConfigurer 的 addInterceptors 方法中。

具体实现如下:

目录结构:

Ⅰ、实现拦截器🍓

关键步骤:
a.实现 HandlerInterceptor 接口
b.重写 preHeadler 方法,在方法中编写自己的业务代码

package com.example.demo.config;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;public class LoginInterceptor implements HandlerInterceptor {/*** 此方法返回一个 boolean,如果为 true 表示验证成功,可以继续执行后续流程如果是 false 表示验证失败,后面的流程不能执行了* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//用户登录业务判断HttpSession session=request.getSession(false);if (session != null && session.getAttribute("userinfo") != null) {// 说明用户已经登录return true;}// 可以调整到登录页面 or 返回一个 401/403 没有权限码response.sendRedirect("/login.html");
//        response.setStatus(403);return false;}
}

Ⅱ、将拦截器添加到配置文件中,并且设置拦截的规则🍓

package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class AppConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") // 拦截所有请求.excludePathPatterns("/user/login") // 排除的url地址(不拦截的url地址).excludePathPatterns("/user/reg");}
}

其中:

  • addPathPatterns:表示需要拦截的 URL,**表示拦截任意方法(也就是所有方法)。
  • excludePathPatterns:表示需要排除的 URL。

说明:以上拦截规则可以拦截此项目中的使用 URL,包括静态文件(图片文件、JS 和 CSS 等文件)。

排除所有的静态资源:

    // 拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") // 拦截所有接⼝.excludePathPatterns("/**/*.js").excludePathPatterns("/**/*.css").excludePathPatterns("/**/*.jpg").excludePathPatterns("/login.html").excludePathPatterns("/**/login"); // 排除接⼝}

UserController:

package com.example.demo.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
public class UserController {/*    @GetMappingpublic String getMethod(){return "执行GET请求!";}@PostMappingpublic String postMethod(){return "执行POST请求!";}*/@RequestMapping("/getuser")public String getUser() {System.out.println("执行了 get User~");return "get user";}@RequestMapping("/login")public String login() {System.out.println("执行了 login~");return "login~";}@RequestMapping("/reg")public String reg() {System.out.println("执行了 reg~");return "reg~";}
}

Ⅲ、启动项目:🍓

不拦截:

 拦截:http://localhost:8080/user/getuser

 为什么会显示重定向次数过多?

这是因为这个login.html页面也被拦截了,所以它去访问时候就会一直重定向重定向去访问。

解决方法:

Ⅳ、拦截器实现原理🍓

正常情况下的调用顺序:

然而有了拦截器之后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图所示:

 所有的 Controller 执行都会通过⼀个调度器 DispatcherServlet 来实现,这⼀点可以从 Spring Boot 控制台的打印信息看出,如下图所示:

而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,doDispatch 源码如下: 

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = this.checkMultipart(request);multipartRequestParsed = processedRequest != request;mappedHandler = this.getHandler(processedRequest);if (mappedHandler == null) {this.noHandlerFound(processedRequest, response);return;}HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());String method = request.getMethod();boolean isGet = HttpMethod.GET.matches(method);if (isGet || HttpMethod.HEAD.matches(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {return;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}this.applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);} catch (Exception var20) {dispatchException = var20;} catch (Throwable var21) {dispatchException = new NestedServletException("Handler dispatch failed", var21);}this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);} catch (Exception var22) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);} catch (Throwable var23) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));}} finally {if (asyncManager.isConcurrentHandlingStarted()) {if (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}} else if (multipartRequestParsed) {this.cleanupMultipart(processedRequest);}}}

从上述源码可以看出在开始执行 Controller 之前,会先调用 预处理方法 applyPreHandle,而applyPreHandle 方法的实现源码如下:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 获取项⽬中使⽤的拦截器 HandlerInterceptorHandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);if (!interceptor.preHandle(request, response, this.handler)) {this.triggerAfterCompletion(request, response, (Exception)null);return false;}}return true;}

从上述源码可以看出,在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor 并执行拦截器中的 preHandle 方法,这样就和咱们前面定义的拦截器对应上了,如下图所示:

此时用户登录权限的验证方法就会执行,这就是拦截器的实现原理。  

二、统⼀异常处理🍭

Ⅰ、实现🍓

统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知, 也就是执行某个方法事件,具体实现代码如下:

package com.example.demo.config;import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;import java.util.HashMap;@ControllerAdvice
@ResponseBody
public class MyExHandler {/*** 拦截所有的空指针异常,进行统一的数据返回*/@ExceptionHandler(NullPointerException.class)public HashMap<String, Object> nullException(NullPointerException e) {HashMap<String, Object> result = new HashMap<>();result.put("code", "-1");result.put("msg", "空指针异常:" + e.getMessage()); // 错误码的描述信息result.put("data", null);return result;}@ExceptionHandler(Exception.class)//保底的默认异常public HashMap<String, Object> exception(Exception e) {HashMap<String, Object> result = new HashMap<>();result.put("code", "-1");result.put("msg", "异常:" + e.getMessage()); // 错误码的描述信息result.put("data", null);return result;}}

Ⅱ、添加异常🍓

UserController

 @RequestMapping("/login")public String login() {Object obj = null;obj.hashCode();System.out.println("执行了 login~");return "login~";}@RequestMapping("/reg")public String reg() {int num = 10 / 0;System.out.println("执行了 reg~");return "reg~";}

Ⅲ、启动程序:🍓

login:

 reg:

 方法名和返回值可以自定义,其中最重要的是 @ExceptionHandler(Exception.class) 注解。 

以上方法表示,如果出现了异常就返回给前端⼀个 HashMap 的对象,其中包含的字段如代码中定义的那样。 我们可以针对不同的异常,返回不同的结果。 

三、统一数据返回格式🍭

1.创建一个类,并添加 @ControllerAdvice
2.实现ResponseBodyAdvice接口,并重写supports和beforeBodywrite (统一对象就是此方法中实现的)

1、为什么需要统一数据返回格式?🍉

统一数据返回格式的优点有很多,比如以下几个:

  1. 便前端程序员更好的接收和解析后端数据接口返回的数据。
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就行了,因为所有接口都是这样返回的。
  3. 有利于项目统⼀数据的维护和修改。
  4. 有利于后端技术部门的统⼀规范的标准制定,不会出现稀奇古怪的返回内容。

2、统一数据返回格式的实现🍉

Ⅰ、实现🍓

统⼀的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式实现。

package com.example.demo.config;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
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.ResponseBodyAdvice;import java.util.HashMap;@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;/*** 此方法返回 true 则执行下面 beforeBodyWrite 方法* 反之则不执行*/@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {HashMap<String, Object> result = new HashMap<>();result.put("code", 200);result.put("msg", "");result.put("data", body);// 需要特殊处理,因为 String 在转换的时候会报错if (body instanceof String) {try {return objectMapper.writeValueAsString(result);} catch (JsonProcessingException e) {e.printStackTrace();}}return result;}
}

如果没有对Sting进行特殊处理,就会有异常出现: 

Ⅱ、添加实现类🍓

UserController

@RequestMapping("/getnum")public Integer getNumber() {return new Random().nextInt(10);}@RequestMapping("/getuser")public String getUser() {System.out.println("执行了 get User~");return "get user";}

Ⅲ、启动程序🍓

Ⅳ、总结🍓

上面只是以一种简单的方式来向大家介绍如何去统一数据返回格式,在实际工作中并不会这样去使用

而是将统⼀的返回格式(包含:status、data、msg 字段)进行封装成一个类:

根据不同操作成功和操作失败的情况,重写了不同的方法:

package com.example.demo.common;import lombok.Data;import java.io.Serializable;/*** 统一数据格式返回*/
@Data
public class AjaxResult implements Serializable {//支持序列化// 状态码private Integer code;// 状态码描述信息private String msg;// 返回的数据private Object data;/*** 操作成功返回的结果,需要 data*/public static AjaxResult success(Object data) {AjaxResult result = new AjaxResult();result.setCode(200);result.setMsg("");result.setData(data);return result;}//重载successpublic static AjaxResult success(int code, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg("");result.setData(data);return result;}public static AjaxResult success(int code, String msg, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}/*** 返回失败结果*/public static AjaxResult fail(int code, String msg) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(null);return result;}public static AjaxResult fail(int code, String msg, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}
}

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/43716.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

18-组件化开发 根组件

组件化开发 & 根组件: 1. 组件化:一个页面可以拆分成一个个组件&#xff0c;每个组件有着自己独立的结构、样式、行为. 好处:便于维护&#xff0c;利于复用->提升开发效率 组件分类: 普通组件 , 根组件 2. 根组件:整个应用最上层的组件&#xff0c;包裹所有普通小组件…

常见指令以及权限理解

常见指令以及权限理解 命令格式&#xff1a; command [-options] parameter1 parameter1 命令 选项 参数1 参数2 1.command为命令名称&#xff0c;例如变化目录的cd等 2.中括号[ ]实际在命令中是不存在的&#xff0c;这个中括号代表可选&#xff0c;通常选项前面会添加一个符号…

react-vite-antd环境下新建项目

vite 创建一个react项目 1. 安装vite并创建一个react项目1. 我使用的 yarn安装&#xff0c;基本配置项目名字, 框架react &#xff0c;js2. cd vite-react进入项目目录安装node包并启动项目 2. 安装引入Ant Design引入依赖&#xff08;我用的yarn&#xff0c;没有安装的也可以使…

视频汇聚/视频云存储/视频监控管理平台EasyCVR添加萤石云设备详细操作来啦!

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

css伪元素实现li列表圆点相连+锚点跳转悬浮窗实现

实现效果&#xff1a; html代码&#xff1a; <div class"sidenav"><ul class"nav-text progressbar"><!-- data-target的值对应要跳转的模块的id --><li data-target"module1"><div class"text">锚点…

Java创建对象的几种方式

在Java中&#xff0c;对象是程序中的一种基本元素&#xff0c;它通过类定义和创建。本篇教程旨在介绍Java中创建对象的几种方式&#xff0c;包括使用new关键字、反射、clone、反序列化等方式。 使用new关键字创建对象 在Java中&#xff0c;最常用的创建对象方式是使用new关键…

linux学习(文件描述符)[13]

所以fork的时候函数执行完毕&#xff0c;但是数据还在缓冲区中未刷新。 所以会有父子两份数据 在fork&#xff08;&#xff09;之前ffush&#xff08;&#xff09;&#xff08;c语言的接口&#xff0c;刷新缓冲区&#xff09;fflush(stdout)&#xff0c;就不会有重复 缓冲区的…

MyBatis的入门级环境搭建及增删改查,详细易懂

目录 一.mybatis的简介 二.MyBatis的环境搭建 2.1 导入pom依赖 2.2 数据库文件导入连接 2.3 修改web.xml文件 2.4 安装插件 2.5 配置文件 2.5.1 mybatis.cfg.xml文件 2.5.2 generatorConfig.xml文件 2.6 最后测试生成代码 三.MyBatis的增删改查 3.1 写service类&#xff…

Linux命令200例:nc非常有用的网络工具(常用)

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌。CSDN专家博主&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &…

【LVS】3、LVS+Keepalived群集

为什么用它&#xff0c;为了做高可用 服务功能 1.故障自动切换 2.健康检查 3.节点服务器高可用-HA Keepalived的三个模块&#xff1a; core&#xff1a;Keepalived的核心&#xff0c;负责主进程的启动、维护&#xff1b;调用全局配置文件进行加载和解析 vrrp&#xff1a;实…

matlab使用教程(16)—图论中图的定义与修改

1.修改现有图的节点和边 此示例演示如何使用 addedge 、 rmedge 、 addnode 、 rmnode 、 findedge 、 findnode 及 subgraph 函数访问和修改 graph 或 digraph 对象中的节点和/或边。 1.1 添加节点 创建一个包含四个节点和四条边的图。s 和 t 中的对应元素用于指定每条…

三本书与三场发布会,和鲸社区重新定义编程类书籍从阅读到实践新体验

当 AI 开发者社区配备 AI 基础设施开发平台工具时&#xff0c;它还能做什么&#xff1f; 答案是&#xff1a;过去半年&#xff0c;和鲸社区凭借在气象、医学、社科等垂直领域的长期积累以及多方伙伴的支持&#xff0c;联合举办了三场新书发布会——从 Python 到 R 语言 、从气…

Midjourney Prompt 提示词速查表 v5.2

Midjourney 最新的版本更新正不断推出令人兴奋的新功能。这虽然不断扩展了我们的AI绘图工具箱&#xff0c;但有时也会让我们难以掌握所有实际可以使用的功能和参数。 针对此问题, 小编整理了 "Midjourney Prompt 提示词速查表"&#xff0c;这是一个非常方便的 Midjo…

Java“牵手“拼多多商品详情页面数据获取方法,拼多多API实现批量商品数据抓取示例

拼多多商城是一个网上购物平台&#xff0c;售卖各类商品&#xff0c;包括服装、鞋类、家居用品、美妆产品、电子产品等。要获取拼多多商品详情数据&#xff0c;您可以通过开放平台的接口或者直接访问拼多多商城的网页来获取商品详情信息。以下是两种常用方法的介绍&#xff1a;…

Linux:shell脚本数组和脚本免交互

目录 一、shell数组的定义 二、定义数组的方式 &#xff08;1&#xff09;数组名(value1 value2 value3 value4 ...) &#xff08;2&#xff09;获取数组的长度 &#xff08;3&#xff09;获取数组下标对应的值 &#xff08;4&#xff09;数组的遍历 &#xff08;5&#x…

qsort函数详解

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解qsort函数&#xff0c;如果你觉得我写的不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 文章目录 一. qsort函数参数详解1.数组首元素地址base2.数组的元素个数num和元素所占内存空间大小w…

QT设置widget背景图片

首先说方法&#xff0c;在给widget或者frame或者其他任何类型的控件添加背景图时&#xff0c;在样式表中加入如下代码&#xff0c;指定某个控件&#xff0c;设置其背景。 类名 # 控件名 { 填充方式&#xff1a;图片路径 } 例如&#xff1a; QWidget#Widget {border-image: url…

无涯教程-TensorFlow - 优化器

Optimizers是扩展类&#xff0c;其中包括用于训练特定模型的附加信息&#xff0c;Optimizers类使用给定的参数初始化&#xff0c;用于提高速度和性能&#xff0c;以训练特定模型。 TensorFlow的基本Optimizers是- tf.train.Optimizer 此类在tensorflow/python/training/opti…

C语言:深度学习知识储备

目录 数据类型 每种类型的大小是多少呢&#xff1f; 变量 变量的命名&#xff1a; 变量的分类&#xff1a; 变量的作用域和生命周期 作用域&#xff1a; 生命周期&#xff1a; 常量 字符串转义字符注释 字符串&#xff1a; 转义字符 操作符&#xff1a; 算术操作符…

图神经网络 day2 图的分类

图神经网络基础算法 1 GCN2 GraphSAGE2.1 采样&#xff1a;采样固定长度的邻居2.2 聚合2.3 GraphSAGE_minibatch2.4 GraphSAGE_embedding 3 GAT4. 图网络的分类4.1 递归图神经网络 RGNN4.2 图卷积神经网络GCN4.3 图注意力网络 GAT4.4 图自动编码 GAE4.5 图时空网络 GSTN4.6 图生…