This article shows you how to build a simple blog application with JHipster 7.0.1. You can also watch a screencast of this tutorial on YouTube.
đź’ˇ
|
It appears you’re reading this document on GitHub. If you want a prettier view, install Asciidoctor.js Live Preview for Chrome, then view the raw document. |
If you’d like to get right to it, the source code for this application is on GitHub. To run the app, use ./mvnw
. To test it, run ./mvnw verify
. To run its end-to-end tests, run ./mvnw
in one terminal and npm run e2e
in another.
JHipster is one of those open-source projects you stumble upon and immediately think, “Of course!” It combines three very successful frameworks in web development: Bootstrap, Angular, and Spring Boot. Bootstrap was one of the first dominant web-component frameworks. Its most substantial appeal was that it only required a bit of HTML, and it worked! All the efforts we made in the Java community to develop web components were shown a better path by Bootstrap. It leveled the playing field in HTML/CSS development, much like Apple’s Human Interface Guidelines did for iOS apps.
Julien Dubois started JHipster in October 2013 (Julien’s first commit was on October 21, 2013). The first public release (version 0.3.1) launched on December 7, 2013. Since then, the project has had over 200 releases! It is an open-source, Apache 2.0-licensed project on GitHub. It has a core team of 38 developers and over 600 contributors. You can find its homepage at www.jhipster.tech. If you look at the project on GitHub, you can see it’s mostly written in JavaScript (52%), TypeScript (19.8%), and Java (17.7%).
JHipster 7 is the same JHipster many developers know and love, with a couple of bright and shiny new features: namely Vue and Spring Boot 2.4 support.
The Installing JHipster instructions show you all the tools you’ll need to use a released version of JHipster.
-
Install Java 11 using SDKMAN!
-
Install Git from https://git-scm.com.
-
Install Node.js from http://nodejs.org. JHipster recommends using an LTS release.
-
Run the following command to install JHipster.
npm i -g generator-jhipster@7.0.1
đź“Ž
|
If you’re using Yarn, run yarn global add generator-jhipster@7.0.1 .
|
To create a project, open a terminal window and create a directory. For example, mkdir blog
. Navigate into the directory and run jhipster
. You’ll be prompted to answer several questions about the type of application you want to create and what features you’d like to include. The screenshot below shows the choices I made to create a simple blog application with Angular.
If you’d like to create the same application I did, you can place the following .yo-rc.json
file in an empty directory and run jhipster
in it. You won’t be prompted to answer any questions because this file provides the answers.
{
"generator-jhipster": {
"blueprints": [],
"otherModules": [],
"applicationType": "monolith",
"baseName": "blog",
"jhipsterVersion": "7.0.1",
"skipClient": false,
"skipServer": false,
"skipUserManagement": false,
"skipCheckLengthOfIdentifier": false,
"skipFakeData": false,
"jhiPrefix": "jhi",
"entitySuffix": "",
"dtoSuffix": "DTO",
"testFrameworks": ["cypress"],
"pages": [],
"creationTimestamp": 1619451584671,
"serviceDiscoveryType": false,
"reactive": false,
"authenticationType": "jwt",
"packageName": "org.jhipster.blog",
"serverPort": "8080",
"cacheProvider": "ehcache",
"enableHibernateCache": true,
"databaseType": "sql",
"devDatabaseType": "h2Disk",
"prodDatabaseType": "postgresql",
"buildTool": "maven",
"serverSideOptions": [],
"websocket": false,
"searchEngine": false,
"messageBroker": false,
"enableSwaggerCodegen": false,
"clientFramework": "angularX",
"withAdminUi": true,
"clientTheme": "none",
"enableTranslation": true,
"nativeLanguage": "en",
"packageFolder": "org/jhipster/blog",
"jwtSecretKey": "ZGM4ZTY3ZDk5MjE3NjA0ZDcxOWYxOGVkYzg4YTBjNDYyOGVhNjdjMjY0MzIyMjNlZDEzNzM5ZDVkYWQ2NWI0OTdiMmJlNDIxZTc4MTc5MmYxYjkzODEzYTQ4YmY5NTU5MjczNTA4YmE4YWFkNDg3NDRiOWJhYjgxYjhkOTBjNzg=",
"clientPackageManager": "npm",
"clientThemeVariant": "",
"languages": ["en", "es"]
}
}
The project creation process will take a couple of minutes to run, depending on your internet connection speed. When it’s finished, you should see output like the following.
Run ./mvnw
to start the application and navigate to http://localhost:8080 in your favorite browser. The first thing you’ll notice is a hipster explaining how you can sign in or register.
Sign in with username admin
and password admin
, and you’ll have access to navigate through the Administration section. This section offers nice-looking UIs on top of some Spring Boot’s many monitoring and configuration features. It also allows you to administer users:
Administration > Metrics gives you insights into Application and JVM metrics:
Administration > API allows you to see the Swagger docs associated with its API.
You can run the following command (in a separate terminal window) to run the Cypress tests and confirm everything is working correctly.
npm run e2e
For each entity you want to create, you will need:
-
a database table;
-
a Liquibase changeset;
-
a JPA entity class;
-
a Spring Data
JpaRepository
interface; -
a Spring MVC
RestController
class; -
an Angular list component, edit component, service; and
-
several HTML pages for each component.
Also, you should have integration tests to verify that everything works and performance tests to confirm that it runs fast. In an ideal world, you’d also have unit tests and integration tests for your Angular code.
The good news is JHipster can generate all of this code for you, including integration tests and performance tests. If you have entities with relationships, it will create the necessary schema to support them (with foreign keys), and the TypeScript and HTML code to manage them. You can also set up validation to require certain fields as well as control their length.
JHipster supports several methods of code generation. The first uses its entity sub-generator. The entity sub-generator is a command-line tool that prompts you with questions that you answer.
JDL-Studio is a browser-based tool for defining your domain model with JHipster Domain Language (JDL). I like the visual nature of JDL-Studio, so I’ll use it for this project.
Below is the entity diagram and JDL code needed to generate a simple blog with blogs, posts, and tags.
đź’ˇ
|
You can find a few other JDL samples on GitHub. |
If you’d like to follow along, copy/paste the contents of the JDL below into a blog.jdl
file.
entity Blog { name String required minlength(3) handle String required minlength(2) } entity Post { title String required content TextBlob required date Instant required } entity Tag { name String required minlength(2) } relationship ManyToOne { Blog{user(login)} to User Post{blog(name)} to Blog } relationship ManyToMany { Post{tag(name)} to Tag{post} } paginate Post, Tag with infinite-scroll
Run the following command to import this file and generate entities, tests, and a UI.
jhipster jdl blog.jdl
You’ll be prompted to overwrite src/main/webapp/i18n/en/global.json
. Type a to overwrite this file, as well as others.
Restart the application with /.mvnw
.
You might notice that each entities' list screen is pre-loaded with data. faker.js creates this data. To turn it off, edit src/main/resources/config/application-dev.yml
, search for faker
and remove it from the liquibase.contexts
configuration. I made this change in this example’s code.
liquibase:
# Add 'faker' if you want the sample data to be loaded automatically
contexts: dev
đź’ˇ
|
If you still have data in your list screens after making this change, run ./mvnw clean to delete the H2 database.
|
Create a couple of blogs for the existing admin
and user
users and a few blog entries.
From these screenshots, you can see that users can see each other’s data and modify it.
đź’ˇ
|
To configure an IDE with your JHipster project, see Configuring your IDE. Instructions exist for Eclipse, IntelliJ IDEA, Visual Studio Code, and NetBeans. |
To add more security around blogs and entries, open BlogResource.java
and find the getAllBlogs()
method. Change the following line:
return blogRepository.findAll();
To:
return blogRepository.findByUserIsCurrentUser();
The findByUserIsCurrentUser()
method is generated by JHipster in the BlogRepository
class and allows limiting results by the current user.
public interface BlogRepository extends JpaRepository<Blog, Long> {
@Query("select blog from Blog blog where blog.user.login = ?#{principal.username}")
List<Blog> findByUserIsCurrentUser();
}
After making this change, re-compiling BlogResource
should trigger a restart of the application thanks to Spring Boot’s Developer tools. If you navigate to http://localhost:8080/blog, you should only see the blog for the current user.
To add this same logic for entries, open PostResource.java
and find the getAllPosts()
method. Change the following line:
Page<Entry> page;
if (eagerload) {
page = postRepository.findAllWithEagerRelationships(pageable);
} else {
page = postRepository.findAll(pageable);
}
To:
page = postRepository.findByBlogUserLoginOrderByDateDesc(
SecurityUtils.getCurrentUserLogin().orElse(null), pageable);
Using your IDE, create this method in the PostRepository
class. It should look as follows:
Page<Post> findByBlogUserLoginOrderByDateDesc(String currentUserLogin, Pageable pageable);
Recompile both changed classes and verify that the user
user only sees the posts you created for them.
You might notice that this application doesn’t look like a blog, and it doesn’t allow HTML in the content field.
The changes you just made to limit data visibility will cause Cypress end-to-end tests to fail. To fix them, you need to change from selecting the last user to selecting the admin
user. Open blog.spec.ts
and change the following line:
cy.setFieldSelectToLastOfEntity('user');
to:
cy.get('[data-cy="user"]').select('admin');
Then, change post.spec.ts
to update the test that creates a new post. At the beginning of the test, add a new blog that the post can relate to.
it('should create an instance of Post', () => {
// add blog before post
cy.clickOnEntityMenuItem('blog');
cy.get(entityCreateButtonSelector).click({ force: true });
cy.get(`[data-cy="name"]`).type('Admin blog', { force: true }).invoke('val');
cy.get(`[data-cy="handle"]`).type('admin', { force: true }).invoke('val');
cy.get('[data-cy="user"]').select('admin');
cy.get(entityCreateSaveButtonSelector).click({ force: true });
// end of add blog
Then, change cy.setFieldSelectToLastOfEntity('blog')
to select this blog.
cy.get('[data-cy="blog"]').select('Admin blog');
It’s a good idea to clean up any data you add in e2e tests. Add the following to the bottom of the last test in post.spec.ts
that deletes the post.
// delete blog added earlier
cy.intercept('GET', '/api/blogs*').as('entitiesRequest');
cy.intercept('DELETE', '/api/blogs/*').as('deleteEntityRequest');
cy.wait('@entitiesRequest').then(({ request, response }) => {
cy.get(entityDeleteButtonSelector).last().click({ force: true });
cy.get(entityConfirmDeleteButtonSelector).click({ force: true });
cy.wait('@deleteEntityRequest');
cy.visit('/');
});
Run npm run e2e
to confirm everything works as expected.
When doing UI development on a JHipster-generated application, it’s nice to see your changes as soon as you save a file. JHipster uses Browsersync and webpack to power this feature. You enable this feature by running the following command in the blog
directory.
npm start
In this section, you’ll change the following:
-
Change the rendered content field to display HTML
-
Change the list of entries to look like a blog
If you enter HTML in the content
field of a blog post, you’ll notice it’s escaped on the list screen.
To change this behavior, open post.component.html
and change the following line:
<td>{{ post.content }}</td>
To:
<td [innerHTML]="post.content"></td>
After making this change, you’ll see that the HTML is no longer escaped.
To make the list of entries look like a blog, replace <div class="table-responsive">
with HTML, so it uses a stacked layout in a single column.
<div class="table-responsive" *ngIf="entries?.length > 0">
<div infinite-scroll (scrolled)="loadPage(page + 1)" [infiniteScrollDisabled]="page >= links['last']" [infiniteScrollDistance]="0">
<div *ngFor="let entry of entries; trackBy: trackId">
<a [routerLink]="['/entry', entry.id, 'view' ]">
<h2>{{entry.title}}</h2>
</a>
<small>Posted on {{entry.date | date: 'short'}} by {{entry.blog.user.firstName}}</small>
<div [innerHTML]="entry.content"></div>
<div class="btn-group mb-2 mt-1">
<button type="submit"
[routerLink]="['/entry', entry.id, 'edit']"
class="btn btn-primary btn-sm">
<fa-icon [icon]="'pencil-alt'"></fa-icon>
<span class="d-none d-md-inline" jhiTranslate="entity.action.edit">Edit</span>
</button>
<button type="submit"
[routerLink]="['/', 'entry', { outlets: { popup: entry.id + '/delete'} }]"
replaceUrl="true"
queryParamsHandling="merge"
class="btn btn-danger btn-sm">
<fa-icon [icon]="'times'"></fa-icon>
<span class="d-none d-md-inline" jhiTranslate="entity.action.delete">Delete</span>
</button>
</div>
</div>
</div>
</div>
Now it looks more like a regular blog!
You can further enhance the security of your API by only allowing users that own a blog (or post) to edit it. Here’s some pseudocode to show the logic:
Optional<Blog> blog = blogRepository.findById(id);
if (blog.isPresent() && <user does not match current user>) {
return new ResponseEntity<>("error.http.403", HttpStatus.FORBIDDEN);
}
return ResponseUtil.wrapOrNotFound(blog);
Below is the refactored BlogResource.java
with additional logic in each method to prevent data tampering.
@PostMapping("/blogs")
public ResponseEntity<?> createBlog(@Valid @RequestBody Blog blog) throws URISyntaxException {
log.debug("REST request to save Blog : {}", blog);
if (blog.getId() != null) {
throw new BadRequestAlertException("A new blog cannot already have an ID", ENTITY_NAME, "idexists");
}
if (!blog.getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))) {
return new ResponseEntity<>("error.http.403", HttpStatus.FORBIDDEN);
}
Blog result = blogRepository.save(blog);
return ResponseEntity
.created(new URI("/api/blogs/" + result.getId()))
.headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.getId().toString()))
.body(result);
}
@PutMapping("/blogs/{id}")
public ResponseEntity<?> updateBlog(@PathVariable(value = "id", required = false) final Long id, @Valid @RequestBody Blog blog)
throws URISyntaxException {
log.debug("REST request to update Blog : {}, {}", id, blog);
if (blog.getId() == null) {
throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull");
}
if (!Objects.equals(id, blog.getId())) {
throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid");
}
if (!blogRepository.existsById(id)) {
throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound");
}
if (blog.getUser() != null && !blog.getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))) {
return new ResponseEntity<>("error.http.403", HttpStatus.FORBIDDEN);
}
Blog result = blogRepository.save(blog);
return ResponseEntity
.ok()
.headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, blog.getId().toString()))
.body(result);
}
@PatchMapping(value = "/blogs/{id}", consumes = "application/merge-patch+json")
public ResponseEntity<?> partialUpdateBlog(
@PathVariable(value = "id", required = false) final Long id,
@NotNull @RequestBody Blog blog
) throws URISyntaxException {
log.debug("REST request to partial update Blog partially : {}, {}", id, blog);
if (blog.getId() == null) {
throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull");
}
if (!Objects.equals(id, blog.getId())) {
throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid");
}
if (!blogRepository.existsById(id)) {
throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound");
}
if (blog.getUser() != null && !blog.getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))) {
return new ResponseEntity<>("error.http.403", HttpStatus.FORBIDDEN);
}
Optional<Blog> result = blogRepository
.findById(blog.getId())
.map(
existingBlog -> {
if (blog.getName() != null) {
existingBlog.setName(blog.getName());
}
if (blog.getHandle() != null) {
existingBlog.setHandle(blog.getHandle());
}
return existingBlog;
}
)
.map(blogRepository::save);
return ResponseUtil.wrapOrNotFound(
result,
HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, blog.getId().toString())
);
}
@GetMapping("/blogs/{id}")
public ResponseEntity<?> getBlog(@PathVariable Long id) {
log.debug("REST request to get Blog : {}", id);
Optional<Blog> blog = blogRepository.findById(id);
if (
blog.isPresent() &&
blog.get().getUser() != null &&
!blog.get().getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))
) {
return new ResponseEntity<>("error.http.403", HttpStatus.FORBIDDEN);
}
return ResponseUtil.wrapOrNotFound(blog);
}
@DeleteMapping("/blogs/{id}")
public ResponseEntity<?> deleteBlog(@PathVariable Long id) {
log.debug("REST request to delete Blog : {}", id);
Optional<Blog> blog = blogRepository.findById(id);
if (
blog.isPresent() &&
blog.get().getUser() != null &&
!blog.get().getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))
) {
return new ResponseEntity<>("error.http.403", HttpStatus.FORBIDDEN);
}
blogRepository.deleteById(id);
return ResponseEntity
.noContent()
.headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id.toString()))
.build();
}
You’ll need to make similar changes in PostResource.java
. See this commit for all the changes that you’ll need in these two classes, as well as their integration tests.
A JHipster application can be deployed anywhere a Spring Boot application can be deployed.
JHipster ships with support for deploying to Heroku, Kubernetes, AWS, and Azure. I’m using Heroku in this example because it doesn’t cost me anything to host it.
When you prepare a JHipster application for production, it’s recommended to use the pre-configured “production” profile. With Maven, you can package your application by specifying the prod
profile when building.
./mvnw -Pprod verify
The production profile is used to build an optimized JavaScript client. You can invoke this using webpack by running yarn run webapp:prod
. The production profile also configures gzip compression with a servlet filter, cache headers, and monitoring via Micrometer. If you have a Graphite server configured in your application-prod.yml
file, your application will automatically send metrics data to it.
To deploy this application to Heroku, I logged in to my account using heroku login
from the command line. I already had the Heroku CLI installed.
$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/d96960ff-82ce-457f-...
Logging in... done
Logged in as matt@raibledesigns.com
I ran jhipster heroku
as recommended in the Deploying to Heroku documentation. I used the name “jhipster7-demo” for my application when prompted. I selected “Git (compile on Heroku)” as the type of deployment and “Java 11”.
When prompted to overwrite files, I typed a.
$ jhipster heroku
INFO! Using JHipster version installed locally in current project's node_modules
Heroku configuration is starting
? Name to deploy as: jhipster7-demo
? On which region do you want to deploy ? us
? Which type of deployment do you want ? Git (compile on Heroku)
? Which Java version would you like to use to build and run your app ? 11
Using existing Git repository
Installing Heroku CLI deployment plugin
Creating Heroku application and setting up node environment
https://jhipster-7-demo.herokuapp.com/ | https://git.heroku.com/jhipster-7-demo.git
Provisioning addons
Provisioning database addon heroku-postgresql --as DATABASE
No suitable cache addon for cacheprovider ehcache available.
Creating Heroku deployment files
create Procfile
create system.properties
conflict pom.xml
? Overwrite pom.xml? (ynarxdeH)
...
Configuring Heroku
Deploying application
remote: Compressing source files... done.
remote: Building source:
... building ...
remote: [INFO] ------------------------------------------------------------------------
remote: [INFO] BUILD SUCCESS
remote: [INFO] ------------------------------------------------------------------------
remote: [INFO] Total time: 33.260 s
remote: [INFO] Finished at: 2021-04-26T19:03:04Z
remote: [INFO] ------------------------------------------------------------------------
remote: Waiting for release.... done.
To https://git.heroku.com/jhipster-7-demo.git
* [new branch] HEAD -> main
Your app should now be live. To view it run
heroku open
And you can view the logs with this command
heroku logs --tail
After application modification, redeploy it with
jhipster heroku
Congratulations, JHipster execution is complete!
Sponsored with ❤️ by @oktadev.
Execution time: 7 min. 37 s.
I ran heroku open
, logged as admin
, and was pleased to see it worked!
The source code for this project is available on GitHub at mraible/jhipster7-demo.
GitHub Actions is continually testing this project with configuration from its .github/workflows/github-actions.yml
file. This file was generated using jhipster ci-cd
and everything passed on the first try!
You can also add continuous delivery with GitHub Actions and Heroku. I ran jhipster ci-cd
again to add this feature. See pull request #2 to see the diff in github-actions.yml
. To make this work, I had to copy my Heroku API key from my account dashboard. Then, I added it in GitHub > Settings > Secrets, naming it HEROKU_API_KEY
.
It’s a good idea to keep your dependencies up-to-date for security reasons. I recommend using Dependabot and adding a .github/dependabot.yml
file with the following YAML:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
I hope you’ve enjoyed learning how JHipster can help you develop modern web applications! It’s a nifty project, with an easy-to-use entity generator, a pretty UI, and many Spring Boot best-practice patterns. The project team follows six simple policies, paraphrased here:
-
The development team votes on policies.
-
JHipster uses technologies with their default configurations as much as possible.
-
Only add options when there is sufficient added value in the generated code.
-
Use strict versions for third-party libraries.
-
Provide similar user/developer experience across different options.
-
Developer experience can take precedence over other policies.
These policies help the project maintain its sharp edge and streamline its development process. If you have features you’d like to add or if you’d like to refine existing features, you can watch the project on GitHub and help with its development and support. We’re always looking for help!
Now that you’ve learned how to use Angular, Bootstrap 4, and Spring Boot with JHipster, go forth and develop great applications!
đź’ˇ
|
Developing microservices with JHipster is possible too! See Reactive Java Microservices with Spring Boot and JHipster to learn how. |
To learn more about JHipster and all it has to offer, look no further than Full Stack Development with JHipster by Deepu K Sasidharan and Sendil Kumar. Both Deepu and Sendil have contributed an incredible amount of time and code to JHipster. We’ve very lucky to have them. They’re both amazing developers! ❤️
Follow @jhipster on Twitter for release announcements, articles, new features, and upcoming talks.
Matt Raible is a web developer, Java Champion, and Developer Advocate at Okta. Matt is a frequent contributor to open source and a big fan of Java, IntelliJ, TypeScript, Angular, and Spring Boot. When he’s not slinging code with open source frameworks, he likes to ski/raft with his family, drive his classic VWs, and enjoy craft beer.
Matt writes on the Okta developer blog, for InfoQ, and on his personal blog. You can find him on Twitter @mraible.
Matt is a developer on the JHipster team and authored the JHipster Mini-Book.