• Fat Cats Boardgame
  • Wicket UI Library
  • About Me

Java and Wicket web development thoughts, tutorials, and tips

roman@coderdreams.com
Coder DreamsCoder Dreams
  • Fat Cats Boardgame
  • Wicket UI Library
  • About Me

Spring MVC vs Wicket

June 9, 2020 Posted by Roman Sery Spring, wicket No Comments

I noticed a Twitter poll a few weeks ago asking Java developers what is their preferred server-side UI framework. The winner was Spring MVC/Thymeleaf by a wide margin with Wicket far behind.

If you're a @java developer that prefers server-side rendering for your web UIs, which framework is your favorite?

— Matt Raible (@mraible) May 28, 2020

Here I want to take a concrete example and show how you might implement it in Wicket compared to Spring MVC/Thymeleaf. I mainly want to focus on the ease of writing the code, the amount of code, and maintainability.

DISCLAIMER: I’ve been using Wicket professionally for 8 years and have only been working with Spring MVC/Thymeleaf on side-projects for 6 months. So please correct me!

The example

For the initial code-comparison let’s keep it simple. We will create a form for users to add a car to our automobile database, specifying the year, make, and model:

Let’s start with the Wicket implementation using Wicket-UI. The code will not be 100% complete for brevity, but you can find the full source code on GitHub.

The Wicket version

HTML
Java
<form wicket:id="carForm" class="form-horizontal">
    <span wicket:id="year"></span>
    <span wicket:id="make"></span>
    <span wicket:id="model"></span>
    <div class="clearfix">
		<div class="col-sm-offset-4 col-sm-8">
	    	<button wicket:id="saveButton" class="btn btn-primary" type="submit">Save</button>
		</div>
    </div>
</form>

Form<Car> carForm = new Form<Car>("carForm");
add(carForm);

feedbackPanel = new FeedbackPanel("feedback", new ContainerFeedbackMessageFilter(carForm));
add(feedbackPanel.setOutputMarkupId(true));

carForm.add(new NumberSpinnerField<Integer>(Builder.of("year", "Year", objModel(newCar::getYear, newCar::setYear)).inputType(Integer.class).min(1920).max(2020).build()));
carForm.add(new TxtField<String>(Builder.of("make", "Make", objModel(newCar::getMake, newCar::setMake)).build()));
carForm.add(new TxtField<String>(Builder.of("model", "Model", objModel(newCar::getModel, newCar::setModel)).build()));

carForm.add(new SingleClickIndicatingAjaxButton("saveButton", carForm, true, null) {
    @Override protected void onSubmit(AjaxRequestTarget target) {
        String errMsg = NewCarValidator.validate(newCar);
        if(errMsg != null) {
            error(errMsg);
            target.add(feedbackPanel);
            return;
        }
        carRepository.save(newCar);
}});

The Spring MVC version

HTML
Java
<script type="text/javascript">
    $(document).ready(function() {
        $("#carForm").submit(function (event) {
            $.ajax({
                type: "POST", url: '/cars/add', data: $('#carForm').serialize(),
                beforeSend: function (xhr) {
                    var token = $('#_csrf').attr('content');
                    var header = $('#_csrf_header').attr('content');
                    xhr.setRequestHeader(header, token);
                },
                success: function (data) {
                    
                }
            });
            return false;
        });
    });
</script>


<form role="form" id="carForm" th:action="@{/cars/add}" th:object="${car}" method="post">
    <div class="form-group row">
        <label for="year" class="col-sm-2 col-form-label">Year</label>
        <div class="col-sm-3">
            <input type="number" class="form-control" th:field="*{year}">
        </div>
    </div>
    <div class="form-group row">
        <label for="year" class="col-sm-2 col-form-label">Make</label>
        <div class="col-sm-3">
            <input type="text" class="form-control" th:field="*{make}">
        </div>
    </div>
    <div class="form-group row">
        <label for="year" class="col-sm-2 col-form-label">Model</label>
        <div class="col-sm-3">
            <input type="text" class="form-control" th:field="*{model}">
        </div>
    </div>
    <div class="form-group row">
        <div class="col-sm-3">
            <button type="submit" class="btn btn-primary">Save</button>
        </div>
    </div>
</form>
@Controller
public class CarController {

    @GetMapping("/cars/add")
    public ModelAndView carForm(Model model) {
        return new ModelAndView("add_car", Map.of("car", new Car()));
    }

    @PostMapping("/cars/add")
    public ModelAndView addCar(@ModelAttribute("car") @Valid Car car, BindingResult bindingResult) {
        NewCarValidator.validate(car, bindingResult);
        if (bindingResult.hasErrors()) {
            return new ModelAndView("add_car", Map.of("car", car));
        }

        carRepository.save(newCar);
        return new ModelAndView(new RedirectView("/cars/add"));
    }
}

Let’s compare

We can see that in this trivial example, the two versions are similar. I still prefer the Wicket version because it’s much more Java-centric. I don’t have to write any client-side Javascript and the HTML is much simpler.

Another disadvantage in the Spring version we are binding the field values to the Car object using strings which is error-prone.

Let’s add complexity

Real-world applications are rarely this simple, so let’s add a bit of complexity to our app. You will see this is where Wicket really shines!

Our automobile database now stores a list of cars that are considered rare. For example the 1942 Chevrolet Special DeLuxe Fleetline. When the user is adding their car, if we determine that it’s rare, we want to collect additional information. We want the mileage and condition. If the condition is really bad, we also want to know if the engine starts.

You might be surprised how little the Wicket version changes.

The Wicket Version

HTML
Java
<form wicket:id="carForm" class="form-horizontal">
    <span wicket:id="year"></span>
    <span wicket:id="make"></span>
    <span wicket:id="model"></span>

    <span wicket:id="rareCar">
        <span wicket:id="mileage"></span>
        <span wicket:id="condition"></span>
        <span wicket:id="engineStarts"></span>
    </span>

    <div class="clearfix">
        <div class="col-sm-offset-4 col-sm-8">
            <button wicket:id="saveButton" class="btn btn-primary" type="submit">Save</button>
        </div>
    </div>
</form>
rareCar = new WebMarkupContainer("rareCar") {
    @Override
    public void onConfigure() {
        super.onConfigure();
        setVisible(isRareCar(newCar));
    }
};
carForm.add(rareCar.setOutputMarkupPlaceholderTag(true));

carForm.add(new AjaxNumberSpinnerField<Integer>(Builder.of("year", "Year", objModel(newCar::getYear, newCar::setYear)).inputType(Integer.class).min(1920).max(2020).build()) {
    public void onFieldChanged(AjaxRequestTarget target) { target.add(rareCar); }
});
carForm.add(new AjaxTxtField<String>(Builder.of("make", "Make", objModel(newCar::getMake, newCar::setMake)).build()) {
    public void onFieldChanged(AjaxRequestTarget target) { target.add(rareCar); }
});
carForm.add(new AjaxTxtField<String>(Builder.of("model", "Model", objModel(newCar::getModel, newCar::setModel)).build()) {
    public void onFieldChanged(AjaxRequestTarget target) { target.add(rareCar); }
});

rareCar.add(new TxtField<Integer>(Builder.of("mileage", "Mileage", objModel(newCar::getMileage, newCar::setMileage)).build()));
rareCar.add(new AjaxDropdownField<CarCondition>(Builder.of("condition", "Condition", objModel(newCar::getCondition, newCar::setCondition))
        .choiceList(new ListModel<CarCondition>(CarCondition.VALUES)).cr(UIHelpers.getEnumChoiceRenderer()).build()) {
    public void onFieldChanged(AjaxRequestTarget target) { target.add(engineStartsField); }
});
rareCar.add(engineStartsField = new CheckBoxField(Builder.of("engineStarts", "Engine Starts?", objModel(newCar::getEngineStarts, newCar::setEngineStarts)).build()) {
    @Override
    public void onConfigure() {
        super.onConfigure();
        setVisible(newCar.getCondition() == CarCondition.BAD);
    }
});

As you can see the main difference is we added a new container for storing the new conditional fields in case the car is rare. Some of the fields have also become ‘Ajax-enabled’. It’s important to note, we still haven’t written any JavaScript and the HTML remains virtually the same!

The Spring MVC version

Ok, we have a lot of work to do 馃檪 We’re going to need to write a bunch of boilerplate Javascript code. On the Java side, it’s not too bad. It only changes a bit.

HTML
Java
<script type="text/javascript">
    $(document).ready(function() {
        $("#carForm").submit(function (event) {
            $.ajax({
                type: "POST", url: '/cars/add', data: $('#carForm').serialize(),
                beforeSend: function (xhr) {
                    var token = $('#_csrf').attr('content');
                    var header = $('#_csrf_header').attr('content');
                    xhr.setRequestHeader(header, token);
                },
                success: function (data) {

                }
            });
            return false;
        });
    });

    $("#year").change(function() {
        carFuncs.checkRareCar();
    });
    $("#make").change(function() {
        carFuncs.checkRareCar();
    });
    $("#model").change(function() {
        carFuncs.checkRareCar();
    });

    carFuncs.checkRareCar = function () {
        $.ajax({
            type: "POST", url: '/cars/check_rare', data: $('#carForm').serialize(),
            success: function (data) {
                if(data.status == "rare") {
                    $('#rareCar').show();
                } else {
                    $('#rareCar').hide();
                }
            }
        });
    };

    $( "#condition" ).change(function() {
        if(this.value == 'bad') {
            $('#engineStartsField').show();
        } else {
            $('#engineStartsField').hide();
        }
    });
</script>



 <form role="form" id="carForm" th:action="@{/cars/add}" th:object="${car}" method="post">
    <div class="form-group row">
        <label for="year" class="col-sm-2 col-form-label">Year</label>
        <div class="col-sm-3">
            <input type="number" class="form-control" th:field="*{year}">
        </div>
    </div>
    <div class="form-group row">
        <label for="make" class="col-sm-2 col-form-label">Make</label>
        <div class="col-sm-3">
            <input type="text" class="form-control" th:field="*{make}">
        </div>
    </div>
    <div class="form-group row">
        <label for="model" class="col-sm-2 col-form-label">Model</label>
        <div class="col-sm-3">
            <input type="text" class="form-control" th:field="*{model}">
        </div>
    </div>

    <span id="rareCar">
        <div class="form-group row">
            <label for="mileage" class="col-sm-2 col-form-label">Milage</label>
            <div class="col-sm-3">
                <input type="number" class="form-control" th:field="*{mileage}">
            </div>
        </div>
        <div class="form-group row">
            <label for="condition" class="col-sm-2 col-form-label">Condition</label>
            <div class="col-sm-3">
                <select class="form-control" th:field="*{condition}" required>
                    <option value="0">Select</option>
                    <option th:each="c : ${conditions}" th:value="${c.id}" th:text="${c.value}"></option>
                </select>
            </div>
        </div>
        <div class="form-row" id="engineStartsField">
            <div class="form-group col-md-6">
                <div class="custom-control custom-checkbox">
                    <input type="checkbox" class="custom-control-input" th:field="*{engineStarts}">
                    <label class="custom-control-label" for="engineStarts">Engine starts</label>
                </div>
            </div>
        </div>
    </span>


    <div class="form-group row">
        <div class="col-sm-3">
            <button type="submit" class="btn btn-primary">Save</button>
        </div>
    </div>
</form>
@Controller
public class CarComplexController {

    @GetMapping("/cars/add")
    public ModelAndView carForm(Model model) {
        return new ModelAndView("add_car", Map.of("car", new Car()));
    }

    @PostMapping("/cars/add")
    public ModelAndView addCar(@ModelAttribute("car") @Valid Car car, BindingResult bindingResult) {
        NewCarValidator.validate(car, bindingResult);
        if (bindingResult.hasErrors()) {
            return new ModelAndView("add_car", Map.of("car", car));
        }

        carRepository.save(newCar);
        return new ModelAndView(new RedirectView("/cars/add"));
    }

    @GetMapping("/cars/check_rare")
    public @ResponseBody AjaxResponse checkIsCarRare(@ModelAttribute("car") Car car) {
        boolean isRare = carService.isRare(car);
        return AjaxResponse.success("Success", isRare ? "rare" : "not_rare");
    }
}

Let’s compare

So what we can see is that as the UI and business logic/requirements become more complex, the size of the client code in the Spring version explodes. There is also a lot of reliance on string mappings which becomes very error-prone.

In the Wicket version, as the complexity increases the amount of client code remains almost the same. We can also easily write Java unit tests for the Wicket version. This is because in Wicket everything is object-based which leads to easy refactorings, easy mocking, and so on. This would be very difficult to do in the Spring version.

Let’s also imagine we will be selling our software to many different dealerships and car collectors. We may have to customize it for each one. We’ve already seen how simple this is using Wicket. I haven’t had to do this with Spring MVC, but I would imagine it will be difficult.

Conclusion

Even our ‘complex’ example is fairly simple compared to real-world applications. I believe the more complex the UI and business requirements become, the more productivity gains you will realize from using Wicket. Keep in mind that you can still use the full power of Spring framework when developing Wicket apps.

Spring MVC may be a better choice in cases where the UI might not be that complex and requiring the maximum speed and performance is critical.

No Comments
Share
8

About Roman Sery

I've been a software developer for over 10 years and still loving Java!

You also might be interested in

Using MySQL JSON columns to simplify your data storage: Part 1

Nov 28, 2019

Simplify data storage in your apps by using JSON column types instead of relying on database normalization.

How to create dynamic HTML markup for Wicket panels

Nov 9, 2019

Use IMarkupResourceStreamProvider and IMarkupCacheKeyProvider to allow your Wicket Panel's to generate their own dynamic markup!

Tracking down a bug in production Wicket application

Jun 20, 2020

Investigating a problem with duplicate markup ID's generated by Wicket and how to solve it.

Categories

  • aws
  • customization
  • database
  • debugging
  • enum
  • java
  • models
  • performance
  • projects
  • react
  • software design
  • Spring
  • tool
  • Uncategorized
  • wicket

Recent Posts

  • Rent Day
  • Self-contained Wicket Fragments
  • Pros and cons of unit testing
  • Themeable React Monopoly board
  • Please dont use client-specific release branches

Recent Comments

  • TCI Express Thanks for sharing such insightful information. TCI Express truly stands out as the best air logistics company, offering fast, secure, and efficient air express and cold chain transportation services....

    Tracking down a bug in production Wicket application 路  March 25, 2025

  • Tom Error: A zip file cannot include itself Can you please correct the plugin part so it doesn't use the same folder as input?

    Deploying Spring Boot app to AWS Beanstalk with Nginx customization 路  September 3, 2021

  • Golfman: Reality always wins I've used both Wicket and front-end JS frameworks and, having worked extensively on both, I can tell you that "Speed of development" is definitely NOT with the JS frameworks. You basically end up...

    Five reasons you should use Apache Wicket 路  August 29, 2021

  • Kiriller Sorry can not agree with you, wicket might be a well built technical framework. But the advantages of using a front-end framework like react.js and vue.js can not be beaten by Wicket nowadays. - Speed...

    Five reasons you should use Apache Wicket 路  August 23, 2021

  • Bernd Lauert Sorry but i have to refute your claims with the following arguments: 1. the Wicket community may be small but it is also very responsive, you usually get a helpful answer from the core devs on the...

    Five reasons you should use Apache Wicket 路  July 1, 2021

Archives

  • May 2021
  • October 2020
  • September 2020
  • August 2020
  • July 2020
  • June 2020
  • May 2020
  • April 2020
  • March 2020
  • February 2020
  • January 2020
  • December 2019
  • November 2019
  • October 2019
  • September 2019
  • August 2019
  • July 2019

Contact Me

Send Message
Prev Next