mike-neckのブログ

JavaかJavaFXかJavaEE(なんかJava8が多め)

Spring MVC で日本語URLにリダイレクトするとURLが文字化けしてしまう場合の対処

Spring MVC で日本語を含むURLにリダイレクトすると…

@GetMapping(path = "bar")
String somePage() {
  return "redirect:/to/日本語url";
}
$ curl -v http://localhost:8080/foo/bar
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /it/japanese HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 302 
< Location: http://localhost:8080/to/???url
< Content-Language: ja-JP
< Content-Length: 0
< Date: Wed, 03 Jan 2018 17:03:47 GMT
< 
* Connection #0 to host localhost left intact

日本語urlにリダイレクトされてほしいけど、日本語がURLエンコーディングされてなくて残念なやつ。

ちょっと調べたのでメモ。

仕組み

  1. ViewResolver (実装は UrlBasedViewResolver/AbstractCachingViewResolver) の resolveViewName から createView を経て RedirectView に解決される。
  2. 解決された RedirectViewDispatcherServlet から View#render(Map, HttpServletRequest, HttpServletResponse) が呼び出される。
  3. RedirectView の継承元の AbstractViewrender メソッドの実装があり、そこで必要なモデルを抽出、ヘッダーに値を設定した後に renderMergedOutputModel を呼び出す。
  4. RedirectView#renderMergedOutputModel(Map, HttpServletRequest, HttpServletResponse) にて引数として返したurlにクエリーなどを付加したurlを作る。
  5. RedirectView で先ほどのurlを HttpServletResponse.sendRedirect(String) にそのまま流す。

単純には次のような形で返すURLをエンコーディングすればよいだけ。

@GetMapping(path = "bar")
String somePage() throws Exception {
  return "redirect:/to/" + URLEncoder.encode("日本語", "UTF-8") + "url" ;
}

とはいえ、日本語URLが大量にあるアプリケーションだと、それもつらいので、次のようなクラスを作ってリダイレクトしてみるとよいかもしれない。

public class EncodingRedirectView extends RedirectView {

  public EncodingRedirectView(final String url, final boolean contextRelative) {
    super(encodeUrl(url), contextRelative);
  }

  private static String encodeUrl(final String url) {
    return Arrays.stream(url.split("/")).map(unhandle(p -> URLEncoder.encode(p, "UTF-8"))).collect(joining("/"));
  }

  private interface UnHandleFunction<A, B> {
    B apply(final A a) throws Exception;
  }

  private static <A, B> Function<A, B> unhandle(final UnHandleFunction<? super A, ? extends B> function) {
    Objects.requireNonNull(function);
    return a -> {
      try {
        return function.apply(a);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    };
  }
}

むしろよい方法あったら、誰か教えて…


2018/01/04 9:42 追記

次のように RedirectAttributes を使う方法があるらしい

@GetMapping
String somePage(final RedirectAttributes attributes) {
  attributes.addAttribute("path", "日本語url");
  return "redirect:/to/{path}";
}

Spring Rest Repositories + JPA で @ManyToOne で指定されているフィールドを用いて検索するリクエストを投げる

ドキュメント探しても、ググってもstack overflowを探しても明確に書かれていなかったっぽいので(本来的には hal の仕様を見るのが正しいかもしれない)

次のような二つのエンティティが存在するものとする。

@Entity
class Book {
  @Id
  private Integer id;
  private String title;
  // getter/setter 省略
}
class Review {
  @Id
  private Integer id;
  @ManyToOne(optional = false)
  private Book book;
  private String text;
  private LocalDateTime created;
  // getter/setter 省略
}

この Review クラスのレポジトリーを次のように作る

@Repository
interface ReviewRepository extends PagingAndSortingRepository<Review, Integer> {
  @RestResource(path = "by_book")
  Page<Review> findByBookOrderByCreatedDesc(@Param("book") final Book book, final Pageable pageable);
}

Spring Rest Repository で 関連するオブジェクトのレコードはオブジェクトそのものではなく、 link(URI) で表されるので、オブジェクトについても URI で指定できる。したがって Review リポジトリーを Book で検索する(上記findByBookOrderByCreatedDesc を呼び出す)ためのリクエストは次のようになる(アプリケーションはポート8080、コンテキストパスは book-repo とする)。

$ curl "http://localhost:8080/book-repo/reviews/search/by_book?book=http://localhost:8080/book-repo/books/2"

SpringのRestTemplate で Rest Repository が返す application/hal+json からオブジェクトを取得する

何か日本語でなかったため。

実行するのは spring-web が jar にある Java SE 環境(Spring context は存在しない)。


  • 必要なライブラリー
    • org.springframework.hateoas:spring-hateoas とその依存ライブラリー
  • 使ったバージョン
    • spring-core: 4.3.13.RELEASE
    • spring-hateoas: 0.23.0.RELEASE
    • (spring-boot: 1.5.9.RELEASE)

public static void main(String[] args) {
    final ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jackson2HalModule())
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
    final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper);
    converter.setSupportedMediaTypes(Collections.singletonList(MediaTypes.HAL_JSON));
    final RestTemplate restTemplate = new RestTemplate(Collections.singletonList(converter));
    final ResponseEntity<PagedResources<Resource<SomeData>>> entity =
            restTemplate.exchange("http://localhost:5000/app/some-data",
                    HttpMethod.GET,
                    null,
                    new ParameterizedTypeReference<PagedResources<Resource<SomeData>>>() {});
    final List<SomeData> list = entity.getBody().getContent().stream()
            .map(Resource::getContent)
            .collect(toList());
    System.out.println(list);
}

Spring Data REST(+ Spring Data JPA)でPOSTする時のメモ

Spring Data REST + Spring Data JPA で こんなエンティティを作る。

@Entity
public class User {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;
  private String name;
}
@Entity
public class Message {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;
  @ManyToOne(optional = false)
  private User user;
  private String text;
}

このとき、新しい User を作る場合はこんな感じになる。

$ curl -X POST http://localhost:8080/user -H "Content-Type:application/json" -d "{\"name\":\"foo\"}"
{
  "name" : "foo",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/user/1"
    },
    "user" : {
      "href" : "http://localhost:8080/user/1"
    }
  }
}

Message を作る場合、 User の参照をどのように指定するかわからなかったのでググった。

user には userhref URL を指定するらしい。

$ curl -X POST http://localhost:8080/message -H "Content-Type:application/json" -d "{\"text\":\"hello\",\"user\":\"http://localhost:8080/user/1\"}"
{
  "text" : "hello",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/messages/1"
    },
    "message" : {
      "href" : "http://localhost:8080/messages/1"
    },
    "user" : {
      "href" : "http://localhost:8080/messages/1/user"
    }
  }
}