从零开始搭建微服务:异常处理

目前系统的异常响应都是以Spring内部构建好的默认的格式返回。这样的格式对于开发的友好度不是很好。

{
    "error": "invalid_grant",
    "error_description": "Bad credentials"
}

下面我们通过自定义各种异常处理器,来将默认的异常响应转换为对我们友好的的格式响应。

认证服务器异常处理

默认情况下,当我们在获取令牌时输入错误的用户名或密码,系统返回如下格式响应:

{
    "error": "invalid_grant",
    "error_description": "Bad credentials"
}

当grant_type错误时,系统返回:

{
    "error": "unsupported_grant_type",
    "error_description": "Unsupported grant type: passwordd"
}

接下来我们定义一个异常翻译器,将这些认证类型异常翻译为友好的格式。在elsa-auth模块com.elsa.auth路径下新建translator包,然后在该包下新建ElsaWebResponseExceptionTranslator:

@Component
public class ElsaWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
	private Logger log = LoggerFactory.getLogger(this.getClass());
    @Override
    public ResponseEntity translate(Exception e) {
        ResponseEntity.BodyBuilder status = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR);
        ElsaResponse response = new ElsaResponse();
        String message = "认证失败";
        log.error(message, e);
        if (e instanceof UnsupportedGrantTypeException) {
            message = "不支持该认证类型";
            return status.body(response.message(message));
        }
        if (e instanceof InvalidGrantException) {
            if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid refresh token")) {
                message = "refresh token无效";
                return status.body(response.message(message));
            }
            if (StringUtils.containsIgnoreCase(e.getMessage(), "locked")) {
                message = "用户已被锁定,请联系管理员";
                return status.body(response.message(message));
            }
            message = "用户名或密码错误";
            return status.body(response.message(message));
        }
        return status.body(response.message(message));
    }
}
  • @Component注解用于将当前类注册为一个Bean
  • ElsaWebResponseExceptionTranslator 实现了WebResponseExceptionTranslator接口,用于覆盖默认的认证异常响应.
  • 在translate方法中,我们通过Exception异常对象的类型和内容将异常归类,并且统一返回500HTTP状态码(HttpStatus.INTERNAL_SERVER_ERROR)
    要让这个异常翻译器生效,我们还需在认证服务器配置类ElsaAuthorizationServerConfigure的configure(AuthorizationServerEndpointsConfigurer endpoints)方法里指定它:
@Configuration
@EnableAuthorizationServer
public class ElsaAuthorizationServerConfigure extends AuthorizationServerConfigurerAdapter {

	......

    @Autowired
    private ElsaWebResponseExceptionTranslator exceptionTranslator;

	......
	
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(tokenStore())
                .userDetailsService(userDetailService)
                .authenticationManager(authenticationManager)
                .tokenServices(defaultTokenServices())
                //使ElsaWebResponseExceptionTranslator翻译器生效
                .exceptionTranslator(exceptionTranslator);
    }
    
    ......

}

配置好后,重启elsa-auth模块,在获取令牌的时候,当输入错误的用户名密码时,系统返回:
从零开始搭建微服务:异常处理_第1张图片
当grant_type填写为password1的时候,系统返回:
从零开始搭建微服务:异常处理_第2张图片
在通过refresh_token刷新令牌的时候,填写错误的refresh_token,系统返回:
从零开始搭建微服务:异常处理_第3张图片

资源服务器异常处理

资源服务器异常主要有两种:令牌不正确返回401和用户无权限返回403。因为资源服务器有多个,所以相关的异常处理类可以定义在elsa-common通用模块里。

401异常类型处理

在elsa-common模块com.elsa.common路径下新建handler包,然后在该包下新建ElsaAuthExceptionEntryPoint:

public class ElsaAuthExceptionEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        ElsaResponse elsaResponse = new ElsaResponse();
        ElsaUtil.makeResponse(
                response, MediaType.APPLICATION_JSON_UTF8_VALUE,
                HttpServletResponse.SC_UNAUTHORIZED, elsaResponse.message("token无效")
        );
    }
}

构造响应的这几行代码后续会经常使用到,所以我们可以将它抽取为一个工具方法,ElsaUtil工具类:

public class ElsaUtil {
   /**
     *
     * @param response    HttpServletResponse
     * @param contentType content-type
     * @param status      http状态码
     * @param value       响应内容
     * @throws IOException IOException
     * @desc 设置响应
     */
    public static void makeResponse(HttpServletResponse response, String contentType,
                                    int status, Object value) throws IOException {
        response.setContentType(contentType);
        response.setStatus(status);
        response.getOutputStream().write(JSONObject.toJSONString(value).getBytes());
    }
}
403异常类型处理

接着在elsa-common的com.elsa.common.handler路径下新建ElsaAccessDeniedHandler用于处理403类型异常:

public class ElsaAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        ElsaResponse elsaResponse = new ElsaResponse();
        ElsaUtil.makeResponse(
                response, MediaType.APPLICATION_JSON_UTF8_VALUE,
                HttpServletResponse.SC_FORBIDDEN, elsaResponse.message("没有权限访问该资源"));
    }
}

因为elsa-common模块是一个普通的maven项目,并不是一个Spring Boot项目,所以即使在这两个类上使用@Component注解标注,它们也不能被成功注册到各个微服务子系统的Spring IOC容器中。我们可以使用@Enable模块驱动的方式来解决这个问题。

@Enable模块驱动注入

在elsa-common模块的com.elsa.common路径下新建configure包,然后在该包下新建ElsaAuthExceptionConfigure配置类:

public class ElsaAuthExceptionConfigure {
    @Bean
    @ConditionalOnMissingBean(name = "accessDeniedHandler")
    public ElsaAccessDeniedHandler accessDeniedHandler() {
        return new ElsaAccessDeniedHandler();
    }

    @Bean
    @ConditionalOnMissingBean(name = "authenticationEntryPoint")
    public ElsaAuthExceptionEntryPoint authenticationEntryPoint() {
        return new ElsaAuthExceptionEntryPoint();
    }
}

在该配置类中,我们注册了ElsaAccessDeniedHandler和ElsaAuthExceptionEntryPoint。@ConditionalOnMissingBean注解的意思是,当IOC容器中没有指定名称或类型的Bean的时候,就注册它。以@ConditionalOnMissingBean(name = “accessDeniedHandler”)为例,当微服务系统的Spring IOC容器中没有名称为accessDeniedHandler的Bean的时候,就将ElsaAccessDeniedHandler注册为一个Bean。这样做的好处在于,子系统可以自定义自个儿的资源服务器异常处理器,覆盖我们在elsa-common通用模块里定义的。
接着定义一个注解来驱动该配置类。
在elsa-common模块的com.elsa.common路径下新建annotation包,然后在该包下新建EnableElsaAuthExceptionHandler注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ElsaAuthExceptionConfigure.class)
public @interface EnableElsaAuthExceptionHandler {

}

在该注解上,我们使用@Import将ElsaAuthExceptionConfigure配置类引入了进来。
使用@EnableElsaAuthExceptionHandler注解。
因为elsa-auth,elsa-server-system,elsa-server-demo都是资源服务器,所以它们三个都需要使用@EnableElsaAuthExceptionHandler注解实现资源服务器异常处理。三者的步骤都是一模一样的,所以这里以elsa-auth模块为例,剩下的elsa-server-system和elsa-server-demo照着配置即可。
在elsa-auth模块的入口类上使用@EnableElsaAuthExceptionHandler注解标注:

@EnableDiscoveryClient
@SpringBootApplication
@EnableElsaAuthExceptionHandler
public class ElsaAuthApp 
{
    public static void main(String[] args) {
        SpringApplication.run(ElsaAuthApp.class, args);
    }
}

通过该注解,elsa-auth模块的IOC容器里就已经注册了ElsaAccessDeniedHandler和ElsaAuthExceptionEntryPoint。

然后在资源服务器配置类ElsaResourceServerConfigurer里注入它们,并配置:

@Configuration
@EnableResourceServer	//开启资源服务器相关配置
public class ElsaResourceServerConfigure extends ResourceServerConfigurerAdapter {
	
    @Autowired
    private ElsaAccessDeniedHandler accessDeniedHandler;
    @Autowired
    private ElsaAuthExceptionEntryPoint exceptionEntryPoint;
    
    ......    
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.authenticationEntryPoint(exceptionEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
    }
}

elsa-server-system和elsa-server-demo模块也按照这两个步骤配置即可。

PostMan

重启elsa-auth、elsa-server-system和elsa-server-demo模块,使用PostMan发送

token无效

从零开始搭建微服务:异常处理_第4张图片

没有权限访问该资源

从零开始搭建微服务:异常处理_第5张图片
说明我们上面的配置已经生效。
注:对Spring@Enable模块驱动不了解的同学可以参考:Spring-Enable模块驱动

Zuul网关服务器异常处理

当Zuul转发请求超时时,系统返回如下响应:

{
    "timestamp": "2019-08-07T07:58:21.938+0000",
    "status": 504,
    "error": "Gateway Timeout",
    "message": "com.netflix.zuul.exception.ZuulException: Hystrix Readed time out"
}

当处理转发请求的微服务模块不可用时,系统返回:

{
    "timestamp": "2019-08-07T08:01:31.829+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "GENERAL"
}

自定义Zuul异常处理可以通过继承Zuul的SendErrorFilter过滤器来实现。

在elsa-gateway模块的com.elsa.gateway路径下新建filter包,然后在该包下新建ElsaGatewayErrorFilter过滤器:

@Component
public class ElsaGatewayErrorFilter extends SendErrorFilter {
	private Logger log = LoggerFactory.getLogger(this.getClass());

	@Override
	public Object run() {
		try {
			ElsaResponse elsaResponse = new ElsaResponse();
			RequestContext ctx = RequestContext.getCurrentContext();
			String serviceId = (String) ctx.get(FilterConstants.SERVICE_ID_KEY);

			ExceptionHolder exception = findZuulException(ctx.getThrowable());
			String errorCause = exception.getErrorCause();
			Throwable throwable = exception.getThrowable();
			String message = throwable.getMessage();
			message = StringUtils.isBlank(message) ? errorCause : message;
			elsaResponse = resolveExceptionMessage(message, serviceId, elsaResponse);

			HttpServletResponse response = ctx.getResponse();
			ElsaUtil.makeResponse(response, MediaType.APPLICATION_JSON_UTF8_VALUE,
					HttpServletResponse.SC_INTERNAL_SERVER_ERROR, elsaResponse);
			log.error("Zull sendError:{}", elsaResponse.getMessage());
		} catch (Exception ex) {
			log.error("Zuul sendError", ex);
			ReflectionUtils.rethrowRuntimeException(ex);
		}
		return null;
	}

	private ElsaResponse resolveExceptionMessage(String message, String serviceId, ElsaResponse elsaResponse) {
		if (StringUtils.containsIgnoreCase(message, "time out")) {
			return elsaResponse.message("请求" + serviceId + "服务超时");
		}
		if (StringUtils.containsIgnoreCase(message, "forwarding error")) {
			return elsaResponse.message(serviceId + "服务不可用");
		}
		return elsaResponse.message("Zuul请求" + serviceId + "服务异常");
	}
}

在该过滤器中,我们可以通过RequestContext获取到当前请求上下文,通过请求上下文可以获取到当前请求的服务名称serviceId和当前请求的异常对象ExceptionHolder等信息。通过异常对象我们可以继续获取到异常内容,根据不同的异常内容我们可以自定义想要的响应。

要让我们自定义的Zuul异常过滤器生效,还需要在elsa-gateway的配置文件中添加如下配置,让默认的异常过滤器失效:

zuul:
  SendErrorFilter:
    error:
      disable: true

重启elsa-gateway模块,当请求服务超时时,响应如下所示:
从零开始搭建微服务:异常处理_第6张图片
当请求的服务不可用时候,响应如下所示:
从零开始搭建微服务:异常处理_第7张图片

全局异常处理

所谓的全局异常处理指的是全局处理Controller层抛出来的异常。因为全局异常处理器在各个微服务系统里都能用到,所以我们把它定义在elsa-common模块里。

在elsa-common模块的com.elsa.common.handler路径下新建BaseExceptionHandler:

public class BaseExceptionHandler {
	private Logger log = LoggerFactory.getLogger(this.getClass());
	@ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ElsaResponse handleException(Exception e) {
        log.error("系统内部异常,异常信息", e);
        return new ElsaResponse().message("系统内部异常");
    }

    @ExceptionHandler(value = ElsaAuthException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ElsaResponse handleFebsAuthException(ElsaAuthException e) {
        log.error("系统错误", e);
        return new ElsaResponse().message(e.getMessage());
    }
    
    @ExceptionHandler(value = AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ElsaResponse handleAccessDeniedException(){
        return new ElsaResponse().message("没有权限访问该资源");
    }
}

然后以elsa-auth为例,在elsa-auth模块的com.elsa.auth路径下新建handler包,然后在该包下新建GlobalExceptionHandler类:

@RestControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler extends BaseExceptionHandler {
}

对于通用的异常类型捕获可以在BaseExceptionHandler中定义,而当前微服务系统独有的异常类型捕获可以在GlobalExceptionHandler中定义。
elsa-server-system和elsa-server-demo模块处理方式和elsa-auth一致,分别在com.elsa.server.system.handler和com.elsa.server.demo.handler包下创建GlobalExceptionHandler类。

源码下载

源码地址:异常处理

你可能感兴趣的:(从零开始搭建微服务,微服务,spring,cloud)