Skip to content

Commit

Permalink
native image with spring boot 3 (#5)
Browse files Browse the repository at this point in the history
* attempting to generate native image

image generates fine.. but fails because of EntityGraph

Revisit after these two issues are fixed
- spring-attic/spring-native#1729
- spring-attic/spring-native#1728

Found a temp workaround for EntityManager injection with lombok
spring-attic/spring-native#1597

* will get this corrected in master branch

* Merge branch 'master' into boot3-native

# Conflicts:
#	src/main/java/gt/app/domain/AppUser.java

* graalvm 22.3+ is required

* h2 required at compile time for graalvm native compile

* native image WIP

* now it works !

* now it works !

* spring-attic/spring-native#1597 is resolved in   spring boot 3

* spring-projects/spring-data-jpa#2681 is fixed

* spring-projects/spring-data-jpa#2681 is fixed

* spring-projects/spring-data-jpa#2681 is fixed
  • Loading branch information
gtiwari333 committed Dec 5, 2022
1 parent 532a955 commit 16a9543
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 65 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,26 @@ Future: do more stuff
- Liquibase/Flyway change log
- Integrate Markdown editor for writing notes

### Dependency/plugin version checker

`./mvnw versions:display-dependency-updates`
`./mvnw versions:display-plugin-updates`

## Generate native executable:
- Required: GraalVM 22.3+ (for Spring Boot 3)
- Install using sdkman
`sdk install java 22.3.r17.ea-nik`
`sdk use java 22.3.r17.ea-nik`

- Create native executable `./mvnw native:compile -Pnative,dev`
- Run it `./target/note-app`

OR

- Generate docker image with native executable `./mvnw spring-boot:build-image -Pnative,dev`
- Run it `docker run --rm -p 8080:8080 docker.io/library/note-app:3.0.0-RC1`


## Native Test:
- Run with `./mvnw test -PnativeTest`
- Spring Boot 3.0.0: native-test is not working due to spock ( and possibly other dependencies too)
73 changes: 32 additions & 41 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0-RC1</version>
<version>3.0.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

Expand All @@ -16,9 +16,7 @@
<properties>
<java.version>17</java.version>

<!-- Spring6 not supported yet https://github.com/springdoc/springdoc-openapi/issues/1284
Caused by: java.lang.ClassNotFoundException: javax.servlet.http.HttpServletRequest-->
<springdoc-openapi-ui.version>2.0.0-M7</springdoc-openapi-ui.version>
<springdoc-openapi-ui.version>2.0.0-RC2</springdoc-openapi-ui.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>

<selenide.version>6.9.0</selenide.version>
Expand Down Expand Up @@ -269,6 +267,28 @@
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>${hibernate.version}</version>
<executions>
<execution>
<id>enhance</id>
<goals>
<goal>enhance</goal>
</goals>
<configuration>
<enableLazyInitialization>true</enableLazyInitialization>
<enableDirtyTracking>true</enableDirtyTracking>
<enableAssociationManagement>true</enableAssociationManagement>
</configuration>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down Expand Up @@ -438,6 +458,10 @@
</executions>
<configuration>
<javaVersion>${java.version}</javaVersion>
<ignoreGeneratedClasses>true</ignoreGeneratedClasses>
<ignoreClassNamePatterns>
<classNamePattern>.*SpringCGLIB.*</classNamePattern>
</ignoreClassNamePatterns>
</configuration>
</plugin>

Expand Down Expand Up @@ -557,7 +581,10 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<!-- GraalVMNativeImage: required at compile time for graalvm native compile -->
<!--target/spring-aot/main/sources/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration__BeanDefinitions.java:4:25
java: package org.h2.server.web does not exist-->
<!--<scope>runtime</scope>-->
</dependency>
</dependencies>
<properties>
Expand Down Expand Up @@ -596,41 +623,5 @@

</profiles>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>

</project>
1 change: 1 addition & 0 deletions spot-bugs.filter-exclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
<Bug pattern="SPRING_CSRF_PROTECTION_DISABLED"/>
<Bug pattern="EI_EXPOSE_REP2"/>
<Bug pattern="EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS"/>
<Class name="~.*.*SpringCGLIB.*.*" />
</FindBugsFilter>
41 changes: 41 additions & 0 deletions src/main/java/gt/app/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@

import gt.app.config.AppProperties;
import gt.app.config.Constants;
import gt.app.config.security.AppUserDetails;
import gt.app.modules.email.dto.EmailDto;
import gt.app.modules.note.dto.NoteCreateDto;
import gt.app.modules.note.dto.NoteEditDto;
import gt.app.modules.note.dto.NoteReadDto;
import gt.app.modules.user.AppPermissionEvaluatorService;
import gt.app.modules.user.dto.PasswordUpdateDTO;
import gt.app.modules.user.dto.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.PageImpl;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import java.net.InetAddress;
Expand All @@ -18,6 +32,7 @@
@Slf4j
@EnableConfigurationProperties(AppProperties.class)
@EnableTransactionManagement(proxyTargetClass = true)
@ImportRuntimeHints(MyRuntimeHints.class) //required for GraalVMNativeImage::
public class Application {

public static void main(String[] args) throws UnknownHostException {
Expand All @@ -41,3 +56,29 @@ public static void main(String[] args) throws UnknownHostException {
}

}

//required for GraalVMNativeImage::
class MyRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
//record and dto classes -> get/set not found
hints
.reflection()
.registerType(AppProperties.class, MemberCategory.values())
.registerType(AppProperties.FileStorage.class, MemberCategory.values())
.registerType(EmailDto.class, MemberCategory.values())
.registerType(EmailDto.FileBArray.class, MemberCategory.values())
.registerType(PasswordUpdateDTO.class, MemberCategory.values())
.registerType(UserDTO.class, MemberCategory.values())
.registerType(NoteCreateDto.class, MemberCategory.values())
.registerType(NoteEditDto.class, MemberCategory.values())
.registerType(NoteReadDto.class, MemberCategory.values())
.registerType(NoteReadDto.FileInfo.class, MemberCategory.values())
.registerType(AppUserDetails.class, MemberCategory.values())
.registerType(UsernamePasswordAuthenticationToken.class, MemberCategory.values())
.registerType(AppPermissionEvaluatorService.class, MemberCategory.values())
.registerType(PageImpl.class, MemberCategory.values()); //EL1004E: Method call: Method getTotalElements() cannot be found on type org.springframework.data.domain.PageImpl


}
}
2 changes: 1 addition & 1 deletion src/main/java/gt/app/config/AuditorResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import gt.app.config.security.SecurityUtils;
import gt.app.domain.LiteUser;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;

import jakarta.persistence.EntityManager;
import java.util.Optional;

@Component
Expand Down
1 change: 1 addition & 0 deletions src/main/java/gt/app/config/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class SecurityConfig{
"/h2-console/**",
"/webjars/**",
"/static/**",
"/error/**",
"/swagger-ui/**",
"/swagger-ui.html/**",
"/signup/**",
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/gt/app/domain/BaseAuditingEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.*;
Expand All @@ -26,7 +26,7 @@ abstract class BaseAuditingEntity extends BaseEntity {
@JsonIgnore//ignore completely to avoid StackOverflow exception by User.createdByUser logic, use DTO
private LiteUser createdByUser;

@CreationTimestamp
@CreatedDate
@Column(name = "created_date", nullable = false)
private Instant createdDate;

Expand All @@ -36,7 +36,7 @@ abstract class BaseAuditingEntity extends BaseEntity {
@JsonIgnore//ignore completely to avoid StackOverflow exception by User.lastModifiedByUser logic, use DTO
private LiteUser lastModifiedByUser;

@UpdateTimestamp
@LastModifiedDate
@Column(name = "last_modified_date")
private Instant lastModifiedDate;
}
18 changes: 9 additions & 9 deletions src/main/java/gt/app/modules/note/NoteService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class NoteService {
private final NoteRepository noteRepository;
private final FileService fileService;

private final NoteMapper noteMapper;

public Note createNote(NoteCreateDto dto) {

List<ReceivedFile> files = new ArrayList<>();
Expand All @@ -38,7 +40,7 @@ public Note createNote(NoteCreateDto dto) {
files.add(new ReceivedFile(FILE_GROUP, mpf.getOriginalFilename(), fileId));
}

Note note = NoteMapper.INSTANCE.createToEntity(dto);
Note note = noteMapper.createToEntity(dto);
note.getAttachedFiles().addAll(files);

return save(note);
Expand All @@ -48,16 +50,14 @@ public Note update(NoteEditDto dto) {

Optional<Note> noteOpt = noteRepository.findById(dto.id());
return noteOpt.map(note -> {
NoteMapper.INSTANCE.createToEntity(dto, note);
return save(note);
}
).orElseThrow();
noteMapper.createToEntity(dto, note);
return save(note);
}).orElseThrow();
}

public NoteReadDto read(Long id) {
return noteRepository.findById(id)
.map(NoteMapper.INSTANCE::mapForRead)
.orElseThrow();
.map(noteMapper::mapForRead).orElseThrow();
}

public Note save(Note note) {
Expand All @@ -66,12 +66,12 @@ public Note save(Note note) {

public Page<NoteReadDto> readAll(Pageable pageable) {
return noteRepository.findAll(pageable)
.map(NoteMapper.INSTANCE::mapForRead);
.map(noteMapper::mapForRead);
}

public Page<NoteReadDto> readAllByUser(Pageable pageable, Long userId) {
return noteRepository.findByCreatedByUserIdOrderByCreatedDateDesc(pageable, userId)
.map(NoteMapper.INSTANCE::mapForRead);
.map(noteMapper::mapForRead);
}

public void delete(Long id) {
Expand Down
5 changes: 1 addition & 4 deletions src/main/java/gt/app/modules/note/dto/NoteMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;

@Mapper
@Mapper(componentModel = "spring")
public interface NoteMapper {

NoteMapper INSTANCE = Mappers.getMapper(NoteMapper.class);

@Mapping(source = "createdByUser.id", target = "userId")
@Mapping(source = "createdByUser.uniqueId", target = "username")
@Mapping(source = "attachedFiles", target = "files")
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/gt/app/modules/note/dto/NoteReadDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@

public record NoteReadDto(Long id, String title, String content, Long userId, String username, Instant createdDate,
List<FileInfo> files) {

////required for GraalVMNativeImage::
//SpelEvaluationException: EL1004E: Method call: Method size() cannot be found on type java.util.ArrayList
//Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "note.files.size()>0" (template: "note/_notes" - line 43, col 18)
public int getFileSize() {
if (files == null) {
return 0;
}
return files.size();
}

public record FileInfo(UUID id, String name) {
}
}
5 changes: 1 addition & 4 deletions src/main/java/gt/app/modules/user/dto/UserMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@
import gt.app.domain.AppUser;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Mapper
@Mapper(componentModel = "spring")
public interface UserMapper {

UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

@Mapping(source = "uniqueId", target = "login")
UserDTO userToUserDto(AppUser user);

Expand Down
6 changes: 5 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ spring:
username:
password:
main:
lazy-initialization: true
lazy-initialization: false

server:
port: 8080
Expand All @@ -42,3 +42,7 @@ wro4j:
app-properties:
file-storage:
upload-folder: ${java.io.tmpdir}


springdoc:
enable-native-support: true
2 changes: 1 addition & 1 deletion src/main/resources/templates/note/_notes.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ <h5 class="card-title"><span th:text="${note.title}"></span></h5>

<div class="card-body">
<p class="card-text"><span th:text="${note.content}"></span></p>
<div th:if="${note.files.size()>0}">
<div th:if="${note.getFileSize()>0}">
<hr/>
Attachments:
<span th:each="file : ${note.files}">
Expand Down

0 comments on commit 16a9543

Please sign in to comment.