JHipster處理異常的核心模塊是zalando,zalando包含兩種方式:
Spring boot
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web-starter</artifactId>
<version>${problem-spring-web.version}</version>
</dependency>
WebMVC
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web</artifactId>
<version>${problem-spring-web.version}</version>
</dependency>
spring boot方式容易在auto configuration上和其它模塊引起衝突,JHipster採用了WebMVC方式。
首先,按照zalando要求,JHipster在JacksonConfiguration類中註冊了兩個模塊,ProblemModule和ConstraintViolationProblemModule,通過註冊這兩個模塊,可以使Response符合RFC 7807,例如:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345",
"/account/67890"]
}
通過註解@ControllerAdvice,以及對ProblemHandling, SecurityAdviceTrait的實現來完成對各類異常的捕捉,以及向前端返回ResponseEntity.
@ControllerAdvice
public class ExceptionTranslator implements ProblemHandling, SecurityAdviceTrait {
@ExceptionHandler
public ResponseEntity<Problem> handleUsernameAlreadyUsedException(com.mycompany.myapp.service.UsernameAlreadyUsedException ex, NativeWebRequest request) {
LoginAlreadyUsedException problem = new LoginAlreadyUsedException();
return create(problem, request, HeaderUtil.createFailureAlert(applicationName, true, problem.getEntityName(), problem.getErrorKey(), problem.getMessage()));
}
}
此外在SecurityConfiguration中添加SecurityProblemSupport來處理安全異常捕捉
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(TokenProvider tokenProvider, CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
this.tokenProvider = tokenProvider;
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf()
.disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.headers()
.contentSecurityPolicy("default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:")
.and()
.referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
.and()
.featurePolicy("geolocation 'none'; midi 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; fullscreen 'self'; payment 'none'")
.and()
.frameOptions()
.deny()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/account/reset-password/init").permitAll()
.antMatchers("/api/account/reset-password/finish").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.httpBasic()
.and()
.apply(securityConfigurerAdapter());
// @formatter:on
}
}
最後,近距離觀察一下JHipster所定義的exception, 以UsernameAlreadyUsedException爲例,JHipster分別在service和web.rest層兩次定義這個異常,避免了service層無法調用web.rest層類的需要。
Service層異常定義:
package com.mycompany.myapp.service;
public class UsernameAlreadyUsedException extends RuntimeException {
private static final long serialVersionUID = 1L;
public UsernameAlreadyUsedException() {
super("Login name already used!");
}
}
web.rest層異常定義:
package com.mycompany.myapp.web.rest.errors;
public class LoginAlreadyUsedException extends BadRequestAlertException {
private static final long serialVersionUID = 1L;
public LoginAlreadyUsedException() {
super(ErrorConstants.LOGIN_ALREADY_USED_TYPE, "Login name already used!", "userManagement", "userexists");
}
}
JHipster對異常信息國際化交給了前端處理,下一篇文章會提到。
Good Luck,
Cheers!