[Spring Boot] インターセプタと統合関数処理: 統合ログイン検証、統合例外処理、統合データ戻り形式

序文

 Spring AOP はアスペクト指向プログラミングに基づくフレームワークであり、横断的な懸念事項 (ロギング、トランザクション管理など) をビジネス ロジックから分離し、メソッドの実行前後にプロキシ オブジェクトを通じてこれらの懸念事項をターゲット オブジェクトに織り込み、スローするために使用されます。例外発生時や結果返却時に特定の箇所で実行することで、プログラムの再利用性、保守性、柔軟性が向上します。ただし、ネイティブ Spring AOP を使用して統合インターセプトを実装するのは非常に面倒で困難です。このセクションでは、AOP の実戦でもある統一関数処理の簡単な方法を次のように使用します。

  • 統合ユーザーのログイン権限の検証
  • 統一されたデータ形式のリターン
  • 統合された例外処理


0 なぜ統一関数処理が必要なのでしょうか?

統合関数処理は、コードの保守性、再利用性、拡張性を向上させるための設計思想です。アプリケーションには、認証、ロギング、例外処理などの一般的な機能要件がいくつかある場合があります。これらの機能は複数の場所で呼び出して処理する必要があり、それぞれの場所に別々に実装するとコードの冗長化、メンテナンスの困難さ、作業の重複が発生します。統合関数処理により、これらの共通機能を抽出して統合的に処理することができる。これにはいくつかの利点があります。

  1. コードの再利用: 共通の機能を独立したモジュールまたはコンポーネントに抽出し、複数の場所で共有して使用できるため、繰り返しコードを記述する作業負荷が軽減されます。
  2. 保守性: 共通の機能を一元化して、複数の場所で変更を加えずに簡単に変更、最適化、または拡張できるようにします。
  3. コードのクリーンさ: 統一された関数処理により、コードがより明確かつ簡潔になり、冗長なコードが削減されます。
  4. スケーラビリティ: 新しい関数を追加する必要がある場合、複数の場所ではなく、統合された関数が処理される場所でのみ変更または拡張する必要があるため、コードの結合が軽減されます。

表現


1 統合ユーザーのログイン権限確認

1.1 ネイティブ Spring AOP を使用した統合インターセプトの実装の難しさ

記事: [Spring] Introduction to Spring AOP and Analysis of Implementation Principlesでは、アスペクト指向プログラミングに AOP を使用する方法を学びました。例として、ネイティブ Spring AOP を使用して統合ユーザー ログイン検証を実装し、主に事前通知とサラウンド通知を使用します。具体的な実装は次のとおりです。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/18 16:37
 */
@Aspect // 表明此类为一个切面
@Component // 随着框架的启动而启动
public class UserAspect {
    
    
    // 定义切点, 这里使用 Aspect 表达式语法
    @Pointcut("execution(* com.hxh.demo.controller.UserController.*(..))")
    public void pointcut(){
    
     }


    // 前置通知
    @Before("pointcut()")
    public void beforeAdvice() {
    
    
        System.out.println("执行了前置通知~");
    }

    // 环绕通知
    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
    
    
        System.out.println("进入环绕通知~");
        Object obj = null;
        // 执行目标方法
        try {
    
    
            obj = joinPoint.proceed();
        } catch (Throwable e) {
    
    
            e.printStackTrace();
        }
        System.out.println("退出环绕通知~");
        return obj;
    }

}

上記のコード例からわかるように、ネイティブ Spring AOP を使用して統合インターセプトを実装する際の困難には、主に次の側面が含まれます。

  1. 傍受ルールの定義は非常に困難です。たとえば、登録メソッドやログインメソッドが傍受されない場合、除外メソッドのルールを定義するのは困難、または定義不可能です。
  2. アスペクトクラスで HttpSession を取得するのはさらに困難です。

Spring AOP のこれらの問題を解決するために、Spring はインターセプターを提供します~

1.2 Spring Interceptorを利用した統一ユーザーログイン認証の実現

Spring インターセプターは、Spring フレームワークによって提供される強力なコンポーネントであり、コントローラーに到達する前または後にリクエストをインターセプトして処理するために使用されます。インターセプターを使用すると、認証、ロギング、パフォーマンス監視などのさまざまな機能を実装できます。

HandlerInterceptorSpring インターセプターを使用するには、インターフェースを実装するインターセプター クラスを作成する必要があります。このインターフェイスでは、preHandle、 、postHandleの3 つのメソッドが定義されていますafterCompletionpreHandleこのメソッドは、リクエストがコントローラーに到達する前に実行され、認証やパラメーターの検証などに使用できます。postHandleメソッドは、コントローラーがリクエストを処理した後に実行され、モデルやビューを操作できます。afterCompletionメソッドは、リクエストの後に実行されます。ビューのレンダリングが完了し、リソースのクリーニングまたはログ記録に使用されます。

インターセプターの実装は、次の 2 つのステップに分けることができます。

  1. カスタム インターセプターを作成して、HandlerInterceptorインターフェイスのメソッドを実装しますpreHandle(特定のメソッドを実行する前の前処理)。
  2. カスタム インターセプターをのメソッドWebMvcConfigurerに追加し、インターセプト ルールを設定します。addInterceptors

具体的な実装は以下の通りです。

step1. 共通クラスであるカスタム インターセプターを作成します。コードは次のとおりです。

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 16:31
 * 统一用户登录权限验证 —— 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        // 用户登录业务判断
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
    
    
            return true; // 验证成功, 继续controller的流程
        }
        // 可以跳转登录界面或者返回 401/403 没有权限码
        response.sendRedirect("/login.html"); // 跳转到登录页面
        return false; // 验证失败
    }
}

step2. インターセプターを構成し、インターセプト ルールを設定します。コードは次のとおりです。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 16:51
 */
@Configuration
public class AppConfig implements WebMvcConfigurer {
    
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/user/login") // 不拦截的 url 地址
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/**/*.html"); // 不拦截所有页面
    }
}

1.3 インターセプターの実装原理とソースコード分析

インターセプタが存在する場合、コントローラを呼び出す前に対応する業務処理を実行します 実行プロセスは以下の図に示されています インターセプタの実装
実装プロセス
原理のソースコード解析
上記の実装結果のコンソールのログ情報よりこの場合、すべてのコントローラーの実行はディスパッチャー DispatcherServlet を通じて実現されることがわかります。
結果を達成する
そして、すべてのメソッドは 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;
            Object 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;
                }
                // 执行 Controller 中的业务
                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 が最初に呼び出されます。このメソッドのソース コードは次のとおりです。

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
    for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
    
    
    // 获取项目中使用的拦截器 HandlerInterceptor
        HandlerInterceptor 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 メソッドが実行されることがわかります。これは、以下の図に示すように、以前にインターセプターを実装するために使用した手順に対応します。 : このとき、対応する
実装の詳細
preHandle内の ビジネスロジックが実行されます。

1.4 ユニファイドアクセスプレフィックスの追加

統合アクセス プレフィックスの追加は、ログイン インターセプターの実装と同様であり、すべてのリクエスト アドレスに /hxh プレフィックスを追加します。サンプル コードは次のとおりです。

@Configuration
public class AppConfig implements WebMvcConfigurer {
    
    
    // 给所有接口添加 /hxh 前缀
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
    
    
        configurer.addPathPrefix("/hxh", c -> true);
    }
}

もう 1 つの方法は、アプリケーション構成ファイルで構成することです。

server.servlet.context-path=/hxh

2 統一された例外処理

統合例外処理とは、すべての例外を処理するためにアプリケーション内で共通の例外処理メカニズムを定義することを指します。このようにして、アプリケーション内での散在的な例外処理を回避し、コードの複雑さと繰り返しを軽減し、コードの保守性とスケーラビリティを向上させることができます。

次の点を考慮する必要があります。

  1. 例外処理の階層構造: 例外処理の階層構造を定義し、どの例外を均一に処理する必要があるか、どの例外を処理のために上位層に渡す必要があるかを決定します。

  2. 例外処理方法: ログの出力、エラー コードの返しなど、例外の処理方法を決定します。

  3. 例外処理の詳細: トランザクションのロールバックが必要かどうか、リソースを解放する必要があるかどうかなど、例外を処理するときに注意する必要がある詳細。

この記事で説明する統合例外処理は、+を使用して実装されます@ControllerAdvice@ExceptionHandler

  • @ControllerAdviceコントローラー通知クラスを表します。
  • @ExceptionHandler例外ハンドラ。

上記 2 つのアノテーションは、例外発生時、つまりメソッドイベントの実行時に通知が実行されることを示すために組み合わせて使用​​されます。具体的な実装コードは次のとおりです。

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;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:27
 * 统一异常处理
 */
@ControllerAdvice // 声明是一个异常处理器
public class MyExHandler {
    
    

    // 拦截所有的空指针异常, 进行统一的数据返回
    @ExceptionHandler(NullPointerException.class) // 统一处理空指针异常
    @ResponseBody // 返回数据
    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;
    }
}

上記のコードでは、すべての null ポインタ例外のインターセプトと統一されたデータの返却が実現されています。

実際には、保証が設定されることがよくあります。たとえば、null 以外のポインター例外が発生した場合、try-catch ブロックで Exception を使用してキャッチするのと同様に、それに対処するための保証措置もあります。コード例は次のとおりです。以下に続きます:

@ExceptionHandler(Exception.class) // 异常处理保底措施
@ResponseBody // 返回数据
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;
}

3 統一されたデータ返却形式

API の一貫性と使いやすさを維持するには、通常、統一されたデータ戻り形式を使用する必要があります。一般に、標準データ戻り形式には次の要素が含まれている必要があります。

  • ステータス コード: リクエストの成功または失敗をマークするために使用されるステータス情報。
  • メッセージ: リクエストのステータスを説明するために使用される特定の情報。
  • データ: 要求されたデータ情報が含まれます。
  • タイムスタンプ: リクエストの時刻情報を記録できるため、デバッグや監視に便利です。

統一されたデータ戻り形式を実現するには、@ControllerAdvice+ResponseBodyAdviceメソッドを使用して実現できます。具体的な手順は次のとおりです。

  1. クラスを作成し、@ControllerAdviceアノテーションを追加します。
  2. ResponseBodyAdviceインターフェイスを実装し、supports メソッドと beforeBodyWrite メソッドをオーバーライドします。

サンプルコードは次のとおりです。

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;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:59
 * 统一数据返回格式
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    
    

    /**
     * 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
    
    
        return true;
    }

    /**
     * 方法返回之前调用此方法
     */
    @Override
    public 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);
        return null;
    }
}

ただし、返された本体プリミティブ データ型が String の場合、型変換例外が発生しますClassCastException

したがって、元の戻りデータ型が String の場合、jackson を使用して別の処理を行う必要があり、実装コードは次のようになります。

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;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:59
 * 统一数据返回格式
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    
    

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
    
    
        return true;
    }

    /**
     * 方法返回之前调用此方法
     */
    @Override
    public 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);
        if (body instanceof String) {
    
    
            // 需要对 String 特殊处理
            try {
    
    
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
    
    
                e.printStackTrace();
            }
        }
        return result;
    }
}

ただし、実際のビジネスでは、ステータス コードは常に 200 を返すため、上記のコードは保証としてのみ使用されます。これは厳密すぎるため、特定の問題の具体的な分析が必要になります。


最後に書きます

この記事はJavaEE プログラミング ロードに含まれており点击订阅专栏、継続的に更新されています。
 以上がこの記事の全内容です!作成は簡単ではありません。ご質問がございましたら、プライベート メッセージへようこそ。ご支援ありがとうございます。

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/m0_60353039/article/details/131810657