Springフレームの下で非同期スレッドにHttpServletRequestパラメータのピットを送る


springの注釈@Request Mappingの下で直接にHttpServletRequestを取得して、request headerなどの重要な要求情報を得ることができる。

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
 
  private static final String HEADER = "app-version";
 
  @RequestMapping(value = "/async", method = RequestMethod.GET)
  public void test(HttpServletRequest request) {
        request.getHeader(HEADER);
  }
}
これらの重要な情報はしばしば非同期スレッドにも用いられる。このように、ここで得られたrequestを直接にパラメータとして他のspawnのサブスレッドに伝えたいという自然な考えがあります。

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
 
  private static final String HEADER = "app-version";
 
  @RequestMapping(value = "/async", method = RequestMethod.GET)
  public void test(HttpServletRequest request) {
    log.info("Main thread: " + request.getHeader(HEADER));   
    new Thread(() -> {
      log.info("Child thread: " + request.getHeader(HEADER));
    }).start();
  }
}
headerに「ap-version」を設定して1.0.1にしてから「base_」を送信します。url>/test/asyncにお願いします。結果が見えます。
Main thread:1.0.1
Child thread:1.0.1
しかし、ピットもここに現れました。
HttpServletRequestはスレッドセキュリティではないので、メインスレッドが自分の作業を完了してレスポンスを返した後、対応するHttpServletRequestなどのオブジェクトは破棄されます。この現象を見るために、主スレッドが子スレッドより先に終わることを保証するために、サブスレッド中でより長い時間待つことができる。

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

  private static final String HEADER = "app-version";
  private static final long CHILD_THREAD_WAIT_TIME = 5000;

  @RequestMapping(value = "/async", method = RequestMethod.GET)
  public void test(HttpServletRequest request) {
    log.info("Main thread: " + request.getHeader(HEADER));

    new Thread(() -> {
      try {
        Thread.sleep(CHILD_THREAD_WAIT_TIME);
      } catch (Throwable e) {

      }
      log.info("Child thread: " + request.getHeader(HEADER));
    }).start();
  }
}

headerに「app-version」を設定して1.0.1後に <base_url>/test/async の要求を送信すると、結果が見られます。
Main thread:1.0.1
Child thread:null
明らかに、誰も自分のspawnがメインスレッドより先に出てくることを保証できないので、直接にサブスレッドにHttpServletRequest パラメータを送ることは不可能です。
オンラインではspringフレームにRequestContextHolder を持参してrequestを取得する方法があります。これは非同期スレッドにとっては不可能です。request処理を担当するスレッドだけがRequestContextHolder オブジェクトに呼び出されるので、他のスレッドは直接に空になります。
次に、考えられる愚かな方法は、requestの値を取り出して、カスタムオブジェクトに注入し、このオブジェクトをパラメータとしてサブスレッドに渡すことである。

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

  private static final String HEADER = "app-version";
  private static final long MAIN_THREAD_WAIT_TIME = 0;
  private static final long CHILD_THREAD_WAIT_TIME = 5000;

  @RequestMapping(value = "/async", method = RequestMethod.GET)
  public void test(HttpServletRequest request) {
    log.info("Main thread: " + request.getHeader(HEADER));
    TestVo testVo = new TestVo(request.getHeader(HEADER));

    new Thread(() -> {
      try {
        Thread.sleep(CHILD_THREAD_WAIT_TIME);
      } catch (Throwable e) {

      }
      log.info("Child thread: " + request.getHeader(HEADER) + ", testVo = " + testVo.getAppVersion());
    }).start();

    try {
      Thread.sleep(MAIN_THREAD_WAIT_TIME);
    } catch (Throwable e) {

    }
  }

  @Data
  @AllArgsConstructor
  public static class TestVo {
    private String appVersion;
  }
}

また、「app-version」に従って1.0.1に要求を送信した後、以下のようになります。
Main thread:1.0.1
Child thread:null、testVo=1.0.1
うん、やっと成功しました。
話はこれで終わりそうですが、細かいところをよく見ると、いくつかの問題が考えられます。
  • もしchild threadの中のrequestはすでに破壊されたならば、どうしてnull exceptionを報告していないで、ただ空の“ap-version”の値を獲得しますか?
  • もしrequestが破壊されたら、TestVoこれは同じメインスレッドで作成されたobjectはなぜ廃棄されていませんか?
  • メインラインのプログラムは本当にオブジェクトを廃棄できますか?廃棄対象はGCではないですか?なぜいつもchild threadでnullの結果が得られますか?
  • 合理的な推理は、メインスレッドが終了すると、destroy()方法が起動され、この方法は、HttpServletRequestにおけるリソースを自発的に解放し、例えばheaderを格納するmapに対応するclear()方法を呼び出す。このように、サブスレッドでは、これまでの「ap-version」に対応したvalueは取得できなくなります。TestVoは、ユーザー自身が作成したものであるため、必然的にdestroy()方法でリソースを解放するコードを書き出すことができない。その値は保存されます。
    また、メインスレッドがdestroy()メソッドを起動しても、本当に回収する時もGCの作業も、サブスレッドではnull exceptionではなく、特定のkeyに対応する値だけが取れないと説明しました。
    さらに、なぜメインスレッドのdestoy()方法では、requestオブジェクトを直接nullにしないかという問題も考えられます。
    この問題はどうもうさんくさいように見えるが、実は全く成り立たない。メインスレッドのrequest変数をnullとして指定しても、サブスレッドの他の変数はこのrequest対応メモリを指していますので、依然として対応する値を得ることができます。たとえば:
    
    @Slf4j
    @RestController
    @RequestMapping("/test")
    public class TestController {
    
      private static final String HEADER = "app-version";
      private static final long MAIN_THREAD_WAIT_TIME = 5000;
      private static final long CHILD_THREAD_WAIT_TIME = 3000;
    
      @RequestMapping(value = "/async", method = RequestMethod.GET)
      public void test(HttpServletRequest request) {
        log.info("Main thread: " + request.getHeader(HEADER));
        TestVo testVo = new TestVo(request);
    
        new Thread(() -> {
          try {
            Thread.sleep(CHILD_THREAD_WAIT_TIME);
          } catch (Throwable e) {
    
          }
          log.info("Child thread: " + testVo.getRequest().getHeader(HEADER));
        }).start();
    
        request = null;
    
        try {
          Thread.sleep(MAIN_THREAD_WAIT_TIME);
        } catch (Throwable e) {
    
        }
      }
    
      @Data
      @AllArgsConstructor
      public static class TestVo {
        private HttpServletRequest request;
      }
    }
    「app-version」で1.0.1に要求を送信した後、以下のようになります。
    Main thread:1.0.1
    Child thread:1.0.1
    ここでは、メインスレッドが十分な時間を持つように、サブスレッドを3秒待ちます。しかし、childスレッドは依然として対応する値を得ることができる。
    したがって、request変数はnullに割り当てられています。リソースを解放することはできません。したがって、requestにheaderを保存しているmapでは、変数の割当値をnullとしています。他のところの引用も一緒に消えます。最も直接的で効果的な方法は、clear()を呼び出して、mapの各要素を無効にすることである。
    だからまとめると:
  • メインスレッドのrequestとtestVoは、いずれもサブスレッドの変数指向があるため、つまり2つのオブジェクトのreference countは0ではなく、GCはこの2つの部分に対応するメモリを本当に回収しない。
  • しかし、requestは、マスタスレッドにおいてdestroy()方法で内部mapのclear()方法を呼び出される可能性が高いため、headerの値を取得できなくなる。
  • testVoは、ユーザが作成したオブジェクトであり、事前にdestry()メソッドにおいてリリースされないため、元の値を維持することができる。
  • 以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。