質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.80%

【Java】【Spring】thymeleafでMapを出力したい

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 510

sakura2685

score 19

【機能概要】
チェックボックスで選択したオブジェクトの情報を表示する機能を作成しています。

チェックボックスの値はカテゴリーわけをしており、
Map<String カテゴリ名, List<String 選択肢名>>
で表現しています。
※ちょっとわかりずらいんですが
カテゴリ名:category
選択肢名:genre
で命名しています。

Mapはコントローラーでmodelに登録し、ビューではthymeleafで呼び出します。

『検索』ボタンをクリックするとformの値が送信されオブジェクトを検索(検索部分は割愛)コントローラーで自身のページに該当するオブジェクトの内容を表示するというものです。

【課題】
formで送信された値は自身のページにreturnするのですが、その時にマップを再利用したいので、ビューから
<input type="hidden" name="attrName" th:value="${mapObj}">
を送信し、コントローラーでは
@RequestParam(name="attrName") Map<String, List<String>> mapObj
で取得、再度マッピングしようとしています。
ですがここでmapObjがStringとして認識される?ようでエラーになってしまいます。

java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Map': no matching editors or conversion strategy found

thymeleafではviewからmapオブジェクトを送信することはできないのでしょうか?

【コード】
■コントローラー

@Controller
@RequestMapping("/genre")
public class GenreController {

    @Autowired
    GenreService genreService;

    @GetMapping("")
    public String genre(Model model) {

        model.addAttribute("genreNamesOfCategories", genreService.getGenreNamesOfCategories(model));
            //Map<String カテゴリ名, List<String 選択肢名>> を取得

        return "genre/index";
    }

    @GetMapping("/search")  //検索ボタンが押されたら
    public String searchGenre(Model model, @RequestParam(name="genreNamesOfCategories") Map<String, List<String>> genreNamesOfCategories) {

        model.addAttribute("genreNamesOfCategories", genreNamesOfCategories);
        return "genre/index";
    }

  ……

■ビュー

<form action="/genre/search" method="get">
    <input type="hidden" name="genreNamesOfCategories" th:value="${genreNamesOfCategories}">

    <table class="table">
        <tr th:each="genres : ${genreNamesOfCategories.get('category1')}">
            <td th:text="${genres}"></td>
            <td><input type="checkbox" name="genreNameaa" th:value="${genres}"></td>
    </table>

    <table class="table">
        <tr th:each="genres : ${genreNamesOfCategories.get('category2')}">
            <td th:text="${genres}"></td>
            <td><input type="checkbox" name="genreNameaa" th:value="${genres}"></td>
    </table>

  ……
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • m.ts10806

    2020/05/13 21:01

    コードやエラーはマークダウンのcode機能を利用してご提示ください。
    https://teratail.com/questions/238564

    キャンセル

  • sakura2685

    2020/05/13 23:48

    不慣れですみません。マークダウン機能で修正しました。

    キャンセル

回答 1

checkベストアンサー

+1

エラーの原因

mapObjがStringとして認識される?ようでエラーになってしまいます。

hiddenフィールドに出力したデータは元の型が何であれ文字列です。

<input type="hidden" name="attrName" th:value="${mapObj}">

ページが表示されたらchromeの開発者ツールでhtml要素を確認してみるとわかりますが、下記のようなフォーマットで出力されていると思います。valueの値は単にマップの文字列表現(toStringメソッドの出力)です。

<input type="hidden" name="genreNamesOfCategories" value="{category2=[DDD, EEE, FFF], category1=[aaa, bbb, ccc]}">

なので、この値をコントローラで受け取るには引数の型をString型とする必要があります。

thymeleafではviewからmapオブジェクトを送信することはできないのでしょうか?

リクエストパラメータをMap<String, String>型で受け取るということはできるようですが、今回の例では簡単にはできそうにないと考えています。
なお、後述しますがjson文字列として扱えば、ある程度簡単に処理することは可能です。

解決方法1

まず、このような実装を行う理由が

その時にマップを再利用したいので、

ということですが、このような要求に対する適切な解決方法は、genreService.getGenreNamesOfCategoriesでその都度データを取得する方法だと思います。
こうすればリクエストパラメータの改ざんの可能性に対応する必要もなくなります。

@GetMapping("/search")
public String searchGenre(Model model) {
    // 必要な都度、データを取得する
    model.addAttribute("genreNamesOfCategories", genreService.getGenreNamesOfCategories(model));
    return "genre/index";
}

もしgenreService.getGenreNamesOfCategoriesの実行コストが高く何度も呼び出せないということであれば、

  • セッションスコープを利用する
  • キャッシュを利用する

等を検討してはどうでしょうか。

解決方法2

どうしても、hiddenフィールドに埋め込んだマップの文字列表現をコントローラの引数で受け取りたいということであれば、少し強引ですがHandlerMethodArgumentResolverの実装クラスで型の変換を行うことができます。
ただしマップの文字列表現をそのまま扱うと変換が大変なのでjson文字列として扱うようにします。そうすればJacksonのObjectMapperが使えるのでコーディング量が減ります。
なお、下記サンプルコードのクラス名やインターフェース名は適当です。

CategoryAnnotation

変換を行う引数に付けるマーカーアノテーションです。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.PARAMETER})
public @interface CategoryAnnotation {
}

CategoryArgumentResolver

このResolverはCategoryAnnotationアノテーションが付いている引数に、下記の変換処理後のデータをバインドします。

@Component
public class CategoryArgumentResolver implements HandlerMethodArgumentResolver {

  @Autowired
  private ObjectMapper objectMapper;

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(CategoryAnnotation.class);
  }

  @Override
  public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    String jsonCategories = webRequest.getParameter("genreNamesOfCategories");
    Map<String, List<String>> genreNamesOfCategories = objectMapper.readValue(jsonCategories, new TypeReference<>(){});
    return genreNamesOfCategories;
  }
}

MyWebMvcConfigurer

上記のResolverを有効にするためのコンフィグレーションです。

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {

  @Autowired
  CategoryArgumentResolver categoryArgumentResolver;

  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    resolvers.add(categoryArgumentResolver);
  }
}

GenreController

コントローラ側でも修正が必要です。
1)マップをjson文字列に変換します。
2)hiddenフィールドの値を受け取る引数に@CategoryAnnotationを付けます。
3)マップをjson文字列に変換します。

@Autowired
ObjectMapper objectMapper;

@GetMapping("")
public String genre(Model model) throws JsonProcessingException {

  Map<String, List<String>> genreNamesOfCategories = genreService.getGenreNamesOfCategories(model);

  // 1)
  String jsonCategories = objectMapper.writeValueAsString(genreNamesOfCategories);
  model.addAttribute("jsonCategories", jsonCategories);

  model.addAttribute("genreNamesOfCategories", genreNamesOfCategories);
  //Map<String カテゴリ名, List<String 選択肢名>> を取得

  return "genre/index";
}


@GetMapping("/search")
public String searchGenre(Model model,
    // 2)
    @CategoryAnnotation Map<String, List<String>> genreNamesOfCategories) throws JsonProcessingException {

    // 3)
    String jsonCategories = objectMapper.writeValueAsString(genreNamesOfCategories);
    model.addAttribute("jsonCategories", jsonCategories);

    model.addAttribute("genreNamesOfCategories", genreNamesOfCategories);
    return "genre/index";
}

genre/index.html

hiddenフィールドに埋め込むのはjson文字列になります。

<input type="hidden" name="genreNamesOfCategories" th:value="${jsonCategories}">

※json文字列が改ざんされてマップに変換できなかったときの例外処理が必要です。
※HandlerMethodArgumentResolverの実装クラスの利用せずに、コントローラ内で同じ変換処理を実装するという方法でもいいと思います。

解決策を2つ提示させて頂きましたが、私がこの課題を解決するとしたら解決方法1の方を選びます。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/05/14 19:17

    やはりビューを経由すれば文字列ですよね…初歩的な質問に丁寧に回答くださいましてありがとうございます。

    jsonを使う方法もあるんですか。。
    良い勉強の機会でもあるのでしょうが、ちょっとまだ力量不足なので(^^;)今回は解決策1で対応することにします。

    改ざんリスクの低減といった視点も勉強になりました!
    この度はありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.80%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る