当前位置:   article > 正文

乐忧商城项目总结-1_乐优商城面试总结

乐优商城面试总结

1.springboot

1.1 springboot基本介绍

1.什么是springboot? SpringBoot是Spring项目中的一个子工程,与Spring-framework 同属于spring的产品,可以将其看做是搭建spring应用程序的脚手架,由于其提供了许多默认的配置,而且具有自己的内嵌的tomcat,使开发人员在开发时不需要花大量时间在写xml等配置文件上,而是关注于业务逻辑的实现,大大提高了开发效率。
2.springboot的好处? 简单来说有两点明显的好处:一个是解决了原先ssm框架配置复杂的问题,使得程序员更多地关注业务逻辑的实现,而不用花太多时间写配置,因为springboot提供了许多默认的配置,当然你也可以编写配置文件来修改这些默认的配置。另外一个是解决了混乱的依赖管理问题:如果一个项目需要使用很多库,有些库又引用了这之中别的库,就会导致依赖关系很复杂,需要弄清楚某些库的版本是否和其他库的版本有冲突,这些不兼容问题一旦出现很难解决,而springboot帮我们解决了这个问题。
3.springboot的特点?

  • 创建独立的spring应用程序
  • 直接内嵌tomcat、jetty和undertow(不需要打包成war包部署)
  • 提供了固定化的“starter”配置,以简化构建配置
  • 尽可能的自动配置spring和第三方库
  • 提供产品级的功能,如:安全指标、运行状况监测和外部化配置等
  • 绝对不会生成代码,并且不需要XML配置

1.2 springboot快速入门

springboot的项目不像传统的ssm框架编写的项目,没有webapp目录,不需要对tomcat进行配置,因为其内置了tomcat,只需要编写一个引导类即可(这里命名为TestApplication),而且整个应用只有一个配置文件,默认命名为application.properties或者application.yml.
springboot入门
SpringBoot提供了一个名为spring-boot-starter-parent的工程,里面已经对各种常用依赖(并非全部)的版本进行了管理,我们的项目需要以这个项目为父工程,这样我们就不用操心依赖的版本问题了,需要什么依赖,直接引入坐标即可(由于springboot整合了各种框架,所以需要使用什么框架就导入该框架的starter即可,注意这里导入的web启动器并没有指定版本,这是因为在引入的父工程中所有的版本都已经管理好了,不会出现冲突) 注意:springboot会根据我们导入的依赖自动地进行猜测我们想要的配置,并自动地进行默认配置,例如我们引入了web启动器,该启动器会导入tomcat和springmvc的依赖,那么springboot就会帮你完成springmvc和tomcat的默认配置!

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itheima</groupId>
    <artifactId>springboot</artifactId>
    <version>1.0-SNAPSHOT</version>

<!--    所有的工程都应该以该工程为父工程-->
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.0.6.RELEASE</version>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.24</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
<!--        启动器,每个启动器背后都是一推依赖,这里的启动器是web启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.drtrang</groupId>
            <artifactId>druid-spring-boot2-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

    </dependencies>

</project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
/**
 * 引导类:springboot应用的入口
 */
//@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}) //声明自动配置,它会根据你导入的依赖来猜测你会需要什么样的配置
//@ComponentScan //包扫描注解,默认扫描被该注解修饰的类所在的包以及该包的子子孙孙包
@SpringBootApplication(exclude={DruidAutoConfiguration.class}) //该注解相当于以上两个注解之和,再加上@SpringBootConfiguration, 该注解相当于@Configuration,但是只能有一个这样的注解,而@Configuration可以有多个
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上图中exclude属性用于从自动配置中剔除指定的配置类,当你想对程序的某个部分进行自己的配置时,需要将该部分的默认配置先剔除,然后再编写自己的配置类.

1.3 默认配置的原理

springboot的默认配置方式没有任何的xml。那么如果自己要新增配置该怎么办?比如我们要配置一个数据库连接池,spring会在配置文件中用如下方式配置:

<!-- 配置连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
      init-method="init" destroy-method="close">
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

现在在springboot中,我们需要使用java配置类的方式.
首先在pom.xml中,引入Druid连接池依赖:

<dependency>
            <groupId>com.github.drtrang</groupId>
            <artifactId>druid-spring-boot2-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

然后在配置文件application.properties中添加配置(这里使用mysql8.0):

jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/test
jdbc.username=root
jdbc.password=a123456
  • 1
  • 2
  • 3
  • 4

然后接下来一共有四种配置方式:

package com.itheima.springboot.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
//@ConfigurationProperties(prefix = "jdbc")
public class JdbcProperties {
    //使用set方法自动注入
    private String driverClassName;
    private String url;
    private String username;
    private String password;

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
package com.itheima.springboot.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import javax.sql.DataSource;

@Configuration //表明该类是一个配置类,相当于一个xml配置文件
//@PropertySource("classpath:application.properties") //读取资源文件
//@EnableConfigurationProperties(JdbcProperties.class)
public class JdbcConfig {
//    @Value("${jdbc.driverClassName}") //方法一
//    private String driverClassName;
//    @Value("${jdbc.url}")
//    private String url;
//    @Value("${jdbc.username}")
//    private String username;
//    @Value("${jdbc.password}")
//    private String password;

//    @Autowired
//    private  JdbcProperties jdbcProperties; 注意这里可以不写@Autowired, 可以写一个该属性的构造函数来进行赋值

//    @Bean //将方法的返回值注入到spring容器   //方法二
//    public DataSource getDataSource(){
//        DruidDataSource druidDataSource = new DruidDataSource();
//        druidDataSource.setDriverClassName(jdbcProperties.getDriverClassName());
//        druidDataSource.setUrl(jdbcProperties.getUrl());
//        druidDataSource.setUsername(jdbcProperties.getUsername());
//        druidDataSource.setPassword(jdbcProperties.getPassword());
//        return druidDataSource;
//    }

//    @Bean //将方法的返回值注入到spring容器
//    public DataSource getDataSource(JdbcProperties jdbcProperties){ //方法三
//        DruidDataSource druidDataSource = new DruidDataSource();
//        druidDataSource.setDriverClassName(jdbcProperties.getDriverClassName());
//        druidDataSource.setUrl(jdbcProperties.getUrl());
//        druidDataSource.setUsername(jdbcProperties.getUsername());
//        druidDataSource.setPassword(jdbcProperties.getPassword());
//        return druidDataSource;
//    }

    @Bean //将方法的返回值注入到spring容器
    @ConfigurationProperties(prefix = "jdbc") // 方法四,这里会自动调用dataSource的set方法进行参数注入
    public DataSource getDataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

注意@EnableAutoConfiguration会开启SpringBoot的自动配置,并且根据你引入的依赖来生效对应的默认配置。这些默认配置在引入的依赖:spring-boot-autoconfigure中,其中定义了大量自动配置类,那么伴随着就有三个问题:

  • 这些默认配置是怎么配置的,在哪里配置的呢?(spring-boot-autoconfigure中)
  • 为何依赖引入就会触发配置呢?(和某些注解有关,具体的现在知道了也会忘)
  • 这些默认配置的属性来自哪里呢?(在默认配置中有写好的属性的默认值)

小结

SpringBoot为我们提供了默认配置,而默认配置生效的条件一般有两个:

  • 你引入了相关依赖
  • 你自己没有配置

1)启动器

之所以,我们如果不想配置,只需要引入依赖即可,而依赖版本我们也不用操心,因为只要引入了SpringBoot提供的stater(启动器),就会自动管理依赖及版本了。

因此,玩SpringBoot的第一件事情,就是找启动器,SpringBoot提供了大量的默认启动器,参考课前资料中提供的《SpringBoot启动器.txt》

2)全局配置

另外,SpringBoot的默认配置,都会读取默认属性,而这些属性可以通过自定义application.properties文件来进行覆盖。这样虽然使用的还是默认配置,但是配置中的值改成了我们自定义的。

因此,玩SpringBoot的第二件事情,就是通过application.properties来覆盖默认属性值,形成自定义配置。我们需要知道SpringBoot的许多默认属性key,这个需要积累,熟能生巧。

1.4 springboot整合常用模块

整合springmvc

首先引入web启动器:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然后编写controller即可:

package com.itheima.springboot.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.sql.DataSource;

@RestController //相当于@Controller和@RequestBody两个注解,表明该类的所有方法返回值均为json类型
@RequestMapping("hello")
public class HelloController {
    @Autowired
    private DataSource dataSource;

    @GetMapping("show")
    public String test(){
        return "hello springboot 1";
    }

    public static void main(String[] args) {
        SpringApplication.run(HelloController.class,args);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

如果需要修改服务运行的端口,就在配置文件中配置server.port即可;另外,由于没有webapp目录,那么静态资源应该存放在哪里呢?配置文件中默认的静态资源路径为:

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/

只要静态资源放在这些目录中任何一个,SpringMVC都会帮我们处理。我们习惯会把静态资源放在classpath:/static/目录下。另外关于编写拦截器,整合连接池等问题,在项目中再复习吧。

整合mybatis

SpringBoot官方并没有提供Mybatis的启动器,不过Mybatis官方自己实现了:

<!--mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

配置,基本没有需要配置的:

# mybatis 别名扫描
mybatis.type-aliases-package=cn.itcast.pojo
# mapper.xml文件位置,如果没有映射文件,请注释掉
mybatis.mapper-locations=classpath:mappers/*.xml
  • 1
  • 2
  • 3
  • 4

需要注意,这里没有配置mapper接口扫描包,因此我们需要给每一个Mapper接口添加@Mapper注解,才能被识别。

集成通用mapper(中国人自己写的,非常好用)

通用Mapper的作者也为自己的插件编写了启动器,我们直接引入即可:

<!-- 通用mapper -->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{
}
  • 1
  • 2
  • 3

整合事务

其实,我们引入jdbc或者web的启动器,就已经引入事务相关的依赖及默认配置了,只需要在使用的地方加上注解@Transactional即可

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryById(Long id){
        return this.userMapper.selectByPrimaryKey(id);
    }

    @Transactional
    public void deleteById(Long id){
        this.userMapper.deleteByPrimaryKey(id);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

1.5 Thymeleaf简介

Thymeleaf之前没听说过,刚开始学的时候用的jsp(一种典型的异步方式,浏览器向服务器发送请求到服务器返回结果给浏览器的这段时间浏览器就干等着什么也不做,不过现在都2022年了,jsp是不是早已经被淘汰了?),后来学了ajax请求(同步方式),基于Jquery框架的ajax用起来比原生的ajax方便多了,springboot默认不支持jsp,但天生支持Thymeleaf,其实用起来之后发现语法和jsp也差不多。
下面是关于Thymeleaf的一些资料:
简单说, Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较于其他的模板引擎,它有如下四个极吸引人的特点:

  • 动静结合:Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。
  • 开箱即用:它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、改jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
  • 多方言支持:Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。
  • 与SpringBoot完美整合,SpringBoot提供了Thymeleaf的默认配置,并且为Thymeleaf设置了视图解析器,我们可以像以前操作jsp一样来操作Thymeleaf。代码几乎没有任何区别,就是在模板语法上有区别。
    在后面的项目中会用到themeleaf,到时候再复习具体怎么用吧!

2.springcloud-1

2.1 系统架构演变

系统架构的变迁:集中式架构,垂直拆分,分布式服务,SOA,微服务架构

在这里插入图片描述
在这里插入图片描述网站流量很小时,可以采用上图的集中式架构,所有服务都耦合在一起,能够减少节点部署的成本。
存在的问题:

  • 代码耦合,开发维护困难
  • 无法针对不同模块进行针对性优化
  • 无法水平扩展
  • 单点容错率低,并发能力差
    在这里插入图片描述
    当访问量逐渐增大,集中式架构无法满足需求,此时为了应对更高的并发和业务需求,可以根据业务功能对系统进行拆分,及上图的垂直拆分。
    优点:
  • 系统拆分实现了流量分担,解决了并发问题
  • 可以针对不同模块进行优化
  • 方便水平扩展,负载均衡,容错率提高
    缺点:
  • 系统间相互独立,会有很多重复开发工作,影响开发效率

在这里插入图片描述
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。

  • 将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
  • 系统间耦合度变高,调用关系错综复杂,难以维护
    在这里插入图片描述
    SOA :面向服务的架构
    当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键

以前出现了什么问题?

  • 服务越来越多,需要管理每个服务的地址
  • 调用关系错综复杂,难以理清依赖关系
  • 服务过多,服务状态难以管理,无法根据服务情况动态管理

服务治理要做什么?

  • 服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
  • 服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
  • 动态监控服务状态监控报告,人为控制服务状态

缺点:

  • 服务间会有依赖关系,一旦某个环节出错会影响较大
  • 服务关系复杂,运维、测试部署困难,不符合DevOps思想

在这里插入图片描述
前面说的SOA,英文翻译过来是面向服务。微服务,似乎也是服务,都是对系统进行拆分。因此两者非常容易混淆,但其实却有一些差别:

微服务的特点:

  • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
  • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
  • 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
  • 自治:自治是说服务间互相独立,互不干扰
    • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
    • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
    • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动端开发不同的接口
    • 数据库分离:每个服务都使用自己的数据源
    • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护

为什么会有系统架构的演变?个人理解就是之前的服务访问流量小,代码量也小,代码只要能写出来完成需要的功能即可,不需要什么别的要求,但是随着服务访问人数的增加,功能的增多,就需要整体设计一个结构,以方便代码的编写(简单来说就是系统太大了,需要很多人合作,那么就需要提前想好这个系统应该怎么设计,每个人负责什么写哪个模块,代码如果出了问题应该找谁负责,如果还是像以前一样一堆代码堆在一起,结构混乱,如果出了问题就根本不知道bug在哪里,也不知道这个bug是谁写的,这样解决问题就更加耗费时间,还不如一开始废点时间设计个结构出来,到时候出了问题可以迅速定位到负责的人),另外就是当访问人数过大时,用一台服务器压力太大,那么就需要使用服务器集群,使得有很多台服务器提供同一个功能,这样即时一台服务器挂了,还可以访问别的服务器来实现这个功能,提高用户的体验度,这样随之而来的问题就是负载均衡啊这些问题,另外还需要一个统一的中心来管理这些服务,慢慢地最终演变为微服务架构。

2.2 服务的调用方式

无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?

常见的远程调用方式有以下2种:

  • RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
  • Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
    现在热门的Rest风格,就可以通过http协议来实现。

如果公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。相反,如果公司的技术栈多样化,而且更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。在这个项目中用的是SpringCloud套件,因此我们会使用Http方式来实现服务间调用。

2.3 SpringCloud简介

微服务是一种架构方式,最终肯定需要技术架构去实施。
微服务的实现方式很多,但是最火的莫过于Spring Cloud了。原因如下:

  • 后台硬:作为Spring家族的一员,有整个Spring全家桶靠山,背景十分强大。
  • 技术强:Spring作为Java领域的前辈,可以说是功力深厚。有强力的技术团队支撑,一般人还真比不了
  • 群众基础好:可以说大多数程序员的成长都伴随着Spring框架,SpringCloud与Spring的各个框架无缝整合,对大家来说一切都很熟悉。
  • 使用方便:SpringCloud完全支持SpringBoot的开发,用很少的配置就能完成微服务框架的搭建

SpringCloud将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:

  • Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现)
  • Zuul:网关组件,提供智能路由,访问过滤功能
  • Ribbon:客户端负载均衡的服务调用组件(客户端负载)
  • Feign:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用)
  • Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错)

架构图:
在这里插入图片描述

2.4 SpringCloud组件-Eureka

Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。

同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。

这就实现了服务的自动注册、发现、状态监控。
简单来说,eureka组件就是一个中间商,用来管理所有的微服务
在这里插入图片描述

  • Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址

  • 提供者:启动后向Eureka注册自己信息(地址,提供什么服务)

  • 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新

  • 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
    一般来说,玩微服务分三步走:

  • 1.在pom.xml导入相应微服务需要的依赖

  • 2.在application.yml文件中添加我们需要的配置

  • 3.添加引导类
    接下来我们详细讲解Eureka的原理及配置。
    Eureka架构中的三个核心角色:

  • 服务注册中心
    Eureka的服务端应用,提供服务注册和发现功能。

  • 服务提供者
    提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。

  • 服务消费者
    消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。

Eureka Server即服务的注册中心,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。

服务提供者
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。

服务注册

服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。

  • 第一层Map的Key就是服务id,一般是配置中的spring.application.name属性
  • 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如:locahost:service-provider:8081
  • 值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。

服务续约

在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);

有两个重要参数可以修改服务续约的行为:

eureka:
  instance:
    lease-expiration-duration-in-seconds: 90
    lease-renewal-interval-in-seconds: 30
  • 1
  • 2
  • 3
  • 4
  • lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
  • lease-expiration-duration-in-seconds:服务失效时间,默认值90秒

也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。

但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。

eureka:
  instance:
    lease-expiration-duration-in-seconds: 10 # 10秒即过期
    lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
  • 1
  • 2
  • 3
  • 4

服务消费者
获取服务列表

当服务消费者启动时,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒会重新获取并更新数据。我们可以通过下面的参数来修改:

eureka:
  client:
    registry-fetch-interval-seconds: 5
  • 1
  • 2
  • 3

生产环境中,我们不需要修改这个值。

但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。

失效剔除和自我保护
服务下线

当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。

失效剔除

有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。

可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒,生产环境不要修改。

这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如:10秒
自我保护

我们关停一个服务,就会在Eureka面板看到一条警告:
在这里插入图片描述
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。

但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:(itcast-eureka)

eureka:
  server:
    enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
    eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
  • 1
  • 2
  • 3
  • 4

2.5 SpringCloud组件-负载均衡Ribbon

在实际环境中,对于同一个微服务,我们往往会开启很多个集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?

一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。

不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。

因为Eureka中已经集成了Ribbon,所以使用ribbon我们无需引入新的依赖。

修改consumer的引导类,在RestTemplate的配置方法上添加@LoadBalanced注解,然后修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用(为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。显然有人帮我们根据service名称,获取到了服务实例的ip和端口。经过源码解读,发现它就是LoadBalancerInterceptor):

//@SpringBootApplication
//@EnableDiscoveryClient
//@EnableCircuitBreaker //开启熔断组件
@SpringCloudApplication //以上三个注解的组合注解
@EnableFeignClients //开启feign组件 开启feign组件之后就不需要restTemplate了
public class TestConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestConsumerApplication.class,args);
    }

    /*@Bean
    @LoadBalanced //开启ribbon组件,即开启负载均衡组件,该组件不需要导入依赖,因为导入eureka的时候会将该组件导入
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }*/

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

import cn.itcast.service.client.UserClient;
import cn.itcast.service.pojo.User;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
 *      4.定义熔断方法:局部(要和被熔断的方法返回值和参数列表一致)  全局(返回值类型要被熔断的方法一致,参数列表必须为空)
 * 		5.@HystrixCommand(fallbackMethod="局部熔断方法名"):声明被熔断的方法
 * 		6.@DefaultProperties(defaultFallback="全局熔断方法名")
 */
@RestController
@RequestMapping("consumer/user")
//@DefaultProperties(defaultFallback = "globalHystrix")
public class UserController {
//    @Autowired
//    private RestTemplate restTemplate;
    @Autowired
    private UserClient userClient;

//    @Autowired
//    private DiscoveryClient discoveryClient; //从服务端拉取到的所有可用服务,包含了拉取的所有服务信息(// eureka客户端,可以获取到eureka中服务的信息)

 //   @GetMapping("{id}")
 //   public User queryById(@PathVariable("id") Long id){
        //return restTemplate.getForObject("http://localhost:8081/user/"+id,User.class);

//        List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
//        ServiceInstance instance = instances.get(0);
//        return restTemplate.getForObject("http://"+instance.getHost()+":"+instance.getPort()+"/user/"+id,User.class);

  //      return restTemplate.getForObject("http://service-provider/user/"+id,User.class);
  //  }

       @GetMapping("{id}")
       //@HystrixCommand(fallbackMethod = "localHystrix")
       //@HystrixCommand
       public String queryById(@PathVariable("id") Long id){
    //return restTemplate.getForObject("http://localhost:8081/user/"+id,User.class);

        //List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
        //ServiceInstance instance = instances.get(0);
        //return restTemplate.getForObject("http://"+instance.getHost()+":"+instance.getPort()+"/user/"+id,User.class);

          //return restTemplate.getForObject("http://service-provider/user/"+id,String.class);
           return this.userClient.queryById(id).toString();
       }

      public String localHystrix(Long id){
           return "局部熔断:请求失败,请稍后再试!";
      }

      public String globalHystrix(){
           return "全局熔断:请求失败,请稍后再试!";
      }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

注意Ribbon默认的负载均衡策略是简单的轮询,当然SpringBoot也帮我们提供了修改负载均衡规则的配置入口,在consumer的application.yml中添加如下配置即可:

server:
  port: 80
spring:
  application:
    name: service-consumer
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
service-provider:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName,值就是IRule的实现类。

2.6 总结

1.引入组件的启动器
2.覆盖默认配置
3.在引导类上添加注解,开发相关组件
Map<serviceId, Map<服务实例名,实例对象(instance)>>

1.架构的演变
传统架构–>水平拆分–>垂直拆分(最早的分布式)–>soa(dubbo)–>微服务(springCloud)

2.远程调用技术:rpc http
rpc协议:自定义数据格式,限定技术,传输速度快,效率高 tcp,dubbo
http协议:统一的数据格式,不限定技术 rest接口 tcp协议 springCloud

3.什么是springCloud
微服务架构的解决方案,是很多组件的集合
eureka:注册中心,服务的注册与发现
zull:网关协议,路由请求,过滤器(ribbon hystrix)
ribbon:负载均衡组件
hystrix:熔断组件
feign:远程调用组件(ribbon hystrix)

*4.eureka
注册中心:itcast-eureka(1.引入启动器, 2.配置spring.application.name=itcast-eureka 3.在引导类上@EnableEurekaServer)
客户端:itcast-service-provider itcast-service-consumer
(1.引入启动器 2.配置spring.application.name eureka.client.service-url.defaultZone=http://localhost:10086/eureka 3.@EnableDiscoveryClient(启用eure客户端))

3.springcloud-2

3.1 SpringCloud组件-Hystrix

Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路
在这里插入图片描述
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。
如果此时,某个服务出现异常:

在这里插入图片描述
例如微服务I发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
在这里插入图片描述
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。

这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。

Hystix解决雪崩问题的手段有两个:

  • 线程隔离
  • 服务熔断

解读:

Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。

用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?

服务降级:优先保证核心服务,而非核心服务不可用或弱可用。

用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。

服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。

触发Hystix服务降级的情况:

  • 线程池已满
  • 请求超时

那么熔断器怎么使用呢?
1.引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

2.开启熔断

//@SpringBootApplication
//@EnableDiscoveryClient
//@EnableCircuitBreaker //开启熔断组件
@SpringCloudApplication //以上三个注解的组合注解
@EnableFeignClients //开启feign组件 开启feign组件之后就不需要restTemplate了
public class TestConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestConsumerApplication.class,args);
    }

    /*@Bean
    @LoadBalanced //开启ribbon组件,即开启负载均衡组件,该组件不需要导入依赖,因为导入eureka的时候会将该组件导入
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }*/

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.编写降级逻辑
我们改造consumer,当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用HystixCommond来完成:

@Controller
@RequestMapping("consumer/user")
public class UserController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping
    @ResponseBody
    @HystrixCommand(fallbackMethod = "queryUserByIdFallBack")
    public String queryUserById(@RequestParam("id") Long id) {
        String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
        return user;
    }

    public String queryUserByIdFallBack(Long id){
        return "请求繁忙,请稍后再试!";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

要注意,因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。说明:

  • @HystrixCommand(fallbackMethod = “queryByIdFallBack”):用来声明一个降级逻辑的方法
    我们刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以我们可以把Fallback配置加在类上,实现默认fallback:
@Controller
@RequestMapping("consumer/user")
@DefaultProperties(defaultFallback = "fallBackMethod") // 指定一个类的全局熔断方法
public class UserController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping
    @ResponseBody
    @HystrixCommand // 标记该方法需要熔断
    public String queryUserById(@RequestParam("id") Long id) {
        String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
        return user;
    }

    /**
     * 熔断方法
     * 返回值要和被熔断的方法的返回值一致
     * 熔断方法不需要参数
     * @return
     */
    public String fallBackMethod(){
        return "请求繁忙,请稍后再试!";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • @DefaultProperties(defaultFallback = “defaultFallBack”):在类上指明统一的失败降级方法
  • @HystrixCommand:在方法上直接使用该注解,使用默认的降级方法。
  • defaultFallback:默认降级方法,不用任何参数,以匹配更多方法,但是返回值一定一致

Hystrix的默认超时时长为1,我们可以通过配置修改这个值:

我们可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds来设置Hystrix超时时间。该配置没有提示。

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

熔断状态机3个状态:

  • Closed:关闭状态,所有请求都正常访问。
  • Open:打开状态,所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
  • Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时

默认的熔断触发要求较高,休眠时间较短,为了测试方便,我们可以通过配置修改熔断策略:

circuitBreaker.requestVolumeThreshold=10
circuitBreaker.sleepWindowInMilliseconds=10000
circuitBreaker.errorThresholdPercentage=50
  • 1
  • 2
  • 3

解读:

  • requestVolumeThreshold:触发熔断的最小请求次数,默认20
  • errorThresholdPercentage:触发熔断的失败请求最小占比,默认50%
  • sleepWindowInMilliseconds:休眠时长,默认是5000毫秒

3.2 SpringCloud组件-Feign

feign的中文意思是伪装,为什么叫伪装?

Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。

如何使用feign组件?
1.导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

2.在启动类上,添加注解,开启Feign功能

@SpringCloudApplication
@EnableFeignClients // 开启feign客户端
public class ItcastServiceConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ItcastServiceConsumerApplication.class, args);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注意删除RestTemplate:因为feign已经自动集成了Ribbon负载均衡的RestTemplate。所以,此处不需要再注册RestTemplate。
3.在服务的调用方编写feign

package cn.itcast.service.client;

import cn.itcast.service.client.impl.UserClientImpl;
import cn.itcast.service.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "service-provider", fallback = UserClientImpl.class) //声明该结构是一个feign接口
public interface UserClient {
    @GetMapping("user/{id}")
    public User queryById(@PathVariable("id") Long id);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像
  • @FeignClient,声明这是一个Feign客户端,类似@Mapper注解。同时通过value属性指定服务名称
  • 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果

Feign中本身已经集成了Ribbon依赖和自动配置:因此我们不需要额外引入依赖,也不需要再注册RestTemplate对象。
Feign默认也有对Hystrix的集成:只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:(在consumer工程添加配置内容)

feign:
  hystrix:
    enabled: true # 开启Feign的熔断功能
  • 1
  • 2
  • 3

但是,Feign中的Fallback配置不像hystrix中那样简单了。首先,我们要定义一个类实现刚才编写的UserClient,作为fallback的处理类,然后在UserClient中,指定刚才编写的实现类。

package cn.itcast.service.client.impl;

import cn.itcast.service.client.UserClient;
import cn.itcast.service.pojo.User;
import org.springframework.stereotype.Component;

/**
 * 实现相应接口,实现的方法就是对应的熔断方法
 */
@Component
public class UserClientImpl implements UserClient {
    @Override
    public User queryById(Long id) {
        User user = new User();
        user.setUserName("请求超时,请稍后访问!");
        return user;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

接下来的部分用的不多,了解即可
请求压缩
Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:

feign:
  compression:
    request:
      enabled: true # 开启请求压缩
    response:
      enabled: true # 开启响应压缩
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置:

feign:
  compression:
    request:
      enabled: true # 开启请求压缩
      mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
      min-request-size: 2048 # 设置触发压缩的大小下限
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

日志级别
Feign支持4种级别:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

日志级别这部分我没测试过,以后再说吧!

3.3 SpringCloud组件-Zuul网关

通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的:
在这里插入图片描述
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。

在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,直接暴露我们的服务地址,这样的实现是否合理,或者是否有更好的实现方式呢?

先来说说这样架构需要做的一些事儿以及存在的不足:

  • 破坏了服务无状态特点。
    为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。
    从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外考虑对接口访问的控制处理。

  • 无法直接复用既有接口。
    当我们需要对一个既有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。

    面对类似上面的问题,我们要如何解决呢?答案是:服务网关!

为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器的 服务网关。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

加入Zuul之后的架构:
在这里插入图片描述
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。

玩zuul的过程:
1.导入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2.编写配置文件

server:
  port: 10010
spring:
  application:
    name: itcast-zuul
eureka:
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka
zuul: #不配置路由也可以,因为有默认配置
  routes:
    service-provider: /user/**     #路由名称,可以随便写
    service-consumer: /consumer/**
      # path: /service-provider/** # 也可以不写path和serviceId,直接把这段写在service-provider后面(前面表示服务ID,后面表示服务前缀等等)
      # url: http://localhost:8081
      # serviceId: service-provider
  prefix: /api #网关前缀,可加可不加,用于标识网关,但是加了之后一定要在请求路径上也加上
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.编写引导类

package cn.itcast.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableZuulProxy //开启Zuul组件
@EnableDiscoveryClient //开启Eureka客户端
public class ItcastZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ItcastZuulApplication.class,args);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。

Zuul指定了默认的路由规则:默认情况下,一切服务的映射路径就是服务名本身。例如服务名为:service-provider,则默认的映射路径就 是:/service-provider/**

过滤器
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:

public abstract ZuulFilter implements IZuulFilter{

    abstract public String filterType();

    abstract public int filterOrder();
    
    boolean shouldFilter();// 来自IZuulFilter

    Object run() throws ZuulException;// IZuulFilter
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true执行,返回false不执行。
  • run:过滤器的具体业务逻辑。
  • filterType:返回字符串,代表过滤器的类型。包含以下4种:
    • pre:请求在被路由之前执行
    • route:在路由请求时调用
    • post:在route和errror过滤器之后调用
    • error:处理请求时发生错误调用
  • filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。

过滤器执行生命周期

这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。
在这里插入图片描述
正常流程:

  • 请求到达首先会经过pre类型过滤器,而后到达route类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。

异常流程:

  • 整个过程中,pre或者route过滤器出现异常,都会直接进入error过滤器,在error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
  • 如果是error过滤器自己出现异常,最终也会进入POST过滤器,将最终结果返回给请求客户端。
  • 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和route不同的是,请求不会再到达POST过滤器了。

使用场景

  • 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
  • 异常处理:一般会在error类型和post类型过滤器中结合来处理。
  • 服务调用时长统计:pre和post结合使用。

自定义过滤器

package cn.itcast.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Component
public class TestFilter extends ZuulFilter {
    /**
     * 返回过滤器的类型,一共有四种类型: pre,post,route,error
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 返回过滤器的优先级,值越小优先级越高
     * @return
     */
    @Override
    public int filterOrder() {
        return 10;
    }

    /**
     * 判断run方法应不应该执行,返回true则执行,否则不执行
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 用该方法判断应该拦截还是放行
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        //得到目前的环境
        RequestContext context = RequestContext.getCurrentContext();

        //拿到HttpServletRequest对象
        HttpServletRequest request = context.getRequest();

        //拿到相应的参数
        String token = request.getParameter("token");
        if(token == null){
            context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED); //设置响应码为401,表示未授权
            context.setResponseBody("request error!!");
        }
        return null; //表示过滤器什么都不做
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.4 总结

1.引入启动器
2.覆盖默认配置
3.在引导类上启用组件

1.高可用 itcast-eureka
10086 10087

2.心跳过期 itcast-service-provider
eureka:
instance:
lease-renewal-interval-in-seconds: 5 # 心跳时间
lease-expiration-duration-in-seconds: 15 # 过期时间

3.拉取服务的间隔时间 itcast-service-consumer
eureka:
client:
registry-fetch-interval-seconds: 5

4.关闭自我保护,定期清除无效连接
eureka:
server:
eviction-interval-timer-in-ms: 5000
enable-self-preservation: false

ribbon: 负载均衡组件
1.eureka集成了
2.@LoadBalanced:开启负载均衡
3.this.restTemplate.getForObject(“http://service-provider/user/” + id, User.class);

hystrix:容错组件
降级:检查每次请求,是否请求超时,或者连接池已满
1.引入hystrix启动器
2.熔断时间,默认1s, 6s
3.在引导类上添加了一个注解:@EnableCircuitBreaker @SpringCloudApplication
4.定义熔断方法:局部(要和被熔断的方法返回值和参数列表一致) 全局(返回值类型要被熔断的方法一致,参数列表必须为空)
5.@HystrixCommand(fallbackMethod=“局部熔断方法名”):声明被熔断的方法
6.@DefaultProperties(defaultFallback=“全局熔断方法名”)
熔断:不再发送请求
1.close:闭合状态,所有请求正常方法
2.open:打开状态,所有请求都无法访问。如果在一定时间内容,失败的比例不小于50%或者次数不少于20次
3.half open:半开状态,打开状态默认5s休眠期,在休眠期所有请求无法正常访问。过了休眠期会进入半开状态,放部分请求通过

feign
1.引入openFeign启动器
2.feign.hystrix.enable=true,开启feign的熔断功能
3.在引导类上 @EnableFeignClients
4.创建一个接口,在接口添加@FeignClient(value=“服务id”, fallback=实现类.class)
5.在接口中定义一些方法,这些方法的书写方式跟之前controller类似
6.创建了一个熔断类,实现feign接口,实现对应的方法,这些实现方法就是熔断方法

zuul
1.引入zuul的启动器
2.配置:
zuul.routes.<路由名称>.path=/service-provider/**
zuul.routes.<路由名称>.url=http://localhost:8082
zuul.routes.<路由名称>.path=/service-provider/**
zuul.routes.<路由名称>.serviceId=service-provider
zuul.routes.服务名=/service-provider/**
不用配置,默认就是服务id开头路径
3.@EnableZuulProxy
过滤器:
创建一个类继承ZuulFilter基类
重写四个方法
filterType:pre route post error
filterOrder:返回值越小优先级越高
shouldFilter:是否执行run方法。true执行
run:具体的拦截逻辑

4.项目搭建及es6语法

4.1 电商行业简介

传统项目
各种企业里面用的管理系统(ERP、HR、OA、CRM、物流管理系统等)

  • 需求方:公司、企业内部
  • 盈利模式:项目本身卖钱
  • 技术侧重点:业务功能

互联网项目
门户网站、电商网站:baidu.com、qq.com、taobao.com、jd.com等

  • 需求方:广大用户群体
  • 盈利模式:虚拟币、增值服务、广告收益…
  • 技术侧重点:网站性能、业务功能
    电商是互联网项目

电商项目技术特点

  • 技术范围广
  • 技术新
  • 高并发(分布式、静态化技术、缓存技术、异步并发、池化、队列)
  • 高可用(集群、负载均衡、限流、降级、熔断)
  • 数据量大
  • 业务复杂
  • 数据安全

常见电商模式

  • B2C:商家对个人,如:亚马逊、当当等
  • C2C平台:个人对个人,如:闲鱼、拍拍网、ebay
  • B2B平台:商家对商家,如:阿里巴巴、八方资源网等
  • O2O:线上和线下结合,如:饿了么、电影票、团购等
  • P2P:在线金融,贷款,如:网贷之家、人人聚财等。
  • B2C平台:天猫、京东、一号店等

一些专业术语

  • SaaS:软件即服务
  • SOA:面向服务
  • RPC:远程过程调用
  • RMI:远程方法调用
  • PV:(page view),即页面浏览量;
    用户每1次对网站中的每个网页访问均被记录1次。用户对同一页面的多次访问,访问量累计
  • UV:(unique visitor),独立访客
    指访问某个站点或点击某条新闻的不同IP地址的人数。在同一天内,uv只记录第一次进入网站的具有独立IP的访问者,在同一天内再次访问该网站则不计数。
  • PV与带宽:
    • 计算带宽大小需要关注两个指标:峰值流量和页面的平均大小。
    • 计算公式是:网站带宽= ( PV * 平均页面大小(单位MB)* 8 )/统计时间(换算到秒)
    • 为什么要乘以8?
      • 网站大小为单位是字节(Byte),而计算带宽的单位是bit,1Byte=8bit
    • 这个计算的是平均带宽,高峰期还需要扩大一定倍数
  • PV、QPS、并发
    • QPS:每秒处理的请求数量。
      • 比如你的程序处理一个请求平均需要0.1S,那么1秒就可以处理10个请求。QPS自然就是10,多线程情况下,这个数字可能就会有所增加。
    • 由PV和QPS如何需要部署的服务器数量?
      • 根据二八原则,80%的请求集中在20%的时间来计算峰值压力:
      • (每日PV * 80%) / (3600s * 24 * 20%) * 每个页面的请求数 = 每个页面每秒的请求数量
      • 然后除以服务器的QPS值,即可计算得出需要部署的服务器数量
        项目开发流程
        在这里插入图片描述

4.2 乐忧商城项目介绍

  • 乐优商城是一个全品类的电商购物网站(B2C)。
  • 用户可以在线购买商品、加入购物车、下单
  • 管理员可以在后台管理商品的上下架、促销活动
  • 管理员可以监控商品销售状况
  • 客服可以在后台处理退款操作
  • 希望未来3到5年可以支持千万用户的使用

系统架构图
在这里插入图片描述
系统架构解读
整个乐优商城可以分为两部分:后台管理系统、前台门户系统。

  • 后台管理:
    • 后台系统主要包含以下功能:
      • 商品管理,包括商品分类、品牌、商品规格等信息的管理(暂时只完成了这一部分)
      • 销售管理,包括订单统计、订单退款处理、促销活动生成等(由于微信支付API无法申请,暂未实现)
      • 用户管理,包括用户控制、冻结、解锁等(和spring-security的功能有点像?)
      • 权限管理,整个网站的权限控制,采用JWT鉴权方案,对用户及API进行权限控制(这个功能类似于企业权限管理系统,之前用ssm框架实现过)
      • 统计,各种数据的统计分析展示(暂未实现)
    • 后台系统会采用前后端分离开发,而且整个后台管理系统会使用Vue.js框架搭建出单页应用(SPA)。
  • 前台门户
    • 前台门户面向的是客户,包含与客户交互的一切功能。例如:
      • 搜索商品
      • 加入购物车
      • 下单
      • 评价商品等等(暂未实现)
    • 前台系统我们会使用Thymeleaf模板引擎技术来完成页面开发。出于SEO优化的考虑,我们将不采用单页应用。

无论是前台还是后台系统,都共享相同的微服务集群,包括:

  • 商品微服务:商品及商品分类、品牌、库存等的服务
  • 搜索微服务:实现搜索功能
  • 订单微服务:实现订单相关
  • 购物车微服务:实现购物车相关功能
  • 用户中心:用户的登录注册等功能
  • Eureka注册中心
  • Zuul网关服务

4.3 项目搭建

前端技术:

  • 基础的HTML、CSS、JavaScript(基于ES6标准)
  • JQuery
  • Vue.js 2.0以及基于Vue的框架:Vuetify(UI框架)
  • 前端构建工具:WebPack
  • 前端安装包工具:NPM
  • Vue脚手架:Vue-cli
  • Vue路由:vue-router
  • ajax框架:axios
  • 基于Vue的富文本框架:quill-editor

后端技术:

  • 基础的SpringMVC、Spring 5.x和MyBatis3
  • Spring Boot 2.0.7版本
  • Spring Cloud Finchley.SR2
  • Redis-4.0
  • RabbitMQ-3.4
  • Elasticsearch-6.3
  • nginx-1.14.2
  • FastDFS - 5.0.8
  • MyCat(这个好像没用到吧)
  • Thymeleaf
  • mysql 8.0.24(课程中要求的是5.6版本,但是8.0版本经过实测也可以,注意修改配置文件中的数据库驱动名称

开发环境我选用的是jdk11,然而它要求使用jdk8,所以在整个过程之中我遇到不少问题,但是最终都解决了,除了一个至今还没解决的bug,但是已经不重要了。

整个项目的结构如下:
在这里插入图片描述
首先创建project(leyou)作为父工程,并在父工程的pom.xml文件之中提前配置规定好各个依赖的版本:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.parent</groupId>
    <artifactId>leyou</artifactId>
    <packaging>pom</packaging>
    <version>1.0.0-SNAPSHOT</version>
    <modules>
        <module>leyou-registry</module>
        <module>leyou-gateway</module>
        <module>leyou-item</module>
        <module>leyou-item/leyou-item-interface</module>
        <module>leyou-item/leyou-item-service</module>
        <module>leyou-common</module>
        <module>leyou-upload</module>
        <module>leyou-search</module>
        <module>leyou-goods-web</module>
        <module>leyou-user</module>
        <module>leyou-sms</module>
        <module>leyou-auth</module>
        <module>leyou-auth/leyou-auth-common</module>
        <module>leyou-auth/leyou-auth-service</module>
        <module>leyou-cart</module>
        <module>leyou-order</module>
    </modules>

    <name>leyou</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
        <spring-cloud.version>Finchley.SR2</spring-cloud.version>
        <mybatis.starter.version>1.3.2</mybatis.starter.version>
        <mapper.starter.version>2.0.2</mapper.starter.version>
        <druid.starter.version>1.1.9</druid.starter.version>
        <mysql.version>8.0.24</mysql.version>
        <pageHelper.starter.version>1.2.3</pageHelper.starter.version>
        <leyou.latest.version>1.0.0-SNAPSHOT</leyou.latest.version>
        <fastDFS.client.version>1.26.1-RELEASE</fastDFS.client.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- springCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- mybatis启动器 -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.starter.version}</version>
            </dependency>
            <!-- 通用Mapper启动器 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper.starter.version}</version>
            </dependency>
            <!-- 分页助手启动器 -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pageHelper.starter.version}</version>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!--FastDFS客户端-->
            <dependency>
                <groupId>com.github.tobato</groupId>
                <artifactId>fastdfs-client</artifactId>
                <version>${fastDFS.client.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

然后加入微服务leyou-registry作为eureka注册中心,配置文件如下:

server:
  port: 10086
spring:
  application:
    name: leyou-registry
eureka:
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka
    register-with-eureka: false
    fetch-registry: false
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 10000
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

再创建微服务leyou-gateway作为zuul网关,配置文件如下:

server:
  port: 10010
spring:
  application:
    name: leyou-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka
    registry-fetch-interval-seconds: 5
zuul:
  prefix: /api
  routes:
    item-service: /item/** #路由到商品的微服务
    search-service: /search/** #路由到搜索的微服务
    goods-web: /goods/** #路由到商品详情的微服务
    user-service: /user/** #路由到用户的微服务
    auth-service: /auth/** #路由到授权的微服务
    cart-service: /cart/** #路由到购物车的微服务
    order-service: /order/** #路由到订单的微服务
  add-host-header: true #携带请求本身的head头信息
  sensitive-headers: # 配置禁止使用的头信息,这里设置为null,否则set-cookie无效

leyou:
  jwt:
    pubKeyPath: C:\\tmp\\rsa\\rsa.pub # 公钥地址
    cookieName: LY_TOKEN
  filter:
    allowPaths:
      - /api/auth
      - /api/search
      - /api/user/register
      - /api/user/check
      - /api/user/code
      - /api/item
      - /api/goods/item

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

另外,由于我使用的是jdk11而不是Jdk8导致我需要增加一些额外的依赖:

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

到这里,我们已经把基础架构搭建完毕。
接下来创建第一个真正意义上的微服务:商品微服务
既然是一个全品类的电商购物平台,那么核心自然就是商品。因此我们要搭建的第一个服务,就是商品微服务。其中会包含对于商品相关的一系列内容的管理,包括:

  • 商品分类管理
  • 品牌管理
  • 商品规格参数管理
  • 商品管理
  • 库存管理

因为与商品的品类相关,我们的工程命名为leyou-item.

需要注意的是,我们的leyou-item是一个微服务,那么将来肯定会有其它系统需要来调用服务中提供的接口,获取的接口数据,也需要对应的实体类来封装,因此肯定也会使用到接口中关联的实体类。

因此这里我们需要使用聚合工程,将要提供的接口及相关实体类放到独立子工程中,以后别人引用的时候,只需要知道坐标即可。

我们会在leyou-item中创建两个子工程:

  • leyou-item-interface:主要是对外暴露的接口及相关实体类
  • leyou-item-service:所有业务逻辑及内部使用接口

调用关系如图所示:在这里插入图片描述

然后根据需要在leyou-item-service导入相应的依赖:

  • Eureka客户端
  • web启动器
  • mybatis启动器
  • 通用mapper启动器
  • 分页助手启动器
  • 连接池,我们用默认的Hykira
  • mysql驱动
  • 我们自己也需要ly-item-interface中的实体类
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou-item</artifactId>
        <groupId>com.leyou.item</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>leyou-item-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.item</groupId>
            <artifactId>leyou-item-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.common</groupId>
            <artifactId>leyou-common</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>

</project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

最后,有些工具或通用的约定内容,我们希望各个服务共享,因此需要创建一个工具模块:leyou-common

4.4 ES6(ECMAScript第6版标准)

ECMAScript是浏览器脚本语言的规范,而各种我们熟知的js语言,如JavaScript则是该规范的具体实现。ECMAScript几乎每年都要更新一次。这里使用的是第6版的标准。
新版的js特性这部分就跳过吧,有点多,没必要列出来。

5.vue入门

这里只简单复习一下vue的知识,这个我没有重点关注,因为这是前端的内容,当前阶段懂得基础的操作能看懂能用就行。
关于前端的内容,我从刚开始学习的jsp(同步)再到基于Jquery的ajax请求(异步)最后到vue。不过jsp现在是不是已经被淘汰了。
2009年,Ryan Dahl在谷歌的Chrome V8引擎基础上,打造了基于事件循环的异步IO框架:Node.js。

  • 基于事件循环的异步IO
  • 单线程运行,避免多线程的变量同步问题
  • JS可以编写后台代码,前后台统一编程语言

node.js的伟大之处不在于让JS迈向了后端开发,而是构建了一个庞大的生态系统。

2010年,NPM作为node.js的包管理系统首次发布,开发人员可以遵循Common.js规范来编写Node.js模块,然后发布到NPM上供其他开发人员使用。目前已经是世界最大的包模块管理系统。

随后,在node的基础上,涌现出了一大批的前端框架:
在这里插入图片描述
MVVM模式

  • M:即Model,模型,包括数据和一些基本操作
  • V:即View,视图,页面渲染结果
  • VM:即View-Model,模型与视图间的双向操作(无需开发人员干涉)

在MVVM之前,开发人员从后端获取需要的数据模型,然后要通过DOM操作Model渲染到View中。而后当用户操作视图,我们还需要通过DOM获取View中的数据,然后同步到Model中。

而MVVM中的VM要做的事情就是把DOM操作完全封装起来,开发人员不用再关心Model和View之间是如何互相影响的:

  • 只要我们Model发生了改变,View上自然就会表现出来。
  • 当用户修改了View,Model中的数据也会跟着改变。

把开发人员从繁琐的DOM操作中解放出来,把关注点放在如何操作Model上。

前端框架三巨头:Vue.js、React.js、AngularJS,vue.js以其轻量易用著称,vue.js和React.js发展速度最快,AngularJS目前用得最多?

5.1 NPM

1.NPM是Node提供的模块管理工具,可以非常方便的下载安装很多前端框架,包括Jquery、AngularJS、VueJs都有
2.Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合
3.可以使用npm来安装vue,node_modules是通过npm安装的所有模块的默认位置

5.2 Vue

vue的一个重点就是其生命周期和钩子函数
每个 Vue 实例在被创建时都要经过一系列的初始化过程 :创建实例,装载模板,渲染模板等等。Vue为生命周期中的每个状态都设置了钩子函数(监听函数)。每当Vue实例处于不同的生命周期时,对应的函数就会被触发调用。
在这里插入图片描述
钩子函数
beforeCreated:我们在用Vue时都要进行实例化,因此,该函数就是在Vue实例化时调用,也可以将他理解为初始化函数比较方便一点,在Vue1.0时,这个函数的名字就是init。

created:在创建实例之后进行调用。

beforeMount:页面加载完成,没有渲染。如:此时页面还是{{name}}

mounted:我们可以将他理解为原生js中的window.οnlοad=function({.,.}),相当于jquery中的$(document).ready(function(){….}),他的功能就是:在dom文档渲染完毕之后将要执行的函数,该函数在Vue1.0版本中名字为compiled。 此时页面中的{{name}}已被渲染成name属性的值

beforeDestroy:该函数将在销毁实例前进行调用 。

destroyed:该函数将在销毁实例时进行调用。

beforeUpdate:组件更新之前调用。

updated:组件更新之后调用。

在这个项目中我几乎只用到了created,别的没怎么测试过。

5.3 Vue指令

指令 (Directives) 是带有 v- 前缀的特殊特性。指令特性的预期值是:单个 JavaScript 表达式。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
1.{{表达式}}

  • 该表达式支持JS语法,可以调用js内置函数(必须有返回值)
  • 表达式必须有返回结果。例如 1 + 1,没有结果的表达式不允许使用,如:var a = 1 + 1;
  • 可以直接获取Vue实例中定义的数据或函数

2.使用{{}}在网速较慢的情况下可能会出现插值闪烁的问题,可以使用使用v-text和v-html指令来替代

  • v-text:将数据输出到元素内部,如果输出的数据有HTML代码,会作为普通文本输出
  • v-html:将数据输出到元素内部,如果输出的数据有HTML代码,会被渲染

3.v-text和v-html可以看做是单向绑定,数据影响了视图渲染,但是反过来就不行。而v-model是双向绑定,视图(View)和模型(Model)之间会互相影响。既然是双向绑定,一定是在视图中可以修改数据,这样就限定了视图的元素类型。目前v-model的可使用元素有:

  • input
  • select
  • textarea
  • checkbox
  • radio
  • components(Vue中的自定义组件)

基本上除了最后一项,其它都是表单的输入项。

  • 多个CheckBox对应一个model时,model的类型是一个数组,单个checkbox值默认是boolean类型
  • radio对应的值是input的value值
  • text 和textarea 默认对应的model是字符串
  • select单选对应字符串,多选对应也是数组

4.v-on指令用于给页面元素绑定事件。事件绑定可以简写,例如v-on:click='add’可以简写为@click=‘add’
5.事件修饰符:
在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。修饰符是由点开头的指令后缀来表示的。

  • .stop :阻止事件冒泡到父元素
  • .prevent:阻止默认事件发生*
  • .capture:使用事件捕获模式
  • .self:只有元素自身触发事件才执行。(冒泡或捕获的都不执行)
  • .once:只执行一次

6.按键修饰符:
在监听键盘事件时,我们经常需要检查常见的键值。Vue 允许为 v-on 在监听键盘事件时添加按键修饰符:

<!-- 只有在 `keyCode` 是 13 时调用 `vm.submit()` -->
<input v-on:keyup.13="submit">
  • 1
  • 2

记住所有的 keyCode 比较困难,所以 Vue 为最常用的按键提供了别名:

<!-- 同上 -->
<input v-on:keyup.enter="submit">

<!-- 缩写语法 -->
<input @keyup.enter="submit">
  • 1
  • 2
  • 3
  • 4
  • 5

全部的按键别名:

  • .enter*
  • .tab
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

7.组合按钮
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。

  • .ctrl
  • .alt
  • .shift

例如:

<!-- Alt + C -->
<input @keyup.alt.67="clear">

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
  • 1
  • 2
  • 3
  • 4
  • 5

8.v-for

v-for="item in items"
v-for="(item,index) in items"
v-for="value in object"
v-for="(value,key) in object"
v-for="(value,key,index) in object"
  • 1
  • 2
  • 3
  • 4
  • 5
  • items:要迭代的数组
  • item:迭代得到的数组元素别名
  • index:迭代到的当前元素索引,从0开始。
  • 1个参数时,得到的是对象的属性值
  • 2个参数时,第一个是属性值,第二个是属性名
  • 3个参数时,第三个是索引,从0开始

9.增加key属性可以提高v-for的渲染效率

<li v-for="(item,index) in items" :key=index></li>
  • 1

10.v-if和v-show
v-if,顾名思义,条件判断。当得到结果为true时,所在的元素才会被渲染。而带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS 属性 display。

11.当v-if和v-for出现在一起时,v-for优先级更高。也就是说,会先遍历,再判断条件。

<li v-for="(user, index) in users" v-if="user.gender == '女'">
            {{index + 1}}. {{user.name}} - {{user.gender}} - {{user.age}}
        </li>
  • 1
  • 2
  • 3

12.v-else
v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。
v-else-if,顾名思义,充当 v-if 的“else-if 块”,可以连续使用:

<div id="app">
    <button v-on:click="random=Math.random()">点我呀</button><span>{{random}}</span>
    <h1 v-if="random >= 0.75">
        看到我啦?!if
    </h1>
    <h1 v-else-if="random > 0.5">
        看到我啦?!if 0.5
    </h1>
    <h1 v-else-if="random > 0.25">
        看到我啦?!if 0.25
    </h1>
    <h1 v-else>
        看到我啦?!else
    </h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: "#app",
        data: {
            random: 1
        }
    })
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

13.v-bind
html属性不能使用双大括号形式绑定,只能使用v-bind指令。

在将 v-bind 用于 class 和 style 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

<div id="app">
    <!--可以是数据模型,可以是具有返回值的js代码块或者函数-->
    <div v-bind:title="title" style="border: 1px solid red; width: 50px; height: 50px;"></div>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: "#app",
        data: {
            title: "title",
        }
    })
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
<div id="app">
    <div v-bind:class="activeClass"></div>
    <div v-bind:class="errorClass"></div>
    <div v-bind:class="[activeClass, errorClass]"></div>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: "#app",
        data: {
            activeClass: 'active',
            errorClass: ['text-danger', 'text-error']
        }
    })
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我们可以传给 v-bind:class 一个对象,以动态地切换 class:

<div v-bind:class="{ active: isActive }"></div>
  • 1

上面的语法表示 active 这个 class 存在与否将取决于数据属性 isActive 的 truthiness(所有的值都是真实的,除了false,0,“”,null,undefined和NaN)。

你可以在对象中传入更多属性来动态切换多个 class。此外,v-bind:class 指令也可以与普通的 class 属性共存。如下模板:

<div class="static"
     v-bind:class="{ active: isActive, text-danger: hasError }">
</div>
  • 1
  • 2
  • 3

和如下data:

data: {
  isActive: true,
  hasError: false
}
  • 1
  • 2
  • 3
  • 4

结果渲染为:

<div class="static active"></div>
  • 1

active样式和text-danger样式的存在与否,取决于isActive和hasError的值。本例中isActive为true,hasError为false,所以active样式存在,text-danger不存在。

数组语法可以将多个样式对象应用到同一个元素上:

<div v-bind:style="[baseStyles, overridingStyles]"></div>
  • 1

数据:

data: {
    baseStyles: {'background-color': 'red'},
    overridingStyles: {border: '1px solid black'}
}
  • 1
  • 2
  • 3
  • 4

渲染后的结果:

<div style="background-color: red; border: 1px solid black;"></div>
  • 1

v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用单引号括起来) 来命名:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
  • 1

数据:

data: {
  activeColor: 'red',
  fontSize: 30
}
  • 1
  • 2
  • 3
  • 4

效果:

<div style="color: red; font-size: 30px;"></div>
  • 1

v-bind:class可以简写为:class

14.计算属性
在插值表达式中使用js表达式是非常方便的,而且也经常被用到。但是如果表达式的内容很长,就会显得不够优雅,而且后期维护起来也不方便,于是Vue中提供了计算属性,来替代复杂的表达式:

var vm = new Vue({
    el:"#app",
    data:{
        birthday:1429032123201 // 毫秒值
    },
    computed:{
        birth(){// 计算属性本质是一个方法,但是必须返回结果
            const d = new Date(this.birthday);
            return d.getFullYear() + "-" + d.getMonth() + "-" + d.getDay();
        }
    }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

计算属性本质就是方法,但是一定要返回数据。然后页面渲染时,可以把这个方法当成一个变量来使用。我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。
15.watch监听机制
watch可以让我们监控一个值的变化。从而做出相应的反应。

<div id="app">
    <input type="text" v-model="message">
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el:"#app",
        data:{
            message:""
        },
        watch:{
            message(newVal, oldVal){
                console.log(newVal, oldVal);
            }
        }
    })
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

接下来是之前练习写的html文件,贴在这里方便复习:

<!DOCTYPE html>
<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml" xmlns:v-bind="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>vue实例</title>
</head>
<style>
    .active{
        color: red;
        background-color: gold;
    }
</style>
<body>
<!--    vue对象的模板-->
    <div id="app">
        <input type="text" v-model="search">
        <br><br>
        {{birth}}
        <br><br>
        <input type="text" v-bind:class="{active: store > 0}" v-model="store"><br>
        <br><br>
        <input type="button" value="点我生成随机数" @click="random=Math.random()">
        <span v-if="random>0.75">{{random}}我大于0.75</span>
        <span v-else-if="random>0.5">{{random}}我大于0.5</span>
        <span v-else-if="random>0.25">{{random}}我大于0.25</span>
        <span v-else>{{random}}我大于0.1</span>

        <br><br>
        <ul>
            <li v-if="user.gender=='女'" v-for="(user,index) in users">{{index}} name:{{user.name}} gender:{{user.gender}} age:{{user.age}}</li>
        </ul>
        <br><br>
        <input type="button" value="点我呀" @click="show=!show"><br>
        <span v-if="show">如果你能看到我,那么说明我是{{show}},if</span><br>
        <span v-show="show">如果你能看到我,那么说明我是{{show}},show</span><br>
        <br>
        <ul>
            <li v-for="(user,index) in users">{{index}} name:{{user.name}} gender:{{user.gender}} age:{{user.age}}</li>
        </ul>
        <br>
        <ul>
            <li v-for="(val,key,index) in user" :key="index">{{index}} {{key}}:{{val}}</li>
        </ul>
<!--        双向绑定,v-model:数据模型-->
        <input type="text" v-model="num" @keyup.enter="submit()">
        <input type="text" v-model="num" @keyup.alt.67="submit()">
<!--        v-on:事件名=js表达式-->
        <input type="button" v-on:click="f" value="点我呀">
        <input type="button" @click="f" value="点我呀">
        <input type="button" @contextmenu.prevent="f" value="点我呀">
<!--        花括号:js表达式-->
        <h1>大家好,我是{{name}},有{{num}}个妹子迷恋我</h1>
        <h1>大家好,我是<span v-text="name">张学友</span>,有{{num}}个妹子迷恋我</h1>
        <h1>大家好,我是<span v-html="name">张学友</span>,有{{num}}个妹子迷恋我</h1>
        <br>

        <input type="checkbox" value="C++" v-model="language">C++
        <input type="checkbox" value="C" v-model="language">C
        <input type="checkbox" value="java" v-model="language">java
        {{language.join(",")}}
    </div>
</body>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
    //初始化一个vue实例
    const app = new Vue({
        el: "#app", //element,选择器
        data:{ // 定义数据模型
            name: "刘德华",
            num: 20,
            language: [],
            users:[
                {name:'柳岩', gender:'女', age: 21},
                {name:'峰哥', gender:'男', age: 18},
                {name:'范冰冰', gender:'女', age: 24},
                {name:'刘亦菲', gender:'女', age: 18},
                {name:'古力娜扎', gender:'女', age: 25}
            ],
            user: {name:'柳岩', gender:'女', age: 21},
            show: true,
            random: 0.1,
            store: 0,
            birthday: 34798549857334, //毫秒值
            search: ""
        },
        methods:{ //定义方法
          f(){
              this.num++;
          },
          submit(){
            console.log("您已经提交了!!");
          }
        },
        created(){
            //发送ajax请求
            this.num = -1;
        },
        computed: { //计算属性,里面也可以定义方法,但是这些方法必须有返回值,计算属性可以像数据模型一样使用,计算属性一个好处是如果其依赖(这里是birthday)没有发生改变,那么下次执行时会立即返回结果,而不会重新计算,但是函数每次都会重新计算
            birth(){
                const date = new Date(this.birthday);
                return date.getFullYear()+"年"+date.getMonth()+"月"+date.getDay()+"日";
            }
        },
        watch:{ //监听
            search(newVal,oldVal){
                //发送ajax请求
                console.log(newVal,oldVal);
            }
        }
    });
</script>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112

5.4 Vue的组件化

在大型应用开发的时候,页面可以划分成很多部分。往往不同的页面,也会有相同的部分。例如可能会有相同的头部导航。但是如果每个页面都独自开发,这无疑增加了我们开发的成本。所以我们会把页面的不同部分拆分成独立的组件,然后在不同页面就可以共享这些组件,避免重复开发。在vue里,所有的vue实例都是组件

全局组件
我们通过Vue的component方法来定义一个全局组件。

<div id="app">
    <!--使用定义好的全局组件-->
    <counter></counter>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
    // 定义全局组件,两个参数:1,组件名称。2,组件参数
    Vue.component("counter",{
        template:'<button v-on:click="count++">你点了我 {{ count }} 次,我记住了.</button>',
        data(){
            return {
                count:0
            }
        }
    })
    var app = new Vue({
        el:"#app"
    })
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 组件其实也是一个Vue实例,因此它在定义时也会接收:data、methods、生命周期函数等
  • 不同的是组件不会与页面的元素绑定,否则就无法复用了,因此没有el属性。
  • 但是组件渲染需要html模板,所以增加了template属性,值就是HTML模板
  • 全局组件定义完毕,任何vue实例都可以直接在HTML中通过组件名称来使用组件了。
  • data必须是一个函数,不再是一个对象。

定义好的组件,可以任意复用多次:

<div id="app">
    <!--使用定义好的全局组件-->
    <counter></counter>
    <counter></counter>
    <counter></counter>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

每个组件内部的值能够互不干扰是因为data 选项是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝

局部组件
一旦全局注册,就意味着即便以后你不再使用这个组件,它依然会随着Vue的加载而加载。因此,对于一些并不频繁使用的组件,我们会采用局部注册。我们先在外部定义一个对象,结构与创建组件时传递的第二个参数一致:

const counter = {
    template:'<button v-on:click="count++">你点了我 {{ count }} 次,我记住了.</button>',
    data(){
        return {
            count:0
        }
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然后在Vue中使用它:

var app = new Vue({
    el:"#app",
    components:{
        counter:counter // 将定义的对象注册为组件
    }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • components就是当前vue对象子组件集合。
    • 其key就是子组件名称
    • 其值就是组件对象名
  • 效果与刚才的全局注册是类似的,不同的是,这个counter组件只能在当前的Vue实例中使用

之前练习写的关于组件的html文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>全局组件</title>
</head>
<body>
    <div id="app">
      haha
      <counter></counter>
      <counter></counter>
      <counter></counter>
      <counter></counter><br>
      <hello1></hello1>
    </div>
</body>
    <script src="node_modules/vue/dist/vue.js"></script>
    <script>

        Vue.component("counter",{
          template: "<button @click='num++'>点我加1呀,{{num}}</button>",
          data(){
            return {
              num: 0
            }
          }
        })

        const hello = {
          template: "<button @click='num=num+2'>点我加2呀,{{num}}</button>",
          data(){
            return {
              num: 0
            }
          }
        }
        const app = new Vue({
          el: "#app",
          components:{
            hello1: hello
          }
        });
    </script>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

组件通信
各个组件之间以嵌套的关系组合在一起,那么这个时候不可避免的会有组件间通信的需求。
props(父向子传递)

  1. 父组件使用子组件时,自定义属性(属性名任意,属性值为要传递的数据)
  2. 子组件通过props接收父组件数据,通过自定义属性的属性名

父组件使用子组件,并自定义了title属性:

<div id="app">
    <h1>打个招呼:</h1>
    <!--使用子组件,同时传递title属性-->
    <introduce title="大家好,我是锋哥"/>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
    Vue.component("introduce",{
        // 直接使用props接收到的属性来渲染页面
        template:'<h1>{{title}}</h1>',
        props:['title'] // 通过props来接收一个父组件传递的属性
    })
    var app = new Vue({
        el:"#app"
    })
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

我们定义一个子组件,并接收复杂数据:

const myList = {
        template: '\
        <ul>\
        	<li v-for="item in items" :key="item.id">{{item.id}} : {{item.name}}</li>\
        </ul>\
        ',
        props: {
            items: {
                type: Array,
                default: [],
                required: true
            }
        }
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 这个子组件可以对 items 进行迭代,并输出到页面。
  • props:定义需要从父组件中接收的属性
    • items:是要接收的属性名称
      • type:限定父组件传递来的必须是数组
      • default:默认值
      • required:是否必须

当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告
我们在父组件中使用它:

<div id="app">
    <h2>传智播客已开设如下课程:</h2>
    <!-- 使用子组件的同时,传递属性,这里使用了v-bind,指向了父组件自己的属性lessons -->
    <my-list :items="lessons"/>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
var app = new Vue({
    el:"#app",
    components:{
        myList // 当key和value一样时,可以只写一个
    },
    data:{
        lessons:[
            {id:1, name: 'java'},
            {id:2, name: 'php'},
            {id:3, name: 'ios'},
        ]
    }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

动态静态传递
给 prop 传入一个静态的值:

<introduce title="大家好,我是锋哥"/>
  • 1

给 prop 传入一个动态的值: (通过v-bind从数据模型中,获取title的值)

<introduce :title="title"/>
  • 1

静态传递时,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个 props。

<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个JavaScript表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

子向父的通信:$emit
来看这样的一个案例:

<div id="app">
    <h2>num: {{num}}</h2>
    <!--使用子组件的时候,传递num到子组件中-->
    <counter :num="num"></counter>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
    Vue.component("counter", {// 子组件,定义了两个按钮,点击数字num会加或减
        template:'\
            <div>\
                <button @click="num++">加</button>  \
                <button @click="num--">减</button>  \
            </div>',
        props:['num']// count是从父组件获取的。
    })
    var app = new Vue({
        el:"#app",
        data:{
            num:0
        }
    })
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 子组件接收父组件的num属性
  • 子组件定义点击按钮,点击后对num进行加或减操作

子组件接收到父组件属性后,默认是不允许修改的。怎么办?
既然只有父组件能修改,那么加和减的操作一定是放在父组件:

var app = new Vue({
    el:"#app",
    data:{
        num:0
    },
    methods:{ // 父组件中定义操作num的方法
        increment(){
            this.num++;
        },
        decrement(){
            this.num--;
        }
    }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

但是,点击按钮是在子组件中,那就是说需要子组件来调用父组件的函数,怎么做?

我们可以通过v-on指令将父组件的函数绑定到子组件上:

<div id="app">
    <h2>num: {{num}}</h2>
    <counter :count="num" @inc="increment" @dec="decrement"></counter>
</div>
  • 1
  • 2
  • 3
  • 4

在子组件中定义函数,函数的具体实现调用父组件的实现,并在子组件中调用这些函数。当子组件中按钮被点击时,调用绑定的函数:

Vue.component("counter", {
            template:'\
                <div>\
                    <button @click="plus">加</button>  \
                    <button @click="reduce">减</button>  \
                </div>',
            props:['count'],
            methods:{
                plus(){
                    this.$emit("inc");
                },
                reduce(){
                    this.$emit("dec");
                }
            }
        })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

vue提供了一个内置的this.$emit()函数,用来调用父组件绑定的函数
最后是我自己练习时写的html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>父子组件的消息传递</title>
</head>
<body>
<div id="app">
  <counter :num1="num" @incr="incr()"></counter>
</div>
</body>
<script src="node_modules/vue/dist/vue.js"></script>
<script>

  Vue.component("counter",{
    template: "<button @click='incr1()'>点我加1呀,{{num1}}</button>",
    data(){
      return {
        num: 0
      }
    },
    props: {
      num1: {
        type: Number,
        default: 100
      }
    },
    methods:{
      incr1(){
        this.num1++;
        //this.$emit("incr");
      }
    }
  })

  const app = new Vue({
    el: "#app",
    data: {
      num: 1
    },
    methods:{
      incr(){
        this.num++;
      }
    }
  });
</script>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

5.5 路由vue-router

这部分不想再解释了,直接贴上之前练习写的文件吧
index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录注册</title>
</head>
<body>
    <div id="app">
        <span><router-link to="/login">登录</router-link></span>
        <span><router-link to="/register">注册</router-link></span>
      <hr>
      <router-view></router-view>
    </div>
</body>
<script src="../node_modules/vue/dist/vue.js"></script>
<script src="../node_modules/vue-router/dist/vue-router.js"></script>
<script src="js/login.js"></script>
<script src="js/register.js"></script>
<script>
    const router = new VueRouter({ //这个名字必须是router
        routes: [
            {
                path: "/login",
                component: loginForm
            },
            {
                path: "/register",
                component: registerForm
            }
        ]
    });
  const app = new Vue({
    el: "#app",
    components:{
      loginForm,
      registerForm
    },
      router
  });
</script>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

login.js

const loginForm = {
    template : `
        <div>
         登录页<br>
         用户名:<input type="text" ><br>
         密&emsp;码:<input type="password" ><br>
         <input type="button" value="登录">
</div>
    `
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

register.js

const registerForm = { //组件内的template只能有一个根标签
    template : `
        <div>
         注册页<br>
         用&ensp;户&ensp;名:<input type="text" ><br>
         密&emsp;&emsp;码:<input type="password" ><br>
         确认密码:<input type="password" ><br>
         <input type="button" value="注册">
</div>
    `
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/145539
推荐阅读
相关标签
  

闽ICP备14008679号