LoginSignup
9
3

More than 3 years have passed since last update.

【Spring Boot】Getリクエストでも配列受け取りたい

Last updated at Posted at 2020-02-28

はじめに

ソースの抜粋は記事を書くためにいろいろ加工してるんで多少の書き間違いは大目に見てください・・・

環境

Spring Boot 2.2.4.RELEASE
Kotlin 1.3.61
axios 0.19.2
vue 2.6.11

環境は以下の記事のまんま

経緯

一覧画面を作ってたわけですよ
よくある一覧画面です
例えば名前と年齢と性別と・・・みたいな
横にカラムが並んで縦にずらーっと・・・みたいな
クライアントに全データ持たせるにはでかすぎるからページングの処理があって、
利用者に入力欄で田中とか入力させて、バックエンド側のSQLでname = LIKE '%田中%・・・とか
セレクトボックスから年齢を10歳11歳15歳って選んでage in ( 10 , 11 , 15 )・・・とか

そういうことがしたかったんですよ

問題点

問題にぶちあたるまで

Vueでフロントの実装を行っていて、バックエンドとのHTTP通信にはaxiosを使ってやり取りをすると

api.js
import axios from 'axios';

/**
 * APIクライアント
 */
function getApiClient(){
    const client = axios.create({
        baseURL:'http://localhost:8088',
        withCredentials : true ,
        timeout : 30000 ,
    });
    return client;
}

/**
 * Hoge一覧取得API
 */
export function getHoges( params ){
    return getApiClient().get( "/api/hoge" , { params : params } );
}

一覧を取得するAPIは検索条件をクエリパラメータに入れてGETリクエストで検索条件に引っかかるHogeの一覧を取得するってもの

HogeList.vue
<template>
    <!-- いろいろ省略 -->
</template>
<script>
import { getHoges } from '@/api.js';
export default {
    // いろいろ省略
    data: () => ({
        /** 一覧表示用配列 */ hoges : [] ,
        /** 1ページに表示させる項目数 */ limit : 20 ,
        /** 一覧ページ数 */ offset : 0 ,
        /** 名前で検索する時の入力値 */ name : "" ,
        /** 複数選択できるセレクトボックスからの入力値 */ ages: [] , 
    }) ,
    methods: {
        // 検索ボタン押下時の処理
        search : function() {
            // クエリパラメータ作成
            let params = {
                offset : this.offset , 
                limit : this.limit ,
                name : this.name ,
                ages: this.ages,
            };
            // APIコール
            getHoges( params )
                .then( response => {
                    // 一覧情報等を更新
                    this.hoges = response.data.hoges;
                })
                .catch( error => {
                    console.error( error );
                });

        }
    }
};
</script>

APIのURLはこんな感じ

http://localhost:8088/api/hoge?offset=0&limit=20&name=田中&ages[]=10&ages[]=11&ages[]=15

実際のURLは田中のところエンコードされてるけどね まぁ割愛

バックエンド側はこんな感じ

HogeController.kt
@RestController
@RequestMapping("/api/hoge")
class HogeController( private val hogeService: HogeService ) {

    /**
     * hoge一覧取得API
     */
    @GetMapping
    fun getHoges(

            @RequestParam("limit", required = false , defaultValue = "#{null}")
            limit: Int?,

            @RequestParam("offset", required = false , defaultValue = "#{null}")
            offset: Int?,

            @RequestParam(value="name" , required = false , defaultValue = "#{null}")
            name : String?,

            @RequestParam(value="ages[]" , required = false , defaultValue = "#{null}" )
            ages : ArrayList<String>?

    )
    // Serviceクラスを経由してDBからhogeの一覧を返す
    = hogeService.getHoges(
            limit , offset , name , ages
    )

}

GETリクエストで飛んでくるので@RequestParamでクエリパラメータを引数にいれて・・・まぁ後はあんま関係ないところなんで省略

Springがいい感じにやってくれる部分で、
クエリパラメータに同じ名前のキーがあった場合に
配列やリストで受け取れるし、Stringで受け取ると、値をカンマ区切りで渡してくれる

さて・・・動作確認だー

java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
    at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:468) ~[tomcat-embed-core-9.0.30.jar:9.0.30]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:260) ~[tomcat-embed-core-9.0.30.jar:9.0.30]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.30.jar:9.0.30]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:860) ~[tomcat-embed-core-9.0.30.jar:9.0.30]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1598) ~[tomcat-embed-core-9.0.30.jar:9.0.30]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.30.jar:9.0.30]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.30.jar:9.0.30]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

なんじゃこりゃ
そもそもコントローラーまでたどり着けてないっぽい

原因

まずいのはクエリパラメータにある[]これ

Spring BootをBootRunで動かしたときに、中でservletが動いてアクセスできるようになるんですけど、|{}[]このあたりの記号が入ってると受け付けてくれない

対策案1 POSTで通信

GETリクエストで送るのが悪いんだよ
クエリパラメータに配列載せるのが悪い!
そもそもURLには文字数制限があるから検索条件とかをクエリパラメータに載せない方がいいんじゃないか・・・?
POSTならリクエストボディに配列入れられるし、文字数も気にしなくていいほどある
解決じゃん!

僕の見解

妥協案かなぁって思う
動けばいいの思想なら一番手っ取り早い気がする

HTTPメソッドってそれぞれ意味合いがあって
GET・・・リソースの取得
POST・・・リソースの追加/登録
PUT・・・リソースの更新
DELETE・・・リソースの削除
っていうのがある

でも、結局はレスポンスで一覧を返すみたいなことはそれぞれできるわけだしやろうと思えばできるっていうね

例えるならsetHogeってメソッドで一覧取得ができてるようなものかな

外部に公開/連携しないとか、この画面でしか使わないAPIとかならありかも

話はずれるけどこういうことする場合はコメント残そうな
// POSTで送信みたいなソース読めばわかる「ソースの日本語訳」じゃなくて// 配列をクエリパラメータに乗せられないのでPOSTで送信するみたいな「なんでこういう書き方をしたか」が大事

対策案2 Servletのプロパティを変更する

WebMvcConfig.kt
@Configuration
class WebMvcConfig : WebMvcConfigurer {

    @Bean
    fun webServerFactory(): ConfigurableServletWebServerFactory? {
        val factory = TomcatServletWebServerFactory()
        factory.addConnectorCustomizers(
            TomcatConnectorCustomizer { 
                connector -> connector.setProperty("relaxedQueryChars", "|{}[]") 
            }
        )
        return factory
    }

}

relaxedQueryChars|{}[]をクエリパラメータに乗せられるようにする

僕の見解

この記事で言いたかったこと

ただ、調べきれてないんだけど、
tomcatにはrequestTargetAllowっていうプロパティもある
こいつも同じようなことするんだけどこっちは非推奨
CVE-2016-6816っていう脆弱性にさらされることになるっぽい

requestTargetAllowの代わりにrelaxedQueryCharsを使ってねとは言ってるけど、これを使ってても大丈夫なのかってところ

対策3 クライアントのロジックを変更する

クエリパラメータに配列が入らないようにすればいいじゃん

// 変更後のクエリパラメータの配列部分
?age=10&age=11&age=15
HogeController.kt
@RestController
@RequestMapping("/api/hoge")
class HogeController( private val hogeService: HogeService ) {

    /**
     * hoge一覧取得API
     */
    @GetMapping
    fun getHoges(

            @RequestParam("limit", required = false , defaultValue = "#{null}")
            limit: Int?,

            @RequestParam("offset", required = false , defaultValue = "#{null}")
            offset: Int?,

            @RequestParam(value="name" , required = false , defaultValue = "#{null}")
            name : String?,

            @RequestParam(value="ages" , required = false , defaultValue = "#{null}" )
            ages : ArrayList<String>?

    )
    // Serviceクラスを経由してDBからhogeの一覧を返す
    = hogeService.getHoges(
            limit , offset , name , ages
    )

}

配列を受け取る@RequestParam

@RequestParam(value="ages[]" , required = false , defaultValue = "#{null}" )

から

@RequestParam(value="ages" , required = false , defaultValue = "#{null}" )

に変更

僕の見解

バックエンド側に懸念点を残すことなく、対応できるベストな選択だと思います

ただ、今回僕の場合、クエリパラメータを作る部分はaxiosの処理で、特にライブラリ外からは何もしてないんだよね・・・objectを引数に渡してるだけで・・・

課題としてはフロント側の実装が可能かどうかってところ

03/01追記:パラメータの渡し方がおかしかっただけだったみたいです☆

まとめ

Spring Bootでこういうことできるよっていう記事が書きたかったけど、根本的な問題はフロントにあるっていう何とも言えない記事になってしまいました・・・

でもこの問題結構いろんな人がぶちあたるんじゃないかなぁと思うんだけどなぁ
axiosを使うのがまずかったか・・・?

問題点としては僕のミスでした

ま、まぁこういうこともできるよっていうことで・・・;;

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3