SpringBoot 处理全局异常,包括 404 错误,返回 JSON 或者页面

2020-01-21   龙德   SpringBoot   异常处理 HandlerExceptionResolver ControllerAdvice ExceptionHandler ErrorController  

第一种方式:实现 HandlerExceptionResolver 接口

BaseException 是我自定义的业务异常类,包含错误码和错误信息。

public class BaseExceptionHandler implements HandlerExceptionResolver {

	@Override
	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
			Exception e) {
		String contentType = request.getContentType();
		// 返回 json
		if (Objects.equals("application/json", contentType)) {
		    // 使用这个类可以返回 json 数据
			MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
			jsonView.setExtractValueFromSingleKeyModel(true);
			ModelAndView mv = new ModelAndView(jsonView);
			if (e instanceof BaseException) {
				BaseException be = (BaseException) e;
				response.setStatus(be.getHttpCode());
				mv.addObject(new Result<>(be.getErrorCode(), be.getMessage()));
				return mv;
			} else {
				response.setStatus(BaseErrorCodeEnum.INTERNAL_ERROR.getHttpCode());
				mv.addObject(new Result<>(BaseErrorCodeEnum.INTERNAL_ERROR.getErrorCode(),
						BaseErrorCodeEnum.INTERNAL_ERROR.getMessage()));
				return mv;
			}
		} else {
		    // 返回页面
			ModelAndView mv = new ModelAndView();
			if (e instanceof BaseException) {
				BaseException be = (BaseException) e;
				response.setStatus(be.getHttpCode());
				mv.addObject("exception", be.getMessage());
				mv.addObject("errorCode", be.getErrorCode());
				mv.setViewName("/default/front/error/error");
				return mv;
			} else {
				response.setStatus(BaseErrorCodeEnum.INTERNAL_ERROR.getHttpCode());
				mv.addObject("exception", BaseErrorCodeEnum.INTERNAL_ERROR.getMessage());
				mv.addObject("errorCode", BaseErrorCodeEnum.INTERNAL_ERROR.getErrorCode());
				mv.setViewName("/default/front/error/error");
				return mv;
			}
		}
	}

}

这种方式只能处理 Controller 层抛出的异常,对于 DispatcherServlet 抛出的异常无能为力。

第二种方式:使用 @ControllerAdvice 和 @ExceptionHandler 注解

@ControllerAdvice
public class GlobalExceptionHandler {
	
	@ExceptionHandler(value = Exception.class)
	public ModelAndView defaultErrorHandler(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {
		String contentType = request.getContentType();
		if (Objects.equals("application/json", contentType)) {
			MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
			jsonView.setExtractValueFromSingleKeyModel(true);
			ModelAndView mv = new ModelAndView(jsonView);
			if (e instanceof BaseException) {
				BaseException be = (BaseException) e;
				response.setStatus(be.getHttpCode());
				mv.addObject(new Result<>(be.getErrorCode(), be.getMessage()));
				return mv;
			} else {
				response.setStatus(BaseErrorCodeEnum.INTERNAL_ERROR.getHttpCode());
				mv.addObject(new Result<>(BaseErrorCodeEnum.INTERNAL_ERROR.getErrorCode(),
						BaseErrorCodeEnum.INTERNAL_ERROR.getMessage()));
				return mv;
			}
		} else {
			ModelAndView mv = new ModelAndView();
			if (e instanceof BaseException) {
				BaseException be = (BaseException) e;
				response.setStatus(be.getHttpCode());
				mv.addObject("exception", be.getMessage());
				mv.addObject("errorCode", be.getErrorCode());
				mv.setViewName("/default/front/error/error");
				return mv;
			} else {
				response.setStatus(BaseErrorCodeEnum.INTERNAL_ERROR.getHttpCode());
				mv.addObject("exception", BaseErrorCodeEnum.INTERNAL_ERROR.getMessage());
				mv.addObject("errorCode", BaseErrorCodeEnum.INTERNAL_ERROR.getErrorCode());
				mv.setViewName("/default/front/error/error");
				return mv;
			}
		}
	  }
	
}

这种方式可以处理 Controller 层和 DispatcherServlet 抛出的异常,所以用这种方式就好了。

处理 404 错误

DispatcherServlet 中有一个 throwExceptionIfNoHandlerFound 属性,如下图所示:

image

它的作用是:如果找不到处理该请求的处理程序,是否抛出 NoHandlerFoundException?

它默认是 false,也就是说找不到处理器时不抛出 NoHandlerFoundException 异常,DispatcherServlet 会自己处理。

所以上面那两个全局处理类都无法捕获到 NoHandlerFoundException 异常,也就无法处理 404 错误。

你可以在 application.properties 加上这段参数,将 throwExceptionIfNoHandlerFound 属性设置为 true。

spring.mvc.throw-exception-if-no-handler-found=true

但是这样还是无法起作用,关键是这段代码:

image

这段代码表明只有找不到处理器,也就是 mappedHandler 为 null 的情况下,才会进入 noHandlerFound 方法。

然而 SpringBoot 会默认配置一个静态资源映射处理器 ResourceHttpRequestHandler,所以不会进入 noHandlerFound 方法。

image

幸好 SpringBoot 还提供了配置接口,我们只要在 application.properties 里加入以下配置,即可关闭默认的静态资源映射处理器。

spring.resources.add-mappings=false

这样,当 DispatcherServlet 找不到处理器时,又没有默认的静态资源映射处理器,同时我们把 throwExceptionIfNoHandlerFound 属性设置了 true,那么 DispatcherServlet 就会抛出 NoHandlerFoundException 异常。就会被我们上面定义的 GlobalExceptionHandler 类捕获到,返回我们自定义的处理结果。

但是这种方式有一个弊端,就是关闭静态资源映射处理器后,无法访问到静态资源,只适合纯后端接口,没有前端的项目。

如果在不影响静态资源访问的情况下,又能处理 404 错误呢?请看下面这种方式:

SpringBoot 提供了一个接口,名字叫做 ErrorController。这个接口有一个实现类 BasicErrorController,它主要定义了两个方法:

image

DispatcherServlet 处理一个不存在的请求时,该请求会转发到 /${context-path}/error,并且不会被 ControllerAdvice 拦截。

它默认的页面是这样的:

image

默认的 JSON 是这样的:

image

所以,我们可以自己定义一个 BasicErrorController,返回自定义的页面和 JSON 数据。

比如我是这样写的:

@RequestMapping(value = "/error", produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request) {
	ModelAndView mv = new ModelAndView();
	mv.addObject("exception", BaseErrorCodeEnum.INTERNAL_ERROR.getMessage());
	mv.addObject("errorCode", getStatus(request).value());
	mv.setViewName("/default/front/error/error");
	return mv;
}

@RequestMapping(value = "/error")
@ResponseBody
public ResponseEntity<Result<String>> error(HttpServletRequest request) {
	HttpStatus status = getStatus(request);
	return new ResponseEntity<Result<String>>(
			new Result<>(String.valueOf(status.value()), BaseErrorCodeEnum.INTERNAL_ERROR.getMessage()), status);
}

这时候 ErrorController 接口应该有两个实现类,一个是 SpringBoot 的,还有一个是自己定义的。

image

但是这样也不会冲突,因为 SpringBoot 会判断,如果已经存在 ErrorController 的实现类时,它就不会加载 BasicErrorController。

源码链接:

https://github.com/miansen/Roothub/blob/master/src/main/java/wang/miansen/roothub/common/handler/BaseExceptionHandler.java

https://github.com/miansen/Roothub/blob/master/src/main/java/wang/miansen/roothub/common/handler/GlobalExceptionHandler.java

https://github.com/miansen/Roothub/blob/master/src/main/java/wang/miansen/roothub/common/controller/BaseErrorController.java

成员变量和静态方法不具有多态性

2020-01-18   龙德   Java   多态  

成员变量不具有多态性,请看下面的例子:

public class Super {

	public int field = 0;

	public int getField() {
		return field;
	}

}
public class Sub extends Super {

	public int field = 1;

	public int getField() {
		return field;
	}
	
	public int getSuperField() {
		return super.field;
	}
	
}
public static void main(String[] args) {
		Super sup = new Sub();
		System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField());
		Sub sub = new Sub();
		System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField()
				+ ", sub.getSuperField() = " + sub.getSuperField());
	}

输出结果:

sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0

本例中,Super 和 Sub 各有一个成员变量 field,当 Sub 继承 Super 后,Sub 实际包含两个名为 field 的成员变量:一个是它自己的,还有一个是从 Super 那里继承得来的。尽管这两个成员变量的名字相同,但是它们不是同一个变量,因为它们在内存中分配了不同的存储空间。当 Sub 对象向上转型为 Super 引用时,任何成员变量的访问操作都将由编译器解析。所以使用 Super 类型的引用访问 field 变量时,指向的是 Super 类中的 field。同理,使用 Sub 类型的引用访问 field 变量时,指向的是 Sub 类中的 field。你如果想访问从 Super 那里继承来的 field 时,必须使用 super 关键字,显示地调用 suepr.field。

虽然这看起来有些混淆,但是实际中,这些问题是不会发生的。首先,你通常会将所有的成员变量的访问权限都设置成 private,因此你无法直接访问它们,而是通过提供的 get 方法访问。另外,你通常不会对子类和父类的变量设置相同的名字,因为这种做法本身就容易令人混淆。

静态方法也不具有多态性,请看下面的例子:

public class Super {

	public static String staticGet() {
		return "Super staticGet()";
	}
	
	public String dynamicGet() {
		return "Super dynamicGet()";
	}

}
public class Sub extends Super {
	
	public static String staticGet() {
		return "Sub staticGet()";
	}
	
	public String dynamicGet() {
		return "Sub dynamicGet()";
	}
	
}
public static void main(String[] args) {
		Super sup = new Sub();
		System.out.println(sup.staticGet());
		System.out.println(sup.dynamicGet());
	}

输出结果:

Super staticGet()
Sub dynamicGet()

静态方法是与类,而非某个对象相关联的,所以静态方法不具有多态性。

Git报错:Your local changes to the following files would be overwritten by merge:xx

2020-01-11   龙德   杂七杂八   Git  

用git pull来更新代码的时候,遇到了下面的问题:

$ git pull
Updating 725dd43..c34cb92
error: Your local changes to the following files would be overwritten by merge:
        src/main/java/cn/roothub/bbs/common/dao/jdbc/builder/DataSourceBuilder.java
Please commit your changes or stash them before you merge.
Aborting

出现这个问题的原因是其他人修改了 xxx.java 并提交到版本库中去了,而你本地也修改了xxx.java,这时候你进行 git pull 操作就好出现冲突了。

有两种解决方法:

1、保留本地的修改

git stash
git pull
git stash pop

通过 git stash 将工作区恢复到上次提交的内容,同时备份本地所做的修改,之后就可以正常 git pull 了,git pull 完成后,执行 git stash pop 将之前本地做的修改应用到当前工作区。

git stash: 备份当前的工作区的内容,从最近的一次提交中读取相关内容,让工作区保证和上次提交的内容一致。同时,将当前的工作区内容保存到Git栈中。

git stash pop: 从Git栈中读取最近一次保存的内容,恢复工作区的相关内容。由于可能存在多个 Stash 的 内容,所以用栈来管理,pop 会从最近的一个 stash 中读取内容并恢复。

git stash list: 显示 Git 栈内的所有备份,可以利用这个列表来决定从那个地方恢复。

git stash clear: 清空 Git 栈。此时使用 gitg 等图形化工具会发现,原来 stash 的哪些节点都消失了。

2、放弃本地修改

这种方法会丢弃本地修改的代码,而且不可找回。

git reset --hard
git pull

通过上述的方法 pull 之后,可能还会报这个错误:

error: The following untracked working tree files would be overwritten by merge:
        src/main/java/wang/miansen/roothub/common/dto/BaseDTO.java
Please move or remove them before you merge.
Aborting

这是由于一些 untracked working tree files(未跟踪的文件)引起的问题。

可以执行这个命令删除这些未跟踪的文件:

# 首先确认要删除的文件
git clean -fd -n
# 如果以上命令给出的文件列表是你想删除的, 那么接下来执行
git clean -d -fx

对于这个命令 git clean -d -fx,-f表示文件 -d表示目录, 如果还要删除.gitignore中的文件那么再加上-x。

命名参考

git删除未跟踪文件

  • 删除 untracked files

git clean -f

  • 连 untracked 的目录也一起删掉

git clean -fd

  • 连 gitignore 的untrack 文件/目录也一起删掉 (慎用,一般这个是用来删掉编译出来的 .o之类的文件用的)

git clean -fdx

  • 在用上述 git clean 前,墙裂建议加上 -n 参数来先看看会删掉哪些文件,防止重要文件被误删
git clean -nf
git clean -nfd
git clean -nfdx

Java 的异常

2019-12-25   龙德   Java   Exception  

异常分类

Java 的异常分为两类,受检异常和非受检异常。也可以叫运行时异常和非运行时异常。都是一样的意思。

先看看受检异常

public void foo() throws Exception {
		System.out.println("hello world!");
	}

throws 关键字作用于方法名后面,作用是抛出异常。

这里抛出了 Exception 异常,这是个受检异常。也就是说调用方必须对这个异常进行处理,否则编译不会通过。

这是 Eclipse 给出的提示,要么继续抛出异常,交给上层调用者处理,要么就在方法里处理。

然后是非受检异常

public void foo() throws RuntimeException {
		System.out.println("hello world!");
	}

RuntimeException 就是一个非受检异常,从名字就可以知道,这是一个在程序运行时才有可能会发生的异常。

由于是运行时才会发生的异常,所以非受检异常不强制要求做处理。

异常堆栈

public void controller() {
	service();
}

public void service() {
	dao();
}

public void dao() {
	System.out.println(1 / 0);
}

public static void main(String[] args) {
	ExceptionTest exceptionTest = new ExceptionTest();
	exceptionTest.controller();
}

控制台输出的异常信息:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.test.ExceptionTest.dao(ExceptionTest.java:31)
	at com.test.ExceptionTest.service(ExceptionTest.java:25)
	at com.test.ExceptionTest.controller(ExceptionTest.java:19)
	at com.test.ExceptionTest.main(ExceptionTest.java:37)

异常堆栈的输出跟方法调用一样,也是先进后出的结构。main 方法最先调用,异常最后输出。dao 方法最后调用,异常最先输出。

异常链

捕捉住异常后,将老异常当做新异常的构造函数参数创建新异常并继续抛出,这样就形成了异常链。

public void controller() throws Exception {
	try {
		service();
	} catch (Exception e) {
		throw new Exception("controller 层抛出的异常", e);
	}
}

public void service() throws Exception {
	try {
		dao();
	} catch (Exception e) {
		throw new Exception("service 层抛出的异常", e);
	}
}

public void dao() throws Exception {
	try {
		System.out.println(1/0);
	} catch (Exception e) {
		throw new Exception("dao 层抛出的异常", e);
	}
}
	
public static void main(String[] args) {
	ExceptionTest exceptionTest = new ExceptionTest();
	try {
		exceptionTest.controller();
	} catch (Exception e) {
		e.printStackTrace();
	}
}

控制台输出的异常信息:

java.lang.Exception: controller 层抛出的异常
	at com.test.ExceptionTest.controller(ExceptionTest.java:21)
	at com.test.ExceptionTest.main(ExceptionTest.java:44)
Caused by: java.lang.Exception: service 层抛出的异常
	at com.test.ExceptionTest.service(ExceptionTest.java:29)
	at com.test.ExceptionTest.controller(ExceptionTest.java:19)
	... 1 more
Caused by: java.lang.Exception: dao 层抛出的异常
	at com.test.ExceptionTest.dao(ExceptionTest.java:37)
	at com.test.ExceptionTest.service(ExceptionTest.java:27)
	... 2 more
Caused by: java.lang.ArithmeticException: / by zero
	at com.test.ExceptionTest.dao(ExceptionTest.java:35)
	... 3 more

异常链也是先进后出的结构,新异常最先输出,老异常最后输出。也就是说第一个异常信息就是异常抛出的地方,最后一个异常信息就是产生异常的源头。

ApplicationContext 发布事件报错-Caused by-java.lang.IllegalStateException-ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context-Root WebApplicationContext-startup date [Sat Dec 14 15:02:30 CST 2019];root of context hierarchy

2019-12-16   龙德   SpringMVC   ApplicationContext 事件  

报错信息如下:

Caused by: java.lang.IllegalStateException: ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: Root WebApplicationContext: startup date [Sat Dec 14 15:02:30 CST 2019]; root of context hierarchy
	at org.springframework.context.support.AbstractApplicationContext.getApplicationEventMulticaster(AbstractApplicationContext.java:344)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:331)
	at wang.miansen.roothub.common.dao.jdbc.spring.DataSourceConfiguration.afterPropertiesSet(DataSourceConfiguration.java:163)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
	... 132 more

抛异常的地方:

可以看到 applicationEventMulticaster 为 null 就抛出了异常。

代码如下

package wang.miansen.roothub.common.dao.jdbc.spring;

public class DataSourceConfiguration implements FactoryBean<DataSource>, ApplicationContextAware, InitializingBean {

	private static final Logger logger = LoggerFactory.getLogger(DataSourceConfiguration.class);

	/**
	 * 数据库基本配置
	 */
	private DataSourceProperties dataSourceProperties;

	/**
	 * 数据源初始化器
	 */
	private DataSourceInitializer dataSourceInitializer;

	/**
	 * 数据源
	 */
	private DataSource dataSource;

	/**
	 * 上下文容器,主要的作用是防止重复初始化数据源。
	 */
	private ApplicationContext applicationContext;

	/**
	 * 创建数据源
	 * @param type
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T> T createDataSource(Class<? extends DataSource> type) {
		return (T) dataSourceProperties.initializeDataSourceBuilder().type(type).build();

	}

    /**
     * 最终注入的数据源
     */
	@Override
	public DataSource getObject() throws Exception {
		return null;
	}

	@Override
	public Class<DataSource> getObjectType() {
		return DataSource.class;
	}

	@Override
	public boolean isSingleton() {
		return true;
	}

	
	public void setDataSourceProperties(DataSourceProperties dataSourceProperties) {
		this.dataSourceProperties = dataSourceProperties;
	}

	public DataSourceInitializer getDataSourceInitializer() {
		if (this.dataSourceInitializer == null) {
			this.dataSourceInitializer = new DataSourceInitializer(this.dataSource, this.dataSourceProperties);
		}
		return this.dataSourceInitializer;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		// 只存在父容器时才初始化数据源,防止重复初始化。
		if (this.applicationContext.getParent() == null) {
			DataSourceInitializer initializer = getDataSourceInitializer();
			this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer));
		}
	}

}

DataSourceConfiguration 类实现了 ApplicationContextAware 和 InitializingBean 接口,本来想在 afterPropertiesSet 方法里发布初始化数据源的事件,结果就报了上面那个错误。

分析一下原因:

在 afterPropertiesSet() 方法中发布事件时,ApplicationContext 的 applicationEventMulticaster 属性为 null,导致抛出异常

跟一下 ApplicationContext 初始化过程看看为什么 applicationEventMulticaster 会是 null。

Spring 中 AbstractApplicationContext 抽象类的 refresh() 方法是用来刷新 Spring 的应用上下文的,所以在 refresh() 方法处打个断点。

afterPropertiesSet() 方法也打上一个断点,然后启动项目。

进入了 refresh() 方法

image

其它的方法先不看,重点是 registerBeanPostProcessors(beanFactory) 方法。

这个方法是用来注册 BeanPostProcessor 的,需要在所有的 application bean 初始化之前调用,拦截 Bean 的创建。

进去看看

调用了一个静态方法,继续往下走。

这里是找出 BeanPostProcessor 接口的所有实现类。就不仔细看了,直接看这里:

可以看到这里调用了 beanFactory.getBean() 方法,触发了 bean 实例的创建。从而触发 setApplicationContext() 和 afterPropertiesSet() 方法。

但是初始化事件广播器的方法 initApplicationEventMulticaster() 是在 registerBeanPostProcessors() 方法之后。

所以导致在 afterPropertiesSet() 方法中发布事件时,ApplicationContext 的 applicationEventMulticaster 属性为 null,导致抛出异常。

解决方法:

既然执行 afterPropertiesSet() 方法时,ApplicationContext 的 applicationEventMulticaster 属性还没准备好,那么可以等 initApplicationEventMulticaster() 方法执行完之后才能发布事件了。

继续往下看,注册监听器的方法是 registerListeners();

那么可以换一种思路,当监听器注册完毕之后在发布事件,因为注册监听器的方法是在初始化事件广播器之后,可以保证在发布事件的时候事件广播器是可用的。

可以这么做,新建一个类,实现 BeanPostProcessor 和 ApplicationContextAware 接口,在 postProcessAfterInitialization() 方法里判断 bean 的类型是否为 DataSourceInitializerListener,如果是就发布事件,具体代码如下:

public class DataSourceInitializedPublisher implements BeanPostProcessor, ApplicationContextAware {

	/**
	 * 上下文容器,可以使用该对象来发布事件。
	 */
	private ApplicationContext applicationContext;

	/**
	 * 数据源
	 */
	private DataSource dataSource;

	/**
	 * 数据库基本配置
	 */
	private DataSourceProperties dataSourceProperties;

	/**
	 * 数据源初始化器
	 */
	private DataSourceInitializer dataSourceInitializer;
	
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		// 当事件监听器初始化好之后发布初始化数据源的事件。
		if (bean instanceof DataSourceInitializerListener) {
			// 只存在父容器时才发布,防止重复初始化。
			if (this.applicationContext.getParent() == null) {
				DataSourceInitializer initializer = getDataSourceInitializer();
				this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer));
			}
		}
		return bean;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	/**
	 * 实例化 DataSourceInitializer
	 * @return DataSourceInitializer
	 */
	public DataSourceInitializer getDataSourceInitializer() {
		if (this.dataSourceInitializer == null) {
			this.dataSourceInitializer = new DataSourceInitializer(this.dataSource, this.dataSourceProperties);
		}
		return this.dataSourceInitializer;
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void setDataSourceProperties(DataSourceProperties dataSourceProperties) {
		this.dataSourceProperties = dataSourceProperties;
	}

}

xml 配置:

<!-- 数据源初始化事件监听器 -->
<bean id="dataSourceInitializerListener" class="wang.miansen.roothub.common.dao.jdbc.spring.DataSourceInitializerListener" />
	
<!-- 发布初始化数据源事件 -->
<bean id="dataSourceInitializedPublisher" class="wang.miansen.roothub.common.dao.jdbc.spring.DataSourceInitializedPublisher" >
		<property name="dataSourceProperties" ref="dataSourceProperties" />
		<property name="dataSource" ref="dataSource" />
</bean>

八、SpringCloud-服务配置中心 Spring-Cloud-Config

2019-10-22   龙德   SpringCloud   SpringCloud Spring-Cloud-Config  

Spring Cloud Config 是用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密 / 解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。

服务端

新建一个子工程,命名为 spring-cloud-config-server

引入依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-config-server</artifactId>
</dependency>

application.properties

# 指定服务名称
spring.application.name=spring-cloud-config-server
# 指定运行端口
server.port=8072

# 配置 Git 仓库地址
spring.cloud.config.server.git.uri=https://github.com/miansen/SpringCloud-Learn
# 配置文件所在的具体目录(也可以用通配符 /**,因为 SpringCloudConfig 只根据配置文件名找配置)
spring.cloud.config.server.git.searchPaths=config
# 配置仓库的分支(默认是master)
spring.cloud.config.label=master
# 配置 Git 仓库的用户名(如果 Git 仓库为公开仓库,可以不填写用户名和密码,如果是私有仓库需要填写)
spring.cloud.config.server.git.username=
# 访问 Git 仓库的用户密码
spring.cloud.config.server.git.password=

在远程 Git 仓库上新建几个配置文件,如图所示:

image

可以看到我新建了4个配置文件

  • application-dev.properties
  • application-dev.yml
  • application-prod.properties
  • application-prod.yml

这4个配置文件的命名都有一定的规律,可以概括为:{应用名} - {环境名} . {格式名}

为什么要这样的命名格式呢,随便命名不行吗?

其实这样命名是跟 http 请求地址和资源文件的映射有关系,SpringCloudConfig 会根据 http 请求地址去查找文件。

查找规则如下:

  • / {应用名} / {环境名} / {分支名}
  • / {应用名} - {环境名} . {格式名}
  • / {分支名} / {应用名} - {环境名} . {格式名}

接着启动工程

按照上面的查找规则,想要获取 application-dev.properties 配置文件,对应的访问地址如下:

  • http://localhost:8072/application/dev/master
  • http://localhost:8072/application-dev.properties
  • http://localhost:8072/master/application-dev.properties

image

可以看到返回了配置文件的内容,证明配置服务端可以从远程 Git 仓库获取到配置信息。

客户端

客户端就是各个微服务应用了,为了演示简单,我这里新建一个子工程,命名为 spring-cloud-config-client

pom.xml 引入下面的依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

注意客户端引入的是 spring-cloud-starter-config

新建 bootstrap.properties 配置文件,boostrap 由父 ApplicationContext 加载,比 applicaton 优先加载

# 指定服务名称
spring.application.name=spring-cloud-config-client
# 指定运行端口
server.port=8073

# 服务配置中心的地址
spring.cloud.config.uri=http://localhost:8072/
# 指定配置文件的分支
spring.cloud.config.label=master
# 指定配置文件的环境
spring.cloud.config.profile=dev

启动类把从服务端获取到的配置输出,以便能直观的看到

@RestController
@SpringBootApplication
public class SpringCloudConfigClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringCloudConfigClientApplication.class, args);
	}
	
	@Value("${key}")
    private String key;

    @GetMapping("/key")
    public String getKey() {
        return key;
    }
}

接着启动客户端,访问 http://localhost:8073/key,输出内容如下:

image

以后客户端切换配置时只需要修改 spring.cloud.config.profile 的值即可

七、Spring Cloud-网关 Spring-Cloud-Gateway

2019-10-21   龙德   SpringCloud   Spring-Cloud-Gateway  

简介

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

概念

  • Route(路由):Route 是网关的基础元素,由 ID、目标 URI、断言、过滤器组成。当请求到达网关时,由 Gateway Handler Mapping 通过断言进行路由匹配,当断言为真时,匹配到路由。
  • Predicate(断言):Predicate 是 Java 8 中提供的一个函数。允许开发人员匹配来自 HTTP 的请求,例如请求头或者请求参数。简单来说它就是匹配条件。
  • Filter(过滤器):Filter 是 Gateway 中的过滤器,可以在请求发出前后进行一些业务上的处理。

工作原理

image

当客户端请求到达 Spring Cloud Gateway 后,Gateway Handler Mapping 会将其拦截,根据 predicates 确定请求与哪个路由匹配。如果匹配成功,则会将请求发送至 Gateway web handler。Gateway web handler 处理请求会经过一系列 “pre” 类型的过滤器,然后执行代理请求。执行完之后再经过一系列的 “post” 类型的过滤器,最后返回给客户端。

快速开始

新建一个子工程,命名为 spring-cloud-gateway

引入依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

application.properties 配置路由

spring.application.name=spring-cloud-gateway
server.port=8074

############ 定义了一个 router(注意是数组的形式) ############
# 路由 ID,保持唯一
spring.cloud.gateway.routes[0].id=my-gateway
# 目标服务地址
spring.cloud.gateway.routes[0].uri=http://httpbin.org
# 路由条件
spring.cloud.gateway.routes[0].predicates[0]=Path=/get

上面这段配置的意思是,配置了一个 id 为 my-gateway 的路由规则,当访问地址为 /get 时会自动转发到 http://httpbin.org/get

还可以通过代码的形式配置路由

/**
 * 通过代码的形式配置路由
 * @param builder
 * @return
 */
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
	return builder.routes()
			.route(
					p -> p.path("/get").uri("http://httpbin.org")
					)
			.build();
}

application.propertise 配置路由和代码配置路由选择其中一个就好了,个人推荐 application.propertise 的形式配置。

启动服务,访问 http://localhost:8074/get

应该会输出以下内容:

{
  "args": {}, 
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 
    "Accept-Encoding": "gzip, deflate, br", 
    "Accept-Language": "zh-CN,zh;q=0.9", 
    "Cache-Control": "max-age=0", 
    "Cookie": "Webstorm-2f8f75da=e0c5ee46-9276-490c-b32b-d5dc1483ca18; acw_tc=2760828015735472938194099e940a3c3ebc07316bcb1096abc6fefde61bf8; BD_UPN=12314353; H_PS_645EC=8749qnWwXCzugp%2FwPJDVeB7bqBisqx6VKFthj5OZOsWBAz1JPX2YkatsizA; BD_HOME=0", 
    "Forwarded": "proto=http;host=\"localhost:8074\";for=\"0:0:0:0:0:0:0:1:51881\"", 
    "Host": "httpbin.org", 
    "Sec-Fetch-Mode": "navigate", 
    "Sec-Fetch-Site": "none", 
    "Sec-Fetch-User": "?1", 
    "Upgrade-Insecure-Requests": "1", 
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36", 
    "X-Forwarded-Host": "localhost:8074"
  }, 
  "origin": "0:0:0:0:0:0:0:1, 183.14.135.71, ::1", 
  "url": "https://localhost:8074/get"
}

整合 Eureka

添加 Eureka Client 的依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

配置基于 Eureka 的路由

spring.application.name=spring-cloud-gateway
server.port=8074

########### 配置注册中心 ###########
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka

############ 定义了一个基于 Eureka 的 router(注意是数组的形式) ############
# 路由 ID,保持唯一
spring.cloud.gateway.routes[0].id=my-gateway
# 目标服务地址
spring.cloud.gateway.routes[0].uri=lb://spring-cloud-provider
# 路由条件
spring.cloud.gateway.routes[0].predicates[0]=Path=/user-service/**

uri 以 lb:// 开头(lb 代表从注册中心获取服务),后面接的就是你需要转发到的服务名称,这个服务名称必须跟 Eureka 中的对应,否则会找不到服务。

spring-cloud-provider 服务提供的接口如下:

@RestController
@RequestMapping("/user-service")
public class UserController {

	@Value("${spring.application.name}")
	private String applicationName;
	
	@Value("${server.port}")
	private String post;
	
	@GetMapping("/users/{name}")
	public String users(@PathVariable("name") String name) {
		return String.format("hello %s,from server %s,post: %s", name, applicationName, post);
	}
}

启动 spring-cloud-eureka-server(注册中心)、spring-cloud-provider 和 spring-cloud-gateway

访问 http://localhost:8074/user-service/users/zhangsan,输出如下:

image

配置默认路由

Spring Cloud Gateway 提供了类似于 Zuul 那种为所有服务转发的功能

配置如下:

spring.application.name=spring-cloud-gateway
server.port=8074

########### 配置注册中心 ###########
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka

# 配置默认路由
spring.cloud.gateway.discovery.locator.enabled=true

开启之后我们需要通过地址去访问服务了,格式如下:

http://网关地址/服务名称(大写)/**

例如:http://localhost:8074/SPRING-CLOUD-PROVIDER/user-service/users/zhangsan

结果如图:

image

服务名称也可以配置成小写的格式,只需要增加一条配置即可:

# 配置服务名称小写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true

路由断言工厂

官方提供了很多个常用的路由断言工厂,如图所示:

image

1. Path 路由断言工厂

Path 路由断言工厂接收一个参数,根据 Path 定义好的规则来判断访问的 URI 是否匹配

固定的 Path

# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/zhangsan

带有前缀的 Path

# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/{segment}

使用通配符的 Path

# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/**

2. Query 路由断言工厂

Query 路由断言工厂接收两个参数,一个必需的参数和一个可选的正则表达式

# spring.cloud.gateway.routes[0].predicates[0]=Query=foo, ba.

如果请求包含 foo 查询参数,则此路由将匹配。bar 和 baz 也会匹配,因为第二个参数是正则表达式(注意 ba 后面有个 .)

测试链接:

http://localhost:8074/users/zhangsan?foo=ba

http://localhost:8074/users/zhangsan?foo=bar

http://localhost:8074/users/zhangsan?foo=baz

3. Method 路由断言工厂

Method 路由断言工厂接收一个参数,即要匹配的 HTTP 方法。

# spring.cloud.gateway.routes[0].predicates[0]=Method=GET

4. Header 路由断言工厂

Header 路由断言工厂接收两个参数,分别是请求头名称和正则表达式。

# spring.cloud.gateway.routes[0].predicates[0]=Header=X-Request-Id, \d+

如果请求中带有请求头名为 x-request-id,其值与 \d+ 正则表达式匹配(值为一个或多个数字),则此路由匹配。

具体的可以看一下官方文档,写的很清楚。

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.0.0.RELEASE/single/spring-cloud-gateway.html

自定义路由断言工厂

自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑。

在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。

apply 方法的参数是自定义的配置类,也就是静态内部类 Config,在使用的时候配置参数,就可以在 apply 方法中直接获取使用。

我们自己写一个 Query 路由断言工厂吧,名字就叫 MyQueryRoutePredicateFactory(命名需要以 RoutePredicateFactory 结尾)

代码如下:

```java @Component public class MyQueryRoutePredicateFactory extends AbstractRoutePredicateFactory {

public MyQueryRoutePredicateFactory() {
	super(Config.class);
}

/**
 * 返回有关 args 数量和快捷方式分析顺序的提示。
 * <p>必须要重写这个方法,否则 Config 设置不了参数。
 */
@Override
public List<String> shortcutFieldOrder() {
	return Arrays.asList("param", "regexp");
}

六、SpringCloud-路由器和过滤器 Zuul

2019-10-12   龙德   SpringCloud   SpringCloud Zuul  

路由是微服务架构中的一部分,例如访问 / 映射到首页,访问 /user/** 映射到用户服务,访问 /order/** 映射到订单服务。Zuul 是 Netflix 的基于 JVM 的路由器和服务器端负载均衡器,默认和 Ribbon 结合实现了负载均衡的功能。

路由

新建一个工程,命名为 spring-cloud-zuul-server

pom.xml 引入下面的依赖

<!-- 引入 web 依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入 eureka server 依赖 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 引入 zuul 依赖 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

启动类添加 @EnableZuulProxy 注解

@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class ZuulServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulServerApplication.class, args);
	}

}

application.properties

#指定服务名称
spring.application.name=spring-cloud-zuul-server
#指定运行端口
server.port=8070
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

# 配置 feign 路由
zuul.routes.feign.path=/feign/**
zuul.routes.feign.serviceId=spring-cloud-consumer-feign-hystrix

# 配置 ribbon 路由
zuul.routes.ribbon.path=/ribbon/**
zuul.routes.ribbon.serviceId=spring-cloud-consumer-ribbon-hystrix

这里配置了两个路由,第一个路由的名字是 feign,访问地址是 /feign/**,映射的服务是 spring-cloud-consumer-feign-hystrix,第二个同理。

接着分别启动 spring-cloud-eureka-server、spring-cloud-provider、spring-cloud-consumer-feign-hystrix、spring-cloud-consumer-ribbon-hystrix、spring-cloud-zuul-server 这5个工程。

如图所示:

image

假如没有 Zuul,看看之前我们是怎么访问的。

访问 spring-cloud-consumer-feign-hystrix 服务,则需要输入相应的地址 http://localhost:8076/users/zhangsan

访问 spring-cloud-consumer-ribbon-hystrix 服务,则需要输入相应的地址 http://localhost:8077/users/zhangsan

有多少个服务,就要维护多少个调用方式。

如图所示:

image

现在有了 Zuul,就不需要一个个的去维护了,统一起来全部交给 Zuul 管理。

访问 spring-cloud-consumer-feign-hystrix 服务,只需要输入我们配置的地址 http://localhost:8070/feign/users/zhangsan

访问 spring-cloud-consumer-ribbon-hystrix 服务,只需要输入我们配置的地址 http://localhost:8070/ribbon/users/zhangsan

如图所示:

image

负载均衡

服务往往是集群部署的,如果一个服务有多个实例,Zuul 也可以做到负载均衡。

分别再开启一个 spring-cloud-consumer-feign-hystrix 和 spring-cloud-consumer-ribbon-hystrix 工程

image

如图所示,spring-cloud-consumer-feign-hystrix 服务现在有两个实例,端口是 8075 和 8076

spring-cloud-consumer-ribbon-hystrix 服务也有两个实例,端口是 8077 和 8078

只需要在配置文件里加入以下参数就可以开启 Zuul 的负载均衡功能

# 配置 feign 路由
zuul.routes.feign.path=/feign/**
zuul.routes.feign.serviceId=spring-cloud-consumer-feign-hystrix
spring-cloud-consumer-feign-hystrix.ribbon.listOfServers=http://localhost:8075,http://localhost:8076

# 配置 ribbon 路由
zuul.routes.ribbon.path=/ribbon/**
zuul.routes.ribbon.serviceId=spring-cloud-consumer-ribbon-hystrix
spring-cloud-consumer-ribbon-hystrix.ribbon.listOfServers=http://localhost:8077,http://localhost:8078

过滤

Zuul 不仅是路由器,而且也是过滤器,可以做到安全校验的功能。

继续改造 spring-cloud-zuul-server 工程,新建一个类 MyFilter

@Component
public class MyFilter extends ZuulFilter {

	/**
	 * 是否要过滤,可以写具体的逻辑进行判断。
	 * <p>我这里为 true,永远过滤。
	 */
	@Override
	public boolean shouldFilter() {
		return true;
	}

	/**
	 * 过滤的具体逻辑
	 */
	@Override
	public Object run() throws ZuulException {
		// 共享 RequestContext,上下文对象
		RequestContext ct = RequestContext.getCurrentContext();
		// 我这里只是简单的获取 token,然后判断是否为空
		String token = ct.getRequest().getParameter("token");
		if(token == null || "".equals(token)) {
			// 过滤该请求,不对其进行路由
			ct.setSendZuulResponse(false);
			// 返回错误代码
			ct.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
			// 返回错误信息
			ct.setResponseBody("token must be not null");
		}
		return null;
	}

	/**
	 * 返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下
	 * <p>pre:路由之前
	 * <p>routing:路由之时
	 * <p>post: 路由之后
	 * <p>error:发送错误调用
	 */
	@Override
	public String filterType() {
		return FilterConstants.PRE_TYPE;
	}

	/**
	 * 过滤的顺序, 越小越靠前
	 */
	@Override
	public int filterOrder() {
		return FilterConstants.SERVLET_DETECTION_FILTER_ORDER - 1;
	}

}

访问 http://localhost:8070/ribbon/users/zhangsan,这时 token 为空,则会过滤掉该请求。

image

访问 http://localhost:8070/ribbon/users/zhangsan?token=aaa,则正常

image

源码下载:https://github.com/miansen/SpringCloud-Learn

五、SpringCloud-服务容错 Hystrix(断路器)

2019-09-30   龙德   SpringCloud   SpringCloud Hystrix  

在微服务架构中,服务与服务之间相互调用,互相依赖。如果其中的一个服务发故障,有可能导致整个系统不能用。Hystrix 就是用来预防这种情况的。

在 Ribbon 中使用 Hystrix

spring-cloud-consumer-ribbon 工程复制一份,命名为 spring-cloud-consumer-ribbon-hystrix

引入 Hystrix 依赖

<!-- 引入 Hystrix 依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

application.properties 改一下服务名字就可以了

#指定服务名称
spring.application.name=spring-cloud-consumer-ribbon-hystrix
#指定运行端口
server.port=8077
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

启动类改成 ConsumerRibbonHystrixApplication,并且加上注解 @EnableCircuitBreaker,开启断路器 Hystrix

为使示例简单,controller 是写在启动类里的。所以我们在启动类里新建一个 getUserFallback() 方法。

@GetMapping("/users/{name}")
@HystrixCommand(fallbackMethod = "getUserFallback")
public String getUser(@PathVariable("name") String name) {
	return restTemplate.getForObject("http://spring-cloud-provider/users/" + name, String.class);
}

public String getUserFallback(String name) {
	return "get " + name +" error";
}

getUser() 方法多了 @HystrixCommand 注解,它的作用是指定 Hystrix 在此方法超时时调用的方法。

可以在配置文件里指定超时时间

#自定义 Hystrix 的超时时间(毫秒)
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000

测试

写好之后测试一下

启动注册中心 spring-cloud-eureka-server 和服务提供者 spring-cloud-provider,紧接着启动现在的服务 spring-cloud-consumber-ribbon-hystrix

启动成功后访问 http://localhost:8077/users/zhangsan

image

可以看到一切正常

接着把服务提供者 spring-cloud-provider 停掉,然后再访问。如果没有引入 Hystrix 的话,我们的访问应该会报错。

但是引入 Hystrix 后,服务就有了容错机制,出现错误后会走到我们指定的方法。

image

在 Feign 中使用 Hystrix

spring-cloud-consumer-feign 工程复制一份,命名为 spring-cloud-consumer-feign-hystrix

Feign 是自带断路器的,不过需要在配置文件中配置打开它

#指定服务名称
spring.application.name=spring-cloud-consumer-feign-hystrix
#指定运行端口
server.port=8076
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka
#开启 Hystrix
feign.hystrix.enabled=true

注解 @FeignClient 加上 fallback 参数,指定由哪个类来处理错误。

@FeignClient(name = "spring-cloud-provider", configuration = FeignContractConfig.class, fallback = UserFeignClientHystrixFallback.class)
public interface UserFeignClient {

UserFeignClientHystrixFallback 类要实现 UserFeignClient 接口,并注入到 IOC 容器中。

@Component
public class UserFeignClientHystrixFallback implements UserFeignClient{

	@Override
	public String getUser(String name) {
		return "get " + name +" error";
	}

}

测试

启动 spring-cloud-consumer-feign-hystrix 工程,访问 http://localhost:8076/users/zhangsan,结果如图:

image

捕获异常

虽然我们已经解决了服务挂掉的问题,但是还不知道服务是咋挂掉的,要是能捕获异常就好了。

这点 Hystrix 也是支持的,看看下面的代码。

注解 @FeignClient 需要改变一下

@FeignClient(name = "spring-cloud-provider", configuration = FeignContractConfig.class, fallbackFactory = UserFeignClientHystrixFallbackFactory.class)

这次 fallbackFactory 属性指定的是 UserFeignClientHystrixFallbackFactory

UserFeignClientHystrixFallbackFactory 类要实现 FallbackFactory 接口并注入 IOC 容器。

@Component
public class UserFeignClientHystrixFallbackFactory implements FallbackFactory<UserFeignClient> {

	private static Logger log = LoggerFactory.getLogger(UserFeignClientHystrixFallbackFactory.class);
	
	@Override
	public UserFeignClient create(Throwable cause) {
		log.info("the server error is: {}", cause.getMessage());
		return new UserFeignClient() {
			@Override
			public String getUser(String name) {
				return "get " + name +" error";
			}
		};
	}
}

create() 这个工厂方法中,它的入参就是服务提供者的异常。

监控与仪表盘

Ribbon 工程的监控

在 spring-cloud-consumer-ribbon-hystrix 工程的启动类里注入一个 bean 来初始化监控的 Servlet

/**
 * 低版本直接启动即可使用 http://ip:port/hystrix.stream 查看监控信息
 * 高版本需要添加本方法方可使用 http://ip:port/{urlMappings} 查看监控信息
 * @return
 */
@Bean
public ServletRegistrationBean getServlet() {
	HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
    registrationBean.setLoadOnStartup(1);
    registrationBean.addUrlMappings("/actuator/hystrix.stream");
    registrationBean.setName("HystrixMetricsStreamServlet");
    return registrationBean;
}

启动 spring-cloud-consumer-ribbon-hystrix 工程

访问 http://localhost:8077/actuator/hystrix.stream

image

可以看到浏览器一直处于请求的状态,并且页面一直在打印 ping。

因为此时项目中注解了 @HystrixCommand 的方法还没有执行,因此也没有任何的监控数据。

访问 http://localhost:8077/users/zhangsan后,再次访问 http://localhost:8077/actuator/hystrix.stream,就可以看到监控数据了。

image

不过这些数据都是以文件形式展示的,很难一眼看出系统当前的运行状态。

Feign 工程的监控

引入一个新的依赖

<!-- 因为需要监控功能,所以引入 Hystrix 依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

启动类添加注解 @EnableCircuitBreaker,并且同样注入一个 bean 来初始化监控的 Servlet

启动后访问 http://localhost:8076/actuator/hystrix.stream 同样可以监控到数据。

仪表盘

上面的监控得到的数据都是以文件形式展示的,很难一眼看出系统当前的运行状态。

Hystrix 的主要优点之一是它收集关于每个 HystrixCommand 的一套指标,并通过 Hystrix 仪表盘有效的显示每个断路器的运行状况。

所以我们可以通过仪表盘来可视化的监控每个断路器的运行状况。

image

仪表盘长这个样子

新建一个子工程,命名为 spring-cloud-hystrix-dashboard

引入下面的依赖:

<!-- 引入 web 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入 eureka server 依赖-->
<dependency>
   	<groupId>org.springframework.cloud</groupId>
   	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 引入仪表盘依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

application.properties

#指定服务名称
spring.application.name=spring-cloud-hystrix-dashboard
#指定运行端口
server.port=8072
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

然后启动类添加注解 @EnableHystrixDashboard,开启仪表盘

访问 http://localhost:8072/hystrix

可以看到这个界面

image

第一个输入框,是要监控的地址,第二个是轮训时间,第三个是仪表盘页面的标题

我们在这3个框里分别输入 http://localhost:8077/actuator/hystrix.stream,2000,RibbonDashboard,然后点击 Monitor Stream,就可以直观的监控到服务运行的状态了。

image

想要监控其他的服务只需要把监控地址换了就行。

Turbine 集群监控

上面的监控只是监控一个单体应用,但在实际应用中,程序往往是集群部署的,所以我们需要对集群进行监控,这时候可以采用 Turbine 进行集群监控。

改造 spring-cloud-hystrix-dashboard 工程

引入新的依赖

<!-- 引入 Turbine 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>

application.properties 新增几个参数

# 表示要聚合的服务,多个用逗号隔开
turbine.app-config=spring-cloud-consumer-ribbon-hystrix,spring-cloud-consumer-feign-hystrix
# 表示集群的名字为default 
turbine.clusterNameExpression= "default"
# 同一主机上的服务通过host和port的组合来进行区分
turbine.combine-host-port=true

启动类新增注解 @EnableTurbine,开启集群监控面板。

启动注册中心 spring-cloud-eureka-server

启动服务 spring-cloud-consumer-ribbon-hystrix,且这个服务启动两个实例

启动集群监控 spring-cloud-hystrix-dashboard

访问集群监控的地址 http://localhost:8072/hystrix

image

分别输入 http://localhost:8072/turbine.stream,2000,Turbine,点击 Monitor Stream

就可以看到集群监控的页面了

image

源码下载:https://github.com/miansen/SpringCloud-Learn

四、SpringCloud-高可用的服务注册中心

2019-09-27   龙德   SpringCloud   SpringCloud  

前面三篇写了服务注册中心和服务之间的调用方式。到目前为止所有的服务都是注册到一台注册中心,万一这台注册中心挂了,那么所有的服务不是都得挂了。

为了解决这个问题,我们可以搭建一个注册中心集群。

准备环境

集群意味着要有多台服务器,虽然可以通过改 host 文件的形式来搭建集群。不过我并不推荐这种形式,因为实际中不可能去改 host 文件的。所以我准备了3台虚拟机,地址分别是 192.168.8.4、192.168.8.6、192.168.8.8

搭建集群

首先把 spring-cloud-eureka-server 工程复制一份,命名为 spring-cloud-eureka-server-hign,将启动类的名字修改为 EurekaServerHignApplication

(1)新建 application-dev1.properties 配置文件

#指定服务名称
spring.application.name=spring-cloud-eureka-server-hign
#指定运行端口
server.port=8080
#指定主机地址
eureka.instance.hostname=192.168.8.4
#应用程序使用IP地址的方式向Eureka注册
eureka.instance.prefer-ip-address=true
#指定是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch-registry=false
#指定是否要注册到注册中心(注册中心不需要开启)
eureka.client.register-with-eureka=false
#关闭保护模式
eureka.server.enable-self-preservation=false
#配置注册中心地址
eureka.client.service-url.defaultZone=http://192.168.8.6:8081/eureka,http://192.168.8.8:8082/eureka

application-dev1.properties 指定了注册中心的IP和端口 192.168.8.4:8080。它同时也向 192.168.8.6:8081192.168.8.8:8082 这两个注册中心注册。

同理可得

application-dev2.properties 配置文件

#指定服务名称
spring.application.name=spring-cloud-eureka-server-hign
#指定运行端口
server.port=8081
#指定主机地址
eureka.instance.hostname=192.168.8.6
#应用程序使用IP地址的方式向Eureka注册
eureka.instance.prefer-ip-address=true
#指定是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch-registry=false
#指定是否要注册到注册中心(注册中心不需要开启)
eureka.client.register-with-eureka=false
#关闭保护模式
eureka.server.enable-self-preservation=false
#配置注册中心地址
eureka.client.service-url.defaultZone=http://192.168.8.4:8080/eureka,http://192.168.8.8:8082/eureka

application-dev3.properties 配置文件

#指定服务名称
spring.application.name=spring-cloud-eureka-server-hign
#指定运行端口
server.port=8082
#指定主机地址
eureka.instance.hostname=192.168.8.8
#应用程序使用IP地址的方式向Eureka注册
eureka.instance.prefer-ip-address=true
#指定是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch-registry=false
#指定是否要注册到注册中心(注册中心不需要开启)
eureka.client.register-with-eureka=false
#关闭保护模式
eureka.server.enable-self-preservation=false
#配置注册中心地址
eureka.client.service-url.defaultZone=http://192.168.8.4:8080/eureka,http://192.168.8.6:8081/eureka

image

(2)将 spring-cloud-eureka-server-hign 工程打成 jar 包,每台虚拟机上都部署一份

(3)启动程序

第一台虚拟机 192.168.8.4

java -jar spring-cloud-eureka-server-hign-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev1

第二台虚拟机 192.168.8.6

java -jar spring-cloud-eureka-server-hign-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev2

第三台虚拟机 192.168.8.8

java -jar spring-cloud-eureka-server-hign-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev3

(6)分别访问,如图:

image

image

image

使用集群

还记得之前我们是如何注册的吗?没有集群的时候是这样写的

#指定服务名称
spring.application.name=spring-cloud-provider
#指定运行端口
server.port=8078
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

现在有了集群,就不能这么写,否则集群就没有意义了。

可以这么写:

#指定服务名称
spring.application.name=spring-cloud-provider
#指定运行端口
server.port=8078
#指定主机地址
eureka.instance.hostname=192.168.8.4
#应用程序使用IP地址的方式向Eureka注册
eureka.instance.prefer-ip-address=true
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://192.168.8.4:8080/eureka,http://192.168.8.6:8081/eureka,http://192.168.8.8:8082/eureka

同理我们将 spring-cloud-provider 工程复制一份,命名为 spring-cloud-provider-hign,新建 application-dev1.propertiesapplication-dev2.propertiesapplication-dev3.properties 三个配置文件

spring-cloud-provider 工程打成 jar 包,每台虚拟机上都部署一份,然后分别启动工程。

访问如图:

image

源码下载:https://github.com/miansen/SpringCloud-Learn