SpringBootでは業務ログ機能をどのように設計しているのでしょうか?

序文

この記事を書こうと思ったのはずっと前で、一度も自由になったことはありませんが、今でも当時の光景が印象に残っています。業務ログのクエリ機能やクエリ機能もあるが、具体的な実装は本当だ。ひどい、具体的な悪いやり方はネガティブな例で詳しく説明される。リーダーも顧客もそれをよく認識している。謎の連続に感銘を受けるオペレーション。

要件の説明と分析

クライアント側の要件は非常に単純です。いくつかの主要なビジネス機能の操作ログ、つまり、誰がいつどの機能を操作したか、操作前のデータ メッセージは何なのか、データ メッセージは何なのかを記録する必要があります。操作後はワンクリックで戻ることができます。

ログは業務システムに欠かせない機能であり、代表的なものとしてはシステムログや操作ログなどが挙げられます。

システムログ

ここでのシステムログとは、プログラム実行プロセスの主要なステップを指し、実際のシーンに応じて、デバッグ、情報、警告、エラーなどのさまざまなレベルのプログラム実行記録情報が出力されます。異常な問題が発生した場合、システム ログに記録された主要なパラメータ情報と異常なプロンプトを使用して、障害を迅速にトラブルシューティングできます。

操作ログ

操作ログとは、どのユーザーがいつ特定のメニューをクリックしたか、どの設定が変更されたかなど、ユーザーの実際の業務動作を記録したもので、一般にデータベースに保存されます。一般ユーザーまたはシステム管理者については、を参照してください。

要件分析を通じて、お客様は業務ログ管理の機能を求めています。

1. ユーザーの業務操作を記録する 記録されるフィールドには、オペレーター、操作時刻、操作機能、ログの種類、操作内容の説明、操作内容メッセージ、操作前内容メッセージが含まれます。

2. ユーザーの業務運営の行動を照会し、重要な業務を遡ることができるビジュアル ページを提供します。

3. 特定の管理機能を提供し、必要に応じてユーザーの誤操作をロールバックします。

否定的な実装

要件を明確にした後、それをどのように実装するかが問題ですが、ここではネガティブな実装例を示しますが、このネガティブなケースがあるからこそ、このシンプルな要件に非常に感銘を受けました。

ここでは、人事管理機能を復元する例として、当時の具体的な実装を取り上げます。

1. 各インターフェースに業務操作ログの記録を追加します。

2. すべてのインターフェイスは例外をキャプチャし、異常なビジネス操作ログを記録する必要があります。

疑似コードは次のとおりです。

@RestController
@Slf4j
@BusLog(name = "人员管理")
@RequestMapping("/person")
public class PersonController2 {
    
    
    @Autowired
    private IPersonService personService;
    @Autowired
    private IBusLogService busLogService;
    //添加人员信息
    @PostMapping
    public Person add(@RequestBody Person person) {
    
    
       try{
    
    
           //添加信息信息
        Person result = this.personService.registe(person);
        //保存业务日志
        this.saveLog(person);
        log.info("//增加person执行完成");        
       }catch(Exception e){
    
    
           //保存异常操作日志
           this.saveExceptionLog(e);       
       }
        return result;
    }
}

このようなハードコーディングによって実現される業務ログ管理機能の最大の問題点は、業務ログの収集がビジネスロジックと密接に結びついており、コードと二重化されてしまうことであり、新たに開発したインターフェースがビジネスロジックを完成させた後、業務ログ保存ロジックのセクションを組み込む必要があり、開発および起動されたインターフェイスは再変更してテストする必要があり、各インターフェイスに組み込む必要がある業務ログ保存ロジックは同じです。

デザインのアイデア

AOP について何らかの印象がある場合は、aop を使用して次のことを実現するのが最善の方法です。

1. ビジネス操作ログの注釈を定義します。これにより、操作関数名、関数の説明などのいくつかの属性を定義できます。

2. ビジネス操作のために記録する必要があるメソッドに関するビジネス操作ログの注釈をマークします (実際のビジネスでは、一部の単純なビジネス クエリ動作は通常、記録する必要がありません)。

3. エントリ ポイントを定義し、アスペクトを作成します。エントリ ポイントはビジネス オペレーション ログの注釈が付けられたターゲット メソッドであり、アスペクトの主なロジックはビジネス オペレーション ログ情報を保存することです。

春のAOP

AOP (Aspect Orient Programming) は、直訳するとアスペクト指向プログラミングとなります。AOP はプログラミングのアイデアであり、オブジェクト指向プログラミング (OOP) を補足するものです。アスペクト指向プログラミングとは、ソースコードを変更することなくプログラムに動的かつ一律に機能を追加する技術で、AOPは業務コードに侵入することなく指定されたメソッドをインターセプトしてメソッドを拡張し、業務処理と非業務処理を論理的に分離することができます。

SpringAOP は AOP の特定の実装です。Spring で SpringAOP を適用するための最も古典的なシナリオは Spring トランザクションです。トランザクション アノテーションの構成を通じて、Spring はビジネス メソッドでビジネスを自動的に開いて送信します。失敗した場合は、対応するロールバック戦略を実装します。フィルターやインターセプターと比較して、その適用範囲が SpringMVC プロジェクトに限定されないことがより重要であり、任意のレイヤーでポイントカットを定義し、対応する操作を織り込むことができ、戻り値も可能です。変更される;
ここに画像の説明を挿入

フィルターとハンドラーインターセプター

Filter と HandlerInterceptor が選択されず、業務ログ機能を実現するために AOP が選択される理由は、Filter と HandlerInterceptor のいくつかの制限のためです。

フィルター

フィルタ (Filter) はサーブレットに関連付けられたインターフェイスであり、主に Java Web プロジェクトに適用できます。サーブレット コンテナに依存します。Java のコールバック メカニズムを使用して、ブラウザからの http リクエストのフィルタリングとインターセプトを実装します。メソッドのリクエストおよびレスポンス(ServletRequestリクエスト、ServletResponseレスポンス)ですが、リクエストおよびレスポンス情報の値は変更できません;通常、文字エンコードや認証動作などの設定に使用されます。

より細かいクラスやメソッドを作成したり、サーブレット以外の環境で使用したりする場合はそれができないため、サーブレット コンテナに依存する環境であれば Struts2 や SpringMVC などのフィルターを使用できます。

ここに画像の説明を挿入

インターセプター

インターセプター (HandlerInterceptor) のスコープと機能はフィルターに似ていますが、相違点もあります。まず、インターセプタ (HandlerInterceptor) は SpringMVC に適しています。HandlerInterceptor インターフェースは SpringMVC に関連するインターフェースであり、Java Web プロジェクトを実装するには、SpringMVC が現在推奨されるオプションですが、唯一のオプションではありません。struts2 などもあります。 .; したがって、非 SpingMVC アイテムの場合は、HandlerInterceptor は使用できません。

次に、インターセプタはフィルタと同様に、アクセス URL に対応するメソッドのリクエストとレスポンス (ServletRequest リクエスト、ServletResponse レスポンス) をインターセプトできますが、リクエストとレスポンスの情報の値を変更することはできません。一般に文字エンコーディングを設定するために使用されます。 、認証 認可操作など; より細かいクラスやメソッドを作りたい場合やサーブレット以外の環境で使いたい場合はできません。

つまり、フィルターとインターセプターの機能は非常に似ていますが、インターセプターの適用範囲はフィルターの適用範囲よりも狭いです。
ここに画像の説明を挿入

Spring AOP、フィルター、インターセプターの比較

同じターゲットに一致する場合、フィルター、インターセプター、Spring AOP の実行優先順位はフィルター > インターセプター > Spring AOP となり、実行順序は先入れ後出しになります。具体的な違いは次の点に反映されます。

1. 適用範囲が異なります

フィルターはサーブレット コンテナーに依存し、リクエスト/レスポンス エントリをフィルター処理してインターセプトするためにサーブレット コンテナーと Web 環境でのみ使用できます。インターセプターは springMVC に依存し、SpringMVC プロジェクトで使用できます。SpringMVC のコアは DispatcherServlet です
。 、DispatcherServlet もサーブレットのサブクラスに属しているため、スコープはフィルターに似ています。SpringAOP にはスコープに
制限がなく、カットポイントが定義されている限り、エントリー層 (コントローラー) でインターセプトして処理できます。リクエスト/レスポンスの層)、または要求されたビジネス層(サービス層)のインターセプト処理で処理できます。

2. 粒度の違い

  • フィルターの制御粒度は比較的粗く、リクエストとレスポンスは doFilter() でのみフィルター処理およびインターセプトできます。
  • インターセプターは、preHandle()、postHandle()、afterCompletion() などのよりきめの細かい制御を提供します。これらにより、コントローラーがリクエストを処理する前、リクエストが処理された後、およびリクエストに応答した後にいくつかのビジネス オペレーションを織り込むことができます。
  • Spring AOP は、事前通知、事後通知、事後通知、例外通知、サラウンド通知、インターセプターよりも詳細な制御を提供し、戻り値を変更することもできます。

実行計画

環境構成

  • jdk バージョン: 1.8 開発ツール: Intellij iDEA 2020.1
  • スプリングブート:2.3.9.RELEASE
  • mybatis-spring-boot-starter:2.1.4

依存関係の構成

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

テーブル構造設計

create table if not exists bus_log
(
   id bigint auto_increment comment '自增id'
      primary key,
   bus_name varchar(100) null comment '业务名称',
   bus_descrip varchar(255) null comment '业务操作描述',
   oper_person varchar(100) null comment '操作人',
   oper_time datetime null comment '操作时间',
   ip_from varchar(50) null comment '操作来源ip',
   param_file varchar(255) null comment '操作参数报文文件'
)
comment '业务操作日志' default charset ='utf8';

コード

1. ビジネス ログ アノテーション @BusLog を定義します。これは、現在のクラスの機能を説明するためにコントローラーまたは他のビジネス クラスで使用できます。また、現在のメソッドの機能を説明するためにメソッドでも使用できます。

/**
 * 业务日志注解
 * 可以作用在控制器或其他业务类上,用于描述当前类的功能;
 * 也可以用于方法上,用于描述当前方法的作用;
 */
@Target({
    
    ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BusLog {
    
    
 
 
    /**
     * 功能名称
     * @return
     */
    String name() default "";
 
    /**
     * 功能描述
     * @return
     */
    String descrip() default "";
 
}

2. PersonController クラスおよびメソッドにビジネス操作ログの注釈 BusLog をマークします。

@RestController
@Slf4j
@BusLog(name = "人员管理")
@RequestMapping("/person")
public class PersonController {
    
    
    @Autowired
    private IPersonService personService;
    private Integer maxCount=100;
 
    @PostMapping
    @NeedEncrypt
    @BusLog(descrip = "添加单条人员信息")
    public Person add(@RequestBody Person person) {
    
    
        Person result = this.personService.registe(person);
        log.info("//增加person执行完成");
        return result;
    }
    @PostMapping("/batch")
    @BusLog(descrip = "批量添加人员信息")
    public String addBatch(@RequestBody List<Person> personList){
    
    
        this.personService.addBatch(personList);
        return String.valueOf(System.currentTimeMillis());
    }
 
    @GetMapping
    @NeedDecrypt
    @BusLog(descrip = "人员信息列表查询")
    public PageInfo<Person> list(Integer page, Integer limit, String searchValue) {
    
    
       PageInfo<Person> pageInfo = this.personService.getPersonList(page,limit,searchValue);
        log.info("//查询person列表执行完成");
        return pageInfo;
    }
    @GetMapping("/{loginNo}")
    @NeedDecrypt
    @BusLog(descrip = "人员信息详情查询")
    public Person info(@PathVariable String loginNo,String phoneVal) {
    
    
        Person person= this.personService.get(loginNo);
        log.info("//查询person详情执行完成");
        return person;
    }
    @PutMapping
    @NeedEncrypt
    @BusLog(descrip = "修改人员信息")
    public String edit(@RequestBody Person person) {
    
    
         this.personService.update(person);
        log.info("//查询person详情执行完成");
        return String.valueOf(System.currentTimeMillis());
    }
    @DeleteMapping
    @BusLog(descrip = "删除人员信息")
    public String edit(@PathVariable(name = "id") Integer id) {
    
    
         this.personService.delete(id);
        log.info("//查询person详情执行完成");
        return String.valueOf(System.currentTimeMillis());
    }
}

3. アスペクトクラス BusLogAop を記述し、@BusLog でエントリポイントを定義し、周囲の通知内の対象メソッドを実行後、対象クラスと対象メソッドの業務ログアノテーションの関数名と関数説明を取得して送信します。メソッドのパラメータメッセージをファイルに書き込み、最後に業務ログ情報を保存します。

@Component
@Aspect
@Slf4j
public class BusLogAop implements Ordered {
    
    
    @Autowired
    private BusLogDao busLogDao;
 
    /**
     * 定义BusLogAop的切入点为标记@BusLog注解的方法
     */
    @Pointcut(value = "@annotation(com.fanfu.anno.BusLog)")
    public void pointcut() {
    
    
    }
 
    /**
     * 业务操作环绕通知
     *
     * @param proceedingJoinPoint
     * @retur
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) {
    
    
        log.info("----BusAop 环绕通知 start");
        //执行目标方法
        Object result = null;
        try {
    
    
            result = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
    
    
            throwable.printStackTrace();
        }
        //目标方法执行完成后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述
        Object target = proceedingJoinPoint.getTarget();
        Object[] args = proceedingJoinPoint.getArgs();
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        BusLog anno1 = target.getClass().getAnnotation(BusLog.class);
        BusLog anno2 = signature.getMethod().getAnnotation(BusLog.class);
        BusLogBean busLogBean = new BusLogBean();
        String logName = anno1.name();
        String logDescrip = anno2.descrip();
        busLogBean.setBusName(logName);
        busLogBean.setBusDescrip(logDescrip);
        busLogBean.setOperPerson("fanfu");
        busLogBean.setOperTime(new Date());
        JsonMapper jsonMapper = new JsonMapper();
        String json = null;
        try {
    
    
            json = jsonMapper.writeValueAsString(args);
        } catch (JsonProcessingException e) {
    
    
            e.printStackTrace();
        }
        //把参数报文写入到文件中
        OutputStream outputStream = null;
        try {
    
    
            String paramFilePath = System.getProperty("user.dir") + File.separator + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN) + ".log";
            outputStream = new FileOutputStream(paramFilePath);
            outputStream.write(json.getBytes(StandardCharsets.UTF_8));
            busLogBean.setParamFile(paramFilePath);
        } catch (FileNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            if (outputStream != null) {
    
    
                try {
    
    
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
 
            }
        }
        //保存业务操作日志信息
        this.busLogDao.insert(busLogBean);
        log.info("----BusAop 环绕通知 end");
        return result;
    }
 
    @Override
    public int getOrder() {
    
    
        return 1;
    }
}

テスト

デバッグ方法

バックエンド デバッグ インターフェイスは通常 postman を使用します。これは Amway 用のツール、つまり Intellij IDEA の Test RESTful Web サービスです。機能と使用法は postman と似ています。唯一の利点は、追加の postman をインストールする必要がないことです。関数エントリ: ツール –> http クライアント –> ツールバーの RESTful Web のテストには
ここに画像の説明を挿入
、別の用途があり、私はそれを使用することを好みます。これは、いくつかの簡単な文で http リクエストを開始でき、一度にバッチで実行することもできます。時間;
ここに画像の説明を挿入
ここに画像の説明を挿入

検証結果

ここに画像の説明を挿入

要約する

業務操作ログには、ユーザー操作の機能名、機能説明、演算子、操作時刻、および操作パラメータメッセージが含まれますが、パラメータメッセージをファイルに保存する理由は、通常の状況では保存する必要がないためです。パラメータ メッセージはロールバック操作中にのみ使用され、最後のパラメータ メッセージに従って逆の操作を実行できます。

おすすめ

転載: blog.csdn.net/Park33/article/details/131046069