Ich habe ein Befehlsobjekt:
public class Job {
private String jobType;
private String location;
}
Welches ist von spring-mvc gebunden:
@RequestMapping("/foo")
public Strnig doSomethingWithJob(Job job) {
...
}
Welches funktioniert gut für http://example.com/foo?jobType=permanent&location=Stockholm
. Aber jetzt muss es stattdessen für die folgende URL funktionieren:
http://example.com/foo?jt=permanent&loc=Stockholm
Natürlich möchte ich mein Befehlsobjekt nicht ändern, da die Feldnamen lang bleiben müssen (wie sie im Code verwendet werden). Wie kann ich das anpassen? Gibt es eine Möglichkeit, so etwas zu tun:
public class Job {
@RequestParam("jt")
private String jobType;
@RequestParam("loc")
private String location;
}
Dies funktioniert nicht ( @RequestParam
kann nicht auf Felder angewendet werden).
Ich denke an einen benutzerdefinierten Nachrichtenkonverter FormHttpMessageConverter
, der einer benutzerdefinierten Anmerkung zum Zielobjekt ähnelt und diese liest
java
spring
spring-mvc
Bozho
quelle
quelle
Antworten:
Diese Lösung ist prägnanter, erfordert jedoch die Verwendung von RequestMappingHandlerAdapter, den Spring bei Aktivierung verwendet
<mvc:annotation-driven />
. Hoffe es wird jemandem helfen. Die Idee ist, ServletRequestDataBinder wie folgt zu erweitern:/** * ServletRequestDataBinder which supports fields renaming using {@link ParamName} * * @author jkee */ public class ParamNameDataBinder extends ExtendedServletRequestDataBinder { private final Map<String, String> renameMapping; public ParamNameDataBinder(Object target, String objectName, Map<String, String> renameMapping) { super(target, objectName); this.renameMapping = renameMapping; } @Override protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { super.addBindValues(mpvs, request); for (Map.Entry<String, String> entry : renameMapping.entrySet()) { String from = entry.getKey(); String to = entry.getValue(); if (mpvs.contains(from)) { mpvs.add(to, mpvs.getPropertyValue(from).getValue()); } } } }
Geeigneter Prozessor:
/** * Method processor supports {@link ParamName} parameters renaming * * @author jkee */ public class RenamingProcessor extends ServletModelAttributeMethodProcessor { @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; //Rename cache private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>(); public RenamingProcessor(boolean annotationNotRequired) { super(annotationNotRequired); } @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) { Object target = binder.getTarget(); Class<?> targetClass = target.getClass(); if (!replaceMap.containsKey(targetClass)) { Map<String, String> mapping = analyzeClass(targetClass); replaceMap.put(targetClass, mapping); } Map<String, String> mapping = replaceMap.get(targetClass); ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping); requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest); super.bindRequestParameters(paramNameDataBinder, nativeWebRequest); } private static Map<String, String> analyzeClass(Class<?> targetClass) { Field[] fields = targetClass.getDeclaredFields(); Map<String, String> renameMap = new HashMap<String, String>(); for (Field field : fields) { ParamName paramNameAnnotation = field.getAnnotation(ParamName.class); if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) { renameMap.put(paramNameAnnotation.value(), field.getName()); } } if (renameMap.isEmpty()) return Collections.emptyMap(); return renameMap; } }
Anmerkung:
/** * Overrides parameter name * @author jkee */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ParamName { /** * The name of the request parameter to bind to. */ String value(); }
Frühlingskonfiguration:
<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="ru.yandex.metrika.util.params.RenamingProcessor"> <constructor-arg name="annotationNotRequired" value="true"/> </bean> </mvc:argument-resolvers> </mvc:annotation-driven>
Und schließlich die Verwendung (wie die Bozho-Lösung):
public class Job { @ParamName("job-type") private String jobType; @ParamName("loc") private String location; }
quelle
DateTimeFormat
Annotation erhalten, dh@ParamName
annotierteDate
Felder können zusätzlich mit Annotationen versehen werden@DateTimeFormat(pattern = "yyyy-MM-dd")
.@Configuration public class WebContextConfiguration extends WebMvcConfigurationSupport { @Override protected void addArgumentResolvers( List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(renamingProcessor()); } @Bean protected RenamingProcessor renamingProcessor() { return new RenamingProcessor(true); } }
Beachten Sie, dass undextends WebMvcConfigurationSupport
ersetzt wird .@EnableWebMvc
<mvc:annotation-driven />
addArgumentResolvers
mit einem Bohne Postprozessor: pastebin.com/07ws0uUZFolgendes habe ich zum Arbeiten gebracht:
Zunächst ein Parameter-Resolver:
/** * This resolver handles command objects annotated with @SupportsAnnotationParameterResolution * that are passed as parameters to controller methods. * * It parses @CommandPerameter annotations on command objects to * populate the Binder with the appropriate values (that is, the filed names * corresponding to the GET parameters) * * In order to achieve this, small pieces of code are copied from spring-mvc * classes (indicated in-place). The alternative to the copied lines would be to * have a decorator around the Binder, but that would be more tedious, and still * some methods would need to be copied. * * @author bozho * */ public class AnnotationServletModelAttributeResolver extends ServletModelAttributeMethodProcessor { /** * A map caching annotation definitions of command objects (@CommandParameter-to-fieldname mappings) */ private ConcurrentMap<Class<?>, Map<String, String>> definitionsCache = Maps.newConcurrentMap(); public AnnotationServletModelAttributeResolver(boolean annotationNotRequired) { super(annotationNotRequired); } @Override public boolean supportsParameter(MethodParameter parameter) { if (parameter.getParameterType().isAnnotationPresent(SupportsAnnotationParameterResolution.class)) { return true; } return false; } @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; bind(servletRequest, servletBinder); } @SuppressWarnings("unchecked") public void bind(ServletRequest request, ServletRequestDataBinder binder) { Map<String, ?> propertyValues = parsePropertyValues(request, binder); MutablePropertyValues mpvs = new MutablePropertyValues(propertyValues); MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class); if (multipartRequest != null) { bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } // two lines copied from ExtendedServletRequestDataBinder String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; mpvs.addPropertyValues((Map<String, String>) request.getAttribute(attr)); binder.bind(mpvs); } private Map<String, ?> parsePropertyValues(ServletRequest request, ServletRequestDataBinder binder) { // similar to WebUtils.getParametersStartingWith(..) (prefixes not supported) Map<String, Object> params = Maps.newTreeMap(); Assert.notNull(request, "Request must not be null"); Enumeration<?> paramNames = request.getParameterNames(); Map<String, String> parameterMappings = getParameterMappings(binder); while (paramNames != null && paramNames.hasMoreElements()) { String paramName = (String) paramNames.nextElement(); String[] values = request.getParameterValues(paramName); String fieldName = parameterMappings.get(paramName); // no annotation exists, use the default - the param name=field name if (fieldName == null) { fieldName = paramName; } if (values == null || values.length == 0) { // Do nothing, no values found at all. } else if (values.length > 1) { params.put(fieldName, values); } else { params.put(fieldName, values[0]); } } return params; } /** * Gets a mapping between request parameter names and field names. * If no annotation is specified, no entry is added * @return */ private Map<String, String> getParameterMappings(ServletRequestDataBinder binder) { Class<?> targetClass = binder.getTarget().getClass(); Map<String, String> map = definitionsCache.get(targetClass); if (map == null) { Field[] fields = targetClass.getDeclaredFields(); map = Maps.newHashMapWithExpectedSize(fields.length); for (Field field : fields) { CommandParameter annotation = field.getAnnotation(CommandParameter.class); if (annotation != null && !annotation.value().isEmpty()) { map.put(annotation.value(), field.getName()); } } definitionsCache.putIfAbsent(targetClass, map); return map; } else { return map; } } /** * Copied from WebDataBinder. * * @param multipartFiles * @param mpvs */ protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) { for (Map.Entry<String, List<MultipartFile>> entry : multipartFiles.entrySet()) { String key = entry.getKey(); List<MultipartFile> values = entry.getValue(); if (values.size() == 1) { MultipartFile value = values.get(0); if (!value.isEmpty()) { mpvs.add(key, value); } } else { mpvs.add(key, values); } } } }
Und dann den Parameter Resolver mit einem Postprozessor registrieren. Es sollte registriert sein als
<bean>
:/** * Post-processor to be used if any modifications to the handler adapter need to be made * * @author bozho * */ public class AnnotationHandlerMappingPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String arg1) throws BeansException { return bean; } @Override public Object postProcessBeforeInitialization(Object bean, String arg1) throws BeansException { if (bean instanceof RequestMappingHandlerAdapter) { RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; List<HandlerMethodArgumentResolver> resolvers = adapter.getCustomArgumentResolvers(); if (resolvers == null) { resolvers = Lists.newArrayList(); } resolvers.add(new AnnotationServletModelAttributeResolver(false)); adapter.setCustomArgumentResolvers(resolvers); } return bean; } }
quelle
SupportsAnnotationParameterResolution
,@CommandPattern
und@SupportsCustomizedBinding
, sowie die Importe fürMaps.*
undLists.*
SupportsCustomizedBinding
. Wenn Sie also beide Anmerkungen erstellen, funktioniert der Ansatz!In Spring 3.1 bietet ServletRequestDataBinder einen Hook für zusätzliche Bindungswerte:
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { }
Die ExtendedServletRequestDataBinder-Unterklasse verwendet sie, um URI-Vorlagenvariablen als Bindungswerte hinzuzufügen. Sie können es weiter erweitern, um befehlsspezifische Feldaliasnamen hinzuzufügen.
Sie können RequestMappingHandlerAdapter.createDataBinderFactory (..) überschreiben, um eine benutzerdefinierte WebDataBinder-Instanz bereitzustellen. Aus der Sicht eines Controllers könnte es so aussehen:
@InitBinder public void initBinder(MyWebDataBinder binder) { binder.addFieldAlias("jobType", "jt"); // ... }
quelle
<mvc:annotation-driven />
, oder?MyWebDataBinder
mit@EnableWebMvc
? Ich sehe, ich muss eine Unterklasse bildenExtendedServletRequestDataBinder
und sie durch Unterklasse zurückgebenServletRequestDataBinderFactory
. Jetzt kann ich diese neue Fabrik durch UnterklassenRequestMappingHandlerAdapter
und Überschreiben zurückgebencreateDataBinderFactory()
. Aber wie kann ich Spring MVC zwingen, meine Unterklasse zu verwendenRequestMappingHandlerAdapter
? Es wird erstellt inWebMvcConfigurationSupport
...Danke die Antwort von @jkee.
Hier ist meine Lösung.
Zunächst eine benutzerdefinierte Anmerkung:
@Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ParamName { /** * The name of the request parameter to bind to. */ String value(); }
Ein Kunde DataBinder:
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder { private final Map<String, String> paramMappings; public ParamNameDataBinder(Object target, String objectName, Map<String, String> paramMappings) { super(target, objectName); this.paramMappings = paramMappings; } @Override protected void addBindValues(MutablePropertyValues mutablePropertyValues, ServletRequest request) { super.addBindValues(mutablePropertyValues, request); for (Map.Entry<String, String> entry : paramMappings.entrySet()) { String paramName = entry.getKey(); String fieldName = entry.getValue(); if (mutablePropertyValues.contains(paramName)) { mutablePropertyValues.add(fieldName, mutablePropertyValues.getPropertyValue(paramName).getValue()); } } } }
Ein Parameter-Resolver:
public class ParamNameProcessor extends ServletModelAttributeMethodProcessor { @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE = new ConcurrentHashMap<>(256); public ParamNameProcessor() { super(false); } @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestParam.class) && !BeanUtils.isSimpleProperty(parameter.getParameterType()) && Arrays.stream(parameter.getParameterType().getDeclaredFields()) .anyMatch(field -> field.getAnnotation(ParamName.class) != null); } @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) { Object target = binder.getTarget(); Map<String, String> paramMappings = this.getParamMappings(target.getClass()); ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), paramMappings); requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest); super.bindRequestParameters(paramNameDataBinder, nativeWebRequest); } /** * Get param mappings. * Cache param mappings in memory. * * @param targetClass * @return {@link Map<String, String>} */ private Map<String, String> getParamMappings(Class<?> targetClass) { if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) { return PARAM_MAPPINGS_CACHE.get(targetClass); } Field[] fields = targetClass.getDeclaredFields(); Map<String, String> paramMappings = new HashMap<>(32); for (Field field : fields) { ParamName paramName = field.getAnnotation(ParamName.class); if (paramName != null && !paramName.value().isEmpty()) { paramMappings.put(paramName.value(), field.getName()); } } PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings); return paramMappings; } }
Schließlich eine Bean-Konfiguration zum Hinzufügen von ParamNameProcessor zum ersten der Argumentauflöser:
@Configuration public class WebConfig { /** * Processor for annotation {@link ParamName}. * * @return ParamNameProcessor */ @Bean protected ParamNameProcessor paramNameProcessor() { return new ParamNameProcessor(); } /** * Custom {@link BeanPostProcessor} for adding {@link ParamNameProcessor} into the first of * {@link RequestMappingHandlerAdapter#argumentResolvers}. * * @return BeanPostProcessor */ @Bean public BeanPostProcessor beanPostProcessor() { return new BeanPostProcessor() { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof RequestMappingHandlerAdapter) { RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(adapter.getArgumentResolvers()); argumentResolvers.add(0, paramNameProcessor()); adapter.setArgumentResolvers(argumentResolvers); } return bean; } }; } }
Param Pojo:
@Data public class Foo { private Integer id; @ParamName("first_name") private String firstName; @ParamName("last_name") private String lastName; @ParamName("created_at") @DateTimeFormat(pattern = "yyyy-MM-dd") private Date createdAt; }
Controller-Methode:
@GetMapping("/foos") public ResponseEntity<List<Foo>> listFoos(@RequestParam Foo foo, @PageableDefault(sort = "id") Pageable pageable) { List<Foo> foos = fooService.listFoos(foo, pageable); return ResponseEntity.ok(foos); }
Das ist alles.
quelle
Es gibt keine gut eingebaute Methode, Sie können nur auswählen, welche Problemumgehung Sie anwenden. Der Unterschied zwischen Handhabung
@RequestMapping("/foo") public String doSomethingWithJob(Job job)
und
@RequestMapping("/foo") public String doSomethingWithJob(String stringjob)
ist, dass Job eine Bohne ist und Stringjob nicht (bisher keine Überraschung). Der wirkliche Unterschied besteht darin, dass Beans mit dem Standard-Spring-Bean-Resolver-Mechanismus aufgelöst werden, während String-Parameter von Spring-MVC aufgelöst werden, die das Konzept der @ RequestParam-Annotation kennt. Um es kurz zu machen, es gibt keine Möglichkeit in der Standard-Spring-Bean-Auflösung (die Klassen wie PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor verwendet), "jt" in eine Eigenschaft namens "jobType" aufzulösen, oder zumindest weiß ich nichts darüber.
Die Problemumgehungen könnten so sein, wie andere vorgeschlagen haben, einen benutzerdefinierten PropertyEditor oder einen Filter hinzuzufügen, aber ich denke, das bringt den Code nur durcheinander. Meiner Meinung nach wäre die sauberste Lösung, eine Klasse wie diese zu deklarieren:
public class JobParam extends Job { public String getJt() { return super.job; } public void setJt(String jt) { super.job = jt; } }
Verwenden Sie das dann in Ihrem Controller
@RequestMapping("/foo") public String doSomethingWithJob(JobParam job) { ... }
UPDATE:
Eine etwas einfachere Option besteht darin, nicht zu verlängern, sondern nur die zusätzlichen Getter und Setter zur ursprünglichen Klasse hinzuzufügen
public class Job { private String jobType; private String location; public String getJt() { return jobType; } public void setJt(String jt) { jobType = jt; } }
quelle
Ich möchte Sie in eine andere Richtung weisen. Aber ich weiß nicht, ob es funktioniert .
Ich würde versuchen, die Bindung selbst zu manipulieren.
Dies erfolgt durch
WebDataBinder
und wird von derHandlerMethodInvoker
Methode aufgerufenObject[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception
Ich habe in Spring 3.1 keinen tiefen Einblick, aber was ich gesehen habe, ist, dass dieser Teil des Frühlings sehr verändert wurde. So ist es möglicherweise möglich, den WebDataBinder auszutauschen. In Spring 3.0 sind Nähte nicht möglich, ohne die zu überschreiben
HandlerMethodInvoker
.quelle
Es gibt eine einfache Möglichkeit, einfach eine weitere Setter-Methode hinzuzufügen, z. B. "setLoc, setJt".
quelle
Sie können Jackson com.fasterxml.jackson.databind.ObjectMapper verwenden, um eine beliebige Karte mit verschachtelten Requisiten in Ihre DTO / POJO-Klasse zu konvertieren. Sie müssen Ihre POJOs mit @JsonUnwrapped für verschachtelte Objekte kommentieren. So was:
public class MyRequest { @JsonUnwrapped private NestedObject nested; public NestedObject getNested() { return nested; } }
Und dann benutze es so:
@RequestMapping(method = RequestMethod.GET, value = "/myMethod") @ResponseBody public Object myMethod(@RequestParam Map<String, Object> allRequestParams) { MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class); ... }
Das ist alles. Ein bisschen Codierung. Sie können Ihren Requisiten auch beliebige Namen geben, indem Sie @JsonProperty verwenden.
quelle
Versuchen Sie, die Anforderung mit abzufangen
InterceptorAdaptor
, und entscheiden Sie dann mithilfe eines einfachen Überprüfungsmechanismus, ob die Anforderung an den Controller-Handler weitergeleitet werden soll. Umschließen Sie auchHttpServletRequestWrapper
die Anforderung, damit Sie die Anforderungen überschreiben könnengetParameter()
.Auf diese Weise können Sie den tatsächlichen Parameternamen und seinen Wert auf die Anforderung zurückführen, die von der Steuerung angezeigt werden soll.
Beispieloption:
public class JobInterceptor extends HandlerInterceptorAdapter { private static final String requestLocations[]={"rt", "jobType"}; private boolean isEmpty(String arg) { return (arg !=null && arg.length() > 0); } public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //Maybe something like this if(!isEmpty(request.getParameter(requestLocations[0]))|| !isEmpty(request.getParameter(requestLocations[1])) { final String value = !isEmpty(request.getParameter(requestLocations[0])) ? request.getParameter(requestLocations[0]) : !isEmpty(request .getParameter(requestLocations[1])) ? request.getParameter(requestLocations[1]) : null; HttpServletRequest wrapper = new HttpServletRequestWrapper(request) { public String getParameter(String name) { super.getParameterMap().put("JobType", value); return super.getParameter(name); } }; //Accepted request - Handler should carry on. return super.preHandle(request, response, handler); } //Ignore request if above condition was false return false; } }
Wickeln Sie zum Schluss den
HandlerInterceptorAdaptor
Controller-Handler wie unten gezeigt um. MitSelectedAnnotationHandlerMapping
können Sie angeben, welcher Handler abgefangen werden soll.<bean id="jobInterceptor" class="mypackage.JobInterceptor"/> <bean id="publicMapper" class="org.springplugins.web.SelectedAnnotationHandlerMapping"> <property name="urls"> <list> <value>/foo</value> </list> </property> <property name="interceptors"> <list> <ref bean="jobInterceptor"/> </list> </property> </bean>
BEARBEITET .
quelle
preHandle
Methode gehalten wird. Wenn Sie daher Ihren Anforderungsparameter wie oben gezeigt überprüfen und true zurückgeben, fährt der Controller-Handler mit der Anforderung fort.Jkees Antwort hat sich ein wenig verbessert.
Um die Vererbung zu unterstützen, sollten Sie auch übergeordnete Klassen analysieren.
/** * ServletRequestDataBinder which supports fields renaming using {@link ParamName} * * @author jkee * @author Yauhen Parmon */ public class ParamRenamingProcessor extends ServletModelAttributeMethodProcessor { @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; //Rename cache private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>(); public ParamRenamingProcessor(boolean annotationNotRequired) { super(annotationNotRequired); } @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) { Object target = binder.getTarget(); Class<?> targetClass = Objects.requireNonNull(target).getClass(); if (!replaceMap.containsKey(targetClass)) { replaceMap.put(targetClass, analyzeClass(targetClass)); } Map<String, String> mapping = replaceMap.get(targetClass); ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping); Objects.requireNonNull(requestMappingHandlerAdapter.getWebBindingInitializer()) .initBinder(paramNameDataBinder); super.bindRequestParameters(paramNameDataBinder, nativeWebRequest); } private Map<String, String> analyzeClass(Class<?> targetClass) { Map<String, String> renameMap = new HashMap<>(); for (Field field : targetClass.getDeclaredFields()) { ParamName paramNameAnnotation = field.getAnnotation(ParamName.class); if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) { renameMap.put(paramNameAnnotation.value(), field.getName()); } } if (targetClass.getSuperclass() != Object.class) { renameMap.putAll(analyzeClass(targetClass.getSuperclass())); } return renameMap; } }
Dieser Prozessor analysiert Felder von Superklassen, die mit @ParamName versehen sind. Es wird auch keine
initBinder
Methode mit 2 Parametern verwendet, die ab Spring 5.0 veraltet ist. Der Rest in Jkees Antwort ist in Ordnung.quelle