微服务学习笔记一

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,提供两类方法:

  1. 读read* :通过 HTTP 请求内容转化成对应的 Bean
  2. 写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 = 小马哥
  1. 实现 AbstractHttpMessageConverter 抽象类
    1. supports 方法:是否支持当前POJO类型
    2. readInternal 方法:读取 HTTP 请求中的内容,并且转化成相应的POJO对象(通过 Properties 内容转化成 JSON)
    3. 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 检验

实现步骤

  1. 复制成熟 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

  2. 实现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;
        }
    }
    
  3. @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!
    

    中文类似

  4. 添加注解到类中:

    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

问答部分

  1. JSON校验如何办?

    答:尝试变成 Bean 的方式

  2. 实际中很多参数都要校验那时候怎么写这样写会增加很多类

    答:确实会增加部分工作量,大多数场景,不需要自定义,除非很特殊情况。Bean Validation 的主要缺点,单元测试不方便

  3. 如果前端固定表单的话,这种校验方式很好。但是灵活性不够,如果表单是动态的话,如何校验呢?

    答: 表单字段与 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

  4. 如何自定义,反回格式?如何最佳实现

    答:可以通过REST来实现,比如 XML 或者 JSON 的格式(视图)

  5. 面试的看法

    答:具备一定的水平

    不该问的不要问,因为面试官的水平可能还不及于你!

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,加载顺序:

  1. application-{profile}.properties
  2. 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优先

  1. 负责加载bootstrap.properties 或者 bootstrap.yaml
  2. 负责初始化 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

实现自定义配置,修改端口号

  1. 实现PropertySourceLocator

  2. 暴露该实现作为一个Spring Bean

  3. 实现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;
        }
    }
    
  4. 定义并且配置 /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);

问题

  1. .yml和.yaml是啥区别?

    答:没有区别,就是文件扩展名不同

  2. 自定义的配置在平时使用的多吗 一般是什么场景

    答:不多,一般用于中间件的开发

  3. Spring 里面有个@EventListenerApplicationListener什么区别

    答:没有区别,前者是 Annotation 编程模式,后者 接口编程

  4. /env 端点的使用场景 是什么

答:用于排查问题,比如要分析@Value("${server.port}")里面占位符的具体值

  1. Spring cloud 会用这个实现一个整合起来的高可用么

答:Spring Cloud 整体达到一个目标,把 Spring Cloud 的技术全部整合到一个项目,比如负载均衡、短路、跟踪、服务调用等

  1. 怎样防止Order一样

    答:Spring Boot 和 Spring Cloud 里面没有办法,在 Spring Security 通过异常实现的。

  2. 服务监控跟鹰眼一样吗

    答:类似

  3. bootstrapApplicationListener是引入cloud组件来有的吗

    答:是的

  4. pom.xml引入哪个cloud组件了?

    答:

    <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    
  5. 为什么spring mvc请求/server.port会变量参数获取到的是server

spring mvc 正常只解析点前面的部分,请求的url改成:/server.port 就可以避免这个问题

Spring Cloud Config Server

构建 Spring Cloud 配置服务器

实现步骤

  1. 在 Configuration Class 标记@EnableConfigServer

  2. 配置文件目录(当前目录有.git),添加文件并commit

    1. gupao.properties (默认) // 默认环境,跟着代码仓库

    2. gupao-dev.properties ( profile = "dev") // 开发环境

    3. gupao-test.properties ( profile = "test") // 测试环境

    4. gupao-staging.properties ( profile = "staging") // 预发布环境

    5. 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
    
  3. 服务端配置配置版本仓库(本地)

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 配置客户端

实现步骤

  1. 创建bootstrap.properties 或者 bootstrap.yml文件

  2. 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 
    
  3. 设置关键 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

HealthEndpointHealthIndicator ,一对多

自定义实现HealthIndicator

  1. 实现AbstractHealthIndicator

    public class MyHealthIndicator extends AbstractHealthIndicator {
    
        @Override
        protected void doHealthCheck(Health.Builder builder)
                throws Exception {
            builder.up().withDetail("MyHealthIndicator","Day Day Up");
        }
    }
    
  2. 暴露 MyHealthIndicatorBean

@Bean
public MyHealthIndicator myHealthIndicator(){
  return new MyHealthIndicator();
}    
  1. 关闭安全控制

    management.security.enabled = false
    
  2. 请求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"
    }]
}

问答

  1. 小马哥,你们服务是基于啥原因采用的springboot 的, 这么多稳定性的问题?

    答:Spring Boot 业界比较稳定的微服务中间件,不过它使用是易学难精!

  2. 小马哥 为什么要把配置项放到 git上,为什么不放到具体服务的的程序里边 ;git在这里扮演什么样的角色 ;是不是和 zookeeper 一样

    答:Git 文件存储方式、分布式的管理系统,Spring Cloud 官方实现基于 Git,它达到的理念和 ZK 一样。

  3. 一个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
    
  4. 如果这样是不是动态刷新就没啥用了吧

    答:不能一概而论,@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 :容错

但是这两个设置并不是影响作为服务器的使用,不过建议关闭,为了减少不必要的异常堆栈,减少错误的干扰(比如:系统异常和业务异常)

问答部分

  1. consul 和 Eureka 是一样的吗

    答:提供功能类似,consul 功能更强大,广播式服务发现/注册

  2. 重启eureka 服务器,客户端应用要重启吗

    答:不用,客户端在不停地上报信息,不过在 Eureka 服务器启动过程中,客户单大量报错

  3. 生产环境中,consumer是分别注册成多个服务,还是统一放在一起注册成一个服务?权限应该如何处理?

    答:consumer 是否要分为多个服务,要情况,大多数情况是需要,根据应用职责划分。权限根据服务方法需要,比如有些敏感操作的话,可以更具不同用户做鉴权。

  4. 客户端上报的信息存储在哪里?内存中还是数据库中

    答:都是在内存里面缓存着,EurekaClient 并不是所有的服务,需要的服务。比如:Eureka Server 管理了 200个应用,每个应用存在 100个实例,总体管理 20000 个实例。客户端更具自己的需要的应用实例。

  5. 要是其他模块查询列表里面 有用到用户信息怎么办呢 是循环调用户接口 还是直接关联用户表呢 怎么实现好呢

    答:用户 API 依赖即可

  6. consumer 调用 Aprovider-a 挂了,会自动切换 Aprovider-b吗,保证请求可用吗

答:当 Aprovider-a 挂,会自动切换,不过不一定及时。不及时,服务端可能存在脏数据,或者轮训更新时间未达。

  1. 一个业务中调用多个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-provideruser-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

问答部分

  1. 为什么要用eureka?

    答:目前业界比较稳定云计算的开发员中间件,虽然有一些不足,基本上可用

  2. 使用的话-每个服务api都需要eureka 插件

    答:需要使用 Eureka 客户端

  3. eureka 主要功能为啥不能用浮动ip 代替呢?

    答:如果要使用浮动的IP,也是可以的,不过需要扩展

  4. 这节内容是不是用eureka 来解释 负载均衡原理、转发规则计算?

    答:是的

  5. eureka 可以替换为 zookeeper和consoul 那么这几个使用有什么差异?

    答:https://www.consul.io/intro/vs/zookeeper.html

    https://www.consul.io/intro/vs/eureka.html

  6. 通讯不是指注册到defaultZone配置的那个么?

    答:默认情况是往 defaultZone 注册

  7. 如果服务注册中心都挂了,服务还是能够运行吧?

    答:服务调用还是可以运行,有可能数据会不及时、不一致

  8. spring cloud 日志收集 有解决方案么?

    答:一般用 HBase、或者 TSDB、elk

  9. spring cloud提供了链路跟踪的方法吗

    答:http://cloud.spring.io/spring-cloud-static/Dalston.SR4/single/spring-cloud.html#_spring_cloud_sleuth

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271