首先配置Hystrix时需要使用到@HystrixCommand
注解,将该注解标记在所需要限制或服务降级的代码块上(目前只在controller层的方法上使用过),其中降级的目标方法,方法入参需要与被降级的方法保持一致,最多添加一个Throwable
类型参数,保证ignoreExceptions
能够使用,具体配置如下:
@Slf4j
@RestController
public class InternationalGMController {
@Autowired
private IInternationalGMjiBiz internationalGMjiBiz;
@Autowired
private RestTemplate restTemplate;
@Value("${fallbackServiceUrl}")
private String fallBackServiceUrl;
@Value("${isManualFallback}")
private String isManualFallback;
@Value("${rocketmq.producer.topic}")
private String defaultTopic;
@SneakyThrows
@PostMapping(value = "/internationalGMWaybill")
@HystrixCommand( fallbackMethod = "fallbackService",
/* 忽略不需要使服务降级的异常,如非法数据校验需要传递response */
ignoreExceptions = {InternationalGMException.class, ApiException.class, UnknownHostException.class},
/* 线程超时时间,如果超过时间则算异常处理*/
commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")},
threadPoolProperties = {/* 核心线程或信号量限制*/
@HystrixProperty(name = "coreSize", value = "10"),
@HystrixProperty(name = "maxQueueSize", value = "20"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "0"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
})
public WaybillResponse<String> internationalGMWaybillServlet(HttpServletRequest request, @RequestParam("data_digest") String data_digest, @RequestParam("logistics_interface") String logistics_interface) {
log.info("internationalGMWaybillServlet data_digest:{},logistics_interface:{}", data_digest, logistics_interface);
//验证项目中拦截器线程和controller线程是否一个线程
System.out.println(Thread.currentThread().getId()+Thread.currentThread().getName()+"controller");
// do something
return new WaybillResponse<>(true, result, ResponseEnums.SUCCESS_OPTION);
} else {
// 通过配置中心设置手动抛出异常,进行手动降级,手法比较低劣哈
throw new ManualFallbackException("应用异常,手动降级");
}
}
//降级方法将会被调用
public WaybillResponse<String> fallbackService(HttpServletRequest request, @RequestParam("data_digest") String data_digest, @RequestParam("logistics_interface") String logistics_interface, Throwable e) {
log.error("调用降级服务 url={} data_digest:{},logistics_interface:{}", fallBackServiceUrl, data_digest, logistics_interface);
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
paramMap.add("data_digest", data_digest);
paramMap.add("logistics_interface", logistics_interface);
return new WaybillResponse<>(false, restTemplate.postForObject(fallBackServiceUrl, paramMap, String.class), ResponseEnums.SERVICE_FALLBACK);
}
}
在这里有个比较隐蔽的东西,我想分享下。
场景:在代码中我有对反序列化(json或者字节数组转实体)进行校验,校验方式使用的JSR-303规范进行校验
扩展阅读:
- JSR-303简单介绍
JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是Hibernate Validator,与Hibernate ORM(
对象关系映射Object Relational Mapping)没有什么关系,JSR-303可以再controller控制器层对数据进行简单方便校验。- JSR-303用到的JAR库
validation-api-1.0.0.GA.jar:JDK的接口;
hibernate-validator-4.2.0.Final.jar是对上述接口的实现(反正我看包名是这个地方)
log4j、slf4j、slf4j-log4j- 简单校验规则
- 判空检查
@Null // 验证对象是否为null @NotNull // 验证对象是否不为null, 无法查检长度为0的字符串 @NotBlank // 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. @NotEmpty // 检查约束元素是否为NULL或者是EMPTY.
- 长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 @Length(min=, max=) 验证字符串长度是否在最大最小值之间.
- 日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期 @Future 验证 Date 和 Calendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期 @Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式,regexp:正则表达式 flags: 指定 Pattern.Flag 的数组,表示正则表达式的相关选项。
- 数字区间检查
- 其他检查
@Valid // 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值 >部分进行校验.(是否进行递归验证) @CreditCardNumber // 信用卡验证 @Email // 验证是否是邮件地址,如果为null,不进行验证,算通过验证。 @ScriptAssert(lang= ,script=, alias=) @URL(protocol=,host=, port=,regexp=, flags=)
先贴上我校验的实体代码,这是一个很简单的JavaBean,然而我却想把这些字段都做校验
package org.tennyson.international;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
/**
* author:Tennyson time:2020/5/8
*/
@Data
public class MDto {
@NotBlank(message = "wNo must not empty")
private String wNo;
@NotBlank(message = "cCode must not empty")
@Length(max = 20,message="cCode length too long")
private String cCode;
private String orderLogisticsCode;
@NotBlank(message = "bCode must not empty")
private String bCode;
@NotBlank(message ="bName" )
private String bName;
}
前面controller接收的是一个string字符串,所以我不能再controller层进行校验,因此我放在了序列化之后使用校验器进行校验。
/* 创建一个全局变量,通过验证器工程获取一个验证器 */
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@SneakyThrows /* 这个东西是悄悄抛异常,反正代码没有try-catch要干净多了 */
@Override
public String sendMessage(String logisticOrder) {
MDto wOrg = JSON.parseObject(logisticOrder, MDto.class); // 反序列化
Set<ConstraintViolation<MDto>> set = validator.validate(wOrg, Default.class); // 开始验证序列化后的实体
if (!set.isEmpty()) {
// 一旦存在验证不通过,就获取验证不通过的原因,并抛异常error输出日志
String reason = set.iterator().next().getMessage();
log.error("Data validation failure,Original data:{} Failure reason:{}", logisticOrder, reason);
// 就是这个异常非常重要,我需要把这个异常信息通过controller的response返回给客户端
throw new InternationalGMException(ResponseEnums.BAD_REQUEST, reason);
}
/**
* do someting
*/
return logisticOrder; // 图方便,我这肯定不是这么写的,自己改改哈
}
在上面验证了实体的数据,不规范的数据直接抛弃,但现在存在一个问题了,我这里抛出来的异常,会被Hystrix捕获然后,它居然给我服务降级了!!!我要把这个异常信息返回给客户端哇,因此就出现了咱们所要使用的忽略异常
,也就是@HystrixCommand
的ignoreExceptions
属性,我随即设置了这个属性,但是没生效,后来查阅资料,发觉别人降级服务的方法中多了一个入参。多了一个Throwable e
然而什么也没有做,只是多了这么个东西,异常信息就抛出去了(emmm……我这里做了相对友好的全局异常处理,所以我感放心大胆的抛异常)
public WaybillResponse<String> fallbackService(@RequestParam("data_digest") String data_digest, @RequestParam("logistics_interface") String logistics_interface, Throwable e) {
log.error("调用降级服务 url={} data_digest:{},logistics_interface:{}", fallBackServiceUrl, data_digest, logistics_interface);
return new WaybillResponse<>(false, "我在这里降级调接口,备胎上位", ResponseEnums.SERVICE_FALLBACK);
}
咱们继续扒一扒源码,我看了看源码大概是在com.netflix.hystrix.contrib.javanica.command.AbstractHystrixCommand
这里头有判断这个异常是否被忽略的异常
/**
* Check whether triggered exception is ignorable.
*
* @param throwable the exception occurred during a command execution
* @return true if exception is ignorable, otherwise - false
*/
boolean isIgnorable(Throwable throwable) {
if (ignoreExceptions == null || ignoreExceptions.isEmpty()) { // ignoreExceptions被初始化时很重要
return false;
}
for (Class<? extends Throwable> ignoreException : ignoreExceptions) {
if (ignoreException.isAssignableFrom(throwable.getClass())) {
return true;
}
}
return false;
}
/**
* Executes an action. If an action has failed and an exception is ignorable then propagate it as HystrixBadRequestException
* otherwise propagate original exception to trigger fallback method.
* Note: If an exception occurred in a command directly extends {@link java.lang.Throwable} then this exception cannot be re-thrown
* as original exception because HystrixCommand.run() allows throw subclasses of {@link java.lang.Exception}.
* Thus we need to wrap cause in RuntimeException, anyway in this case the fallback logic will be triggered.
*
* @param action the action
* @return result of command action execution
*/
Object process(Action action) throws Exception {
Object result;
try {
result = action.execute();
flushCache();
} catch (CommandActionExecutionException throwable) {
Throwable cause = throwable.getCause();
if (isIgnorable(cause)) {
// 如果是忽略的异常就抛出一个HystrixBadRequest异常,又来了一次封装
throw new HystrixBadRequestException(cause.getMessage(), cause);
}
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Exception) {
throw (Exception) cause;
} else {
// instance of Throwable
throw new CommandActionExecutionException(cause);
}
}
return result;
}
后面的因为我断点时间太长了,服务又被降级了,害~下载再深入了解下吧
hystrix.stream
属于静态资源,因此在加载过程前,虽然
/**
* author:Tennyson time:2020/5/9
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RestTemplateBuilder builder;
// 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
@Bean
public RestTemplate restTemplate() {
return builder.build();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/statics/**")
.addResourceLocations("classpath:/statics/");
// 解决 SWAGGER 404报错
registry.addResourceHandler("/swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
// 解决Hystrix静态资源访问404问题,作用与xml中配置静态资源访问一致
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/META-INF/resources/")
.addResourceLocations("classpath:/resources/")
.addResourceLocations("classpath:/static/")
.addResourceLocations("classpath:/public/");
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
/**
**添加Hystrix Dashboard 的servlet 能够通过httprequest进行访问
**/
@Bean
public ServletRegistrationBean<HystrixMetricsStreamServlet> getServlet() {
HystrixMetricsStreamServlet hystrixMetricsStreamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean<HystrixMetricsStreamServlet> servletRegistrationBean = new ServletRegistrationBean();
servletRegistrationBean.setServlet(hystrixMetricsStreamServlet);
servletRegistrationBean.setLoadOnStartup(1);
servletRegistrationBean.addUrlMappings("/hystrix.stream");
servletRegistrationBean.setName("HystrixMetricsStreamServlet");
return servletRegistrationBean;
}
}
SpringBoot启动类使用@EnableCircuitBreaker
和@EnableHystrixDashboard
开启熔断器和hystrix仪表盘
/**
* author:Tennyson time:2020/5/7
*/
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class, SessionAutoConfiguration.class, DruidDataSourceAutoConfigure.class}) //关闭springboot的自动配置
@EnableHystrixDashboard // 启用hystrix监控仪表盘
@EnableCircuitBreaker // 启用hystrix熔断器
@EnableSwagger2 //启用swagger2 接口文档
@EnableAspectJAutoProxy //启用AspectJ进行AOP
@ImportResource(
locations = {
"classpath*:spring-*.xml",
"classpath*:*/spring-*.xml"
})
@ComponentScan(basePackages = {"org.tennyson" })
public class InternationalGMProducerApp {
public static void main(String[] args) {
SpringApplication.run(InternationalGMProducerApp.class, args);
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.tennyson.internationalgroupId>
<artifactId>internationalartifactId>
<version>alphaversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>internationalartifactId>
<version>alphaversion>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>com.ctrip.framework.apollogroupId>
<artifactId>apollo-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.7.0version>
dependency>
dependencies>
<build>
<finalName>${artifactId}-${version}finalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
在输出日志过程中,我有尝试使用MDC
将部分key-value追加到日志中,日志输出到ELK日志中心,使用的com.github.danielwegener.logback.kafka.KafkaAppender
进行日志输出至kafka,其中编码器使用的net.logstash.logback.encoder.LogstashEncoder
。
使用MDC
追加时发现追加不进去,kafkaAppender输出的日志根本就没有我追加的key-value,因此查看了MDC
的使用姿势和它的暂存方式。
将MDC
简单封装成一个工具,只需要进行调用即可,此处涉及到java8新特性中的static方法
java8中为接口新增了一项功能:定义一个或者更多个静态方法。用法和普通的static方法一样。实现接口的类或者子接口不会继承接口中的静态方法
扩展阅读:
JAVA8 新特性详解
- Lambda表达式和函数式接口(也称为闭包),简单来说就是方法也成为了第一公民
- 接口的默认方法和静态方法
- 方法引用
- 重复注解
- 泛型类,泛型方法
- 注解应用场景拓宽
package cn.yto.international.utils;
import lombok.val;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* author:admin date:2020/6/4
**/
public interface MDCUtils{
/**
* 设置请求中对于日志查询和统计具有意义的字段
*/
static void putRequestFields(HttpServletRequest request) {
// 此处为测试拦截器中线程是否与MDCUtil的线程一致
System.out.println(Thread.currentThread().getId()+"mdcutil");
val IS_DEBUG = request.getHeader("IS_DEBUG");
val DEVICE_ID = request.getHeader("DEVICE_ID");
MDC.put("req_is_debug", StringUtils.isEmpty(IS_DEBUG) ? "false" : "true");
MDC.put("req_device_id", StringUtils.isEmpty(DEVICE_ID) ? "anonymity_device_id" : DEVICE_ID);
MDC.put("req_user_agent", request.getHeader("User-Agent"));
// MDC.put("req_request_url", request.getRequestURL());
// 获取请求对应的交易名称
MDC.put("req_request_uri", request.getRequestURI());
// 获取请求对应的交易对应的方法(如:POST)
MDC.put("req_request_method", request.getMethod());
// 返回请求体内容的长度,不包含url query string,字节为单位
MDC.put("req_content_length", String.valueOf(request.getContentLength()));
// 获取发出请求的客户端的IP地址
MDC.put("req_remote_addr", request.getRemoteAddr());
// 获取发出请求的客户端的端口号
// MDC.put("req_remote_port", String.valueOf(request.getRemotePort()));
// 如果用户已经过认证,则返回发出请求的用户登录信息
val userid = request.getRemoteUser();
MDC.put("req_remote_user", StringUtils.isEmpty(userid) ? "anonymity_user" : userid);
// qr放在这里设置不生效,目前调整到控制器的拦截器进行设置
// MDC.put("req_query_string", request.getQueryString()));
}
/**
* 放置spring mvc拦截器触发时机才能获取到的字段
*/
static void putSpringMVCInterceptorFields(HttpServletRequest request) {
MDC.put("req_query_string", String.valueOf(request.getQueryString()));
}
/**
* 放置响应结果中有意义的字段
*
* @param response
*/
static void putResponseFields(HttpServletResponse response) {
MDC.put("resp_status", String.valueOf(response.getStatus()));
}
}
接下来就是通过实现HandlerInterceptor
创建拦截器了,不过在spring boot项目中,创建完拦截器后,还需要使用InterceptorRegistry
也就是配置WebMvcConfigurer
将拦截器注册(添加)到拦截器栈中,这是重点!!!
拦截器栈应该也是一种责任链设计模式,这个在设计模式模块会进行简单实现
package org.tennyson.international.intercepter;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* author:admin date:2020/6/4
**/
@Component
public class HttpServiceIntercepter implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MDCUtils.putRequestFields(request);
// 此处依然是验证拦截器线程是否与controller线程,一致
System.out.println(Thread.currentThread().getId()+Thread.currentThread().getName()+"intercepter");
System.out.println("Pre Handle method is Calling");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("Request and Response is completed");
}
}
拦截器注册配置
package org.tennyson.international.config;
import cn.yto.international.intercepter.HttpServiceIntercepter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* author:tennyson date:2020/6/4
**/
@Configuration
public class ProductServiceInterceptorAppConfig implements WebMvcConfigurer {
@Autowired
HttpServiceIntercepter productServiceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(productServiceInterceptor);
}
}
现在基本的都准备完毕了,
# 先用命令打个包,可视化界面都可以的,只不过我喜欢装X,哈哈哈哈哈
mvn clean package -Dskip.test=true -Pdev # 跳过测试,并只打包dev环境
# 进入target目录下,就是你jar包输出的目录,准备启动
mvn springboot:run
# 或者是使用jvm的参数,指定配置文件时要是配置文件在jar包内就不用这个参数了,-D都是jvm的参数嗷
java -jar -Dspring.profiles.active=dev -Dspring.config.location=<你指定的application.properties路径> international-alpha.jar
项目启动了,在浏览器中打开项目的swagger接口文档http://localhost:8049/swagger-ui.html,添加参数开始发包
由上图可知intercepter
的线程是容器的线程,而进入controller
代码中的线程为hystrix
的线程,所以咱们在拦截器中调用MDCUtil的线程与controller的线程不属于一个线程,断点发现,request资源在hystrix线程当中,这也是为什么MDCUtil在拦截器中获取request资源了,我的解决方法就是取消Hystrix的使用(因为在我这个项目中对于Hystrix的使用并没有特别强的需求),当然也可以尝试将hystrix线程中的资源拷贝一份到http-nio
线程中。