首页 > 技术文章 > SpringBoot学习笔记

zhou-zr 2021-04-22 22:23 原文

SpringBoot

什么是Spring:Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。

如何简化Java的开发的

  • 基于POJO的轻量级和最小侵入性编程
  • 通过IOC,依赖注入和面向接口实现松耦合
  • 基于切面和惯例进行声明式编程
  • 通过切面和模板减少样式代码

SppringBoot主要优点:

  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化Web项目
  • 没有冗余代码和生成XML配置的要求
  • 让所有的Spring开发者更快的入门

什么是微服务:微服务是一种架构风格,它要求我们在开发一个应用的时候,这个应用必须构建成一系列小服务的组合,可以通过http的方式进行互通,要说微服务架构,可以先看看我们以前的单体应用架构。

单体应用架构:我们将一个应用中的所有应用服务都封装在一个应用中,数据访问,web访问等放在一个war包中。

  • 这样做的好处是,易于开发和测试,也十分方便部署,当要拓展时,只要将war复制多份,然后放到多个服务器上,在做个负载均衡就可以了。
  • 单体架构应用的缺点是,哪怕要修改一个非常小的地方,都要停掉整个服务,重新打包,再部署这个应用的war包,特别是对于一个大型应用,我们不可能把所有内容放在一个应用里面,如何维护,如何分工合作都是问题。

微服务架构就是打破 all in one 的架构方式,把每个功能元素独立出来,把独立出来的功能元素动态组合,节省调用资源,每个功能元素的服务器都是一个可替换的,可独立升级的代码。

第一个SpringBoot

  1. 可以从官网新建项目后下载,解压后使用 IDEA 打开
  2. IDEA中新建 spring initiali 项目,选中 spring web 直接创建

导入 jar 包即可

自动装配

pom.xml

  • Spring Boot Dependencies 核心依赖在父工程中
  • 我们在写或者引入一些 SpringBoot 依赖的时候,不需要指定版本,就是因为父工程中已经写了。

启动器

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

比如:spring-boot-starter-web,它就会自动帮我们导入web项目所有的依赖。

springboot会将所有的的功能场景,都变成一个个的启动器。

需要什么功能,找到对应的启动器就可以了(官网找) 。

主程序

//标注这是一个springboot的应用
@SpringBootApplication
public class Springboot01HelloworldApplication {

    public static void main(String[] args) {
        //启动springboot应用
        SpringApplication.run(Springboot01HelloworldApplication.class, args);
    }

}

注解

@SpringBootConfiguration   //springboot的配置
	@Configuration   //spring配置类
			@Component    //说明这也是一个spring的组件

@EnableAutoConfiguration   //自动配置
	@AutoConfigurationPackage   //自动配置包
		@Import({Registrar.class})   //导入注册器
	@Import({AutoConfigurationImportSelector.class})   //导入选择器
			//获取所有的配置
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

获取候选的配置

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
    return configurations;
}

META-INF/spring.factories:自动配置的核心文件

SpringBoot所有装配是在启动时扫描并加载,所有的自动配置类都在spring.factories,但只有导入了对应的start,有了启动器,对应的配置才会生效,然后自动装配。

  1. SpringBoot在启动的时候,从类路径下META-INF/spring.factories获取指定的值。

  2. 将这些自动配置的类导入容器,自动配置类就会自动生效,帮我们自动配置。

  3. 以前我们需要自动配置的东西,现在SpringBoot帮我们做了。

  4. 整合JavaEE,解决方案和自动配置的东西都在springframework.boot.autoconfigure:2.4.0包下。

  5. 它会把所有需要导入的组件以类名的方式返回,这些组件就会被添加到容器。

  6. 容器中也会存在非常多的 ***AutoConfigure 的文件,就是这个类给容器导入了这个场景所需要的所有组件。

  7. 有了自动装配,免去了我们手动编写配置注入功能组件的工作。

SpringApplication这个类主要是:

  • 推断应用的类型是普通的项目还是web项目。
  • 查找并加载所有可用初始化器,设置的initializers属性中。
  • 找出所有的应用程序监听器,设置到listeners中。
  • 推断并设置main方法的定义类,找到运行的主类。

自动装配的原理:

  1. SpringBoot启动会加载大量的自动配置类。

  2. 我们看我们需要的配置类有没有在SpringBoot写好的自动配置类中。

  3. 再看这个配置类中到底装配了哪些组件(如果我们需要的组件在里面,就不用手动装配了)。

  4. 给容器中自动配置类添加组件时,会从properties类中获取某些属性,我们只需要在配置文件中指定这些属性即可。

    ***AutoConfiguration:自动配置类,给容器中添加组件

    ***Properties:封装配置文件的相关属性

yaml中配置debug:true,启动可以查看哪些配置类生效了。

在springboot中,有非常多的***.Configuretion会帮助我们进行拓展配置,只要看见了这个,需要注意。

@EnableWebMvc,这配置是导入了一个类,DelegatingWebMvcConfiguration,从容器中获取所有的WebMvcConfigurer。

yaml讲解

SpringBoot使用一个全局的配置文件,配置文件名称是固定的

  • application.properties
    • 语法结构:key=value
  • application.yaml
    • 语法结构:key:空格 value

配置文件可以帮我们修改SpringBoot自动配置的默认值。

YAML是"YAML Ain't a Markup Language"(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为重点,而用反向缩略语重命名。

标记语言

以前的配置文件,大多数使用xml来配置:比如一个简单的端口配置,对比一下yaml和xml

yaml配置:

server:
  port: 8088

xml配置

<server>
    <port>8088</port>
</server> 

使用注解@ConfigurationProperties将配置文件中的值映射到这个组件中,将类中的属性与配置文件一一对应。

yaml也可以这样使用

person:
  name: 周周${random.uuid}
  age:  ${random.int}
  happy: true
  birth: 2020/10/10
  maps: {k1: v1,k2: v2}
  list:
    - code
    - music
    - girl
  dog:
    name: ${person.zr:hello}_旺财
    age: 5

也可以使用@PropertySource加载指定的properties文件【不建议使用,建议使用yaml】

@ConfigurationProperties只用绑定一次,@Value需要每个字段添加。

yaml可以松散绑定,即yaml中写first-name,实体类写firstName,-后的字母默认是大写的,这就是松散绑定。

JSR303数据验证,就是我们可以在字段增加一层过滤器验证,可以保证数据的合法性。

复杂类型对象,yaml可以封装对象,@Value就不可以。

结论:

  • 配置yaml和properties都可以获取到值,强烈推荐yaml。
  • 如果我们在某个业务中,只获取配置文件中的某个值,可以使用一下@Value。
  • 如果在javaBean和配置文件进行映射,就直接使用@ConfigurationProperties。

JSR303

验证是否为邮箱格式

@Component
@ConfigurationProperties(prefix = "person")
@Validated//数据校验
public class Person {

    @Email
    private String name;
}

其它注解

空检查
@Null       验证对象是否为null
@NotNull    验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.

Booelan检查
@AssertTrue     验证 Boolean 对象是否为 true
@AssertFalse    验证 Boolean 对象是否为 false

长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.

日期检查
@Past           验证 Date 和 Calendar 对象是否在当前时间之前
@Future     验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern    验证 String 对象是否符合正则表达式的规则

数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
@Min            验证 Number 和 String 对象是否大等于指定的值
@Max            验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits     验证 Number 和 String 的构成是否合法
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。

@Range(min=, max=) 检查数字是否介于min和max之间.
@Range(min=10000,max=50000,message="range.bean.wage")
private BigDecimal wage;

@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber信用卡验证
@Email  验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)
    
@Pattern 正则表达式

Web开发

静态资源

在springboot中,我们可以使用以下路径处理静态资源

优先级:resorces>static(默认)>public

首页

WebMvcAutoConfiguration下部分源码

private Optional<Resource> getWelcomePage() {
    String[] locations = WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations());
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}

private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

即在资源配置目录下放入index.html即可自动识别。

Thymeleaf

pom中引入Thymeleaf,就可以将.html放在templates下访问。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

部分源码

@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";
    private String suffix = ".html";
  ......
}

可以看出来前缀是classpath:/templates/,后缀.html,即html文件放在classpath:/templates/下

测试

package com.zr.controller;

//在templates下的所有页面,只能通过controller来跳转
//需要模板引擎的支持
@Controller
public class IndexController {
    @RequestMapping("/test")
    public String index(Model model){
        model.addAttribute("msg","hello test");
        return "test";
    }
}

test.html

<body>
<div th:text="${msg}"></div>
</body>

拓展SpringMvc

package com.zr.config;

//扩展springmvc
@Configuration
//@EnableWebMvc //这配置是导入了一个类,DelegatingWebMvcConfiguration,从容器中获取所有的WebMvcConfigurer。
public class MyMvcConfig implements WebMvcConfigurer {
    //视图跳转
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/zr").setViewName("test");
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }

    @Bean
     public ViewResolver myViewResolver(){
         return new MyViewResolver();
     }
    
      //自定义视图解析器
     public static class MyViewResolver implements ViewResolver{
    
         @Override
         public View resolveViewName(String s, Locale locale) throws Exception {
             return null;
         }
     }
}

员工管理系统

首页配置,使用了thymeleaf接管,所有的herf引用改为th:herf="@{}"(th标签支持xmlns:th="http://www.thymeleaf.org")

页面国际化

  1. 需要配置i18n文件
  2. 如果需要在项目中进行按钮自由切换,我们需要自定义一个组件LocaleResolver
  3. 然后将自己写的组件配置到spring容器 @Bean
  4. {}

整合JDBC使用

对于数据访问层,无论使SQL(关系型数据库)还是NOSQL(非关系型数据库),SpringBoot底层都是采用 Spring Data 的方式进行统一处理。

application.yml

spring:
  datasource:
    username: root
    password: 123456
    #时区配置报错 增加时区配置serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&chacterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis数据库 user表

测试

@SpringBootTest
class Springboot04DataApplicationTests {

    @Autowired
    DataSource dataSource;
    @Test
    void contextLoads() throws SQLException {
        //查看默认的数据源
        System.out.println(dataSource.getClass());

        //获得数据库连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);

        //***templete: SpringBoot已经配置好的bean 拿来即用

        connection.close();
    }
}

controller

@RestController
public class JdbcController {
    @Autowired
    JdbcTemplate jdbcTemplate;

    //查询数据库的所有信息
    //没有实体类,数据库的东西怎么取  Map
    @GetMapping("/userlist")
    public List<Map<String,Object>> userList(){
        String sql = "select * from user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }

    @GetMapping("/addUser")
    public String addUser(){
        String sql = "insert into user(id,name,pwd) values(7,'韩信','6666')";
        jdbcTemplate.update(sql);
        return "add OK!";
    }
    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") int id){
        String sql = "update user set name=?,pwd=? where id="+id;
        //封装
        Object[] objects = new Object[2];
        objects[0] = "小周啊";
        objects[1] = "8888";

        jdbcTemplate.update(sql,objects);
        return "update OK!";
    }
    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") int id){
        String sql = "delete from user where id=?";
        jdbcTemplate.update(sql,id);
        return "delele OK!";
    }
}

整合Druid数据源

Druid是阿里巴巴开源平台上的一个数据库连接池实现,结合了C3P0,DBCP,PEOXOOL等DB池的优点,同时加入了日志监控。

SpringBoot默认的是Hikari数据源。

application.yml

spring:
  datasource:
    username: root
    password: 123456
    #时区配置报错 增加时区配置serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&chacterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #SpringBoot默认不注入这些属性的,需要自己绑定
    #durid配置
    initialSize: 5
    minIdel: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #配置监控拦截,stat 监控统计, wall 防御sql注入,log4j 日志
    #如果允许时报错,导入log4j依赖
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

监控

DruidConfig

@Configuration
public class DruidConfig {
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }
    //后台监控
    //springboot内置了Servlet,所有没有web.xml,替代方法: ServletRegistrationBean
    @Bean
    public ServletRegistrationBean statViewServlet(){
        // ServletRegistrationBean<StatusManagerServlet> bean = new ServletRegistrationBean<>(new StatusManagerServlet(), "/druid/*");
        ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        //后台需要有人登录,账号密码配置
        HashMap<String, String> initParameters = new HashMap<>();
        //增加配置
        initParameters.put("loginUsername","admin"); //登录的key是固定的
        initParameters.put("loginPassword","123456");

        //允许谁可以访问
        initParameters.put("allow","");

        //禁止谁能访问   initParameters.put("zr","39.405.48.101");

        bean.setInitParameters(initParameters); //设置初始化参数
        return bean;
    }
    @Bean
    //filter
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();

        bean.setFilter(new WebStatFilter());

        //可以过滤哪些请求

        HashMap<String, String> initParameters = new HashMap<>();
        //这些不进行统计
        initParameters.put("exclusions","*.js,*.css,/druid/*");
        bean.setInitParameters(initParameters);

        return bean;
    }
}

启动后访问localhost:8080/druid ,账号密码为配置中设置的值,登陆后进入监控界面。

整合MyBatis

依赖mybatis-spring-boot-starter

application.properties

spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&chacterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#整合mybatis
mybatis.type-aliases-package=com.zr.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml

User

package com.zr.pojo;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String name;
    private String pwd;
}

UserMapper

package com.zr.mapper;
//这个注解表示了这是一个mybatis的mapper类
@Mapper
@Repository
public interface UserMapper {

    List<User> queryUserList();

    User queryById(int id);

    int addUser(User user);

    int updateUser(User user);

    int deleteUser(int id);
}

resource/mapper/UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zr.mapper.UserMapper">
    <select id="queryUserList" resultType="User">
    select * from user;
  </select>
    <select id="queryById" resultType="User">
    select * from user where id=#{id};
  </select>

    <insert id="addUser" parameterType="User">
        insert into user(id,name,pwd) values (#{id},#{name},#{pwd});
    </insert>

    <update id="updateUser" parameterType="User">
        update user set name = #{name},pwd=#{pwd} where id=#{id};
    </update>

    <delete id="deleteUser" parameterType="int">
        delete from user where id=#{id};
    </delete>
</mapper>

UserController

package com.zr.controller;
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;
    @GetMapping("/queryUserList")
    public List<User> queryUserList(){
        List<User> userList = userMapper.queryUserList();
        return userList;
    }

    @GetMapping("/queryById/{id}")
    public User queryById(@PathVariable("id") int id){
        User user = userMapper.queryById(id);
        System.out.println(user);
        return user;
    }

    @GetMapping("/addUser")
    public String addUser(){
        userMapper.addUser(new User(7,"韩信","33333"));
        return "addUser OK";
    }
    @GetMapping("/updateUser")
    public String updateUser(){
        userMapper.updateUser(new User(7,"韩信","54321"));
        return "updateUser OK";
    }

    @GetMapping("/deleteUser")
    public String deleteUser(){
        userMapper.deleteUser(7);
        return "deleteUser OK";
    }
}

以上仅为测试mybatis,没有编写业务层。

SpringSecurity

SpringSecurity是针对spring项目的安全框架,也是 SpringBoot 底层安全模块默认的技术选型,它可以实现强大的web安全控制,对于安全控制,我们只需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理。

记住以下几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

SpringSecurity的两个主要目标是“认证” 和 “授权”(访问控制)

“认证” Authentication

“授权” Authorization

这两个概念是统用的,而不只是SpringSecurity中存在。

项目结构

application.properties

spring.thymeleaf.cache=false

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>登录</title>
    <!--semantic-ui-->
    <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
</head>
<body>

<!--主容器-->
<div class="ui container">

    <div class="ui segment">

        <div style="text-align: center">
            <h1 class="header">登录</h1>
        </div>

        <div class="ui placeholder segment">
            <div class="ui column very relaxed stackable grid">
                <div class="column">
                    <div class="ui form">
                        <form th:action="@{/login}" method="post">
                            <div class="field">
                                <label>Username</label>
                                <div class="ui left icon input">
                                    <input type="text" placeholder="Username" name="username">
                                    <i class="user icon"></i>
                                </div>
                            </div>
                            <div class="field">
                                <label>Password</label>
                                <div class="ui left icon input">
                                    <input type="password" name="password">
                                    <i class="lock icon"></i>
                                </div>
                            </div>
                            <div class="field">
                                <input type="checkbox" name="remember"> 记住我
                            </div>

                            <input type="submit" class="ui blue submit button"/>
                        </form>
                    </div>
                </div>
            </div>
        </div>

        <div style="text-align: center">
            <div class="ui label">
                </i>注册
            </div>
            <br><br>
            <small>blog.kuangstudy.com</small>
        </div>
        <div class="ui segment" style="text-align: center">
            <h3>Spring Security Study by 秦疆</h3>
        </div>
    </div>


</div>

<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/qinjiang/js/semantic.min.js}"></script>

</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>首页</title>
    <!--semantic-ui-->
    <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    <link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet">
</head>
<body>

<!--主容器-->
<div class="ui container">

    <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
        <div class="ui secondary menu">
            <a class="item"  th:href="@{/index}">首页</a>

            <!--登录注销-->
            <div class="right menu">
                <!--如果未登录-->
                <div sec:authorize="!isAuthenticated()">
                    <a class="item" th:href="@{/toLogin}">
                        <i class="address card icon"></i> 登录
                    </a>
                </div>

                <!--如果已登录 用户名,注销按钮-->
                <!--注销-->
                <div sec:authorize="isAuthenticated()">
                    <a class="item">
                        用户名:<span sec:authentication="name"></span>
                        <!--角色:<span sec:authentication="principal.getAuthorities()"></span>-->
                        <!--角色:<span sec:authentication="authorities"></span>-->
                    </a>
                </div>
                <div sec:authorize="isAuthenticated()">
                <a class="item" th:href="@{/logout}">
                    <i class="sign-out icon"></i> 注销
                </a>
                </div>
                <!--已登录
                <a th:href="@{/usr/toUserCenter}">
                    <i class="address card icon"></i> admin
                </a>
                -->
            </div>
        </div>
    </div>

    <div class="ui segment" style="text-align: center">
        <h3>Spring Security Study by 秦疆</h3>
    </div>

    <div>
        <br>
        <div class="ui three column stackable grid">
            <!--菜单根据用户的角色动态显示sec:authorize="hasRole('vip1')-->
            <div class="column" sec:authorize="hasRole('vip1')">
                <div class="ui raised segment">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 1</h5>
                            <hr>
                            <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                            <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                            <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column" sec:authorize="hasRole('vip2')">
                <div class="ui raised segment">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 2</h5>
                            <hr>
                            <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                            <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                            <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="column" sec:authorize="hasRole('vip3')">
                <div class="ui raised segment">
                    <div class="ui">
                        <div class="content">
                            <h5 class="content">Level 3</h5>
                            <hr>
                            <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                            <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                            <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                        </div>
                    </div>
                </div>
            </div>

        </div>
    </div>
    
</div>


<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/qinjiang/js/semantic.min.js}"></script>

</body>
</html>

RouterController

package com.zr.controller;

@Controller
public class RouterController {
    @RequestMapping({"/","/index"})
    public String index(){
        return "index";
    }
    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }

    @RequestMapping("/level1/{id}")
    public String level1(@PathVariable("id") int id){
        return "views/level1/"+id;
    }
    @RequestMapping("/level2/{id}")
    public String level2(@PathVariable("id")int id){
        return "views/level2/"+id;
    }
    @RequestMapping("/level3/{id}")
    public String level3(@PathVariable("id")int id){
        return "views/level3/"+id;
    }
}

SecurityConfig

package com.zr.config;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首页所有人可以访问,功能页只有对应有权限的人才能访问
        //请求授权的规则
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        //没有权限默认会到登录页 开启登录页面
        //usernameParameter("username").passwordParameter("password")
        // 括号内为前端传入的用户名为username,底层源码默认为username,当前端为name,pwd时就需要更改括号内问name,pwd
        //loginProcessingUrl("/login");//点登录跳转的路径
        http.formLogin().loginPage("/toLogin").usernameParameter("username").passwordParameter("password").loginProcessingUrl("/login");

        http.csrf().disable();//防止跨站脚本请求攻击关闭  登出失败可能的原因
        //开启了注销功能
        http.logout().logoutSuccessUrl("/index");

        //开启记住我功能,cookie 默认保存两周,自定义接收前端的参数
        //开启记住我功能 默认保存两周
        http.rememberMe().rememberMeParameter("remember");
    }

    //认证
    //密码编码:PasswordEncoder
    //在Spring Security 5.0+中,新增了很多的加密方式
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //正常应该从数据库中读  这里是从内存中读取
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("zr").password(new BCryptPasswordEncoder().encode("12345")).roles("vip2","vip3")
                .and()
                .withUser("zz").password(new BCryptPasswordEncoder().encode("12345")).roles("vip1","vip2")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("12345")).roles("vip1","vip2","vip3");
    }
}

Shiro

Apache Shiro 是一个Java的安全(权限)框架。

Shiro 可以非常容易地开发出足够好的应用,其不仅可以用在JavaSE环境,还可以用在JavaEE环境。

Shiro可以完成认证,授权,加密,会话管理,Web集成,缓存等。

重要对象:

Subject:应用代码直接交互的对象是subject,也就是说shiro的对外核心API就是subject,subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是subject,如网络爬虫,机器人等,与subject的所有交互都会委托给SecurityManager,subject是一个门面,SubjectManager才是具体的执行者。

SecurityManager:安全管理器,即所有与安全有关的操作都会与SecurityManager交互,并且它管理着所有的subject,可以看出它是shiro的核心,它负责与shiro的其它组件交互,相当于SprinfMvc的DispatcherServlet的角色。

Realm:shiro从realm获得安全数据(如用户,角色,权限),就是说SecurityManager要验证用户身份,那么它需要从realm获得相应的用户进行比较,来确定用户的身份是否合法,也需要从realm得到用户相应的角色,权限来验证用户的操作是否能够进行,可以把realm当成DataSource。

整合shiro练习

目录结构

数据库为整合JDBC中的数据库增加一个perms字段

依赖pom.xml

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.21</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.1</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
    </dependency>
<!--    shrio,thymeleaf整合-->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
</dependencies>

application.yml

spring:
  datasource:
    username: root
    password: 123456
    #时区配置报错 增加时区配置serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&chacterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #SpringBoot默认不注入这些属性的,需要自己绑定
    #durid配置
    initialSize: 5
    minIdel: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #配置监控拦截,stat 监控统计, wall 防御sql注入,log4j 日志
    #如果允许时报错,导入log4j依赖
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

application.properties

mybatis.type-aliases-package=com.zr.pojo
mybatis.mapper-locations=classpath:mapper/*.xml

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
   <div th:if="${session.loginUser==null}">
       <a th:href="@{/toLogin}">登录</a>
   </div>

<p th:text="${msg}"></p>
<hr>
<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a>
</div>

<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>
</body>
</html>

login.html

<body>
<h1>登录</h1>
<p th:text="${msg}" style="color: red"></p>
<form th:action="@{/login}">
    <p>用户名:<input name="username" type="text"></p>
    <p> 密码:<input name="password" type="password"></p>
   <p><input type="submit"></p>
</form>
</body>

add.html

<body>
<h1>add</h1>
</body>

update.html

<body>
<h1>update</h1>
</body>

User实体类

package com.zr.pojo;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String pwd;
    private String perms;
}

UserMapper

package com.zr.mapper;

@Repository
@Mapper
public interface UserMapper {
    public User queryUserByName(String name);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zr.mapper.UserMapper">

    <select id="queryUserByName" parameterType="String" resultType="User">
    select * from user where name=#{name};
  </select>

</mapper>

UserService

package com.zr.service;

import com.zr.pojo.User;

public interface UserService {
    public User queryUserByName(String name);
}

UserServiceImpl

package com.zr.service;

@Service
public class UserServiceImpl implements UserService{
    @Autowired
    UserMapper userMapper;
    @Override
    public User queryUserByName(String name) {

        return userMapper.queryUserByName(name);
    }
}

MyController

package com.zr.controller;

@Controller
public class MyController {
    @RequestMapping({"/","/index"})
    public String toIndex(Model model){
        model.addAttribute("msg","hello shiro");
        return "index";
    }
    @RequestMapping("/user/add")
    public String add(){
        return "user/add";
    }
    @RequestMapping("/user/update")
    public String update(){
        return "user/update";
    }
    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }

    @RequestMapping("/login")
    public String login(String username,String password,Model model){

        //获取当前的用户
        Subject subject = SecurityUtils.getSubject();
        //封装用户的登录数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try{
            subject.login(token); //执行登录的方法,如果没有一次就ok
            return "index";
        }catch (UnknownAccountException a){ //用户名不存在
            model.addAttribute("msg","用户名错误!");
            return "login";
        }catch (IncorrectCredentialsException a){ //密码不存在
            model.addAttribute("msg","密码错误!");
            return "login";
        }
    }
    @RequestMapping("/noauth")
    @ResponseBody
    public String unAuthorized(){
        return "未授权不能访问此页面!";
    }
}

UserRealm

package com.zr.config;

//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了==>>授权:AuthorizationInfo");

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //info.addStringPermission("user:add");
        //拿到当前登录的对象
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User)subject.getPrincipal(); //拿到user对象
        //设置当前用户的权限
        info.addStringPermission(currentUser.getPerms());

        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了==>>认证:AuthenticationInfo");

        //用户名密码 数据库中取

        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        User user = userService.queryUserByName(userToken.getUsername());

        if (user==null){
            return null;
        }

        Subject currentSubject = SecurityUtils.getSubject();
        Session session = currentSubject.getSession();
        session.setAttribute("loginUser",user);


        //密码认证 shiro默认做
        return new SimpleAuthenticationInfo(user,user.getPwd(),"");
    }
}

ShiroConfig

package com.zr.config;

@Configuration
public class ShiroConfig {

    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);

        //添加shiro的内置过滤器
        /*
            anon:无需认证就可以访问
            authc:必须认证
            user:必须拥有记住我功能才能用
            perms:拥有某个资源的权限才能访问
            role:拥有某个角色权限才能访问
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        //授权 没有授权会跳转到未授权界面(401)
        filterMap.put("/user/add","perms[user:add]");
        filterMap.put("/user/update","perms[user:update]");

        filterMap.put("/user/*","authc");

        bean.setFilterChainDefinitionMap(filterMap);
        //返回登录页
        bean.setLoginUrl("/toLogin");
        //未授权页面
        bean.setUnauthorizedUrl("/noauth");
        return bean;
    }

    //DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联realm
        securityManager.setRealm(userRealm);

        return securityManager;
    }

    //创建realm对象,自定义
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    //整合shiroDialect 用来整合shiro和thymeleaf
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }
}

Swagger

学习目标:

  • 了解Swagger的概念和作用
  • 巩固前后端分离
  • 在SpringBoot中巩固Swagger

Swagger简介

  • 号称最流行的Api框架
  • RestFul Api 文档在线自动生成工具=>Api文档与Api定义同步更新
  • 直接运行,可以在线测试Api接口
  • 支持多种语言

在项目中使用swagger需要导入jar包

  • swagger2
  • ui

SpringBoot集成Swagger

  1. 新建一个SpringBoot-web项目

  2. 导入相关依赖

    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    
  3. 编写helloword工程

  4. 配置swagger===>config

    package com.zr.config;
    
    @Configuration
    @EnableSwagger2  //开启swagger2
    public class SwaggerConfig {
    }
    
  5. 访问http://localhost:8080/swagger-ui.html

配置Swagger

Swagger的Bean实例 Docket

package com.zr.config;

import java.util.ArrayList;

@Configuration
@EnableSwagger2  //开启swagger2
public class SwaggerConfig {
    //配置swagger的Docket的 Bean实例
    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo());
    }

    //配置swagger文档的信息
    private ApiInfo apiInfo(){
        //作者信息
        Contact contact = new Contact("周周", "http://www.cnblogs.com/zhou-zr", "813794474@qq.com");
        return new ApiInfo("周周的SwaggerApi文档",
                "coding",
                "1.0",
                "http://www.cnblogs.com/zhou-zr",
                contact,
                "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE-2.0",
                new ArrayList()
        );
    }
}

http://localhost:8080/swagger-ui.html

Swagger配置扫描接口

Docket select()

在配置Swagger中修改

//配置swagger的Docket的 Bean实例
//enable 是否启动swagger 如果为false 不能再浏览器中访问swagger
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
	        .enable(false)
            .select()
            //配置要扫描接口的方式
            //basePackage 指定扫描的包
            //any() 扫描全部
            //none() 都不扫描
            //withClassAnnotation() 扫描类上的注解 参数是一个注解的反射对象
            //withMethodAnnotation() 扫描方法上的注解
            .apis(RequestHandlerSelectors.basePackage("com.zr.controller"))
            //过滤什么路径
            .paths(PathSelectors.ant("/zr/**"))
            .build();
}

根据是否是生产环境来开启swagger

//配置swagger的Docket的 Bean实例
     //enable 是否启动swagger 如果为false 不能再浏览器中访问swagger
     @Bean
     public Docket docket(Environment environment){
         //设置要显示的swagger环境
         Profiles profiles = Profiles.of("dev");
         //通过environment.acceptsProfiles判断是否处在自己设定的环境当中
         boolean flag = environment.acceptsProfiles(profiles);

         return new Docket(DocumentationType.SWAGGER_2)
                 .apiInfo(apiInfo())
                 .enable(flag)
                 .select()
                 .apis(RequestHandlerSelectors.basePackage("com.zr.controller"))
                 //过滤什么路径
                 //.paths(PathSelectors.ant("/zr/**"))
             .build();
 }

application.properties

spring.profiles.active=dev

application-dev.properties

server.port=8081

application-pro.properties

server.port=8082

配置Api文档分组

.groupName("zzr")

如何配置多个分组:配置多个Docket实例即可。

@Bean
public Docket docket2(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("A");
}
@Bean
public Docket docket3(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("B");
}
@Bean
public Docket docket4(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("C");
}

接口测试

实体类配置

package com.zr.pojo;

// @Api(注释)
@ApiModel("用户实体类")
public class User {
    @ApiModelProperty("用户名")
    public String username;
    @ApiModelProperty("密码")
    public String password;
}

HelloController

package com.zr.controller;

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
    //只有我们的接口中,返回值中存在实体类,就会被扫描到swagger中
    @PostMapping("/user")
    public User user(){
        return new User();
    }
    @ApiOperation("Hello控制类")
    @GetMapping("/hello2")
    public String hello(@ApiParam("用户名") String username){
        return "hello"+username;
    }
    @ApiOperation("post测试")
    @PostMapping("/postt")
    public User postt(@ApiParam("用户名")User user){
        return user;
    }
}

总结:

  1. 我们以通过Swagger给一些比较难理解的属性或者接口,增加注释信息
  2. 接口文档实时更新
  3. 可以在线测试

注意点:在正式上线的时候,关闭Swagger。

任务

异步任务

AsyncService

package com.zr.service;

@Service
public class AsyncService {
    //告诉spring这是一个异步的方法
    @Async
    public void hello(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据正在处理");
    }
}

AsyncController

package com.zr.controller;

@RestController
public class AsyncController {

    @Autowired
    AsyncService asyncService;
    @RequestMapping("/hello")
    public String hello(){
        asyncService.hello();  //停止3秒
        return "ok";
    }
}

主程序开启异步注解的功能

package com.zr;

//开启异步注解的功能
@EnableAsync
@SpringBootApplication
public class Springboot09TestApplication {

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

}

定时任务

TaskScheduler  任务调度者
TaskExecutor  任务执行者

@EnableScheduling //开启定时功能的注解
@Scheduled  //什么时候执行

Cron 表达式

主程序开启定时任务的支持

package com.zr;

@EnableScheduling //开启定时功能的注解
@SpringBootApplication
public class Springboot09TestApplication {

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

定时任务设置

package com.zr.service;

@Service
public class ScheduledService {
    //在一个特定的时间执行

    /*
        0 30 10 * * ?   每天的10.30 执行一次
        0 0/5 10,16 * * ?   每天的10点到16点 每隔五分钟执行一次
        0 20 10 ? * 1-6    每个月的周一到周六 10.20执行一次
     */
    //cron表达式  秒 分 时 日 月 星期几(0-7  ?每一天)
    @Scheduled(cron = "0 * * * * ?")
    public void hello(){
        System.out.println("hello,你被执行了");
    }
}

邮件任务

package com.zr;

@SpringBootTest
class Springboot09TestApplicationTests {

    @Autowired
    JavaMailSenderImpl mailSender;
    @Test
    void contextLoads() {
        //一个简单的邮件
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setSubject("小周,你好!");
        mailMessage.setText("好好学习!");
        mailMessage.setTo("813794474@qq.com");
        mailMessage.setFrom("813794474@qq.com");
        mailSender.send(mailMessage);
    }

    @Test
    void contextLoads2() throws MessagingException {
        //一个复杂的邮件
        // MimeMessage 也可以直接new创建
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        //组装
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
        helper.setSubject("周周,你好呀!");
        helper.setText("<h1><p style='color:red'>好好学习!</p></h1>",true);
        //附件
        helper.addAttachment("winC.PNG",new File("C:\\Users\\zr\\Pictures\\截图\\winC.PNG"));
        helper.addAttachment("2.jpg",new File("C:\\Users\\zr\\Pictures\\截图\\winC.PNG"));

        helper.setTo("813794474@qq.com");
        helper.setFrom("813794474@qq.com");
        mailSender.send(mimeMessage);
    }
}

分布式

分布式系统理论

什么是分布式系统:

在《分布式系统原理与范型》 一书中有如下定义:分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统。

分布式系统是有一组通过网络进行通信,为了完成共同的任务而协调工作的计算机节点组成的系统,分布式系统的出现是为了用廉价的,普通的机器完成单个计算机无法完成的计算,存储任务。其目的是利用更多的机器,处理更多的数据。

只有当单个节点无法满足日益增长的计算,存储任务需求时,且硬件的提升高昂到得不偿失时,且程序也不能进一步优化的时候,我们才需要考虑分布式系统。

单一应用架构:当网站流量很小时,只需一个应用,将所有的功能部署在一起,以减少部署节点和成本,此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

适用于小型网站,小型管理系统,将所有的应用部署到一个应用中,简单易用。

缺点:性能扩展比较难,协同开发问题,不利于升级维护。

垂直应用架构:当数据量逐渐增大,单一应用增加机器带来的加速度越来越小,将一个应用拆分成互不相干的几个应用,以提升效率,此时,用于加速前端页面开发的Web框架(MVC)是关键。

通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职,性能更易扩展。

缺点:公用模块无法重复利用,开发性的浪费。

分布式服务架构:当垂直应用越来越多,应用之间交互不可避免。将核心应用抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。

流动计算架构:当服务越来越多,容量的评估,小服务资源的浪费问题等问题逐渐显现,此时需要增加一个调度中心基于访问的压力实时管理集群的容量,提高集群利用率。此时。用于提高资源利用率的资源调度和治理中心(SOA)是关键。

RPC

什么是RPC:

RPC【Remote Procedure Call】是指远程过程调用,是一种进程间的通信方式,它是一种技术的思想,而不是规范,它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不是程序员显示编码这个远程调用的细节,即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

RPC两个核心:通信和序列化。

序列化:数据传输需要转换。

分布式 Dubbo + Zookeeper + SpringBoot

Apache Dubbo是一款高性能,轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

服务提供者(Provider):暴漏服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的 。

服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

容器(Container):服务运行容器。

调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。

  2. 服务提供者在启动时,向注册中心注册自己提供的服务。

  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。

  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

window下安装zookeeper

官网下载zookeeper,解压至指定目录,管理员模式下进入到zookeeper的bin目录下,启动zkServer.cmd。

【注意报错】在zkServer.cmd源文件中加pause,可查看报错信息。

解决:进入conf目录,将zoo_sample.cfg复制一份将名字改为zoo.cfg。

启动zkServer.cmd , 启动zkCli.cmd

window下安装Dubbo

dubbo本身并不是一个服务软件,它其实就是一个jar包,把Java程序连接到zookeeper,并利用zookeeper消费,提供服务。

为了让用户更好的监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不安装也不影响使用。

下载dubbo-admin

cmd进入下载的目录下,执行mvn clean package -Dmaven.test.skip=true

打包成功

执行dubbo-admin\target下的dubbo-admin-0.0.1-SNAPSHOT.jar

命令:java -jar dubbo-admin-0.0.1-SNAPSHOT.jar 【注意 zookeeper的服务一定要打开】

执行完毕后访问 http://localhost:7001 默认账号密码是root,root。

登录成功界面

zookeeper:注册中心

dubbo-admin是一个监控管理后台,可以查看我们注册了哪些服务,哪些服务被消费了。

Dubbo:jar包

步骤:前提开启zookeeper

  1. 提供者提供服务
    1. 导入依赖
    2. 配置注册中心的地址,以及服务发现名,和要扫描的包
    3. 在想要被注册的服务上增加一个注解@Service (Dubbo的)
  2. 消费者如何消费
    1. 导入依赖
    2. 配置注册中心的地址,配置自己的服务名
    3. 从远程注入服务@Reference

项目结构:

provider-server

TickerService

package com.zr.service;

public interface TickerService {
    public String getTicker();
}

TickerServiceImpl

package com.zr.service;

//服务注册与发现
@Service  //可以被扫描到  项目一启动就自动注册到注册中心
@Component  //使用dubbo后尽量不要使用service
public class TickerServiceImpl implements TickerService{

    @Override
    public String getTicker() {
        return "zzr";
    }
}

application.properties

server.port=8001

#服务应用的名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#哪些服务要被注册
dubbo.scan.base-packages=com.zr.service

pom.xml(consumer-server的pom相同)

<!-- dubbo -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.8</version>
</dependency>
<!-- zkclient -->
<dependency>
    <groupId>com.github.sgroschupf</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.1</version>
</dependency>
<!--日志冲突-->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
    <!--    排除slf4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

consumer-server

TickerService

package com.zr.service;

public interface TickerService {
    public String getTicker();
}

UserService

package com.zr.service;

@Service  //放到容器中
public class UserService {

    //想拿到票provider-server提供的票  去注册中心拿
    @Reference //引用 pom坐标 定义路径相同的接口名
    TickerService tickerService;

    public void byTicket(){
        String ticker = tickerService.getTicker();
        System.out.println("在注册中心拿到了一张票"+ticker);
    }

}

application.properties

server.port=8002
#消费者从那里去拿需要暴漏自己的名字
dubbo.application.name=consumer-server
#注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

测试

package com.zr;

@SpringBootTest
class ConsumerServerApplicationTests {

    @Autowired
    UserService userService;
    @Test
    void contextLoads() {
        userService.byTicket();
    }
}

结果

dubbo-admin中查看服务提供者

推荐阅读