blog.smart-java.nl
Ordina J-Technologies – Java Blog

Archief ‘Spring’ categorie




Getting on the cloud!

Door: Roy van Rijn, 5 June 2009

Google App Engine

You’ve probably heard people talking before about ‘cloud computing’.
But what exacly is this cloud computing you might ask?

To figure this out I decided to create my own Google App Engine project and find out about cloud computing along the way.

Wikipedia states that cloud computing is:

Cloud computing is a style of computing in which dynamically scalable and often virtualized resources are provided as a service over the Internet. Users need not have knowledge of, expertise in, or control over the technology infrastructure in the “cloud” that supports them”

So, cloud computing doesn’t only have a vague name, the description isn’t very helpful either. But the key ingredients are “scalable” and “virtualized” and “as a service”. And you don’t have control over the infrastructure…

Lets take a look at Google App Engine. It was released in April 2008 as a platform for developing (Python)cloud applications. App Engine hosts these applications virtually on many machines, the programs are distributed and scaled across a vast amount of servers. And now, since the beginning of this year Google App Engine also supports Java!

The concept is simple, you get to build an application, preferably using Google’s App Engine-eclipse-plugin. And the JRE you build on is a slightly stripped-down version to make it usable on a cloud, like a sandbox.

Because you don’t know on what kind of servers your application will run on, or on how many servers, Google has decided you can’t do the following:

  • Start threads
  • Go to the Filesystem, no I/O
  • Open sockets directly, but you can open connections through HTTP/HTTPS
  • Make calls to System (like exit();, gc(); etc)

And there is more. Because of these restrictions you can’t access a database! So you can’t use something like Hibernate and some Oracle/MySQL/Postgress machine. To still be able to save/persist objects Google has teamed up with Datanucleus. Using JDO or (a stripped down version of) JPA you can persist and retrieve objects on the cloud.

With this in mind I started making my own application. And the frameworks I wanted to use are:

  • Wicket (Web Framework)
  • Spring IOC
  • Spring ORM (for transaction management, using annotations)
  • JPA (instead of the default JDO)

The first problems I encountered was getting Wicket to load. Because of the sandbox-restrictions there are a couple of things you can’t do. For example, Wicket can’t save temporary data to disk (what it normally does). And there are problems with Wicket being in ‘development-mode’ where is wants to start Threads to poll for changed resources.

A good overview on what it takes to get Wicket working is explained here:
http://www.danwalmsley.com/2009/04/08/apache-wicket-on-google-app-engine-for-java/

Next up was installing and running Spring. This was relatively easy at first. The core Spring code ran pretty much as expected. I added the JARs to my project and added this to the web.xml:

	<!-- Spring -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:applicationcontext-*.xml</param-value>
	</context-param>
	<listener>
		<listener-class>
			org.springframework.web.context.ContextLoaderListener
		</listener-class>
	</listener>
	<listener>
		<listener-class>
			org.springframework.web.context.request.RequestContextListener
		</listener-class>
	</listener>

As you can see, I load up multiple XML files. I decided to go with the all-out-annotations method using Spring ORM. This proved to be pretty challenging…

With these annotations you are able to do the following in the code:

@Repository("loginDao")
@Transactional
public class LoginDaoImpl implements LoginDao {
 
	@PersistenceContext
	private EntityManager entityManager;
	... (and more)

As you can see I’m using Spring to inject my EntityManager into the DAO. But you can’t just load the entity manager in Google App Engine, you need a specific piece of configuration. I used the following XML:

	<bean id="data.emf"
		class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
		<property name="persistenceUnitName" value="transactions-optional" />
	</bean>

	<bean class="org.springframework.orm.jpa.JpaTemplate">
		<property name="entityManagerFactory" ref="data.emf" />
	</bean>

	<bean id="transactionManager"
		class="org.springframework.orm.jpa.JpaTransactionManager">
		<property name="entityManagerFactory" ref="data.emf" />
	</bean>

To tell Spring to scan for these annotations you need to add the following lines in your applicationcontext:

	<context:annotation-config />
	<context:component-scan base-package="nl.redcode.*" />

And now the problems start… The first problem is that Google App Engine doesn’t support all core classes. When loading these annotations Spring will load its PersistenceAnnotationBeanPostProcessor. But it contains the following piece of code:

try {
	return (EntityManagerFactory) lookup(jndiName, EntityManagerFactory.class);
}
catch (NamingException ex) {
	throw new IllegalStateException("Could not obtain 
		EntityManagerFactory [" + jndiName + "]from JNDI", ex);
}

And the Exception we get is:

org.springframework.beans.factory.BeanCreationException: Error creating
bean with name
'org.springframework.context.annotation.internalPersistenceAnnotationProcessor':
Initialization of bean failed; nested exception is
java.lang.NoClassDefFoundError: javax/naming/NamingException
	at
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:480)
	at
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:409)
	at java.security.AccessController.doPrivileged(Native Method)
	at
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:380)
	at
org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:264)
	at
...etc

And when I looked at the white-list it was clear, NamingException isn’t part of the sandbox Google App Engine uses. So I started to google how to solve this. The first thing I encoutered was somebody who added the following lines to his/her applicationContext:

	<bean id="org.springframework.context.annotation.internalPersistenceAnnotationProcessor"
		class="java.lang.String" />

This piece of code, when executed before the annotation-scan, loads a String in the Spring Container under the name “internalPersistenceAnnotationProcessor”. This causes Spring to ignore its own instantiation of the PersistenceAnnotationBeanPostProcessor and we don’t get the Exception anymore.

But this causes some more damage we don’t want in the application. Before my Dao’s received a valid EntityManager, but they are Null now…!

So I took the code of the original Spring PersistenceAnnotationBeanPostProcessor and replaced all the instances of NamingException with just Exception. This removed the dependency to NamingException. I called this new bean “AppEngineJPAPostProcessor”. This is how I configured it in the applicationContext:

	<bean id="org.springframework.context.annotation.internalPersistenceAnnotationProcessor"
		class="nl.redcode.springhack.AppEngineJPAPostProcessor" />

The EntityManager(Factory) is now created, it gets injected into the DAO’s, they have transactions using annotations and everybody is happy!

When I got a little further in my project I decided to deploy my application to the cloud and test it online. Deploying your application to App Engine is very simple, just push the “Deploy” button in the Eclipse plugin and you only need to enter your credentials and a version-number of your release!

But then the old BeanPostProcessor bit me in the back again. On the server I got the following Exception when deploying:

java.lang.SecurityException: Unable to get members for class org.springframework.jndi.JndiLocatorSupport
	at com.google.apphosting.runtime.security.shared.intercept.java.lang.Class_$10.run(Class_.java:357)
	at com.google.apphosting.runtime.security.shared.intercept.java.lang.Class_$10.run(Class_.java:347)
	at java.security.AccessController.doPrivileged(Native Method)
	at com.google.apphosting.runtime.security.shared.intercept.java.lang.Class_.getMembers(Class_.java:347)
	at com.google.apphosting.runtime.security.shared.intercept.java.lang.Class_.getDeclaredMethods(Class_.java:174)
	at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:460)
	at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:443)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.findAutowiringMetadata(AutowiredAnnotationBeanPostProcessor.java:299)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition(AutowiredAnnotationBeanPostProcessor.java:179)

It seems the runtime is a bit more strict then the development server Google App Engine uses. For some reason it doesn’t like the JndiLocatorSupport. This is excepted because Google App Engine, due to the nature of the cloud, prohibits the use of JNDI.

Soon I found the problem, the only reference to JndiLocatorSupport is my own BeanPostProcessor:

/**
 * Rewritten for use in Google AppEngine
 *
 * @author Roy van Rijn
 */
public class AppEngineJPAPostProcessor extends JndiLocatorSupport implements
		InstantiationAwareBeanPostProcessor, BeanFactoryAware {
 
	private Map persistenceUnits;
	...

When I removed the ‘extends’ part there were two pieces of code that stopped working, they both look like this:

	try {
	return (EntityManagerFactory) lookup(jndiName, EntityManagerFactory.class);
}

It seems this PostProcessor always does a JNDI lookup to find the correct EntityManager! But we can’t do this if we don’t have access to the JndiLocatorSupport methods anymore. So I decided to hack a little bit in this code, my solution was to load the EntityManagerFactory and EntityManager from the Spring container:

try {
	return ((JpaTransactionManager)beanFactory
		.getBean("transactionManager"))
		.getEntityManagerFactory();
/*	return (EntityManagerFactory) lookup(jndiName,
		EntityManagerFactory.class);*/
} catch (Exception ex) {
...

This solved all the problems and with the changes to the BeanPostProcessor I’m now able to use all the Spring annotations, for persistency and transactions, in Google AppEngine.

I’m now still in the middle of developing my application on Google App Engine, but it seems that Google App Engine works like a charm. The problem is, most frameworks can’t really cope with the sandbox out-of-the-box. But with (some) minor patches and tweaking most frameworks will run using Google App Engine. The only major problem I’m having (which can’t be solved) is Datanucleus. I chose to use JPA because it is much richer and has more features then JDO, but Datanucleus hasn’t implemented much of these features yet.

For example, I had the following annotation on a field: @Column(unique=true)
Datanucleus threw “java.lang.UnsupportedOperationException: No support for uniqueness
constraints
“.

Also, I created a query with this: “username = :username OR emailAddress = :emailAddress
But Datanucleus doesn’t support the operator “OR”.

Other things DataNucleus can’t currently do:

  • Many-to-many relationships
  • Joins in a query (WHAT??)
  • Aggregation queries (group by, having, sum, avg, max, min)
  • Polymorphic queries. You cannot perform a query of a class to get instances of a subclass. Each class is represented by a separate entity kind in the datastore.

So, we’ve seen what a cloud is, what Google App Engine is, and I explained some tweaks/patches needed to get Wicket and Spring working.

Why use Google App Engine? It is free (up to some CPU/mail/data limits) and your project runs on a cloud, and thus is very scalable. You don’t have to worry about the environment or server. Your application will scale when its needed and everything is pre-installed and ready to run.
But be prepared for difficult classloading issues and missing classes. You just can’t expect all frameworks to be working out-of-the-box with the sandbox-limitations. Also don’t expect much support with persisting, the JPA-support is very minimal and won’t let you do much more then persisting and retrieving single objects.

Enough blogging, now its time for me again to tinker on my application, maybe I’ll tell about it here in the future!




Cyclische dependencies @ runtime

Door: Frank Verbruggen, 16 April 2009

In een gelaagd project zijn er meestal aparte lagen gedefinieerd voor services, domein logica en data access. Stel nu dat er een noodzaak is om vanuit een lager gelegen laag gebruik te maken van functionaliteit die thuis hoort in een hoger gelegen laag. Bijvoorbeeld omdat er in de domein objecten een noodzaak is om gebruik te maken van een service. Hoe zorg je er dan voor dat je nette object georienteerde code houdt, zonder dat je compile time dependencies krijgt tussen je projecten / lagen? Het hier gepresenteerde design pattern is een elegante object georienteerde oplossing voor het bovenstaande probleem.

Gegeven een DomeinObject als object uit de lagere laag, en een ServiceImplementation als object uit de hogere laag. De serviceImplementation heeft een een interface methode ’someMethod()’ uit de ServiceInterface die benodigd is om de methode ‘businessMethod()’ uit het DomeinObject te implementeren (zie figuur 1, originele klasse diagram).

N.B. Het design pattern is uitgewerkt met domein objecten en service implementaties, maar dat is slechts een invulling.

—————————————————————-

—————————————————————-
Figuur 1, originele klasse diagram
—————————————————————-

Los dit als volgt op. Trek de ServiceInterface uit de hogere laag, en zet deze in de lagere laag. Maak een RequiredInterfaceFactory die een instantie van de ServiceInterface kan bevatten. Initialiseer bij het opstarten van je applicatie vanuit de bovenste laag de RequiredInterfaceFactory met de ServiceImplementation (dit kan bijvoorbeeld goed met de Spring configuratie uit Listing 1, Spring configuratie). En implementeer de businessMethod door de aanroep naar de ServiceImplementation als volgt te abstraheren:

RequiredInterfaceFactory.getInstance().getServiceInterface().someMethod();

Het klasse diagram ziet er dan uit zoals weergegeven in Figuur 2, klasse diagram.

—————————————————————-

 

—————————————————————-
Listing 1, Spring configuratie
—————————————————————-

—————————————————————-

—————————————————————-
Figuur 2, klasse diagram
—————————————————————-

Voordelen en nadelen

Het gebruik van dit design pattern biedt de volgende voordelen:

  • Compile time dependencies zijn niet nodig
  • At runtime kunnen interface methoden van bovenliggende klassen toch gebruikt worden
  • Ieder object kan gebruik maken van de interface, ongeacht in welk package het zit, dus het ophalen van de interface kan in de code altijd op dezelfde manier gebeuren

Het gebruik van dit design pattern biedt de volgende nadelen:

  • Je moet de RequiredInterfaceFactory injecteren met de ServiceImplementation voordat de rest van de applicatie aangesproken wordt



Flat file parsing met Spring Batch

Door: Richard Kettelerij, 7 November 2008

Legacy systemen gebruiken vaak flat files voor gegevens uitwisseling. Bij integratie met dergelijk systemen moeten deze files worden geparst. In zo’n geval kan je natuurlijk zelf met de Java Scanner of StingTokenizer aan de slag gaan maar het is waarschijnlijk verstandiger om een bestaand framework te gebruiken. Spring Batch biedt hiervoor een elegante oplossing.

Mocht je overigens meer willen weten over Spring Batch en batch processing in het algemeen? Schrijf je dan in voor Special Meeting op 18 november a.s.

Bestandsdefinitie
Als voorbeeld gebruiken we een fixed-width file afkomstig uit een legacy systeem van een fictieve online videotheek. Zoals gebruikelijk bij deze vorm van gegevensuitwisseling bestaat het bestand uit verschillende soorten records: Een header record met metadata als een bedrijfsnaam en batchnummer, een footer record met een controlegetal en uiteraard een aantal records met de daadwerkelijke informatie; in dit geval film titels.

H OrdinaVideoStore.nl 12
M Shrek II
M Lord of War
M Godfather, The
M Kungfu Panda
F 0000000000000000006

Tokenizing
Om bovenstaand bestand te kunnen parsen dient allereerst onderscheid te worden gemaakt tussen de verschillende soorten records. Aangezien dit uit het eerste karakter van elk record kan worden afgeleid gebruiken we de PrefixMatchingCompositeLineTokenizer.

<bean id="movieFileLayout"
class="org.springframework.batch.item.file.transform.PrefixMatchingCompositeLineTokenizer">
	<property name="tokenizers">
		<map>
			<entry key="H" value-ref="movieHeaderRecordLayout" />
			<entry key="M" value-ref="movieRecordLayout" />
			<entry key="F" value-ref="movieFooterRecordLayout" />
		</map>
	</property>
</bean>
 
<bean id="movieHeaderRecordLayout"
	class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
	<property name="names" value="recordtype, videostore, batchid" />
	<property name="columns" value="1,5-25,28-30" />
</bean>

Deze tokenizer geeft records op basis van het type door aan een nieuwe LineTokenizer. Zoals je in bovenstande configuratie kunt zien mapped deze tokenizer elke fixed-width kolom naar een bijbehorende kolomnaam. Hier zie je duidelijk de kracht van de configuratie mogelijkheden in Spring. Je geeft gewoon in de application context de ranges van gerelateerde kolommen op en Spring Batch doet de rest. Om deze magic mogelijk te maken heeft Spring wel wat hulp nodig in de vorm van een CustomEditorConfigurer. Deze vertaald de range definities (5-3, etc) naar betekenisvolle Range objecten.

<bean id="customEditorConfigurer"
	class="org.springframework.beans.factory.config.CustomEditorConfigurer">
	<property name="customEditors">
	  <map>
	    <entry key="org.springframework.batch.item.file.transform.Range[]">
	      <bean class="org.springframework.batch.item.file.transform.RangeArrayPropertyEditor" />
	    </entry>
	  </map>
	</property>
</bean>

Mapping naar domein objecten
Vervolgens is het de bedoeling om de kolom/veld definities naar domein objecten te vertalen. Hiervoor heeft Spring Batch de FieldSetMapper interface geïntroduceerd. Hoewel we hiermee zelf de mapping van velden naar domein objecten kunnen verzorgen is het mogelijk om dit aan Spring Batch over te laten middels een FieldSetMapper gebaseerd op Spring’s BeanWrapper. Er moet echter ook onderscheid worden gemaakt tussen de verschillende record soorten. Hiervoor is – in tegenstelling tot de prefix-enabled tokenizer – geen standaard fieldset mapper aanwezig. Daarom schijven we zelf een eenvoudige mapper:

public class PrefixAwareMovieFieldSetMapper implements FieldSetMapper {
	private FieldSetMapper headerFieldSetMapper;
	private FieldSetMapper movieFieldSetMapper;
	private FieldSetMapper footerFieldSetMapper;
 
	public Object mapLine(FieldSet fieldSet) {
		final String recordType = fieldSet.readString("recordtype");
		if (recordType.equals("H")) {
			return headerFieldSetMapper.mapLine(fieldSet);
		}
		else if (recordType.equals("M")) {
			return movieFieldSetMapper.mapLine(fieldSet);
		}
		else if (recordType.equals("F")) {
			return footerFieldSetMapper.mapLine(fieldSet);
		}
		throw new IllegalStateException("onbekend record type");
	}
	// setters
}

Wanneer we deze FieldSetMapper configureren is de mapping van tokens naar domein objecten compleet.

<bean id="movieFileFieldSetMapper"
	class="nl.ordina.batch.PrefixAwareMovieFieldSetMapper">
	<property name="headerFieldSetMapper" ref="movieHeaderFieldSetMapper" />
	<property name="movieFieldSetMapper" ref="movieFieldSetMapper" />
	<property name="footerFieldSetMapper" ref="movieFooterFieldSetMapper" />
</bean>
 
<bean id="movieHeaderFieldSetMapper" 
   class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
	<property name="prototypeBeanName" value="headerRecord" />
</bean>
 
<bean id="headerRecord" class="nl.ordina.batch.model.MovieHeaderRecord"
scope="prototype" />

Parsen
Nu we hebben gedefinieerd welke karakters naar welke velden mappen, en welke velden naar welk domein object mapped wordt het tijd om bestanden te parsen. Hiervoor gebruiken we een FlatFileItemReader die we injecteren met de eerder gecreërde tokenizer en fieldset mapper. Geconfigureerd binnen een batch job ziet dit er als volgt uit. Wanneer deze job wordt uitgevoerd ontvangt de “DummyWriter” voor elke record in het bronbestand een overeenkomstig domein object.

<bean id="videotheekJob" parent="simpleJob">
  <property name="steps">
    <list>
      <bean id="printRecordStep" parent="simpleStep">
	<property name="itemReader">
	   <bean class="org.springframework.batch.item.file.FlatFileItemReader">
	     <property name="resource" ref="file:///videotheek/films.txt" />
	     <property name="lineTokenizer" ref="movieFileLayout" />
	     <property name="fieldSetMapper" ref="movieFileFieldSetMapper" />
	   </bean>
	</property>
	<property name="itemWriter" ref="dummyWriter" />
      </bean>
    </list>
  </property>
</bean>

Zoals je wellicht opvalt wijst de FlatFileItemReader naar een hardcoded filepath. Momenteel biedt Spring Batch nog geen mogelijkheid om dit voor meerdere bestanden configurabel te maken. Dit heb ik inmiddels gemeld via een JIRA ticket en een oplossing bijgevoegd in de vorm van een DynamicMultiResourceItemReader.

Tot slot, Spring Batch biedt m.i. behoorlijke ondersteuning om diverse soorten bestanden te parsen. Dit is met name handig in applicaties die reeds gebruik maken van het Spring Framework. Maar Spring Batch bevat méér en is zeker het overwegen waard buiten pure Spring applicaties.




Spring MVC 2.5: BindingResult probleempje

Door: Jan-Kees van Andel, 9 October 2008

Zojuist liep ik tegen een vervelend probleempje aan. Ik was bezig met het bouwen van een weblog met Spring 2.5 en alles ging goed, tot het laatste schermpje. Namelijk een klein schermpje om je password te wijzigen.

Dit is de controller:

@Controller
@SessionAttributes("changePassword")
public class ProfileController extends AbstractAdminController {
 
    public static final class ChangePasswordBean {
        private String password1;
        private String password2;
        // Getters and setters
    };
 
    @Autowired private BlogService blogService;
 
    @RequestMapping(value="/admin/users/profile", method=RequestMethod.GET)
    public ModelAndView setupForm(ModelMap model, HttpSession session) {
        model.addAttribute("changePassword", new ChangePasswordBean());
        return new ModelAndView("admin/users/profile", model);
    }
 
    @RequestMapping(value="/admin/users/profile", method=RequestMethod.POST)
    public String processSubmit(
            @ModelAttribute ChangePasswordBean changePassword,
            BindingResult result, HttpSession session, SessionStatus status) {
        new PasswordValidator().validate(changePassword, result);
        if (result.hasErrors()) {
            return "admin/users/profile";
        } else {
            // Call to service layer
            return "redirect:/admin/home.html";
        }
    }
}

Dit is de JSP met het form:

  ...
  <form:form modelAttribute="changePassword">
    <form:errors />
    <table class="form">
      <tr>
        <td><form:label for="password1" path="password1">Password</form:label></td>
        <td><form:password id="password1" maxlength="20" path="password1" /></td>
        <td><form:errors path="password1" /></td>
      </tr>
      <tr>
        <td><form:label for="password2" path="password2">Retype password</form:label></td>
        <td><form:password id="password2" maxlength="20" path="password2" /></td>
        <td><form:errors path="password2" /></td>
      </tr>
      <tr>
        <td colspan="3">
          <input type="reset" value="Cancel" /> |
          <input type="submit" value="Save" />
        </td>
      </tr>
    </table>
  </form:form>
  ...

Deze opzet werkte bij alle schermen, maar om de een of andere reden niet voor dit “simpele” schermpje. Het form openen ging goed, submitten ging ook goed, zolang er geen validatiefouten waren, maar bij validatiefouten kreeg ik de volgende melding:

java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'changePassword' available as request attribute
    at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:141)
    at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getBindStatus(AbstractDataBoundFormElementTag.java:172)
    at org.springframework.web.servlet.tags.form.AbstractHtmlElementTag.resolveCssClass(AbstractHtmlElementTag.java:412)
    at org.springframework.web.servlet.tags.form.AbstractHtmlElementTag.writeOptionalAttributes(AbstractHtmlElementTag.java:388)
    ...

Toch creemd. Ik had de JSP en de controller gewoon gekopieerd van een wel werkend scherm, maar om de een of andere reden werkte het toch niet. Ligt het aan de inner class? Nee, vast niet. Hoewel, het is wel een final class. Misschien wil Spring wel een proxy aanmaken en mag de class niet final zijn ofzo. Hup, final eraf. Scheisse, nog steeds hetzelfde probleem. Wat kan het dan zijn?

Oplossing

Toen viel mijn oog op de processSubmit methode in de controller. Daar week de naam van de parameter af van de klassenaam. Class name: ChangePasswordBean, argument name: changePassword.

Hmm, dat zal wel het probleem zijn, want bij alle andere schermen waren deze gelijk. Toevallig wist ik dat in de bytecode van de klasse geen parameternamen staan en dus ook niet @runtime uit te lezen zijn. De mannen van Spring zijn waarschijnlijk tegen dezelfde beperking aan gelopen en hebben hier omheen gewerkt door standaard de type name als request attribute name te gebruiken, in plaats van de parameter naam (wat logischer is, maar dus niet kan in Java).

Dit was de oplossing:

    // Before fix (this didn't work)
    public String processSubmit(
			@ModelAttribute ChangePasswordBean changePassword,
			...) {
 
    // After fix (working solution)
    public String processSubmit(
			@ModelAttribute("changePassword") ChangePasswordBean changePassword,
			...) {

Dusss, gebruik in je JSP’s de klassenaam of geef expliciet de naam op in de @ModelAttribute annotation.