• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.69
today-context/src/main/java/infra/validation/beanvalidation/MethodValidationInterceptor.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.validation.beanvalidation;
19

20
import org.aopalliance.intercept.MethodInterceptor;
21
import org.aopalliance.intercept.MethodInvocation;
22
import org.jspecify.annotations.Nullable;
23

24
import java.lang.reflect.Method;
25
import java.lang.reflect.Parameter;
26
import java.util.Collections;
27
import java.util.List;
28
import java.util.Set;
29
import java.util.function.Supplier;
30

31
import infra.aop.ProxyMethodInvocation;
32
import infra.beans.factory.FactoryBean;
33
import infra.beans.factory.SmartFactoryBean;
34
import infra.core.MethodParameter;
35
import infra.core.OrderedSupport;
36
import infra.core.ReactiveAdapter;
37
import infra.core.ReactiveAdapterRegistry;
38
import infra.core.ReactiveStreams;
39
import infra.core.annotation.AnnotationUtils;
40
import infra.lang.Assert;
41
import infra.lang.VisibleForTesting;
42
import infra.util.ReflectionUtils;
43
import infra.validation.BeanPropertyBindingResult;
44
import infra.validation.Errors;
45
import infra.validation.annotation.Validated;
46
import infra.validation.method.MethodValidationException;
47
import infra.validation.method.MethodValidationResult;
48
import infra.validation.method.ParameterErrors;
49
import infra.validation.method.ParameterValidationResult;
50
import jakarta.validation.ConstraintViolation;
51
import jakarta.validation.ConstraintViolationException;
52
import jakarta.validation.Valid;
53
import jakarta.validation.Validator;
54
import jakarta.validation.ValidatorFactory;
55
import jakarta.validation.executable.ExecutableValidator;
56
import reactor.core.publisher.Flux;
57
import reactor.core.publisher.Mono;
58

59
/**
60
 * An AOP Alliance {@link MethodInterceptor} implementation that delegates to a
61
 * JSR-303 provider for performing method-level validation on annotated methods.
62
 *
63
 * <p>Applicable methods have JSR-303 constraint annotations on their parameters
64
 * and/or on their return value (in the latter case specified at the method level,
65
 * typically as inline annotation).
66
 *
67
 * <p>E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)}
68
 *
69
 * <p>Validation groups can be specified through Framework's {@link Validated} annotation
70
 * at the type level of the containing target class, applying to all public service methods
71
 * of that class. By default, JSR-303 will validate against its default group only.
72
 *
73
 * <p>this functionality requires a Bean Validation 1.1+ provider.
74
 *
75
 * @author Juergen Hoeller
76
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
77
 * @see MethodValidationPostProcessor
78
 * @see ExecutableValidator
79
 * @since 4.0
80
 */
81
public class MethodValidationInterceptor extends OrderedSupport implements MethodInterceptor {
82

83
  private final MethodValidationAdapter delegate;
84

85
  @VisibleForTesting
86
  final boolean adaptViolations;
87

88
  /**
89
   * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
90
   */
91
  public MethodValidationInterceptor() {
92
    this(new MethodValidationAdapter(), false);
6✔
93
  }
1✔
94

95
  /**
96
   * Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
97
   *
98
   * @param validatorFactory the JSR-303 ValidatorFactory to use
99
   */
100
  public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
101
    this(new MethodValidationAdapter(validatorFactory), false);
×
102
  }
×
103

104
  /**
105
   * Create a new MethodValidationInterceptor using the given JSR-303 Validator.
106
   *
107
   * @param validator the JSR-303 Validator to use
108
   */
109
  public MethodValidationInterceptor(Validator validator) {
110
    this(validator, false);
×
111
  }
×
112

113
  /**
114
   * Create a new MethodValidationInterceptor for the supplied
115
   * (potentially lazily initialized) Validator.
116
   *
117
   * @param validator a Supplier for the Validator to use
118
   */
119
  public MethodValidationInterceptor(Supplier<Validator> validator) {
120
    this(validator, false);
×
121
  }
×
122

123
  /**
124
   * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
125
   */
126
  public MethodValidationInterceptor(boolean adaptViolations) {
127
    this(new MethodValidationAdapter(), adaptViolations);
6✔
128
  }
1✔
129

130
  /**
131
   * Create a new MethodValidationInterceptor for the supplied
132
   * (potentially lazily initialized) Validator.
133
   *
134
   * @param validator the JSR-303 Validator to use
135
   * @param adaptViolations whether to adapt {@link ConstraintViolation}s, and
136
   * if {@code true}, raise {@link MethodValidationException}, of if
137
   * {@code false} raise {@link ConstraintViolationException} instead
138
   */
139
  public MethodValidationInterceptor(Validator validator, boolean adaptViolations) {
140
    this(new MethodValidationAdapter(validator), adaptViolations);
7✔
141
  }
1✔
142

143
  /**
144
   * Create a new MethodValidationInterceptor for the supplied
145
   * (potentially lazily initialized) Validator.
146
   *
147
   * @param validator a Supplier for the Validator to use
148
   * @param adaptViolations whether to adapt {@link ConstraintViolation}s, and
149
   * if {@code true}, raise {@link MethodValidationException}, of if
150
   * {@code false} raise {@link ConstraintViolationException} instead
151
   */
152
  public MethodValidationInterceptor(Supplier<Validator> validator, boolean adaptViolations) {
153
    this(new MethodValidationAdapter(validator), adaptViolations);
7✔
154
  }
1✔
155

156
  private MethodValidationInterceptor(MethodValidationAdapter validationAdapter, boolean adaptViolations) {
2✔
157
    this.delegate = validationAdapter;
3✔
158
    this.adaptViolations = adaptViolations;
3✔
159
  }
1✔
160

161
  @Override
162
  @Nullable
163
  @SuppressWarnings("NullAway")
164
  public Object invoke(MethodInvocation invocation) throws Throwable {
165
    // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
166
    if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
5✔
167
      return invocation.proceed();
3✔
168
    }
169

170
    Object target = getTarget(invocation);
3✔
171
    Method method = invocation.getMethod();
3✔
172
    @Nullable Object[] arguments = invocation.getArguments();
3✔
173
    Class<?>[] groups = determineValidationGroups(invocation);
4✔
174

175
    if (ReactiveStreams.reactorPresent) {
2!
176
      arguments = ReactorValidationHelper.insertAsyncValidation(
4✔
177
              delegate.getValidatorAdapter(), this.adaptViolations, target, method, arguments);
6✔
178
    }
179

180
    Set<ConstraintViolation<Object>> violations;
181

182
    if (this.adaptViolations) {
3✔
183
      this.delegate.applyArgumentValidation(target, method, null, arguments, groups);
9✔
184
    }
185
    else {
186
      violations = this.delegate.invokeValidatorForArguments(target, method, arguments, groups);
8✔
187
      if (!violations.isEmpty()) {
3✔
188
        throw new ConstraintViolationException(violations);
5✔
189
      }
190
    }
191

192
    Object returnValue = invocation.proceed();
3✔
193

194
    if (this.adaptViolations) {
3✔
195
      this.delegate.applyReturnValueValidation(target, method, null, returnValue, groups);
9✔
196
    }
197
    else {
198
      violations = this.delegate.invokeValidatorForReturnValue(target, method, returnValue, groups);
8✔
199
      if (!violations.isEmpty()) {
3✔
200
        throw new ConstraintViolationException(violations);
5✔
201
      }
202
    }
203

204
    return returnValue;
2✔
205
  }
206

207
  private static Object getTarget(MethodInvocation invocation) {
208
    Object target = invocation.getThis();
3✔
209
    if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) {
8!
210
      // Allow validation for AOP proxy without a target
211
      target = methodInvocation.getProxy();
3✔
212
    }
213
    Assert.state(target != null, "Target is required");
6!
214
    return target;
2✔
215
  }
216

217
  private boolean isFactoryBeanMetadataMethod(Method method) {
218
    Class<?> clazz = method.getDeclaringClass();
3✔
219

220
    // Call from interface-based proxy handle, allowing for an efficient check?
221
    if (clazz.isInterface()) {
3✔
222
      return (clazz == FactoryBean.class || clazz == SmartFactoryBean.class)
8✔
223
              && !method.getName().equals("getObject");
7!
224
    }
225

226
    // Call from CGLIB proxy handle, potentially implementing a FactoryBean method?
227
    Class<?> factoryBeanType = null;
2✔
228
    if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
4!
229
      factoryBeanType = SmartFactoryBean.class;
×
230
    }
231
    else if (FactoryBean.class.isAssignableFrom(clazz)) {
4✔
232
      factoryBeanType = FactoryBean.class;
2✔
233
    }
234
    return factoryBeanType != null
4✔
235
            && !method.getName().equals("getObject")
6!
236
            && ReflectionUtils.hasMethod(factoryBeanType, method);
5✔
237
  }
238

239
  /**
240
   * Determine the validation groups to validate against for the given method invocation.
241
   * <p>Default are the validation groups as specified in the {@link Validated} annotation
242
   * on the method, or on the containing target class of the method, or for an AOP proxy
243
   * without a target (with all behavior in advisors), also check on proxied interfaces.
244
   *
245
   * @param invocation the current MethodInvocation
246
   * @return the applicable validation groups as a Class array
247
   */
248
  protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
249
    Object target = getTarget(invocation);
3✔
250
    return delegate.determineValidationGroups(target, invocation.getMethod());
7✔
251
  }
252

253
  /**
254
   * Helper class to decorate reactive arguments with async validation.
255
   */
256
  private static final class ReactorValidationHelper {
257

258
    private static final ReactiveAdapterRegistry reactiveAdapterRegistry =
1✔
259
            ReactiveAdapterRegistry.getSharedInstance();
2✔
260

261
    @Nullable
262
    static Object[] insertAsyncValidation(InfraValidatorAdapter validatorAdapter,
263
            boolean adaptViolations, Object target, Method method, @Nullable Object[] arguments) {
264

265
      for (int i = 0; i < method.getParameterCount(); i++) {
8✔
266
        if (arguments[i] == null) {
4✔
267
          continue;
1✔
268
        }
269
        Class<?> parameterType = method.getParameterTypes()[i];
5✔
270
        ReactiveAdapter reactiveAdapter = reactiveAdapterRegistry.getAdapter(parameterType);
4✔
271
        if (reactiveAdapter == null || reactiveAdapter.isNoValue()) {
2!
272
          continue;
×
273
        }
274
        Class<?>[] groups = determineValidationGroups(method.getParameters()[i]);
×
275
        if (groups == null) {
×
276
          continue;
×
277
        }
278
        MethodParameter param = new MethodParameter(method, i);
×
279
        arguments[i] = (reactiveAdapter.isMultiValue() ?
×
280
                Flux.from(reactiveAdapter.toPublisher(arguments[i])).doOnNext(value ->
×
281
                        validate(validatorAdapter, adaptViolations, target, method, param, value, groups)) :
×
282
                Mono.from(reactiveAdapter.toPublisher(arguments[i])).doOnNext(value ->
×
283
                        validate(validatorAdapter, adaptViolations, target, method, param, value, groups)));
×
284
      }
285
      return arguments;
2✔
286
    }
287

288
    private static Class<?> @Nullable [] determineValidationGroups(Parameter parameter) {
289
      Validated validated = AnnotationUtils.findAnnotation(parameter, Validated.class);
×
290
      if (validated != null) {
×
291
        return validated.value();
×
292
      }
293
      Valid valid = AnnotationUtils.findAnnotation(parameter, Valid.class);
×
294
      if (valid != null) {
×
295
        return new Class<?>[0];
×
296
      }
297
      return null;
×
298
    }
299

300
    @SuppressWarnings("unchecked")
301
    private static <T> void validate(InfraValidatorAdapter validatorAdapter, boolean adaptViolations,
302
            Object target, Method method, MethodParameter parameter, Object argument, Class<?>[] groups) {
303

304
      if (adaptViolations) {
×
305
        Errors errors = new BeanPropertyBindingResult(argument, argument.getClass().getSimpleName());
×
306
        validatorAdapter.validate(argument, errors);
×
307
        if (errors.hasErrors()) {
×
308
          ParameterErrors paramErrors = new ParameterErrors(parameter, argument, errors, null, null, null);
×
309
          List<ParameterValidationResult> results = Collections.singletonList(paramErrors);
×
310
          throw new MethodValidationException(MethodValidationResult.create(target, method, results));
×
311
        }
312
      }
×
313
      else {
314
        Set<ConstraintViolation<T>> violations = validatorAdapter.validate((T) argument, groups);
×
315
        if (!violations.isEmpty()) {
×
316
          throw new ConstraintViolationException(violations);
×
317
        }
318
      }
319
    }
×
320
  }
321

322
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc