web-dev-qa-db-ja.com

Springのオブジェクトのリストの検証

次のコントローラーメソッドがあります。

@RequestMapping(value="/map/update", method=RequestMethod.POST, produces = "application/json; charset=utf-8")
@ResponseBody
public ResponseEntityWrapper updateMapTheme(
        HttpServletRequest request, 
        @RequestBody @Valid List<CompanyTag> categories,
        HttpServletResponse response
        ) throws ResourceNotFoundException, AuthorizationException {
...
}

CompanyTagは次のように定義されます。

public class CompanyTag {
    @StringUUIDValidation String key;
    String value;
    String color;
    String icon;
    Icon iconObj;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }
   ...
}

問題は、検証がトリガーされず、CompanyTagリストが検証されず、「StringUUIDValidation」バリデーターが呼び出されないことです。

リストを削除して、次の代わりに単一のCompanyTagのみを送信しようとした場合:

@RequestBody @Valid List<CompanyTag> categories,

つかいます:

@RequestBody @Valid CompanyTag category,

それは期待通りに動作するので、明らかにSpringは物事のリストを検証することを好まない(代わりに配列で試したが、どちらも動作しなかった)。

誰が何が欠けているのか分かりますか?

38
TheZuck

うまくいく別のアプローチを見つけました。基本的な問題は、サービスの入力ペイロードとしてリストを作成したいが、javax.validationはリストを検証せず、JavaBeanだけを検証することです。秘Theは、List and JavaBeanの両方として機能するカスタムリストクラスを使用することです。

@RequestBody @Valid List<CompanyTag> categories

への変更:

@RequestBody @Valid ValidList<CompanyTag> categories

リストのサブクラスは次のようになります。

public class ValidList<E> implements List<E> {

    @Valid
    private List<E> list;

    public ValidList() {
        this.list = new ArrayList<E>();
    }

    public ValidList(List<E> list) {
        this.list = list;
    }

    // Bean-like methods, used by javax.validation but ignored by JSON parsing

    public List<E> getList() {
        return list;
    }

    public void setList(List<E> list) {
        this.list = list;
    }

    // List-like methods, used by JSON parsing but ignored by javax.validation

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public boolean isEmpty() {
        return list.isEmpty();
    }

    // Other list methods ...
}
47
Paul Strack

オリジン回答

私はプロジェクトでPaulの方法を使用しようとしましたが、一部の人々はそれが複雑すぎると言いました。それから間もなく、次のコードのように機能する別の簡単な方法を見つけました。

@Validated
@RestController
@RequestMapping("/parent")
public class ParentController {

  private FatherRepository fatherRepository;

  /**
   * DI
   */
  public ParentController(FatherRepository fatherRepository) {
    this.fatherRepository = fatherRepository;
  }

  @PostMapping("/test")
  public void test(@RequestBody @Valid List<Father> fathers) {

  }
}

動作し、使いやすいです。キーポイントは、クラスの@Valiatedアノテーションです。ところで、私が使用しているのはspringBootVersion = '2.0.4.RELEASE'です。

例外ハンドル部分の更新

コメントで述べたように、ここでは次のコードのように例外を処理する必要があります。

@RestControllerAdvice
@Component
public class ControllerExceptionHandler {

  /**
   * handle controller methods parameter validation exceptions
   *
   * @param exception ex
   * @return wrapped result
   */
  @ExceptionHandler
  @ResponseBody
  @ResponseStatus(HttpStatus.OK)
  public DataContainer handle(ConstraintViolationException exception) {

    Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
    StringBuilder builder = new StringBuilder();
    for (ConstraintViolation<?> violation : violations) {
      builder.append(violation.getMessage());
      break;
    }
    DataContainer container = new DataContainer(CommonCode.PARAMETER_ERROR_CODE, builder.toString());
    return container;
  }
}

ネットワークを表すものとしてhttpステータスコードを使用しても問題ありません。最初の違反メッセージのみがここに返されます。カスタマイズした要件を満たすように変更できます。

11
Lebecca

リストカテゴリをDTO Beanにラップして検証することをお勧めします。有効な検証に加えて、より柔軟なAPIを利用できます。

@RequestMapping(value="/map/update", method=RequestMethod.POST, produces = "application/json; charset=utf-8")
@ResponseBody
public ResponseEntityWrapper updateMapTheme(
    HttpServletRequest request, 
    @RequestBody @Valid TagRequest tagRequest,
    HttpServletResponse response
    ) throws ResourceNotFoundException, AuthorizationException {
...
}

public static class TagRequest {
    @Valid
    List<CompanyTag> categories;    
    // Gettes setters
}
9
Babl

最もエレガントなソリューションは、コレクションのカスタムバリデーターとWebDataBindersでそのバリデーターを登録する@ControllerAdviceを作成することだと思います コントローラーメソッドのコレクションにバインドされたRequestBodyパラメーターの春の検証

5
eruiz

@Paul Strack's great solution Lombok magicと混合:

@Data
public class ValidList<E> implements List<E> {
    @Valid
    @Delegate
    private List<E> list = new ArrayList<>();
}

使用法(ValidListのスワップリスト):

public ResponseEntityWrapper updateMapTheme(
        @RequestBody @Valid ValidList<CompanyTag> categories, ...)

(Needs Lombok 、しかしまだ使用していない場合は、実際に試してみたい)

4
laffuste

コレクションの検証は直接機能しません。

たとえば、複数の要素が検証に失敗した場合はどうすればよいですか?最初の検証後に停止しますか?すべてを検証する(もしそうなら、メッセージのコレクションをどうするか)

構成でSpringがHibernate ValidatorなどのBean Validatorプロバイダーに委任する場合は、そこでコレクションバリデーターを実装する方法を調べる必要があります。

Hibernateについては、同様の問題について説明します here

3

@ Validatedコントローラーに注釈を付ける
use @ Valid @RequestBodyに注釈を付けます

1
ShaneSun

Spring-boot 1.5.19.RELEASEを使用しています

サービスに注釈を付ける@validatedを適用してから@ValidをメソッドのListパラメーターに追加すると、リスト内のアイテムが検証されます。

@Data
@ApiModel
@Validated
public class SubscriptionRequest {
    @NotBlank()
    private String soldToBpn;

    @NotNull
    @Size(min = 1)
    @Valid
    private ArrayList<DataProducts> dataProducts;

    private String country;

    @NotNull
    @Size(min = 1)
    @Valid
    private ArrayList<Contact> contacts;
}

サービスインターフェイス(またはインターフェイスがない場合は具象型で使用)

@Validated
public interface SubscriptionService {
    List<SubscriptionCreateResult> addSubscriptions(@NonNull @Size(min = 1) @Valid List<SubscriptionRequest> subscriptionRequestList)
        throws IOException;
}

グローバル例外ハンドラーメソッド(ApiError Typeは私の設計ではありません)

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseBody
public ApiError[] handleConstraintViolationException(ConstraintViolationException exception) {
    List<InvalidField> invalidFields = exception.getConstraintViolations().stream()
        .map(constraintViolation -> new InvalidField(constraintViolation.getPropertyPath().toString(),
                                                     constraintViolation.getMessage(),
                                                     constraintViolation.getInvalidValue()))
        .collect(Collectors.toList());
    return new ApiError[] {new ApiError(ErrorCodes.INVALID_PARAMETER, "Validation Error", invalidFields)};
}

コントローラーからの悪いメソッド呼び出しの例

 LinkedList<SubscriptionRequest> list = new LinkedList<>();
 list.add(new SubscriptionRequest());
 return subscriptionService.addSubscriptions(list);

応答本文(インデックス[0]に注意)

[
    {
        "errorCode": "invalid.parameter",
        "errorMessage": "Validation Error",
        "invalidFields": [
            {
                "name": "addSubscriptions.arg0[0].soldToBpn",
                "message": "may not be empty",
                "value": null
            },
            {
                "name": "addSubscriptions.arg0[0].dataProducts",
                "message": "may not be null",
                "value": null
            },
            {
                "name": "addSubscriptions.arg0[0].contacts",
                "message": "may not be null",
                "value": null
            }
        ]
    }
]
1

エンティティクラスを作成します。

import javax.validation.Valid;
import Java.util.List;

public class ValidList<E> {

    @Valid
    private List<E> list;

    public List<E> getList() {
        return list;
    }

    public void setList(List<E> list) {
        this.list = list;
    }
}

コントローラーを使用する

    @RequestMapping(value = "/sku", method = RequestMethod.POST)
    public JsonResult createSKU(@Valid @RequestBody ValidList<Entity> entityList, BindingResult bindingResult) {
        if (bindingResult.hasErrors())
            return ErrorTools.build().handlerError(bindingResult);
        return new JsonResult(200, "result");
    }
0
QiongLee