First project with Java Spring
Hi!
It has been a long time since I last wrote any meaningful Java code. Aside from some Jenkins plugin debugging here and there, the bulk of my Java experience is from the 2000s, mostly desktop apps (Swing/AWT), so after a nudge from a friend I’ve decided to dust off my long forgotten Java skills and I set to rewrite YT Email using Spring as a learning exercise.
First impressions
A big chunk of my software engineering experience is using PHP, that’s where it’s easier to compare things to, and right from the start I felt right at home, as most of the concepts are also present in Symfony/Doctrine and there is a high grade of translatability between both frameworks. There were a few things that Magento also does similarly to Spring but with different names.
start.spring.io makes it a breeze easy to bootstrap a new project, similar to the Symfony CLI, although in a web interface. Define your dependencies, extract the zip on your machine and git init
is all you need to be up and running. Ah, and also IntelliJ IDEA, which allowed me to keep my PhpStorm keybindings.
Hello world
We start with a simple hello world.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HomeController
{
@RequestMapping(path = "/")
@ResponseBody
public String home()
{
return "Hello World!";
}
}
Comparing that to Symfony:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class HomeController extends AbstractController
{
#[Route('/', name: 'home')]
public function index()
{
return new Response('Hello World!');
}
}
There are a few differences here that I’d like to unpack here. First, you need the @ResponseBody
annotation in order to return the contents directly in the controller, otherwise the default will be to render a template with that name available in src/main/resources/templates
. With Symfony, you’d return an object that implements the ResponseInterface
, be that a JSON, a redirect or a supertype that handles the template parsing (i.e.: return $this->render('my/template.html.twig');
). Annoyingly, if you want to redirect in Spring, you have to either return "redirect:/my/new/url";
or change the method signature to return a org.springframework.web.servlet.view.RedirectView
object, which sounds like it would reduce the flexibility, although I haven’t had troubles with it yet.
The second thing that caught my eye was the @Controller
annotation. In Symfony, the controllers are often placed in the src/controllers
folder, although you can configure a different folder to be scanned (if you’re using the #[Route]
annotation) or directly point each route to a method in any PHP class. In Spring, you just give your class the @Controller
annotation and the routes (@RequestMapping
or a subtype) will be parsed from it. This also means you can put your controller anywhere.
This sounds a bit too magic at first, but the answer lies in the @SpringBootApplication
annotation from your main Application
class (which came in the ZIP file from start.spring.io). @SpringBootApplication
inherits @ComponentScan
, which tells Spring to scan everything in the current package (and sub-packages), and that is true for other types of objects (like your @Configuration
classes can be anywhere). I’m assuming this is done at compile-time, so in theory the resulting JAR could have a Map/List/Array with all the controllers named (similar to what composer dump-autoloader
would do, but for controllers/routes/configuration/etc). Even if it doesn’t work as I imagine, I still appreciate the flexibility of just using the @Controller
annotation, as it gives me more freedom to organize the code.
Dependency Injection
It’s not quite clear to me yet if it’s Java or Spring that provides each feature that I use here, but I do really enjoyed working with dependencies in this project. Both Symfony and Magento also have advanced dependency injection capabilities, so I started with the standard that I was used to, declaring the dependencies in the constructor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@RequestMapping(path = "/settings")
public class Settings
{
private final MailService mailService;
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
public Settings(MailService mailService, PasswordEncoder passwordEncoder, UserRepository userRepository)
{
this.mailService = mailService;
this.passwordEncoder = passwordEncoder;
this.userRepository = userRepository;
}
}
I could also just use the @Autowired
annotation without having them in the constructor, but you will get a NullPointerException if you ever do instantiate the class without using the DI container, so I rather avoid it.
Similarly to Symfony, you can also declare dependencies in your controller’s methods:
1
2
3
4
5
@GetMapping
public String index(@AuthenticationPrincipal User user)
{
//
}
That injects the authenticated User
into the controller, ready to be used.
Beans
Another concept that I see here and there are Beans. My first encounter was with Spring Security, in a situation similar to:
1
2
3
4
5
6
7
8
9
10
@EnableWebSecurity
@Configuration
public class SecurityConfig
{
@Bean
PasswordEncoder getPasswordEncoder()
{
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
The best comparision I can make is to the di.xml
’s <preference>
in Magento. With @Bean
I’m telling Spring’s DI container whenever the PasswordEncoder
is to be injected, pass the result of this method. The method getPasswordEncoder
in the example above will only be called once (unclear to me if once per-request or once during the lifetime of the application), so Spring will just send the same instance of the object returned by the method whenever another class needs a PasswordEncoder
object. Alternatively, this is basically the same as auto-wiring the objects (or interfaces!), but they require some setup to be instantiated. I’m also allowed to have a Bean for a ConcreteClass
and it return a subtype of it, but unlike Magento, I can’t use a @Bean
to override a class that has already been registered as a Bean somewhere else (i.e.: the @Service
annotation). Maybe there is a package hierarchy involved, but I haven’t tested that deeper yet.
Relational databases
Having worked extensively with Doctrine 2, Hibernate felt like home. Entities, repositories, custom queries and life cycle hooks behave the same, using them in services is straight-forward, the schema can be auto-generated from the entities, etc. However, I do miss something like Doctrine Migrations in the Java world, as both Flyway and Liquibase don’t allow me to write any Java code to handle migrations. With Doctrine Migrations, I can write PHP to handle some edge cases in migrations, like this snippet I’ve extracted from the Money project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230825001 extends AbstractMigration
{
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE `transaction` ADD COLUMN amount_in_cents BIGINT;');
$updateStmt = $this->connection->prepare('UPDATE `transaction` SET amount_in_cents = :amount_in_cents WHERE id = :id');
$result = $this->connection->fetchAllAssociative('SELECT id, amount FROM `transaction`');
foreach ($result as $item) {
$amount = $this->convertFromStringToInteger($item['amount']);
$updateStmt->bindValue('id', $item['id']);
$updateStmt->bindValue('amount_in_cents', $amount);
$updateStmt->executeQuery();
}
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE `transaction` DROP COLUMN;');
}
}
This would have been a nice feature, specially given YT Email already had a database and I wanted to just point the Spring project to the same DB in production. Thankfully I had only a few cases that would need a more elaborated migration, so I just fixed them manually.
Templating
I haven’t done much complex templating with Thymeleaf yet, but looking at the documentation it seems as feature-complete as Twig. Its syntax is much more tied to the HTML syntax, so to some extent you could also render the view directly in the browser for a quicker turnaround. I rather have Twig’s style though, but this isn’t unpleasant to use. To transfer data between the Controller and a Thymeleaf template, you can add attributes to either a org.springframework.ui.Model
or org.springframework.web.servlet.ModelAndView
that you declare in your controller method’s signature, i.e.:
1
2
3
4
5
6
@GetMapping
public String index(Model model, @AuthenticationPrincipal User user)
{
model.addAttribute("username", user.getUsername());
return "settings/index";
}
And to read them in Thymeleaf, <input th:value="${username}" />
will do the trick.
Deployment
Go allows me to deploy to deploy a single file, and that simplifies a lot the deployment process. With Spring Boot, I can do the same (provided I have java installed in the server), or the OCI image is also quite straight-forward:
1
2
3
FROM docker.io/library/openjdk:20
COPY build/libs/*.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]
Although I love PHP and have used the mainstream options to deploy projects with it, it is unfortunately a lot more involved, especially so that PHARs never took off and it’s cumbersome to make them work for web projects. If you are using OCI containers in and out, it won’t make much difference. Spring handles signals for you, so I don’t need supervisord in my containers anymore.
Outro
In general, I quite enjoyed working with Spring, and will likely build a few more projects with it in the future. I do miss a few tools, however, like Symfony’s Profiler and the Web Debug Toolbar, as they give me so much information during development, and the SymfonyMakerBundle, that with a single CLI command auto-generate a bunch of different classes to speed up the process. I haven’t really searched for them though, and there might also be IntelliJ extensions that do some parts of their job. All in all, I’m looking forward for my next feature/project using Java and Spring!
Thank you.