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

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

新規登録して質問してみよう
ただいま回答率
85.48%
Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Thymeleaf

Thymeleaf(タイムリーフ)とは、Java用のテンプレートエンジンで、特定のフレームワークに依存せず使用することが可能です。

Q&A

解決済

1回答

8567閲覧

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

sakura2685

総合スコア20

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Thymeleaf

Thymeleaf(タイムリーフ)とは、Java用のテンプレートエンジンで、特定のフレームワークに依存せず使用することが可能です。

0グッド

0クリップ

投稿2020/05/13 10:44

編集2020/05/13 14:52

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

チェックボックスの値はカテゴリーわけをしており、
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として認識される?ようでエラーになってしまいます。

err

1java.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オブジェクトを送信することはできないのでしょうか?

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

Java

1@Controller 2@RequestMapping("/genre") 3public class GenreController { 4 5 @Autowired 6 GenreService genreService; 7 8 @GetMapping("") 9 public String genre(Model model) { 10 11 model.addAttribute("genreNamesOfCategories", genreService.getGenreNamesOfCategories(model)); 12 //Map<String カテゴリ名, List<String 選択肢名>> を取得 13 14 return "genre/index"; 15 } 16 17 @GetMapping("/search")  //検索ボタンが押されたら 18 public String searchGenre(Model model, @RequestParam(name="genreNamesOfCategories") Map<String, List<String>> genreNamesOfCategories) { 19 20 model.addAttribute("genreNamesOfCategories", genreNamesOfCategories); 21 return "genre/index"; 22 } 23 24  …… 25

■ビュー

HTML

1<form action="/genre/search" method="get"> 2 <input type="hidden" name="genreNamesOfCategories" th:value="${genreNamesOfCategories}"> 3 4 <table class="table"> 5 <tr th:each="genres : ${genreNamesOfCategories.get('category1')}"> 6 <td th:text="${genres}"></td> 7 <td><input type="checkbox" name="genreNameaa" th:value="${genres}"></td> 8 </table> 9 10 <table class="table"> 11 <tr th:each="genres : ${genreNamesOfCategories.get('category2')}"> 12 <td th:text="${genres}"></td> 13 <td><input type="checkbox" name="genreNameaa" th:value="${genres}"></td> 14 </table> 15 16  …… 17

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sakura2685

2020/05/13 14:48

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

回答1

0

ベストアンサー

エラーの原因

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

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

html

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

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

html

1<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でその都度データを取得する方法だと思います。
こうすればリクエストパラメータの改ざんの可能性に対応する必要もなくなります。

java

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

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

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

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

解決方法2

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

CategoryAnnotation

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

java

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

CategoryArgumentResolver

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

java

1@Component 2public class CategoryArgumentResolver implements HandlerMethodArgumentResolver { 3 4 @Autowired 5 private ObjectMapper objectMapper; 6 7 @Override 8 public boolean supportsParameter(MethodParameter parameter) { 9 return parameter.hasParameterAnnotation(CategoryAnnotation.class); 10 } 11 12 @Override 13 public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 14 String jsonCategories = webRequest.getParameter("genreNamesOfCategories"); 15 Map<String, List<String>> genreNamesOfCategories = objectMapper.readValue(jsonCategories, new TypeReference<>(){}); 16 return genreNamesOfCategories; 17 } 18}

MyWebMvcConfigurer

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

java

1@Configuration 2public class MyWebMvcConfigurer implements WebMvcConfigurer { 3 4 @Autowired 5 CategoryArgumentResolver categoryArgumentResolver; 6 7 @Override 8 public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { 9 resolvers.add(categoryArgumentResolver); 10 } 11}

GenreController

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

java

1@Autowired 2ObjectMapper objectMapper; 3 4@GetMapping("") 5public String genre(Model model) throws JsonProcessingException { 6 7 Map<String, List<String>> genreNamesOfCategories = genreService.getGenreNamesOfCategories(model); 8 9 // 1) 10 String jsonCategories = objectMapper.writeValueAsString(genreNamesOfCategories); 11 model.addAttribute("jsonCategories", jsonCategories); 12 13 model.addAttribute("genreNamesOfCategories", genreNamesOfCategories); 14 //Map<String カテゴリ名, List<String 選択肢名>> を取得 15 16 return "genre/index"; 17} 18 19 20@GetMapping("/search") 21public String searchGenre(Model model, 22 // 2) 23 @CategoryAnnotation Map<String, List<String>> genreNamesOfCategories) throws JsonProcessingException { 24 25 // 3) 26 String jsonCategories = objectMapper.writeValueAsString(genreNamesOfCategories); 27 model.addAttribute("jsonCategories", jsonCategories); 28 29 model.addAttribute("genreNamesOfCategories", genreNamesOfCategories); 30 return "genre/index"; 31}

genre/index.html

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

html

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

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

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

投稿2020/05/14 04:53

rubytomato

総合スコア1752

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sakura2685

2020/05/14 10:17

やはりビューを経由すれば文字列ですよね…初歩的な質問に丁寧に回答くださいましてありがとうございます。 jsonを使う方法もあるんですか。。 良い勉強の機会でもあるのでしょうが、ちょっとまだ力量不足なので(^^;)今回は解決策1で対応することにします。 改ざんリスクの低減といった視点も勉強になりました! この度はありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問