Spring MVC
MVC:
M : Model
V : View
C : Controller 也就是DispatcherServlet,也称为前端控制器(Front Controller)
WebApplicationContext可以有多个,并且它们之间呈继承关系,Root WebApplicationContext的创建:
ServletContextListener -> ContextLoaderListener -> Root WebApplicationContext
请求映射
Servlet一般是精确匹配,模糊匹配: / 匹配当前目录和 /* 匹配当前下的所有目录
项目前缀:ServletContext path= /servlet-demo 源自tomat server.xml的配置,因此完整的URI : /servlet-demo/IndexServlet
DispatcherServlet 的继承关系
DispatcherServlet < FrameworkServlet < HttpServletBean < HttpServlet
正常启动DispatcherServlet是映射/路径的即
ServletContext path = "" or "/"
Request URI = ServletContex path + @RequestMapping("")/ @GetMapping()
当前例子中:
Request URI = "" + "" = "" 即Request URI : "" 或者 "/"
会调用到 RestDemoController#index()
@Controller
public class IndexController {
@RequestMapping("")
public String index(){
return "index";
}
}
HandlerMapping处理URL映射,寻找Request URI,找到匹配的 Handler :
Handler:处理的方法,当然这是一种实例
整体流程:Request -> Handler -> 执行结果 -> 返回(REST) -> 普通的文本
请求处理映射:RequestMappingHandlerMapping 解释:-> @RequestMapping Handler Mapping
拦截器:HandlerInterceptor 可以理解 Handler 到底是什么
处理顺序:preHandle(true) -> Handler: (一般为)HandlerMethod 反射执行(Method#invoke) -> postHandle -> afterCompletion
如果是使用的SpringBoot:Spring Web MVC 的配置 Bean :WebMvcProperties
Spring Boot 允许通过 application.properties 去定义一下配置,配置外部化
WebMvcProperties 配置前缀:spring.mvc,比如:
spring.mvc.servlet
异常处理
传统的Servlet web.xml 错误页面
- 优点:统一处理,业界标准
- 不足:灵活度不够,只能定义 web.xml文件里面
- <error-page> 处理逻辑:
- 处理状态码 <error-code>
- 处理异常类型 <exception-type>
- 处理服务:<location>
Servelt规范中错误码以及相关错误信息(错误码,请求路径,异常信息)存储在Request的Attribute中
Spring Web MVC 异常处理
//Spring抛出
@RestControllerAdvice(basePackages = "com.gupao.vip.springwebmvc.controller")
public class RestControllerAdvicer {
@ExceptionHandler(value = {NullPointerException.class
,IllegalAccessException.class,
IllegalStateException.class,
})
public Object handleNPE(
Throwable throwable) {
Map<String,Object> data = new HashMap<>();
data.put("message",throwable.getMessage());
return data;
}
}
- @ExceptionHandler
- 优点:易于理解,尤其是全局异常处理
- 不足:很难完全掌握所有的异常类型(不同错误有不同的异常类型)
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody
- @ControllerAdvice 专门拦截(AOP) @Controller
Spring Boot 错误处理页面
- 实现 ErrorPageRegistrar
- 状态码:比较通用,不需要理解Spring WebMVC 异常体系
- 不足:页面处理的路径必须固定
- 注册 ErrorPage 对象
- 实现 ErrorPage 对象中的Path 路径Web服务
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(
new ErrorPage(HttpStatus.NOT_FOUND,"/404.html"));
}
视图技术
View
render 方法
处理页面渲染的逻辑,例如:Velocity、JSP、Thymeleaf
ViewResolver
View Resolver = 页面 + 解析器 -> resolveViewName 寻找合适/对应 View 对象
RequestURI-> RequestMappingHandlerMapping ->HandleMethod -> return "viewName" ->完整的页面名称 = prefix + "viewName" + suffix -> ViewResolver -> View -> render -> HTML
Spring Boot 解析完整的页面路径:
spring.view.prefix + HandlerMethod return + spring.view.suffix
ContentNegotiationViewResolver
用于处理多个ViewResolver:JSP、Velocity、Thymeleaf
当所有的ViewResover 配置完成时,他们的order 默认值一样,所以先来先服务(List)
当他们定义自己的order,通过order 来倒序排列
Thymeleaf
自动装配类:ThymeleafAutoConfiguration
配置类:ThymeleafProperties
配置项前缀:spring.thymeleaf
模板寻找前缀:spring.thymeleaf.prefix
模板寻找后缀:spring.thymeleaf.suffix
代码示例:/thymeleaf/index.htm
prefix: /thymeleaf/
return value : index
suffix: .htm
国际化(i18n)
Locale
LocaleContextHolder
Spring Rest
消息转换
请求头的Accept一般如下:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8
其含义如下:
第一优先顺序:text/html -> application/xhtml+xml -> application/xml
第二优先顺序:image/webp -> image/apng
其中q是指权重
学习源码的路径:
@EnableWebMvc
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
引入了DelegatingWebMvcConfiguration配置类
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
该类有方法extendMessageConverters可以扩展自己定义的消息转换器,注意,老版本的addDefaultHttpMessageConverters有bug已经删除了:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
public void extendMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.add(new PropertiesPersonHttpMessageConverter());
}
}
消息处理器的原理
所有的 HTTP 自描述消息处理器均在 messageConverters(类型:HttpMessageConverter
),这个集合会传递到 RequestMappingHandlerAdapter,最终控制写出。
messageConverters,其中包含很多自描述消息类型的处理,比如 JSON、XML、TEXT等等
以 application/json 为例,Spring Boot 中默认使用 Jackson2 序列化方式,其中媒体类型:application/json,它的处理类 MappingJackson2HttpMessageConverter,提供两类方法:
- 读read* :通过 HTTP 请求内容转化成对应的 Bean
- 写write*: 通过 Bean 序列化成对应文本内容作为响应内容
测试疑问
问题:在不填Accept头的情况下,为什么第一次是JSON,后来怎加了 XML 依赖,又变成了 XML 内用输出
回答:Spring Boot 应用默认没有增加XML 处理器(HttpMessageConverter)实现,所以最后采用轮训的方式去逐一尝试是否可以 canWrite(POJO) ,如果返回 true,说明可以序列化该 POJO 对象,那么 Jackson 2 恰好能处理,那么Jackson 输出了。
代码详见:
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#getProducibleMediaTypes
问题:当 Accept 请求头未被制定时,为什么还是 JSON 来处理
回答:这个依赖于 messageConverters 的插入顺序。
问题:优先级是默认的是吧 可以修改吗
回答:是可以调整的,通过extendMessageConverters 方法调整
扩展自描述消息
Person
JSON 格式(application/json)
{
"id":1,
"name":"小马哥"
}
XML 格式(application/xml)
<Person>
<id>1</id>
<name>小马哥</name>
</Person>
Properties 格式(application/properties+person)
(需要扩展)
person.id = 1
person.name = 小马哥
- 实现 AbstractHttpMessageConverter 抽象类
- supports 方法:是否支持当前POJO类型
- readInternal 方法:读取 HTTP 请求中的内容,并且转化成相应的POJO对象(通过 Properties 内容转化成 JSON)
- writeInternal 方法:将 POJO 的内容序列化成文本内容(Properties格式),最终输出到 HTTP 响应中(通过 JSON 内容转化成 Properties )
- @RequestMappng 中的 consumes 对应 请求头 “Content-Type”
- @RequestMappng 中的 produces 对应 请求头 “Accept”
HttpMessageConverter 执行逻辑:
- 读操作:尝试是否能读取,canRead 方法去尝试,如果返回 true 下一步执行 read
- 写操作:尝试是否能写入,canWrite 方法去尝试,如果返回 true 下一步执行 write
代码如下:
/**
* Person 自描述消息处理
*
* @author mercyblitz
* @date 2017-10-14
**/
public class PropertiesPersonHttpMessageConverter extends
AbstractHttpMessageConverter<Person> {
public PropertiesPersonHttpMessageConverter(){
super(MediaType.valueOf("application/properties+person"));
setDefaultCharset(Charset.forName("UTF-8"));
}
@Override
protected boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(Person.class);
}
/**
* 讲请求内容中 Properties 内容转化成 Person 对象
* @param clazz
* @param inputMessage
* @return
* @throws IOException
* @throws HttpMessageNotReadableException
*/
@Override
protected Person readInternal(Class<? extends Person> clazz,
HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
/**
* person.id = 1
* person.name = 小马哥
*/
InputStream inputStream = inputMessage.getBody();
Properties properties = new Properties();
// 将请求中的内容转化成Properties
properties.load(new InputStreamReader(inputStream,getDefaultCharset()));
// 将properties 内容转化到 Person 对象字段中
Person person = new Person();
person.setId(Long.valueOf(properties.getProperty("person.id")));
person.setName(properties.getProperty("person.name"));
return person;
}
@Override
protected void writeInternal(Person person, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
OutputStream outputStream = outputMessage.getBody();
Properties properties = new Properties();
properties.setProperty("person.id",String.valueOf(person.getId()));
properties.setProperty("person.name",person.getName());
properties.store(new OutputStreamWriter(outputStream,getDefaultCharset()),"Written by web server");
}
}
Spring Boot JDBC
数据源
通用性数据源:DataSource
分布式数据源:XADataSource
嵌入式数据源:EmbeddedDatabase
Spring Boot 实际使用场景
题外话:在 Spring Boot 2.0.0 ,如果应用采用 Spring Web MVC 作为 Web 服务, 默认情况下,使用 嵌入式 Tomcat。
如果采用Spring Web Flux,默认情况下,使用 Netty Web Server(嵌入式)
从 Spring Boot 1.4 支持 FailureAnalysisReporter 实现
WebFlux
Mono : 0 - 1 Publisher(类似于Java 8 中的 Optional)
Flux: 0 - N Publisher(类似于Java 中的 List)
传统的 Servlet 采用 HttpServletRequest、HttpServletResponse
WebFlux 采用:ServerRequest、ServerResponse(不再限制于 Servlet 容器,可以选择自定义实现,比如 Netty Web Server)
单数据源的场景
数据连接池技术
Apache Commons DBCP
-
commons-dbcp2
- 依赖:commons-pool2
-
commons-dbcp(老版本)
- 依赖:commons-pool
Tomcat DBCP
事务
重要感念
自动提交模式
AutoCommit
事务隔离级别(Transaction isolation levels)
- TRANSACTION_READ_UNCOMMITTED
- TRANSACTION_READ_COMMITTED
- TRANSACTION_REPEATABLE_READ
- TRANSACTION_SERIALIZABLE
从上至下,级别越高,性能越差
Spring Transaction 实现重用了 JDBC API:
Isolation类 -> TransactionDefinition
- ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED
- ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED
- ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ
- ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE
保护点(Savepoints)
save(){
// 建立一个SP 1
SP 1
SP 2 {
// 操作
}catch(){
rollback(SP2);
}
commit();
release(SP1);
}
@Transaction
代理执行 - TransactionInterceptor
- 可以控制 rollback 的异常粒度:rollbackFor() 以及 noRollbackFor()
- 可以执行 事务管理器:transactionManager()
通过 API 方式进行事务处理 - PlatformTransactionManager
详见下面的代码
事务的传播
线程 1:
调用 save ->
// DS表示数据源
@Transactional T1 save() 控制 DS 1 insert ->
// save2没有加事务注解
save2() DS 1 insert
@Transactional
save() {
//insert DS1
save2() // insert DS1, 没有Transactional
}
// 传播就是将事务传播给save2
@Transactional(NESTED)
save2(){
}
代码:
@Repository
public class UserRepository {
private final DataSource dataSource;
private final DataSource masterDataSource;
private final DataSource salveDataSource;
private final JdbcTemplate jdbcTemplate;
private final PlatformTransactionManager platformTransactionManager;
@Autowired
public UserRepository(DataSource dataSource,
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("salveDataSource") DataSource salveDataSource,
JdbcTemplate jdbcTemplate,
PlatformTransactionManager platformTransactionManager) {
this.dataSource = dataSource;
this.masterDataSource = masterDataSource;
this.salveDataSource = salveDataSource;
this.jdbcTemplate = jdbcTemplate;
this.platformTransactionManager = platformTransactionManager;
}
private boolean jdbcSave(User user) {
boolean success = false;
System.out.printf("[Thread : %s ] save user :%s\n",
Thread.currentThread().getName(), user);
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO users(name) VALUES (?);");
preparedStatement.setString(1, user.getName());
success = preparedStatement.executeUpdate() > 0;
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.commit();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return success;
}
@Transactional
public boolean transactionalSave(User user) {
boolean success = false;
success = jdbcTemplate.execute("INSERT INTO users(name) VALUES (?);",
new PreparedStatementCallback<Boolean>() {
@Nullable
@Override
public Boolean doInPreparedStatement(PreparedStatement preparedStatement) throws SQLException, DataAccessException {
preparedStatement.setString(1, user.getName());
return preparedStatement.executeUpdate() > 0;
}
});
return success;
}
public boolean save(User user) {
boolean success = false;
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
// 开始事务
TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
success = jdbcTemplate.execute("INSERT INTO users(name) VALUES (?);",
new PreparedStatementCallback<Boolean>() {
@Nullable
@Override
public Boolean doInPreparedStatement(PreparedStatement preparedStatement) throws SQLException, DataAccessException {
preparedStatement.setString(1, user.getName());
return preparedStatement.executeUpdate() > 0;
}
});
platformTransactionManager.commit(transactionStatus);
return success;
}
public Collection<User> findAll() {
return Collections.emptyList();
}
}
问题集合
1. 用reactive web,原来mvc的好多东西都不能用了?
答:不是, Reactive Web 还是能够兼容 Spring WebMVC
2. 开个线程池事务控制用API方式?比如开始写的Excutor.fixExcutor(5)
答:TransactionSynchronizationManager 使用大量的ThreadLocal 来实现的
3. 假设一个service方法给了@Transaction标签,在这个方法中还有其他service 的某个方法,这个方法没有加@Transaction,那么如果内部方法报错,会回滚吗?
答:会的,当然可以过滤掉一些不关紧要的异常noRollbackFor()
4. spring 分布式事务生产环境实现方式有哪些?
答:https://docs.spring.io/spring-boot/docs/2.0.0.M5/reference/htmlsingle/#boot-features-jta
Spring Boot Bean Validator
Bean Validation 1.1 JSR-303
Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
命名规则(Since Spring Boot 1.4):Spring Boot 大多数情况采用
starter
(启动器,包含一些自动装配的Spring 组件),官方的命名规则:spring-boot-starter-{name}
,业界或者民间:{name}-spring-boot-starter
JSR是规范,采用的实现是hibernate-validator
常用验证技术
Spring Assert API
Assert.assert
JVM/Java assert 断言
assert a!=null
以上方式的缺点,耦合了业务逻辑,虽然可以通过HandlerInterceptor
或者Filter
做拦截,但是也是非常恶心的
还可以通过 AOP 的方式,也可以提升代码的可读性。
以上方法都有一个问题,不是统一的标准。
Filter拦截校验
HandlerInterceptor
验证的案例:
@SpringBootApplication
public class SpringBootBeanValidationApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(SpringBootBeanValidationApplication.class, args);
}
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserControllerInterceptor());
}
}
public class UserControllerInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 把校验逻辑存放在这里
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
Integer status = response.getStatus();
if(status== HttpStatus.BAD_REQUEST.value()){
response.setStatus(HttpStatus.OK.value());
}
}
}
自定义 Bean Validation
需求:通过员工的卡号来校验,需要通过工号的前缀和后缀来判断
前缀必须以"GUPAO-"
后缀必须是数字
需要通过 Bean Validator 检验
实现步骤
-
复制成熟 Bean Validation Annotation的模式
将实现
ConstraintValidator
接口 定义到@Constraint#validatedBy
@Target(FIELD) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class}) public @interface ValidCardNumber { // 消息的国际化key String message() default "{com.gupao.bean.validation.invalid.card.number.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }
参考和理解
@Constraint
-
实现
ConstraintValidator
接口public class ValidCardNumberConstraintValidator implements ConstraintValidator<ValidCardNumber, String> { public void initialize(ValidCardNumber validCardNumber) { } /** * 需求:通过员工的卡号来校验,需要通过工号的前缀和后缀来判断 * <p> * 前缀必须以"GUPAO-" * <p> * 后缀必须是数字 * <p> * 需要通过 Bean Validator 检验 * * @param value * @param context * @return */ @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 前半部分和后半部分 String[] parts = StringUtils.split(value, "-"); // 为什么不用 String#split 方法,原因在于该方法使用了正则表达式 // 其次是 NPE 保护不够 // 如果在依赖中,没有 StringUtils.delimitedListToStringArray API 的话呢,可以使用 // Apache commons-lang StringUtils // JDK 里面 StringTokenizer(不足类似于枚举 Enumeration API) if (ArrayUtils.getLength(parts) != 2) { return false; } String prefix = parts[0]; String suffix = parts[1]; boolean isValidPrefix = Objects.equals(prefix, "GUPAO"); boolean isValidInteger = StringUtils.isNumeric(suffix); return isValidPrefix && isValidInteger; } }
-
给
@ValidCardNumber
添加message
参数添加国际化Bundle配置:ValidationMessages.properties
com.gupao.bean.validation.invalid.card.number.message=the card number must start with "GUPAO" ,\ and its suffix must be a number!
中文类似
-
添加注解到类中:
public class User { @Max(value = 10000) private long id; @NotNull //@NotNull //@NonNull private String name; // 卡号 -- GUPAO-123456789 @NotNull @ValidCardNumber private String cardNumber; }
疑问:spring boot中是怎么识别这些注解的
在SpringValidatorAdapter类中,注入了Validator的实现:
public SpringValidatorAdapter(javax.validation.Validator targetValidator) {
Assert.notNull(targetValidator, "Target Validator must not be null");
this.targetValidator = targetValidator;
}
在spring mvc解析参数的时候回调用校验:
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument
问答部分
-
JSON校验如何办?
答:尝试变成 Bean 的方式
-
实际中很多参数都要校验那时候怎么写这样写会增加很多类
答:确实会增加部分工作量,大多数场景,不需要自定义,除非很特殊情况。Bean Validation 的主要缺点,单元测试不方便
-
如果前端固定表单的话,这种校验方式很好。但是灵活性不够,如果表单是动态的话,如何校验呢?
答: 表单字段与 Form 对象绑定即可,再走 Bean Validation 逻辑
<form action="" method="POST" command="form"> <input value="${form.name}" /> ... <input value="${form.age}" /> </form>
一个接一个验证,责任链模式(Pipeline):
field 1-> field 2 -> field 3 -> compute -> result
-
如何自定义,反回格式?如何最佳实现
答:可以通过REST来实现,比如 XML 或者 JSON 的格式(视图)
-
面试的看法
答:具备一定的水平
不该问的不要问,因为面试官的水平可能还不及于你!
Spring Cloud Config Client
预备知识
发布/订阅模式
java.util.Observable
是一个发布者
java.util.Observer
是订阅者
发布者和订阅者:1 : N
发布者和订阅者:N : M
代码案例:
观察者模式属于主动感知,也就是推的形式
public class ObserverDemo {
public static void main(String[] args) {
MyObservable observable = new MyObservable();
// 增加订阅者
observable.addObserver(new Observer() {
@Override
public void update(Observable o, Object value) {
System.out.println(value);
}
});
observable.setChanged();
// 发布者通知,订阅者是被动感知(推模式)
observable.notifyObservers("Hello,World");
echoIterator();
}
public static class MyObservable extends Observable {
public void setChanged() {
super.setChanged();
}
}
}
也可以采用拉取得方式获取消息,类似于迭代器:
private static void echoIterator(){
List<Integer> values = Arrays.asList(1,2,3,4,5);
Iterator<Integer> integerIterator = values.iterator();
while(integerIterator.hasNext()){ // 通过循环,主动去获取
System.out.println(integerIterator.next());
}
}
事件/监听模式
java.util.EventObject
:事件对象
事件对象总是关联着事件源(source)
java.util.EventListener
:事件监听接口(标记)
EventListener是一个标记接口,没有实现
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);
}
Spring 事件/监听
ApplicationEvent
: 应用事件
public abstract class ApplicationEvent extends EventObject
ApplicationListener
: 应用监听器
代码案例:
public class SpringEventListenerDemo {
public static void main(String[] args) {
// Annotation 驱动的 Spring 上下文
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext();
// 注册监听器
context.addApplicationListener(
new ApplicationListener<MyApplicationEvent>() {
/**
* 监听器得到事件
* @param event
*/
@Override
public void onApplicationEvent(MyApplicationEvent event) {
System.out.println("接收到事件:" + event.getSource() +" @ "+event.getApplicationContext());
}
});
context.refresh();
// 发布事件
context.publishEvent(new MyApplicationEvent(context,"Hello,World"));
context.publishEvent(new MyApplicationEvent(context,1));
context.publishEvent(new MyApplicationEvent(context,new Integer(100)));
}
private static class MyApplicationEvent extends ApplicationEvent {
private final ApplicationContext applicationContext;
/**
* Create a new ApplicationEvent.
*
* @param source the object on which the event initially occurred (never {@code null})
*/
public MyApplicationEvent(ApplicationContext applicationContext, Object source) {
super(source);
this.applicationContext=applicationContext;
}
public ApplicationContext getApplicationContext() {
return applicationContext;
}
}
}
Spring Boot 事件/监听器
核心事件:
// 当应用启动一级环境可以访问的时候
ApplicationEnvironmentPreparedEvent
// 应用年启动,ApplicationContext已经完全准备好了,但是没有refresh,bean 定义已经加载
ApplicationPreparedEvent
// 在环境和ApplicationContext可以访问之前,ApplicationListener注册后,数据源是SpringApplication应用本身
ApplicationStartingEvent
// 应用准备接受请求
ApplicationReadyEvent
// 应用启动失败
ApplicationFailedEvent
ConfigFileApplicationListener
是一个关键的减轻器,管理配置文件,比如:application.properties
以及 application.yaml
application-{profile}.properties
:
profile = dev 、test,加载顺序:
application-{profile}.properties
- application.properties
Spring Boot 在相对于 ClassPath : /META-INF/spring.factories配置了接口和实现类,和Java SPI一样
Java SPI :
java.util.ServiceLoader
配置文件如下:
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener,\
org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.logging.LoggingApplicationListener
如何控制顺序
实现Ordered
以及 标记@Order
在 Spring 里面,数值越小,越优先
Spring Cloud 事件/监听器
BootstrapApplicationListener
Spring Cloud 的配置 "/META-INF/spring.factories":
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\
org.springframework.cloud.context.restart.RestartListener
spring cloud中:
bootstrap是父容器
application是子容器
所以加载BootstrapApplicationListener的优先级 高于
ConfigFileApplicationListener
,所以 application.properties 文件即使定义也配置不到(配置文件配置spring.cloud.bootstrap.name,BootstrapApplicationListener获取不到)!原因在于:
BootstrapApplicationListener
第6优先
ConfigFileApplicationListener
第11优先
- 负责加载
bootstrap.properties
或者bootstrap.yaml
- 负责初始化 Bootstrap ApplicationContext ID = "bootstrap"
ConfigurableApplicationContext context = builder.run();
Bootstrap 是一个根 Spring 上下文,parent = null
联想 ClassLoader:
ExtClassLoader <- AppClassLoader <- System ClassLoader -> Bootstrap Classloader(null)
ConfigurableApplicationContext
标准实现类:AnnotationConfigApplicationContext
Env 端点:EnvironmentEndpoint
Env端点可以查看配置,从系统配置到配置文件的配置按照优先级展示:
{
"profiles": [],
"server.ports": {
"local.server.port": 9090
},
"bootstrapProperties:my-property-source": {
"server.port": "9090"
},
"servletContextInitParams": {},
"systemProperties": {
"java.runtime.name": "Java(TM) SE Runtime Environment",
"sun.boot.library.path": "/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib",
"java.vm.version": "25.151-b12",
"gopherProxySet": "false",
"java.vm.vendor": "Oracle Corporation",
"java.vendor.url": "http://java.oracle.com/",
"path.separator": ":",
"java.vm.name": "Java HotSpot(TM) 64-Bit Server VM",
"file.encoding.pkg": "sun.io",
"user.country": "CN",
"sun.java.launcher": "SUN_STANDARD",
"sun.os.patch.level": "unknown",
"PID": "9894",
"java.vm.specification.name": "Java Virtual Machine Specification",
"user.dir": "/Users/guanhang/learning/xiaomage-space-master-769283dc49ebe7e0fe8af1d277e21f4dcb930e20/VIP课/spring-cloud/lesson-1/spring-cloud-config-client",
"java.runtime.version": "1.8.0_151-b12",
"java.awt.graphicsenv": "sun.awt.CGraphicsEnvironment",
"org.jboss.logging.provider": "slf4j",
"java.endorsed.dirs": "/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/endorsed",
"os.arch": "x86_64",
"java.io.tmpdir": "/var/folders/g4/jq646swd4cz88kjfqwd_9nh40000gn/T/",
"line.separator": "\n",
"java.vm.specification.vendor": "Oracle Corporation",
"https.proxyHost": "localhost",
"os.name": "Mac OS X",
"sun.jnu.encoding": "UTF-8",
"spring.beaninfo.ignore": "true",
"java.library.path": "/Users/guanhang/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.",
"http.proxyPort": "52065",
"java.specification.name": "Java Platform API Specification",
"java.class.version": "52.0",
"sun.management.compiler": "HotSpot 64-Bit Tiered Compilers",
"os.version": "10.13.3",
"user.home": "/Users/guanhang",
"catalina.useNaming": "false",
"user.timezone": "Asia/Shanghai",
"java.awt.printerjob": "sun.lwawt.macosx.CPrinterJob",
"file.encoding": "UTF-8",
"java.specification.version": "1.8",
"catalina.home": "/private/var/folders/g4/jq646swd4cz88kjfqwd_9nh40000gn/T/tomcat.4235981281366985912.9090",
"java.class.path": "太多省略",
"user.name": "guanhang",
"java.vm.specification.version": "1.8",
"sun.java.command": "com.gupao.springcloudconfigclient.SpringCloudConfigClientApplication",
"java.home": "/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre",
"sun.arch.data.model": "64",
"user.language": "zh",
"java.specification.vendor": "Oracle Corporation",
"awt.toolkit": "sun.lwawt.macosx.LWCToolkit",
"java.vm.info": "mixed mode",
"java.version": "1.8.0_151",
"java.ext.dirs": "太多省略",
"http.proxyHost": "localhost",
"java.awt.headless": "true",
"java.vendor": "Oracle Corporation",
"catalina.base": "/private/var/folders/g4/jq646swd4cz88kjfqwd_9nh40000gn/T/tomcat.4235981281366985912.9090",
"file.separator": "/",
"java.vendor.url.bug": "http://bugreport.sun.com/bugreport/",
"sun.io.unicode.encoding": "UnicodeBig",
"sun.cpu.endian": "little",
"https.proxyPort": "52065",
"sun.cpu.isalist": ""
},
"systemEnvironment": {
"PATH": "太多省略",
"JAVA_HOME": "/Library/Java/JavaVirtualMachines/jdk-11.0.5.jdk/Contents/Home",
"MAVEN_HOME": "/Users/guanhang/Soft/apache-maven-3.5.2",
"VERSIONER_PYTHON_VERSION": "2.7",
"LOGNAME": "guanhang",
"PWD": "/Users/guanhang/learning/xiaomage-space-master-769283dc49ebe7e0fe8af1d277e21f4dcb930e20/VIP课/spring-cloud/lesson-1/spring-cloud-config-client",
"XPC_SERVICE_NAME": "com.jetbrains.intellij.ce.30852",
"SHELL": "/bin/zsh",
"PAGER": "less",
"JAVA_MAIN_CLASS_9894": "com.gupao.springcloudconfigclient.SpringCloudConfigClientApplication",
"LSCOLORS": "Gxfxcxdxbxegedabagacad",
"HOMEBREW_BOTTLE_DOMAIN": "https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles",
"OLDPWD": "/Applications/IntelliJ IDEA CE.app/Contents/bin",
"VERSIONER_PYTHON_PREFER_32_BIT": "no",
"USER": "guanhang",
"CLASSPATH": "/Library/Java/JavaVirtualMachines/jdk-11.0.5.jdk/Contents/Home/lib/tools.jar:/Library/Java/JavaVirtualMachines/jdk-11.0.5.jdk/Contents/Home/lib/dt.jar:.",
"ZSH": "/Users/guanhang/.oh-my-zsh",
"TMPDIR": "/var/folders/g4/jq646swd4cz88kjfqwd_9nh40000gn/T/",
"SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.PF2nhbu0rg/Listeners",
"XPC_FLAGS": "0x0",
"__CF_USER_TEXT_ENCODING": "0x1F5:0x19:0x34",
"Apple_PubSub_Socket_Render": "/private/tmp/com.apple.launchd.ZBywAjRu9F/Render",
"LESS": "-R",
"LC_CTYPE": "zh_CN.UTF-8",
"HOME": "/Users/guanhang"
},
"applicationConfig: [classpath:/application.properties]": {
"server.port": "9090",
"management.security.enabled": "false",
"spring.cloud.bootstrap.name": "abc"
},
"springCloudClientHostInfo": {
"spring.cloud.client.hostname": "192.168.221.1",
"spring.cloud.client.ipAddress": "192.168.221.1"
},
"defaultProperties": {}
}
可以通过post方法,请求env端点修改配置,但是正常是要权限的,关闭权限的配置:
management.security.enabled= false
Environment
关联多个带名称的PropertySource
可以参考一下Spring Framework 源码:AbstractRefreshableWebApplicationContext
protected void initPropertySources() {
ConfigurableEnvironment env = getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(this.servletContext, this.servletConfig);
}
}
Environment
有两种实现方式:
普通类型:StandardEnvironment
Web类型:StandardServletEnvironment
Environment
AbstractEnvironment
StandardEnvironment
Enviroment 关联着一个PropertySources
实例
PropertySources
关联着多个PropertySource
,并且有优先级
其中比较常用的PropertySource
实现:
Java System#getProperties 实现: 名称"systemProperties",对应的内容 System.getProperties()
Java System#getenv 实现(环境变量): 名称"systemEnvironment",对应的内容 System.getProperties()
关于 Spring Boot 优先级顺序,可以参考:https://docs.spring.io/spring-boot/docs/2.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#boot-features-external-config
实现自定义配置,修改端口号
-
实现
PropertySourceLocator
-
暴露该实现作为一个Spring Bean
-
实现
PropertySource
:@Configuration @Order(Ordered.HIGHEST_PRECEDENCE) public static class MyPropertySourceLocator implements PropertySourceLocator { @Override public PropertySource<?> locate(Environment environment) { Map<String, Object> source = new HashMap<>(); source.put("server.port","9090"); MapPropertySource propertySource = new MapPropertySource("my-property-source", source); return propertySource; } }
-
定义并且配置 /META-INF/spring.factories:
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.gupao.springcloudconfigclient.SpringCloudConfigClientApplication.MyPropertySourceLocator
注意事项:
Environment
允许出现同名的配置,不过优先级高的胜出
内部实现:MutablePropertySources
关联代码:
List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<PropertySource<?>>();
propertySourceList FIFO,它有顺序
可以通过 MutablePropertySources#addFirst 提高到最优先,相当于调用:
List#add(0,PropertySource);
问题
-
.yml和.yaml是啥区别?
答:没有区别,就是文件扩展名不同
-
自定义的配置在平时使用的多吗 一般是什么场景
答:不多,一般用于中间件的开发
-
Spring 里面有个
@EventListener
和ApplicationListener
什么区别答:没有区别,前者是 Annotation 编程模式,后者 接口编程
-
/env
端点的使用场景 是什么
答:用于排查问题,比如要分析@Value("${server.port}")
里面占位符的具体值
- Spring cloud 会用这个实现一个整合起来的高可用么
答:Spring Cloud 整体达到一个目标,把 Spring Cloud 的技术全部整合到一个项目,比如负载均衡、短路、跟踪、服务调用等
-
怎样防止Order一样
答:Spring Boot 和 Spring Cloud 里面没有办法,在 Spring Security 通过异常实现的。
-
服务监控跟鹰眼一样吗
答:类似
-
bootstrapApplicationListener是引入cloud组件来有的吗
答:是的
-
pom.xml引入哪个cloud组件了?
答:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
-
为什么spring mvc请求/server.port会变量参数获取到的是server
spring mvc 正常只解析点前面的部分,请求的url改成:/server.port 就可以避免这个问题
Spring Cloud Config Server
构建 Spring Cloud 配置服务器
实现步骤
-
在 Configuration Class 标记
@EnableConfigServer
-
配置文件目录(当前目录有.git),添加文件并commit
-
gupao.properties (默认) // 默认环境,跟着代码仓库
-
gupao-dev.properties ( profile = "dev") // 开发环境
-
gupao-test.properties ( profile = "test") // 测试环境
-
gupao-staging.properties ( profile = "staging") // 预发布环境
-
gupao-prod.properties ( profile = "prod") // 生产环境
十月 25 20:46 gupao.properties 十月 25 20:52 gupao-dev.properties 十月 25 20:53 gupao-prod.properties 十月 25 20:52 gupao-staging.properties 十月 25 20:47 gupao-test.properties
-
-
服务端配置配置版本仓库(本地)
spring.cloud.config.server.git.uri = \
file:///E:/Google%20Driver/Private/Lessons/gupao/xiaomage-space/
注意:放在存有
.git
的根目录
完整的配置项:
### 配置服务器配置项
spring.application.name = config-server
### 定义HTTP服务端口
server.port = 9090
### 本地仓库的GIT URI 配置 中文要转义
spring.cloud.config.server.git.uri = \
file:///E:/Google%20Driver/Private/Lessons/gupao/xiaomage-space/
### 全局关闭 Actuator 安全,也可以细粒度的开放
# management.security.enabled = false
### 细粒度的开放 Actuator Endpoints
### sensitive 关注是敏感性,安全
# 参考源码org.springframework.boot.actuate.endpoint.AbstractEndpoint
endpoints.env.sensitive = false
然后可以通过localhost:9090/gupao-dev.properties来访问配置,注意,不commit没法访问
查看版本
访问:
localhost:9090/gupao/dev 可以查看配置的版本version(是一个uuid)
构建 Spring Cloud 配置客户端
实现步骤
-
创建
bootstrap.properties
或者bootstrap.yml
文件 -
bootstrap.properties
或者bootstrap.yml
文件中配置客户端信息### bootstrap 上下文配置 # 配置服务器 URI spring.cloud.config.uri = http://localhost:9090/ # 配置客户端应用名称:{application} spring.cloud.config.name = gupao # profile 是激活配置 spring.cloud.config.profile = dev # label 在Git中指的分支名称 spring.cloud.config.label = master
-
设置关键 Endpoints 的敏感性
### 配置客户端配置项 spring.application.name = config-client ### 全局关闭 Actuator 安全 management.security.enabled = false ### 细粒度的开放 Actuator Endpoints ### sensitive 关注是敏感性,安全 endpoints.env.sensitive = false endpoints.refresh.sensitive = false endpoints.beans.sensitive = false endpoints.health.sensitive = false endpoints.actuator.sensitive = false
访问配置信息:
访问客户端端点,可以看到两个配置文件(只配了两个)都能加载且dev的优先级更高,同时gupao.properties的my.name被gupao-dev.properties覆盖了,都是guanhang
{
"profiles": [],
"server.ports": {
"local.server.port": 8080
},
"configService:configClient": {
"config.client.version": "bf0b3dfdd531845ccd75c92ea7eb1f995ce270d2"
},
"configService:file:///D://tmp//config/gupao-dev.properties": {
"my.name": "guanhang"
},
"configService:file:///D://tmp//config/gupao.properties": {
"my.name": "guanhang"
},
....
}
刷新配置
当服务端端修改配置项后,可以通过端点/refresh刷新客户端配置(注意是POST请求),这样再去请求/evn端口,返回的配置会更新
@RefreshScope 用法
RefreshScope 在配置项发生变更的时候可以自动刷新值
@RestController
@RefreshScope
public class EchoController {
@Value("${my.name}")
private String myName;
@GetMapping("/my-name")
public String getName(){
return myName;
}
}
通过调用/refresh
Endpoint 控制客户端配置更新
实现定时更新客户端
// ScheduledAnnotationBeanPostProcessor除了这个注解
// private final ContextRefresher contextRefresher;是注入的
@Scheduled(fixedRate = 5 * 1000, initialDelay = 3 * 1000)
public void autoRefresh() {
Set<String> updatedPropertyNames = contextRefresher.refresh();
updatedPropertyNames.forEach( propertyName ->
System.err.printf("[Thread :%s] 当前配置已更新,具体 Key:%s , Value : %s \n",
Thread.currentThread().getName(),
propertyName,
environment.getProperty(propertyName)
));
}
健康检查
意义
比如应用可以任意地输出业务健康、系统健康等指标
端点URI:/health
实现类:HealthEndpoint
健康指示器:HealthIndicator
,
HealthEndpoint
:HealthIndicator
,一对多
自定义实现HealthIndicator
-
实现
AbstractHealthIndicator
public class MyHealthIndicator extends AbstractHealthIndicator { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { builder.up().withDetail("MyHealthIndicator","Day Day Up"); } }
-
暴露
MyHealthIndicator
为Bean
@Bean
public MyHealthIndicator myHealthIndicator(){
return new MyHealthIndicator();
}
-
关闭安全控制
management.security.enabled = false
-
请求localhost:8080/health,会返回多个健康检查信息:
{ "status": "DOWN", "my": { "status": "DOWN", "MyHealthIndicator": "Down" }, "diskSpace": { "status": "UP", "total": 787313848320, "free": 736843862016, "threshold": 10485760 }, "refreshScope": { "status": "UP" }, "configServer": { "status": "UP", "propertySources": [ "configClient", "file:///D://tmp//config/gupao-dev.properties", "file:///D://tmp//config/gupao.properties" ] } }
其他内容
REST API = /users , /withdraw
HATEOAS = REST 服务器发现的入口,类似 UDDI (Universal Description Discovery and Integration)
HAL
/users
/withdraw
...
Spring Boot 激活 actuator
需要增加 Hateoas 的依赖:
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
</dependency>
以客户端为例,请求/actuator返回
{
"links": [{
"rel": "self",
"href": "http://localhost:8080/actuator"
}, {
"rel": "heapdump",
"href": "http://localhost:8080/heapdump"
}, {
"rel": "beans",
"href": "http://localhost:8080/beans"
}, {
"rel": "resume",
"href": "http://localhost:8080/resume"
}, {
"rel": "autoconfig",
"href": "http://localhost:8080/autoconfig"
}, {
"rel": "refresh",
"href": "http://localhost:8080/refresh"
}, {
"rel": "env",
"href": "http://localhost:8080/env"
}, {
"rel": "auditevents",
"href": "http://localhost:8080/auditevents"
}, {
"rel": "mappings",
"href": "http://localhost:8080/mappings"
}, {
"rel": "info",
"href": "http://localhost:8080/info"
}, {
"rel": "dump",
"href": "http://localhost:8080/dump"
}, {
"rel": "loggers",
"href": "http://localhost:8080/loggers"
}, {
"rel": "restart",
"href": "http://localhost:8080/restart"
}, {
"rel": "metrics",
"href": "http://localhost:8080/metrics"
}, {
"rel": "health",
"href": "http://localhost:8080/health"
}, {
"rel": "configprops",
"href": "http://localhost:8080/configprops"
}, {
"rel": "pause",
"href": "http://localhost:8080/pause"
}, {
"rel": "features",
"href": "http://localhost:8080/features"
}, {
"rel": "trace",
"href": "http://localhost:8080/trace"
}]
}
问答
-
小马哥,你们服务是基于啥原因采用的springboot 的, 这么多稳定性的问题?
答:Spring Boot 业界比较稳定的微服务中间件,不过它使用是易学难精!
-
小马哥 为什么要把配置项放到 git上,为什么不放到具体服务的的程序里边 ;git在这里扮演什么样的角色 ;是不是和 zookeeper 一样
答:Git 文件存储方式、分布式的管理系统,Spring Cloud 官方实现基于 Git,它达到的理念和 ZK 一样。
-
一个DB配置相关的bean用@RefreshScope修饰时,config service修改了db的配置,比如mysql的url,那么这个Bean会不会刷新?如果刷新了是不是获取新的连接的时候url就变了?
如果发生了配置变更,我的解决方案是重启 Spring Context。@RefreshScope 最佳实践用于配置Bean,比如:开关、阈值、文案等等
数据库变更一般要重启微服务,多台机器需要按顺序轮流重启,如下:
A B C 1 1 1 A* B C 0 1 1 A* B* C 1 0 1 A* B* C 1 1 0 A* B* C* 1 1 1
-
如果这样是不是动态刷新就没啥用了吧
答:不能一概而论,@RefreshScope 开关、阈值、文案等等场景使用比较多
Spring Cloud Netflix Eureka
传统的服务治理
通讯协议
XML-RPC -> XML 方法描述、方法参数 -> WSDL(WebServices 定义语言)
WebServices -> SOAP(HTTP、SMTP) -> 文本协议(头部分、体部分)
REST -> JSON/XML( Schema :类型、结构) -> 文本协议(HTTP Header、Body)
W3C Schema :xsd:string 原子类型,自定义自由组合原子类型
Java POJO : int、String
Response Header -> Content-Type: application/json;charset=UTF-8
Dubbo:Hession、 Java Serialization(二进制),跨语言不变,一般通过 Client(Java、C++)
二进制的性能是非常好(字节流,免去字符流(字符编码),免去了字符解释,机器友好、对人不友好)
序列化:把编程语言数据结构转换成字节流、反序列化:字节流转换成编程语言的数据结构(原生类型的组合)
高可用架构
URI:统一资源定位符
http://git.gupaoedu.com/vip/xiaomage-space/tree/master/VIP课/spring-cloud/lesson-3
URI:用于网络资源定位的描述 Universal Resource Identifier
URL: Universal Resource Locator
网络是通讯方式
资源是需要消费媒介
定位是路由
Proxy:一般性代理,路由
Nginx:反向代理
Broker:包括路由,并且管理,老的称谓(MOM)
Message Broker:消息路由、消息管理(消息是否可达)
可用性比率计算
可用性比率:通过时间来计算(一年或者一月)
比如:一年 99.99 %
可用时间:365 * 24 * 3600 * 99.99%
不可用时间:365 * 24 * 3600 * 0.01% = 3153.6 秒 < 一个小时
不可以时间:1个小时 推算一年 1 / 24 / 365 = 0.01 %
单台机器不可用比率:1%
两台机器不可用比率:1% * 1%
N 机器不可用比率:1% ^ n
可靠性
微服务里面的问题:
一次调用:
A -> B -> C
99% -> 99% -> 99% = 97%
A -> B -> C -> D
99% -> 99% -> 99% -> 99% = 96%
结论:增加机器可以提高可用性,增加服务调用会降低可靠性,同时降低了可用性
Eureka 客户端
提供者
主类:
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceProviderBootstrap {
public static void main(String[] args) {
SpringApplication.run(UserServiceProviderBootstrap.class, args);
}
}
控制器:
@RestController
public class UserServiceProviderRestApiController {
@Autowired
private UserService userService;
/**
* @param user User
* @return 如果保存成功的话,返回{@link User},否则返回<code>null</code>
*/
@PostMapping("/user/save")
public User saveUser(@RequestBody User user) {
if (userService.save(user)) {
System.out.println("UserService 服务方:保存用户成功!" + user);
return user;
} else {
return null;
}
}
/**
* 罗列所有的用户数据
*
* @return 所有的用户数据
*/
@GetMapping("/user/list")
public Collection<User> list() {
return userService.findAll();
}
}
配置文件:
spring.application.name = user-service-provider
## Eureka 注册中心服务器端口
eureka.server.port = 9090
## 服务提供方端口
server.port = 7070
## Eureka Server 服务 URL,用于客户端注册
eureka.client.serviceUrl.defaultZone=\
http://localhost:${eureka.server.port}/eureka
## Management 安全失效
management.security.enabled = false
消费者
主类:
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceConsumerBootstrap {
public static void main(String[] args) {
SpringApplication.run(UserServiceConsumerBootstrap.class, args);
}
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
消费接口:
@RestController
public class UserRestApiController {
//这里注入的接口和提供者的接口是一个
@Autowired
private UserService userService;
/**
* @param name 请求参数名为“name”的数据
* @return 如果保存成功的话,返回{@link User},否则返回<code>null</code>
*/
@PostMapping("/user/save")
public User saveUser(@RequestParam String name) {
User user = new User();
user.setName(name);
if (userService.save(user)) {
return user;
} else {
return null;
}
}
/**
* 罗列所有的用户数据
* @return 所有的用户数据
*/
@GetMapping("/user/list")
public Collection<User> list() {
return userService.findAll();
}
}
消费服务:
@Service
public class UserServiceProxy implements UserService {
private static final String PROVIDER_SERVER_URL_PREFIX = "http://user-service-provider";
/**
* 通过 REST API 代理到服务器提供者
*/
@Autowired
private RestTemplate restTemplate;
@Override
public boolean save(User user) {
User returnValue =
restTemplate.postForObject(PROVIDER_SERVER_URL_PREFIX + "/user/save", user, User.class);
return returnValue != null;
}
@Override
public Collection<User> findAll() {
return restTemplate.getForObject(PROVIDER_SERVER_URL_PREFIX + "/user/list", Collection.class);
}
}
配置文件:
spring.application.name = user-service-consumer
## Eureka 注册中心服务器端口
eureka.server.port = 9090
## 服务消费方端口
server.port = 8080
## Eureka Server 服务 URL,用于客户端注册
eureka.client.serviceUrl.defaultZone=\
http://localhost:${eureka.server.port}/eureka
## Management 安全失效
management.security.enabled = false
Eureka 服务器
代码:
@SpringBootApplication
@EnableEurekaServer
public class SpringCloudEurekaServerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudEurekaServerDemoApplication.class, args);
}
}
配置文件:
### Eureka Server 应用名称
spring.application.name = spring-cloud-eureka-server
### Eureka Server 服务端口
server.port= 9090
### 取消服务器自我注册
eureka.client.register-with-eureka=false
### 注册中心的服务器,没有必要再去检索服务
eureka.client.fetch-registry = false
## Eureka Server 服务 URL,用于客户端注册
eureka.client.serviceUrl.defaultZone=\
http://localhost:${server.port}/eureka
Eureka 服务器一般不需要自我注册,也不需要注册其他服务器,也不需要检索服务
Eureka 自我注册的问题:服务器本身没有启动
Fast Fail : 快速失败
Fault-Tolerance :容错
但是这两个设置并不是影响作为服务器的使用,不过建议关闭,为了减少不必要的异常堆栈,减少错误的干扰(比如:系统异常和业务异常)
问答部分
-
consul 和 Eureka 是一样的吗
答:提供功能类似,consul 功能更强大,广播式服务发现/注册
-
重启eureka 服务器,客户端应用要重启吗
答:不用,客户端在不停地上报信息,不过在 Eureka 服务器启动过程中,客户单大量报错
-
生产环境中,consumer是分别注册成多个服务,还是统一放在一起注册成一个服务?权限应该如何处理?
答:consumer 是否要分为多个服务,要情况,大多数情况是需要,根据应用职责划分。权限根据服务方法需要,比如有些敏感操作的话,可以更具不同用户做鉴权。
-
客户端上报的信息存储在哪里?内存中还是数据库中
答:都是在内存里面缓存着,EurekaClient 并不是所有的服务,需要的服务。比如:Eureka Server 管理了 200个应用,每个应用存在 100个实例,总体管理 20000 个实例。客户端更具自己的需要的应用实例。
-
要是其他模块查询列表里面 有用到用户信息怎么办呢 是循环调用户接口 还是直接关联用户表呢 怎么实现好呢
答:用户 API 依赖即可
-
consumer 调用 Aprovider-a 挂了,会自动切换 Aprovider-b吗,保证请求可用吗
答:当 Aprovider-a 挂,会自动切换,不过不一定及时。不及时,服务端可能存在脏数据,或者轮训更新时间未达。
-
一个业务中调用多个service时如何保证事务
答:需要分布式事务实现(JTA),可是一般互联网项目,没有这种昂贵的操作。
Spring Cloud Netflix Ribbon
Eureka 高客户端高可用
高可用注册中心集群
只需要增加 Eureka 服务器注册URL:
## Eureka Server 服务 URL,用于客户端注册
eureka.client.serviceUrl.defaultZone=\
http://localhost:9090/eureka,http://localhost:9091/eureka
如果 Eureka 客户端应用配置多个 Eureka 注册服务器,那么默认情况只有第一台可用的服务器,存在注册信息。
如果 第一台可用的 Eureka 服务器 Down 掉了,那么 Eureka 客户端应用将会选择下一台可用的 Eureka 服务器。
配置源码(EurekaClientConfigBean)
配置项 eureka.client.serviceUrl
实际映射的字段为 serviceUrl
`(来源于配置bean:EurekaClientConfigBean),它是 Map 类型,Key 为自定义,默认值“defaultZone”,value 是需要配置的Eureka 注册服务器URL。
private Map<String, String> serviceUrl = new HashMap<>();
{
this.serviceUrl.put(DEFAULT_ZONE, DEFAULT_URL);
}
value 可以是多值字段,通过“,” 分割:
String serviceUrls = this.serviceUrl.get(myZone);
if (serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = this.serviceUrl.get(DEFAULT_ZONE);
}
if (!StringUtils.isEmpty(serviceUrls)) {
final String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
}
获取注册信息时间间隔
Eureka 客户端需要获取 Eureka 服务器注册信息,这个方便服务调用。
Eureka 客户端:EurekaClient
,关联应用集合:Applications
// EurekaClient接口定义,实现类为com.netflix.discovery.DiscoveryClient
public Applications getApplications(String serviceUrl);
单个应用信息:Application
,关联多个应用实例,详见com.netflix.discovery.shared.Applications
单个应用实例:InstanceInfo
当 Eureka 客户端需要调用具体某个服务时,比如user-service-consumer
调用user-service-provider
,user-service-provider
实际对应对象是Application
,关联了许多应用实例(InstanceInfo
)。
如果应用user-service-provider
的应用实例发生变化时,那么user-service-consumer
是需要感知的。比如:user-service-provider
机器从10 台降到了5台,那么,作为调用方的user-service-consumer
需要知道这个变化情况。可是这个变化过程,可能存在一定的延迟,可以通过调整注册信息时间间隔来减少错误。
具体配置项
## 调整注册信息的获取周期,默认值:30秒
eureka.client.registryFetchIntervalSeconds = 5
实例信息复制时间间隔
具体就是客户端信息的上报到 Eureka 服务器时间。当 Eureka 客户端应用上报的频率越频繁,那么 Eureka 服务器的应用状态管理一致性就越高。
具体配置项
## 调整客户端应用状态信息上报的周期
eureka.client.instanceInfoReplicationIntervalSeconds = 5
Eureka 的应用信息获取的方式:拉模式
Eureka 的应用信息上报的方式:推模式
实例Id
从 Eureka Server Dashboard 里面可以看到具体某个应用中的实例信息,比如:
UP (2) - 192.168.1.103:user-service-provider:7075 , 192.168.1.103:user-service-provider:7078
其中,它们命名模式:${hostname}:${spring.application.name}:${server.port}
实例类:EurekaInstanceConfigBean
配置项
## Eureka 应用实例的ID
eureka.instance.instanceId = ${spring.application.name}:${server.port}
实例端点映射
源码位置:EurekaInstanceConfigBean
private String statusPageUrlPath = "/info";
配置项
## Eureka 客户端应用实例状态 URL
eureka.instance.statusPageUrlPath = /health
Eureka服务端高可用
构建 Eureka 服务器相互注册
Eureka Server 1 -> Profile : peer1
配置项
### Eureka Server 应用名称
spring.application.name = spring-cloud-eureka-server
### Eureka Server 服务端口
server.port= 9090
### 取消服务器自我注册
eureka.client.register-with-eureka=true
### 注册中心的服务器,没有必要再去检索服务
eureka.client.fetch-registry = true
## Eureka Server 服务 URL,用于客户端注册
## 当前 Eureka 服务器 向 9091(Eureka 服务器) 复制数据
eureka.client.serviceUrl.defaultZone=\
http://localhost:9091/eureka
Eureka Server 2 -> Profile : peer2
配置项
### Eureka Server 应用名称
spring.application.name = spring-cloud-eureka-server
### Eureka Server 服务端口
server.port= 9091
### 取消服务器自我注册
eureka.client.register-with-eureka=true
### 注册中心的服务器,没有必要再去检索服务
eureka.client.fetch-registry = true
## Eureka Server 服务 URL,用于客户端注册
## 当前 Eureka 服务器 向 9090(Eureka 服务器) 复制数据
eureka.client.serviceUrl.defaultZone=\
http://localhost:9090/eureka
通过--spring.profiles.active=peer1
和 --spring.profiles.active=peer2
分别激活 Eureka Server 1 和 Eureka Server 2
Eureka Server 1 里面的replicas 信息:
registered-replicas | http://localhost:9091/eureka/ |
---|---|
Eureka Server 2 里面的replicas 信息:
registered-replicas | http://localhost:9090/eureka/ |
---|---|
Spring RestTemplate
HTTP消息装换器:HttpMessageConvertor
自义定实现
编码问题
切换序列化/反序列化协议
HTTP Client 适配工厂:ClientHttpRequestFactory
这个方面主要考虑大家的使用 HttpClient 偏好:
- Spring 实现
- SimpleClientHttpRequestFactory
- HttpClient
- HttpComponentsClientHttpRequestFactory
- OkHttp
- OkHttp3ClientHttpRequestFactory
- OkHttpClientHttpRequestFactory
举例说明
RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); // HTTP Client
切换HTTP 通讯实现,提升性能
HTTP 请求拦截器:ClientHttpRequestInterceptor
加深RestTemplate 拦截过程的
整合Netflix Ribbon
@LoadBalanced让RestTemplate
增加一个LoadBalancerInterceptor
,调用Netflix 中的LoadBalander
实现,根据 Eureka 客户端应用获取目标应用 IP+Port 信息,轮训的方式调用。
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
// 实现类为RibbonLoadBalancerClient,会调用SpringClientFactory获取负载均衡器,也就是ILoadBalancer的实现类
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
// for backwards compatibility
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
实际请求客户端
- LoadBalancerClient
- RibbonLoadBalancerClient
负载均衡上下文
- LoadBalancerContext
- RibbonLoadBalancerContext
负载均衡器
- ILoadBalancer
- BaseLoadBalancer
- DynamicServerListLoadBalancer
- ZoneAwareLoadBalancer
- NoOpLoadBalancer
负载均衡规则
核心规则接口
- IRule
- 随机规则:RandomRule
- 最可用规则:BestAvailableRule
- 轮训规则:RoundRobinRule
- 重试实现:RetryRule
- 客户端配置:ClientConfigEnabledRoundRobinRule
- 可用性过滤规则:AvailabilityFilteringRule
- RT权重规则:WeightedResponseTimeRule
- 规避区域规则:ZoneAvoidanceRule
PING 策略
核心策略接口
- IPingStrategy
PING 接口
- IPing
- NoOpPing
- DummyPing
- PingConstant
- PingUrl
Discovery Client 实现
- NIWSDiscoveryPing
问答部分
-
为什么要用eureka?
答:目前业界比较稳定云计算的开发员中间件,虽然有一些不足,基本上可用
-
使用的话-每个服务api都需要eureka 插件
答:需要使用 Eureka 客户端
-
eureka 主要功能为啥不能用浮动ip 代替呢?
答:如果要使用浮动的IP,也是可以的,不过需要扩展
-
这节内容是不是用eureka 来解释 负载均衡原理、转发规则计算?
答:是的
-
eureka 可以替换为 zookeeper和consoul 那么这几个使用有什么差异?
-
通讯不是指注册到defaultZone配置的那个么?
答:默认情况是往 defaultZone 注册
-
如果服务注册中心都挂了,服务还是能够运行吧?
答:服务调用还是可以运行,有可能数据会不及时、不一致
-
spring cloud 日志收集 有解决方案么?
答:一般用 HBase、或者 TSDB、elk
-
spring cloud提供了链路跟踪的方法吗
答:http://cloud.spring.io/spring-cloud-static/Dalston.SR4/single/spring-cloud.html#_spring_cloud_sleuth
网友评论