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.pswp.cn/news/43716.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/43716.shtml
英文地址,請注明出處:http://en.pswp.cn/news/43716.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

18-組件化開發 根組件

組件化開發 & 根組件: 1. 組件化:一個頁面可以拆分成一個個組件&#xff0c;每個組件有著自己獨立的結構、樣式、行為. 好處:便于維護&#xff0c;利于復用->提升開發效率 組件分類: 普通組件 , 根組件 2. 根組件:整個應用最上層的組件&#xff0c;包裹所有普通小組件…

智能家居(4)---火災報警線程封裝

封裝火災報警線程實現智能家居中的火災報警功能 mainPro.c&#xff08;主函數&#xff09; #include <stdio.h> #include "controlDevice.h" #include "inputCommand.h"#include <pthread.h>struct Devices *pdeviceHead NULL; …

分布式理論

CAP和BASE CAP C一致性&#xff08;Consistency&#xff09; 在分布式環境下&#xff0c;一致性是指數據在多個副本之間能否保持一致性的特征。在一致性的需求下&#xff0c;當一個系統在數據一致的狀態下執行更新操作后&#xff0c;應該保證系統的數據仍然處于一致性的狀態…

Python-迭代

1、迭代器 迭代器是一個對象&#xff0c;它可以記錄遍歷的相關信息&#xff0c;迭代器對象從集合的第一個元素開始訪問&#xff0c;直到所有的元素被訪問完結束。迭代器有兩個基本的方法&#xff1a;iter() 和 next()。我們都過命令行工具&#xff0c;了解一下python的底層迭代…

常見指令以及權限理解

常見指令以及權限理解 命令格式&#xff1a; command [-options] parameter1 parameter1 命令 選項 參數1 參數2 1.command為命令名稱&#xff0c;例如變化目錄的cd等 2.中括號[ ]實際在命令中是不存在的&#xff0c;這個中括號代表可選&#xff0c;通常選項前面會添加一個符號…

Go和Java實現模板模式

Go和Java實現模板模式 下面通過一個游戲的例子來說明模板模式的使用。 1、模板模式 在模板模式中&#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">錨點…

Effective Java 案例分享(九)

46、使用無副作用的Stream 本章節主要舉例了Stream的幾種用法。 案例一&#xff1a; // Uses the streams API but not the paradigm--Dont do this! Map<String, Long> freq new HashMap<>(); try (Stream<String> words new Scanner(file).tokens()) …

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;就不會有重復 緩沖區的…

Trie樹(前綴樹)的實現與應用

Trie樹&#xff0c;也被稱為前綴樹&#xff0c;是一種用于處理字符串的數據結構。它可以高效地進行字符串的插入、刪除和搜索操作&#xff0c;并且能夠快速找到具有相同前綴的字符串。本篇博客將詳細介紹Trie樹的實現原理和應用場景&#xff0c;并給出Java代碼示例。 Trie樹的…

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 中的對應元素用于指定每條…

使用 MBean 和 日志查看 Tomcat 線程池核心屬性數據

文章目錄 CustomTomcatThreadPoolMBeanCustomTomcatThreadPool CustomTomcatThreadPoolMBean com.qww.config;public interface CustomTomcatThreadPoolMBean {String getStatus(); }CustomTomcatThreadPool package com.qww.config;import com.alibaba.fastjson.JSON; impor…

三本書與三場發布會,和鯨社區重新定義編程類書籍從閱讀到實踐新體驗

當 AI 開發者社區配備 AI 基礎設施開發平臺工具時&#xff0c;它還能做什么&#xff1f; 答案是&#xff1a;過去半年&#xff0c;和鯨社區憑借在氣象、醫學、社科等垂直領域的長期積累以及多方伙伴的支持&#xff0c;聯合舉辦了三場新書發布會——從 Python 到 R 語言 、從氣…

Midjourney Prompt 提示詞速查表 v5.2

Midjourney 最新的版本更新正不斷推出令人興奮的新功能。這雖然不斷擴展了我們的AI繪圖工具箱&#xff0c;但有時也會讓我們難以掌握所有實際可以使用的功能和參數。 針對此問題, 小編整理了 "Midjourney Prompt 提示詞速查表"&#xff0c;這是一個非常方便的 Midjo…