Skip to content

Commit a58542e

Browse files
committed
8_01_migrate_spring_boot_3_1_2
1 parent 3ee3478 commit a58542e

37 files changed

+478
-380
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
<img src="http://javaops.ru/static/img/logo/javaops_30.png" width="223"/>
22

33
Открытый курс для всех желающих приобщиться к живой современной разработке на Java
4-
# [Разработка Spring Boot 2.x HATEOAS приложения (BootJava)](http://javaops.ru/view/bootjava?ref=gh)
4+
# [Разработка Spring Boot 3.x HATEOAS приложения (BootJava)](http://javaops.ru/view/bootjava?ref=gh)
55
## [Программа](http://javaops.ru/view/bootjava#program)
66

7-
### Java приложения на самом современном и востребованном стеке: Spring Boot 2.x, Spring Data Rest/HATEOAS, Lombok, JPA, H2, ....
8-
Мы создадим с нуля основу любого современного REST веб-приложения: аутентификация и авторизация на основе ролей, регистрация пользователя в приложении, управление своим профилем и администрирование пользователей.
7+
### Java приложения на самом современном и востребованном стеке: Spring Boot 3.x, Spring Data Rest/HATEOAS, Lombok, JPA, H2, ....
8+
Мы создадим с нуля основу любого современного REST веб-приложения: аутентификация и авторизация на основе ролей, регистрация пользователя в приложении, управление своим профилем и администрирование пользователей.
9+
-------------------------------------------------------------
10+
- Stack: [JDK 17](http://jdk.java.net/17/), Spring Boot 3.x, Lombok, H2, Caffeine Cache, SpringDoc OpenApi 2.x
11+
- Run: `mvn spring-boot:run` in root directory.
12+
-----------------------------------------------------
13+
[REST API documentation](http://localhost:8080/)
14+
Креденшелы:
15+
```
16+
User: user@yandex.ru / password
17+
Admin: admin@gmail.com / admin
18+
Guest: guest@gmail.com / guest
19+
```

lombok.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier

pom.xml

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@
55
<parent>
66
<groupId>org.springframework.boot</groupId>
77
<artifactId>spring-boot-starter-parent</artifactId>
8-
<version>2.6.7</version>
8+
<version>3.1.2</version>
99
<relativePath/> <!-- lookup parent from repository -->
1010
</parent>
1111
<groupId>ru.javaops</groupId>
1212
<artifactId>bootjava</artifactId>
1313
<version>1.0</version>
1414
<name>BootJava</name>
15-
<description>Spring Boot 2.x HATEOAS application (BootJava)</description>
15+
<description>Spring Boot 3.x HATEOAS application (BootJava)</description>
1616
<url>https://javaops.ru/view/bootjava</url>
1717

1818
<properties>
1919
<java.version>17</java.version>
20-
<springdoc.version>1.6.8</springdoc.version>
20+
<springdoc.version>2.2.0</springdoc.version>
21+
<jsoup.version>1.16.1</jsoup.version>
2122
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
2223
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
2324
</properties>
@@ -43,18 +44,13 @@
4344
<!-- jackson-->
4445
<dependency>
4546
<groupId>com.fasterxml.jackson.datatype</groupId>
46-
<artifactId>jackson-datatype-hibernate5</artifactId>
47+
<artifactId>jackson-datatype-hibernate5-jakarta</artifactId>
4748
</dependency>
4849

49-
<!--Swagger-->
50+
<!--Springdoc / Swagger-->
5051
<dependency>
5152
<groupId>org.springdoc</groupId>
52-
<artifactId>springdoc-openapi-ui</artifactId>
53-
<version>${springdoc.version}</version>
54-
</dependency>
55-
<dependency>
56-
<groupId>org.springdoc</groupId>
57-
<artifactId>springdoc-openapi-security</artifactId>
53+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
5854
<version>${springdoc.version}</version>
5955
</dependency>
6056

@@ -72,24 +68,22 @@
7268
<groupId>com.h2database</groupId>
7369
<artifactId>h2</artifactId>
7470
</dependency>
71+
<dependency>
72+
<groupId>org.jsoup</groupId>
73+
<artifactId>jsoup</artifactId>
74+
<version>${jsoup.version}</version>
75+
</dependency>
7576
<dependency>
7677
<groupId>org.projectlombok</groupId>
7778
<artifactId>lombok</artifactId>
7879
<optional>true</optional>
7980
</dependency>
80-
<dependency>
81-
<groupId>org.jsoup</groupId>
82-
<artifactId>jsoup</artifactId>
83-
<version>1.14.3</version>
84-
</dependency>
8581

86-
<!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.4-Release-Notes#junit-5s-vintage-engine-removed-from-spring-boot-starter-test-->
8782
<dependency>
8883
<groupId>org.springframework.boot</groupId>
8984
<artifactId>spring-boot-starter-test</artifactId>
9085
<scope>test</scope>
9186
</dependency>
92-
<!-- https://www.baeldung.com/spring-security-integration-tests -->
9387
<dependency>
9488
<groupId>org.springframework.security</groupId>
9589
<artifactId>spring-security-test</artifactId>
Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
package ru.javaops.bootjava.config;
22

3-
import com.fasterxml.jackson.databind.Module;
4-
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
3+
import com.fasterxml.jackson.annotation.JsonAnyGetter;
4+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule;
57
import lombok.extern.slf4j.Slf4j;
68
import org.h2.tools.Server;
9+
import org.springframework.beans.factory.annotation.Autowired;
710
import org.springframework.cache.annotation.EnableCaching;
811
import org.springframework.context.annotation.Bean;
912
import org.springframework.context.annotation.Configuration;
1013
import org.springframework.context.annotation.Profile;
14+
import org.springframework.http.ProblemDetail;
15+
import ru.javaops.bootjava.util.JsonUtil;
1116

1217
import java.sql.SQLException;
18+
import java.util.Map;
19+
20+
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
21+
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
1322

1423
@Configuration
1524
@Slf4j
1625
@EnableCaching
17-
// TODO: cache only most requested data!
1826
public class AppConfig {
1927

2028
@Profile("!test")
@@ -24,9 +32,18 @@ Server h2Server() throws SQLException {
2432
return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092");
2533
}
2634

27-
// https://stackoverflow.com/a/46947975/548473
28-
@Bean
29-
Module module() {
30-
return new Hibernate5Module();
35+
// https://stackoverflow.com/a/74630129/548473
36+
@JsonAutoDetect(fieldVisibility = NONE, getterVisibility = ANY)
37+
interface MixIn {
38+
@JsonAnyGetter
39+
Map<String, Object> getProperties();
40+
}
41+
42+
@Autowired
43+
void configureAndStoreObjectMapper(ObjectMapper objectMapper) {
44+
objectMapper.registerModule(new Hibernate5JakartaModule());
45+
// ErrorHandling: https://stackoverflow.com/questions/7421474/548473
46+
objectMapper.addMixIn(ProblemDetail.class, MixIn.class);
47+
JsonUtil.setMapper(objectMapper);
3148
}
3249
}

src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import io.swagger.v3.oas.annotations.info.Info;
77
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
88
import io.swagger.v3.oas.annotations.security.SecurityScheme;
9-
import org.springdoc.core.GroupedOpenApi;
9+
import org.springdoc.core.models.GroupedOpenApi;
1010
import org.springframework.context.annotation.Bean;
1111
import org.springframework.context.annotation.Configuration;
1212

@@ -24,8 +24,9 @@
2424
description = """
2525
Приложение по <a href='https://javaops.ru/view/bootjava'>курсу BootJava</a>
2626
<p><b>Тестовые креденшелы:</b><br>
27-
- user@gmail.com / password<br>
28-
- admin@javaops.ru / admin</p>
27+
- user@yandex.ru / password<br>
28+
- admin@gmail.com / admin<br>
29+
- guest@gmail.com / guest</p>
2930
""",
3031
contact = @Contact(url = "https://javaops.ru/#contacts", name = "Grigory Kislin", email = "admin@javaops.ru")
3132
),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package ru.javaops.bootjava.config;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import lombok.AllArgsConstructor;
6+
import org.springframework.beans.factory.annotation.Qualifier;
7+
import org.springframework.security.core.AuthenticationException;
8+
import org.springframework.security.web.AuthenticationEntryPoint;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.web.servlet.HandlerExceptionResolver;
11+
12+
@Component
13+
@AllArgsConstructor
14+
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
15+
@Qualifier("handlerExceptionResolver")
16+
private final HandlerExceptionResolver resolver;
17+
18+
@Override
19+
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
20+
resolver.resolveException(request, response, null, authException);
21+
}
22+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package ru.javaops.bootjava.config;
2+
3+
import jakarta.persistence.EntityNotFoundException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.validation.ValidationException;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Getter;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.context.MessageSource;
10+
import org.springframework.context.i18n.LocaleContextHolder;
11+
import org.springframework.core.NestedExceptionUtils;
12+
import org.springframework.dao.DataIntegrityViolationException;
13+
import org.springframework.http.ProblemDetail;
14+
import org.springframework.lang.NonNull;
15+
import org.springframework.security.core.AuthenticationException;
16+
import org.springframework.security.web.firewall.RequestRejectedException;
17+
import org.springframework.validation.BindException;
18+
import org.springframework.validation.BindingResult;
19+
import org.springframework.validation.FieldError;
20+
import org.springframework.validation.ObjectError;
21+
import org.springframework.web.ErrorResponse;
22+
import org.springframework.web.HttpRequestMethodNotSupportedException;
23+
import org.springframework.web.bind.MissingServletRequestParameterException;
24+
import org.springframework.web.bind.annotation.ExceptionHandler;
25+
import org.springframework.web.bind.annotation.RestControllerAdvice;
26+
import org.springframework.web.servlet.NoHandlerFoundException;
27+
import ru.javaops.bootjava.error.*;
28+
29+
import java.io.FileNotFoundException;
30+
import java.nio.file.AccessDeniedException;
31+
import java.util.LinkedHashMap;
32+
import java.util.Map;
33+
import java.util.Optional;
34+
35+
import static ru.javaops.bootjava.error.ErrorType.*;
36+
37+
@RestControllerAdvice
38+
@AllArgsConstructor
39+
@Slf4j
40+
public class RestExceptionHandler {
41+
public static final String ERR_PFX = "ERR# ";
42+
43+
@Getter
44+
private final MessageSource messageSource;
45+
46+
// https://stackoverflow.com/a/52254601/548473
47+
static final Map<Class<? extends Throwable>, ErrorType> HTTP_STATUS_MAP = new LinkedHashMap<>() {
48+
{
49+
// more specific first
50+
put(NotFoundException.class, NOT_FOUND);
51+
put(FileNotFoundException.class, NOT_FOUND);
52+
put(NoHandlerFoundException.class, NOT_FOUND);
53+
put(DataConflictException.class, DATA_CONFLICT);
54+
put(IllegalRequestDataException.class, BAD_REQUEST);
55+
put(AppException.class, APP_ERROR);
56+
put(UnsupportedOperationException.class, APP_ERROR);
57+
put(EntityNotFoundException.class, DATA_CONFLICT);
58+
put(DataIntegrityViolationException.class, DATA_CONFLICT);
59+
put(IllegalArgumentException.class, BAD_DATA);
60+
put(BindException.class, BAD_REQUEST);
61+
put(ValidationException.class, BAD_REQUEST);
62+
put(HttpRequestMethodNotSupportedException.class, BAD_REQUEST);
63+
put(MissingServletRequestParameterException.class, BAD_REQUEST);
64+
put(RequestRejectedException.class, BAD_REQUEST);
65+
put(AccessDeniedException.class, FORBIDDEN);
66+
put(AuthenticationException.class, UNAUTHORIZED);
67+
}
68+
};
69+
70+
@ExceptionHandler(BindException.class)
71+
ProblemDetail bindException(BindException ex, HttpServletRequest request) {
72+
return processException(ex, request, Map.of("invalid_params", getErrorMap(ex.getBindingResult())));
73+
}
74+
75+
// https://howtodoinjava.com/spring-mvc/spring-problemdetail-errorresponse/#5-adding-problemdetail-to-custom-exceptions
76+
@ExceptionHandler(Exception.class)
77+
ProblemDetail exception(Exception ex, HttpServletRequest request) {
78+
return processException(ex, request, Map.of());
79+
}
80+
81+
ProblemDetail processException(@NonNull Exception ex, HttpServletRequest request, Map<String, Object> additionalParams) {
82+
String path = request.getRequestURI();
83+
Class<? extends Exception> exClass = ex.getClass();
84+
Optional<ErrorType> optType = HTTP_STATUS_MAP.entrySet().stream()
85+
.filter(
86+
entry -> entry.getKey().isAssignableFrom(exClass)
87+
)
88+
.findAny().map(Map.Entry::getValue);
89+
if (optType.isPresent()) {
90+
log.error(ERR_PFX + "Exception {} at request {}", ex, path);
91+
return createProblemDetail(ex, optType.get(), ex.getMessage(), additionalParams);
92+
} else {
93+
Throwable root = getRootCause(ex);
94+
log.error(ERR_PFX + "Exception " + root + " at request " + path, root);
95+
return createProblemDetail(ex, APP_ERROR, "Exception " + root.getClass().getSimpleName(), additionalParams);
96+
}
97+
}
98+
99+
private ProblemDetail createProblemDetail(Exception ex, ErrorType type, String defaultDetail, @NonNull Map<String, Object> additionalParams) {
100+
ErrorResponse.Builder builder = ErrorResponse.builder(ex, type.status, defaultDetail);
101+
ProblemDetail pd = builder.build().updateAndGetBody(messageSource, LocaleContextHolder.getLocale());
102+
additionalParams.forEach(pd::setProperty);
103+
return pd;
104+
}
105+
106+
private Map<String, String> getErrorMap(BindingResult result) {
107+
Map<String, String> invalidParams = new LinkedHashMap<>();
108+
for (ObjectError error : result.getGlobalErrors()) {
109+
invalidParams.put(error.getObjectName(), getErrorMessage(error));
110+
}
111+
for (FieldError error : result.getFieldErrors()) {
112+
invalidParams.put(error.getField(), getErrorMessage(error));
113+
}
114+
log.warn("BindingException: {}", invalidParams);
115+
return invalidParams;
116+
}
117+
118+
private String getErrorMessage(ObjectError error) {
119+
return messageSource.getMessage(error.getCode(), error.getArguments(), error.getDefaultMessage(), LocaleContextHolder.getLocale());
120+
}
121+
122+
// https://stackoverflow.com/a/65442410/548473
123+
@NonNull
124+
private static Throwable getRootCause(@NonNull Throwable t) {
125+
Throwable rootCause = NestedExceptionUtils.getRootCause(t);
126+
return rootCause != null ? rootCause : t;
127+
}
128+
}

0 commit comments

Comments
 (0)