SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)

文章目录

  • 十一、Shiro
    • 1.Shiro 简介
        • 什么是 shiro
        • 有哪些功能
        • Shiro架构(外部)
        • Shiro 架构(内部)
    • 2.快速使用
        • Subject 分析
    • 3.SpringBoot 集成 Shiro
        • SpringBoot整合Shiro环境搭建
        • Shiro实现登录拦截
        • Shiro实现用户认证
        • Shiro整合Mybatis
        • Shiro实现用户授权
        • Shiro整合Thymeleaf
        • 完整代码
        • Shiro 个人理解
  • 十二、SpringBoot 开源项目
  • 十三、Swagger
    • 1.Swagger简介
    • 2.SpringBoot集成Swagger
        • 创建项目
        • 配置Swagger
        • 配置扫描接口
        • 配置Swagger开关
        • 配置API分组
        • 实体配置
        • 常用注解
        • 拓展:其他皮肤
  • 十四、异步、定时、邮件任务
    • 1.异步任务
    • 2.邮件任务
    • 3.定时任务
  • 十五、SpringBoot 整合 Redis
        • 项目准备
        • 项目配置
        • 整合测试
        • redis 序列化问题
        • 自定义 RedisTemplate 配置
        • 使用自定义 RedisTemplate
        • 自定义 Redis 工具类
  • 十六、分布式 Dubbo+Zookeeper
    • 1.分布式理论
        • 什么是分布式系统?
        • Dubbo文档
        • 单一应用架构
        • 垂直应用架构
        • 分布式服务架构
        • 流动计算架构
    • 2.RPC
    • 3.Dubbo
        • Dubbo 概念
        • Dubbo环境搭建
        • Window下安装zookeeper
        • window下安装dubbo-admin
    • 4.SpringBoot + Dubbo + zookeeper
        • 项目搭建
        • 服务提供者
        • 服务消费者
        • 启动测试
        • 小结
        • 富文本编辑器

十一、Shiro

1.Shiro 简介

什么是 shiro

  • Apache Shiro 是一个Java安全(权限)框架
  • Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEEhuanjing
  • Shiro可以完成 认证、授权、加密、会话管理、Web集成、缓存等

下载地址:http://shiro.apache.org/

有哪些功能

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第1张图片

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web 环境的
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
  • Web Support:Web 支持,可以非常容易的集成到Web 环境
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率
  • Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去
  • Testing:提供测试支持
  • “Run As”:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

Shiro架构(外部)

从外部来看Shiro,即从应用程序角度的来观察如何使用Shiro完成工作
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第2张图片

  • Subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject 的所有交互都会委托给SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者
  • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色
  • Realm:Shiro从Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm 看成DataSource

Shiro 架构(内部)

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第3张图片

  • Subject:任何可以与应用交互的“用户”;
  • SecurityManager:相当于SpringMVC中的DispatcherServlet;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证、授权、会话及缓存的管理。
  • Authenticator:负责Subject 认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  • Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • Realm:可以有1 个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的Realm;
  • SessionManager:管理Session 生命周期的组件;而Shiro并不仅仅可以用在Web 环境,也可以用在如普通的JavaSE环境
  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能
  • Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密。

2.快速使用

  • 查看官方文档:http://shiro.apache.org/tutorial.html
  • 官方的quickstart : https://github.com/apache/shiro/tree/master/samples/quickstart/
  • GitHub官方案例源码:https://github.com/apache/shiro

我们参照git上的官方项目,创建自己的项目
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第4张图片

  1. 创建springboot项目,删除src,多余文件,创建maven子模块,
    SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第5张图片
  2. 根据官方文档案例,我们导入Shiro的依赖,官网的案例使用的spring-boot-starter,所以没有版本号,这里我们直接根据依赖名称到maven官网找对应的完整依赖
<dependencies>
    <dependency>
        <groupId>org.apache.shirogroupId>
        <artifactId>shiro-coreartifactId>
        <version>1.5.3version>
    dependency>

    
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>jcl-over-slf4jartifactId>
        <version>1.7.26version>
    dependency>
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-log4j12artifactId>
        <version>1.7.26version>
    dependency>
    <dependency>
        <groupId>log4jgroupId>
        <artifactId>log4jartifactId>
        <version>1.2.17version>
    dependency>
dependencies>
  1. 添加 log4j.properties 配置文件,用来在控制台输出相关日志
log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

  1. 添加 shiro.ini 文件

如果idea安装了ini插件,文档将高亮显示

[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# 
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

  1. 添加 启动类 Quickstart

直接从官网案例中复制,先运行,再慢慢分析

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Simple Quickstart application showing how to use Shiro's API.
 * 简单入门Shiro使用API
 *
 * @since 0.9 RC2
 */
public class Quickstart {
     

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {
     

        // The easiest way to create a Shiro SecurityManager with configured
        // realms, users, roles and permissions is to use the simple INI config.
        // We'll do that by using a factory that can ingest a .ini file and
        // return a SecurityManager instance:

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):
        // 读取配置文件:
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();

        // for this simple example quickstart, make the SecurityManager
        // accessible as a JVM singleton.  Most applications wouldn't do this
        // and instead rely on their container configuration or web.xml for
        // webapps.  That is outside the scope of this simple quickstart, so
        // we'll just do the bare minimum so you can continue to get a feel
        // for things.
        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        // 获取当前的用户对象 Subject
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        //通过当前用户拿到Shiro的Session 可以脱离web存值取值
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
     
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        //判断当前的用户是否被认证
        if (!currentUser.isAuthenticated()) {
     
            //Token 令牌
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            //设置记住我
            token.setRememberMe(true);
            try {
     
                //执行登录操作
                currentUser.login(token);
            } catch (UnknownAccountException uae) {
     
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
     
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
     
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
     
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        // 检查角色
        if (currentUser.hasRole("schwartz")) {
     
            log.info("May the Schwartz be with you!");
        } else {
     
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        //粗粒度
        if (currentUser.isPermitted("lightsaber:wield")) {
     
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
     
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        //细粒度
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
     
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
     
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //all done - log out!
        //注销
        currentUser.logout();

        //结束
        System.exit(0);
    }
}

启动测试

在这里插入图片描述

Subject 分析

从注释中,我们可以看出,前几行代码其实可以通过配置来完成,官方也推荐这么做,因此,去掉注释,精简代码

核心代码为

// 获取当前的用户对象 Subject
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
currentUser.isAuthenticated()
currentUser.getPrincipal()
currentUser.hasRole("schwartz")
currentUser.isPermitted("lightsaber:wield")
currentUser.logout();

与 spring security 非常详相近

3.SpringBoot 集成 Shiro

SpringBoot整合Shiro环境搭建

新建springboot 模块
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第6张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第7张图片
添加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";
    }
}

首页 index.html页面


<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页title>
head>
<body>
<div>
    <h1>首页h1>
    <p th:text="${msg}">p>

    <hr>
    <a th:href="@{/user/add}">adda>   | <a th:href="@{/user/update}">updatea>
div>
body>
html>

新建一个add.html页面


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
    <h1>addh1>
body>
html>

新建一个update.html页面


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
    <h1>updateh1>
body>
html>

项目结构
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第8张图片
运行测试
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第9张图片
添加 shiro 整合 spring 的依赖




<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-springartifactId>
    <version>1.5.3version>
dependency>

编写 ShiroConfig 配置类

  • 创建realm对象,需要自定义类
  • DefaultWebSecurityManage 关联userRealm
  • ShiroFilterFactoryBean 设置安全管理器
@Configuration
public class ShiroConfig {
     
    //3. shiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
     
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        return bean;
    }
    //2. DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
     
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 关联userRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    //1. 创建realm对象,需要自定义类
    @Bean
    public UserRealm userRealm() {
     
        return new UserRealm();
    }
}

这里有个细节注意:UserRealm 被注册bean,默认名字是 userRealm,因此在上面的方法参数中我们用注解@Qualifier(“userRealm”)来进行诸如,其实和使用 @Autowired 是一样的

编写一个自定义类 UserRealm

//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
     
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
     
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        return null;
    }
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
     
        System.out.println("执行了=>认证doGetAuthorizationInfo");
        return null;
    }
}

Shiro实现登录拦截

在ShiroConfig中的getShiroFilterFactoryBean方法中添加如下配置

  • anon: 无需认证就可以访问
  • authc: 必须认证了才能访问
  • user: 必须拥有记住我功能才能用
  • perms: 拥有对某个资源的权限才能访问
  • role: 拥有某个角色权限
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
bean.setFilterChainDefinitionMap(filterMap);

点击首页的add或者update之后
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第10张图片
添加拦截成功页面,登录页面login.html

shiro没有security那样的默认登录页,需要我们自定义


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面title>
head>
<body>
<h1>登录h1>
<hr>

<form action="">
    <p>用户名:<input type="text" name="username">p>
    <p>密码:<input type="text" name="password">p>
    <p>密码:<input type="submit">p>
form>
body>
html>

在MyConfig中添加

@RequestMapping("/toLogin")
public String toLogin() {
     
    return "login";
}

ShiroConfig中的getShiroFilterFactoryBean方法中添加如下配置

//设置登录的请求
bean.setLoginUrl("/toLogin");

测试页面是否可以拦截成功
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第11张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第12张图片

Shiro实现用户认证

ShiroConfig主要用于登录拦截配置。

UserRealm 主要用于用户的认证,授权操作,这两个类相互联动,实现功能

  1. 在MyController中编写用户提交表单之后处理
@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 e) {
     //用户名不存在
        model.addAttribute("msg","用户名错误");
        return "login";
    } catch (IncorrectCredentialsException e) {
     //密码不存在
        model.addAttribute("msg","密码错误");
        return "login";
    }
}
  1. login.html的修改,表单提交账户信息

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页面title>
head>
<body>
<h1>登录h1>
<hr>
<p th:text="${msg}" style="color: red;">p>
<form th:action="@{/login}">
    <p>用户名:<input type="text" name="username">p>
    <p>密码:<input type="text" name="password">p>
    <p>密码:<input type="submit">p>
form>
body>
html>
  1. 用户认证编写 UserRealm 中的认证(doGetAuthenticationInfo方法)

shiro为了增加安全性,我们从数据库得到密码直接封装到 AuthenticationInfo对象中返回即可,shiro自行判断

//认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
     
        System.out.println("执行了=>认证doGetAuthorizationInfo");
        // 用户名、密码, 从数据库中取
        String name = "root";
        String password = "123456";
        // 这个token中包含了用户请求传过来的账户信息
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        if (!userToken.getUsername().equals(name)) {
     
            return null;//这里返回null.controller中的验证就会抛出异常 UnknownAccountException,
        }
        // 密码认证,shiro做
        // 1.3参数可以为空串,不影响
        return new SimpleAuthenticationInfo("",password,"");
    }

看似controller获取账户,封装token,验证token,realm中配置账户信息,之间没有什么联系,其实底层经过了很多层方法,实现关联

  1. 启动登录测试
    SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第13张图片

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第14张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第15张图片
在这里插入图片描述

测试成功,shiro成功实现了拦截、认证

Shiro整合Mybatis

  1. 导入依赖
<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
dependency>
<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
dependency>

<dependency>
    <groupId>log4jgroupId>
    <artifactId>log4jartifactId>
    <version>1.2.17version>
dependency>

<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druidartifactId>
    <version>1.1.23version>
dependency>


<dependency>
    <groupId>org.mybatis.spring.bootgroupId>
 	<artifactId>mybatis-spring-boot-starterartifactId>
    <version>2.1.3version>
dependency>
  1. 配置文件application.yml的编写
spring:
  datasource:
    username: root
    password: admin
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://localhost:3306/my_study?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

mybatis:
  type-aliases-package: com.swy.pojo
  mapper-locations: classpath:mapper/*.xml
  1. User类的编写
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
     
    private int id;
    private String name;
    private String pwd;
}
  1. UserMapper.xml映射,在resources中添加mapper目录,在mapper中添加映射文件



<mapper namespace="com.swy.mapper.UserMapper">
    <select id="queryUserList" resultType="User">
        select * from mybatis.user;
    select>
    <select id="queryUserByName" resultType="User">
        select * from mybatis.user where name = #{name};
    select>
    <insert id="addUser" parameterType="User">
        insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd});
    insert>
    <update id="updateUser" parameterType="User">
        update mybatis.user set name=#{name},pwd = #{pwd} where id = #{id};
    update>
    <delete id="deleteUser" parameterType="int">
        delete from mybatis.user where id = #{id}
    delete>
mapper>
  1. UserMapper接口实现
@Repository
@Mapper
public interface UserMapper {
     
    public User queryUserByName(String name);
}
  1. UserService接口
public interface UserService {
     
    public User queryUserByName(String name);
}
  1. UserServiceImpl业务逻辑实现类
@Service
public class UserServiceImpl implements UserService {
     
    @Autowired
    UserMapper userMapper;
    @Override
    public User queryUserByName(String name) {
     
        return userMapper.queryUserByName(name);
    }
}

检查数据库
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第16张图片
测试类

@SpringBootTest
class ShiroSpringbootApplicationTests {
     
	@Autowired
	UserService userService;
	@Test
	void contextLoads() {
     
		System.out.println(userService.queryUserByName("rose"));
	}
}

在这里插入图片描述
8. UserRealm连接真实数据库

@Autowired
UserService userService;
//...
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
     
    System.out.println("执行了=>认证doGetAuthorizationInfo");
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;
    // 真实数据库 用户名、密码, 数据中取
    User user = userService.queryUserByName(userToken.getUsername());
    if (user == null) {
     //没有这个人
        return null;
    }
    // 密码认证,shiro做
    return new SimpleAuthenticationInfo("",user.getPwd(),"");
}

由此可见,在shiro我们根本不用出现明文密码,就可以直接提取数据校验,防止安全问题

  1. 登录测试
    SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第17张图片
  2. 断点测试密码加密类型

在这里插入图片描述
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第18张图片
默认是SimpleCredentialsMatcher加密,进入这个CredentialsMatcher接口中可以发现,

查看 CredentialsMatcher 实现类可以看到有哪些支持的加密算法,默认使用了 SimpleCredentialsMatcher简单加密,也就是明文密码
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第19张图片

我们也可以选择 MD5 加密方式,

MD5加密测试链接 http://tool.chinaz.com/tools/md5.aspx

Shiro实现用户授权

在实现了拦截、认证之后,我们还要确保,当用户访问有权限要求的页面的时候,该如何处置,

实现逻辑:如果用户有授权,那么可以访问;如果用户没有授权,则返回无授权页面

修改 ShiroConfig,使用 getShiroFilterFactoryBean 方法,对指定的请求url做权限设置

将之前的无权限可以访问代码去掉

filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
  1. 添加权限设定
//授权,正常情况下,没有授权会跳转到为授权页面
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");

这个代码的含义是,必须是 user 用户的 add 权限才可以访问 /user/add

测试页面,发现即使我们已经登录,也无法访问,401对应的就是未授权,因为此时我们的登录用户还没有添加权限

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第20张图片

  1. 我们可以添加一个未授权页面,修改controller
@RequestMapping("/noauto")
@ResponseBody
public String unauthorized() {
     
    return "未经授权,无法访问此页面";
}

ShiroConfig中的getShiroFilterFactoryBean方法中添加

// 未授权页面
bean.setUnauthorizedUrl("/noauto");

再次测试
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第21张图片

  1. UserRealm类的修改

那么如何给用户添加权限?在 realm中进行配置

我们可以直接使用

info.addStringPermission("user:add");

这种方式授权,这样所有用户都有这个权限,但是我们需要根据不同的用户来判断是否授权,

所以这里,我们针对单个用户user授权,需要修改user表,增加表字段用于存放权限符号
在这里插入图片描述
手动在表中给用户添加权限
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第22张图片
修改 User 类,添加对应属性

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

这样我们可以根据当前登录的用户,得到该用户有哪些权限标识符,在交给 info 即可

如何在 realm 中获取当前用户对象?

可以使用 Subject 获取,subject就代表当前用户对象;

传值也可以使用session传递

//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
     
    @Autowired
    UserService userService;
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
     
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //拿到当前登录的这个对象
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User)subject.getPrincipal();//拿到user对象
        //设置当前用户的权限
        info.addStringPermission(currentUser.getPerms());
        return info;
    }
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
     
        ......
        // 密码认证,shiro做
        return new SimpleAuthenticationInfo(user,user.getPwd(),"");
    }
}

为什么subject.getPrincipal()可以强转user对象,因为下面return new SimpleAuthenticationInfo(user,user.getPwd(),"")中我们将当前用户的对象user添加在第一个参数上了,作为用户资源

再次重启测试,发现不同的登录用户有不同的访问权限

Shiro整合Thymeleaf

通过整合,可以在thymeleaf模板页面中实现shiro功能实现

  1. shiro-thymeleaf整合包导入

<dependency>
    <groupId>com.github.theborakompanionigroupId>
    <artifactId>thymeleaf-extras-shiroartifactId>
    <version>2.0.0version>
dependency>
  1. 在ShiroConfig中整合ShiroDialect
// 整合ShiroDialect: 用来整合 Shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect() {
     
    return new ShiroDialect();
}
  1. index.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>
head>
<body>
<div>
    <h1>首页h1>
    <p th:text="${msg}">p>
    
    
    <div shiro:notAuthenticated>
        <a th:href="@{/toLogin}">登录a>
    div>
    <hr>
    <div shiro:hasPermission="user:add">
        <a th:href="@{/user/add}">adda>
    div>

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

这样在前端页面,我们可以实现定制化动态页面,不同的登录用户展示不同的菜单

这里的 shiro:hasPermission就是从 realm 中获取的当前用户前线,放在这里判断是否可以显示该标签

访问测试
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第23张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第24张图片

完整代码

ShiroConfig

@Configuration
public class ShiroConfig {
     
    //3. shiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
     
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        Map<String, String> filterMap = new LinkedHashMap<>();
        /*filterMap.put("/user/add","authc");
        filterMap.put("/user/update","authc");*/
        filterMap.put("/user/add","perms[user:add]");
        filterMap.put("/user/update","perms[user:update]");
        bean.setFilterChainDefinitionMap(filterMap);
        //设置登录的请求
        bean.setLoginUrl("/toLogin");
        // 未授权页面
        bean.setUnauthorizedUrl("/noauto");

        return bean;
    }
    //2. DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
     
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 关联userRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    //1. 创建realm对象,需要自定义类
    @Bean
    public UserRealm userRealm() {
     
        return new UserRealm();
    }
    // 整合ShiroDialect: 用来整合 Shiro thymeleaf
    @Bean
    public ShiroDialect getShiroDialect() {
     
        return new ShiroDialect();
    }
}

realm

//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
     
    @Autowired
    UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
     
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //拿到当前登录的这个对象
        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("执行了=>认证doGetAuthorizationInfo");
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        // 真实数据库 用户名、密码, 数据中取
        User user = userService.queryUserByName(userToken.getUsername());
        if (user == null) {
     //没有这个人
            return null;
        }
        // 密码认证,shiro做
        return new SimpleAuthenticationInfo(user,user.getPwd(),"");
    }
}

MyController

@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 e) {
     //用户名不存在
            model.addAttribute("msg","用户名错误");
            return "login";
        } catch (IncorrectCredentialsException e) {
     //密码不存在
            model.addAttribute("msg","密码错误");
            return "login";
        }
    }
    @RequestMapping("/noauto")
    @ResponseBody
    public String unauthorized() {
     
        return "未经授权,无法访问此页面";
    }
}

pom

	<dependencies>
        
        
        
        <dependency>
            <groupId>com.github.theborakompanionigroupId>
            <artifactId>thymeleaf-extras-shiroartifactId>
            <version>2.0.0version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>log4jgroupId>
            <artifactId>log4jartifactId>
            <version>1.2.17version>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.1.23version>
        dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.3version>
        dependency>
        
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-springartifactId>
            <version>1.5.3version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
    dependencies>

扩展阅读:让 Apache Shiro 保护你的应用
https://blog.csdn.net/weixin_44635198/article/details/107701061?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-2&spm=1001.2101.3001.4242

Shiro 个人理解

  • shiro的使用可以做到高度的定制化、精细化,因此实现逻辑比较繁琐,需要深刻理解
  • shiro的易错点在于 config 类 和 realm 类的前后配合比较紧密,很多的配置都在相互作用,排错是要多加关注

十二、SpringBoot 开源项目

开源博客:https://github.com/WinterChenS/my-site

项目里已经包含数据库,以及部署方法,点赞!

非常规范容易理解的项目,强烈建议学习使用

扩展:七牛云服务

十三、Swagger

1.Swagger简介

学习目标:

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

前后端分离:Vue+SpringBoot

  • 前端 -> 前端控制层、视图层,前端已经可以工程化,伪造数据,不需要后端也能独立运行
  • 后端 -> 后端控制层、服务层、数据访问层
  • 前后端通过API进行交互
  • 前后端相对独立且松耦合
  • 前后端甚至可以部署在不同的服务器上

产生的问题

  • 前后端集成,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发

解决方案

  • 首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险
  • 早年指定word计划文档
  • 前后端分离,postman

Swagger

  • 号称世界上最流行的API框架
  • Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
  • 直接运行,在线测试API
  • 支持多种语言 (如:Java,PHP等)
  • 官网:https://swagger.io/

2.SpringBoot集成Swagger

SpringBoot集成Swagger => springfox,两个jar包

  • Springfox-swagger2
  • swagger-springmvc

要求:jdk 1.8 + 否则swagger2无法运行

创建项目

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第25张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第26张图片
添加 swagger 依赖


<dependency>
   <groupId>io.springfoxgroupId>
   <artifactId>springfox-swagger2artifactId>
   <version>2.9.2version>
dependency>

<dependency>
   <groupId>io.springfoxgroupId>
   <artifactId>springfox-swagger-uiartifactId>
   <version>2.9.2version>
dependency>

编写HelloController,测试确保运行成功

@RestController
public class HelloController {
     
    @RequestMapping("/hello")
    public String hello() {
     
        return "hello swagger";
    }
}

要使用Swagger,我们需要编写一个配置类-SwaggerConfig来配置 Swagger

@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig {
       
}

访问测试 :http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第27张图片

配置Swagger

  1. Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger。

在 config 配置类中添加

@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
     
   return new Docket(DocumentationType.SWAGGER_2);
}

DocumentationType有三个值,对应三种版本

查看 Docker 源码可以看到默认已经有了很多属性配置,我们只需要返回 docker实例即可,用 docker调用各种属性或者方法来添加配置参数

  1. 通过添加apiInfo()属性配置文档信息,

apiInfo()方法需要 ApiInfo 对象作为参数,我们将配置信息放在 ApiInfo 对象中

添加一个方法用来配置参数,并返回 ApiInfo 对象

//配置文档信息
private ApiInfo apiInfo() {
     
	// 作者信息
   Contact contact = new Contact("联系人名字", "http://xxx.xxx.com/联系人访问链接", "联系人邮箱");
   return new ApiInfo(
           "Swagger学习", // 标题
           "学习演示如何配置Swagger", // 描述
           "v1.0", // 版本
           "http://terms.service.url/组织链接", // 组织链接
           contact, // 联系人信息
           "Apach 2.0 许可", // 许可
           "许可链接", // 许可连接
           new ArrayList<>()// 扩展
  );
}

Docket 实例关联上 apiInfo()

@Bean
public Docket docket() {
     
   return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}

重启项目,访问测试 http://localhost:8080/swagger-ui.html 看下效果;

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第28张图片
需要注意的是,ApiInfo 源码没有给属性提供set方法,必须使用构造器,因此,不重要的信息我们只能使用空串占位了,醉了

配置扫描接口

由于我们在controller中,给方法使用@RequestMapping,没有指明具体的请求方式,所以这里出现了这么多接口,因此推荐注解中指明具体的请求方式
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第29张图片
为什么我们要访问 /swagger-ui.html 路径,看源码就知道了
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第30张图片

  1. 构建Docket时通过select()方法配置怎么扫描接口。
@Bean
public Docket docket() {
     
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.swy.controller"))
      .build();
}

通过 select 调出 apis 调出 build

除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:

any() // 扫描所有,项目中的所有接口都会被扫描到
none() // 不扫描接口
// 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求
withMethodAnnotation(final Class<? extends Annotation> annotation)
// 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
withClassAnnotation(final Class<? extends Annotation> annotation)//这里要使用注解的class
basePackage(final String basePackage) // 根据包路径扫描接口
  1. 除此之外,我们还可以配置接口扫描过滤:
@Bean
public Docket docket() {
     
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.swy.controller"))
       // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
      .paths(PathSelectors.ant("/kuang/**"))
      .build();
}

这里的可选值还有

any() // 任何请求都扫描
none() // 任何请求都不扫描
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制

配置Swagger开关

  1. 通过enable()方法配置是否启用swagger,如果是false,swagger将不能在浏览器中访问了

注意:select后面接了一套方法,因此我们要向做其他配置应该在select前面加

@Bean
public Docket docket() {
     
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .enable(false) //配置是否启用Swagger,如果是false,在浏览器将无法访问
      .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.swy.controller"))
       // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
      .paths(PathSelectors.ant("/kuang/**"))
      .build();
}
  1. 如何动态配置当项目处于test、dev环境时显示swagger,处于prod时不显示?

通过判断当前的配置环境,来返回 true 或 false,决定是否启用swagger,添加 Environment 可以获取当前环境

还需要我们在配置文件中添加当前的环境名称比如,dev test prod

@Bean
public Docket docket(Environment environment) {
     
   // 设置要显示swagger的环境
   Profiles of = Profiles.of("dev", "test");
   // 判断当前是否处于该环境
   // 通过 enable() 接收此参数判断是否要显示
   boolean b = environment.acceptsProfiles(of);
   
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .enable(b) //配置是否启用Swagger,如果是false,在浏览器将无法访问
      .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
       // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
      .paths(PathSelectors.ant("/kuang/**"))
      .build();
}

其实不一定非要使用 Environment ,properties文件也可以将外部环境的参数传进来

配置API分组

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第31张图片

  1. 如果没有配置分组,默认是default。通过groupName()方法即可配置分组
@Bean
public Docket docket(Environment environment) {
     
   return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
      .groupName("hello") // 配置分组
       // 省略配置....
}
  1. 如何配置多个分组?配置多个分组只需要配置多个docket即可

各组扫描各自的接口

@Bean
public Docket docket1(){
     
   return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
@Bean
public Docket docket2(){
     
   return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}
@Bean
public Docket docket3(){
     
   return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
}

实体配置

配置我们的实体类,可以在 swagger 上看到我们的实体类信息

  1. 新建一个实体类
@ApiModel("用户实体")
public class User {
     
   @ApiModelProperty("用户名")
   public String username;
   @ApiModelProperty("密码")
   public String password;
}
  1. 只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:
@RequestMapping("/getUser")
public User getUser(){
     
   return new User();
}

注意:并不是因为@ApiModel这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,

  • @ApiModel和@ApiModelProperty这两个注解只是为实体添加注释的。
  • @ApiModel为类添加注释
  • @ApiModelProperty为类属性添加注释

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第32张图片

常用注解

Swagger的所有注解定义在io.swagger.annotations包下

下面列一些经常用到的,未列举出来的可以另行查阅说明:

Swagger注解 简单说明
@Api(tags = “xxx模块说明”) 作用在模块类上
@ApiOperation(“xxx接口说明”) 作用在接口方法上
@ApiModel(“xxxPOJO说明”) 作用在模型类上:如VO、BO
@ApiModelProperty(value = “xxx属性说明”,hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性
@ApiParam(“xxx参数说明”) 作用在参数、方法和字段上,类似@ApiModelProperty

我们也可以给请求的接口配置一些注释

@ApiOperation("狂神的接口")
@PostMapping("/kuang")
@ResponseBody
public String kuang(@ApiParam("这个名字会被返回")String username){
     
   return username;
}

我们还可以使用swagger模拟请求测试
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第33张图片

这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!

相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。

Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。

拓展:其他皮肤

我们可以导入不同的包实现不同的皮肤定义:

默认的 访问 http://localhost:8080/swagger-ui.html

<dependency>
   <groupId>io.springfoxgroupId>
   <artifactId>springfox-swagger-uiartifactId>
   <version>2.9.2version>
dependency>
  1. bootstrap-ui 访问 http://localhost:8080/doc.html

<dependency>
   <groupId>com.github.xiaoymingroupId>
   <artifactId>swagger-bootstrap-uiartifactId>
   <version>1.9.1version>
dependency>

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第34张图片
3. Layui-ui 访问 http://localhost:8080/docs.html


<dependency>
   <groupId>com.github.caspar-chengroupId>
   <artifactId>swagger-ui-layerartifactId>
   <version>1.1.3version>
dependency>

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第35张图片
4. mg-ui 访问 http://localhost:8080/document.html


<dependency>
   <groupId>com.zyplayergroupId>
   <artifactId>swagger-mg-uiartifactId>
   <version>1.0.6version>
dependency>

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第36张图片

十四、异步、定时、邮件任务

在我们的工作中,常常会用到异步处理任务,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。还有一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息。还有就是邮件的发送,微信的前身也是邮件服务呢?这些东西都是怎么实现的呢?其实SpringBoot都给我们提供了对应的支持,我们上手使用十分的简单,只需要开启一些注解支持,配置一些配置文件即可!那我们来看看吧~

1.异步任务

新建项目 springboot-09-test
在这里插入图片描述

  1. 创建一个service包
  2. 创建一个类AsyncService

异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。

编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;

@Service
public class AsyncService {
     
   public void hello(){
     
       try {
     
           Thread.sleep(3000);
      } catch (InterruptedException e) {
     
           e.printStackTrace();
      }
       System.out.println("业务进行中....");
  }
}
  1. 编写controller包

  2. 编写AsyncController类

我们去写一个Controller测试一下

@RestController
public class AsyncController {
     
   @Autowired
   AsyncService asyncService;
   @GetMapping("/hello")
   public String hello(){
     
       asyncService.hello();
       return "success";
  }
}
  1. 访问http://localhost:8080/hello进行测试,3秒后出现success,这是同步等待的情况。

问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:

  1. 给hello方法添加@Async注解;
//告诉Spring这是一个异步方法
@Async
public void hello(){
     
   try {
     
       Thread.sleep(3000);
  } catch (InterruptedException e) {
     
       e.printStackTrace();
  }
   System.out.println("业务进行中....");
}

SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;

主启动类添加注解

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

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

}

启动测试,我们发现请求立刻既可以得到相应,打印 “业务进行中”,但是中间的异步线程业务则继续执行业务

2.邮件任务

邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持

  • 邮件发送需要引入spring-boot-start-mail
  • SpringBoot 自动配置MailSenderAutoConfiguration
  • 定义MailProperties内容,配置在application.yml中
  • 自动装配JavaMailSender

这样我们可以在我们的Java项目登录我们的邮箱,向外界发送邮件

  1. 引入pom依赖
<dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-mailartifactId>
dependency>

看它引入的依赖,可以看到 jakarta.mail

<dependency>
   <groupId>com.sun.mailgroupId>
   <artifactId>jakarta.mailartifactId>
   <version>1.6.4version>
   <scope>compilescope>
dependency>
  1. 查看自动配置类:MailSenderAutoConfiguration

这个类中存在bean,JavaMailSenderImpl

然后我们去看下配置文件

@ConfigurationProperties(
   prefix = "spring.mail"
)
public class MailProperties {
     
   private static final Charset DEFAULT_CHARSET;
   private String host;
   private Integer port;
   private String username;
   private String password;
   private String protocol = "smtp";
   private Charset defaultEncoding;
   private Map<String, String> properties;
   private String jndiName;
}
  1. 配置文件:

比如我们使用qq邮箱发送

# 邮箱账户
[email protected]
# qq邮箱提供的授权码
spring.mail.password=你的qq授权码
# 发送邮件所需的服务器
spring.mail.host=smtp.qq.com
# qq需要配置ssl开启加密验证(qq特有)
spring.mail.properties.mail.smtp.ssl.enable=true

获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第37张图片

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第38张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第39张图片
修改springboot测试类

@Autowired
JavaMailSenderImpl mailSender;

@Test
public void contextLoads() {
     
   //邮件设置1:一个简单的邮件
   SimpleMailMessage message = new SimpleMailMessage();
   message.setSubject("通知-明天来狂神这听课");
   message.setText("今晚7:30开会");
   message.setTo("[email protected]");
   message.setFrom("[email protected]");
   mailSender.send(message);
}
@Test
public void contextLoads2() throws MessagingException {
     
   //邮件设置2:一个复杂的邮件
   MimeMessage mimeMessage = mailSender.createMimeMessage();
   MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
   helper.setSubject("通知-明天来狂神这听课");
   helper.setText("今天 7:30来开会",true);// 开启解析html
   //发送附件
   helper.addAttachment("1.jpg",new File("D:\picture\1.jpg"));
   helper.addAttachment("2.jpg",new File("D:\picture\2.jpg"));
   helper.setTo("[email protected]");
   helper.setFrom("[email protected]");
   mailSender.send(mimeMessage);
}

附件路径可以写绝对路径

测试发送,然后查看邮箱
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第40张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第41张图片
我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了

3.定时任务

项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。

自带功能,不用添加依赖,拿来即用,

  • TaskExecutor接口,任务执行
  • TaskScheduler接口,任务调度

两个注解:

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

cron表达式:

字段 允许值 允许特殊字符
0-59 , - * /
0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * / ? L W C
月份 1-12 , - * /
星期 0-1或SUN-SAT 0,7是SUN , - * / ? L W C
特殊字符 代表含义
, 枚举
- 区间
* 任意
/ 步长
? 日/星期冲突匹配
L 最后
W 工作日
C 和calendar练习后计算过的值
# 星期,4#2 第二个星期三

具体使用技巧可以在百度搜多到很多资料

测试步骤:

  1. 创建一个ScheduledService

我们里面存在一个hello方法,他需要定时执行,怎么处理呢?

@Service
public class ScheduledService {
     
    // 在一个特定的时间执行这个方法——Timer
    //cron表达式
    // 秒 分 时 日 月 周几
    /*
        0 49 11 * * ?   每天的11点49分00秒执行
        0 0/5 11,12 * * ?   每天的11点和12点每个五分钟执行一次
        0 15 10 ? * 1-6     每个月的周一到周六的10点15分执行一次
        0/2 * * * * ?     每2秒执行一次
     */
    @Scheduled(cron = "0/2 * * * * ?")
    public void hello() {
     
        System.out.println("hello,你被执行了");
    }
}
  1. 这里写完定时任务之后,我们需要在主程序上增加@EnableScheduling 开启定时任务功能
@EnableAsync //开启异步注解功能
@EnableScheduling //开启基于注解的定时任务
@SpringBootApplication
public class SpringbootTaskApplication {
     
   public static void main(String[] args) {
     
       SpringApplication.run(SpringbootTaskApplication.class, args);
  }
}

启动测试,只要程序启动,定时任务就会自动执行,不需要其他程序再触发
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第42张图片

  1. 详细了解下cron表达式;

http://www.bejson.com/othertools/cron/

  1. 常用的表达式
(1)0/2 * * * * ?   表示每2秒 执行任务
(1)0 0/2 * * * ?   表示每2分钟 执行任务
(1)0 0 2 1 * ?   表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ?   每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED   表示每个星期三中午12点
(7)0 0 12 * * ?   每天中午12点触发
(8)0 15 10 ? * *   每天上午10:15触发
(9)0 15 10 * * ?     每天上午10:15触发
(10)0 15 10 * * ?   每天上午10:15触发
(11)0 15 10 * * ? 2005   2005年的每天上午10:15触发
(12)0 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ?   在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ?   在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED   每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI   周一至周五的上午10:15触发
(18)0 15 10 15 * ?   每月15日上午10:15触发
(19)0 15 10 L * ?   每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L   每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3   每月的第三个星期五上午10:15触发

十五、SpringBoot 整合 Redis

SpringBoot 操作数据:spring-data jpa jdbc mongodb redis

springdata 也是和 springboot 齐名的项目

官网:https://spring.io/projects/spring-data-mongodb

项目准备

创建springboot 项目,spring-10-redis
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第43张图片
说明:

  • 在springboot 2.x.x 之后,原来使用的jedis换成了lettuce
  • jedis:采用的直连,多个线程操作的时候不安全,要想避免不安全,需要使用jedis pool,但是线程量大时也不好用,像 BIO模式
  • lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况,可以减少线程数量,更像 NIO 模式

项目配置

redis配置:

  • springboot 中所有的配置类都有一个自动配置类,自动配置类都会绑定一个 properties 配置文件,如何配置redis需要仔细查看这两个文件
  • RedisAutoConfigurationRedisProperties
  • springdata使用redistemplate操作redis数据库
    源码分析:
	@Bean
    @ConditionalOnMissingBean( // 当我们定义template时,这个默认的就会失效,通常我们都会自己定义
        name = {
     "redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
     
    	// 默认的redistemplate 没有过多设置,redis对象都是需要序列化的
    	// 两个泛型都是object,因此我们需要强转
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean // 由于string使我们的最长使用类型,所以单独提取一个方法,如果操作string直接用这个方法
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
     
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

整合测试

  1. 导入依赖
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>
  1. 配置连接

如果使用连接池,推荐lettuce,不在推荐使用jedis,而且jedis连接池已经没有源码,使用可能出问题

spring.redis.host=127.0.0.1
spring.redis.port=6379
  1. 启动测试

修改springboot测试类

@Autowired
    private RedisTemplate redisTemplate;
    @Test
    void contextLoads() {
     
        // redistemplate方法
        // opsForValue opsForList opsForSet opsForHash opsForZSet opsForGeo opsForValue opsForHyperLogLog
        // 每一个方法对应操作不同数据类型,通过连式编程继续对本类型的数据进行处理
        // api和我们的指令是一样的
        // 除了基本操作,一些常用的操作也被单独设置了

        // 获取redis连接对象
        // RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        // connection.flushDb();
        // connection.flushAll();
        redisTemplate.opsForValue().set("mykey", "swy");
        System.out.println(redisTemplate.opsForValue().get("mykey"));
    }
}

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第44张图片

redis 序列化问题

查看 redistemplate 源码,可以看到,redistemplate中有序列化参数,

为什么默认情况下当我们向redis中存储中文时会出现乱码

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第45张图片
继续往下看源码,可以发现redistemplate默认使用了jdk序列化,jdk序列化会让字符串转义,
在这里插入图片描述
Redis配置类

我们可以自己创建一个配置类,RedisConfig,实现redis配置,包括重建 redistemplate

@Configuration
public class RedisConfig {
     
    // 重建 RedisTemplate
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
     
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 详细配置template...
        return template;
    }
}

准备一个实体类,这里先不序列化,在template中处理

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
     
    private String name;
    private int age;
}

添加测试类方法,向redis中保存对象

首先我们通过对象转json字符串的方式传递

	@Test
    void contextLoads1() throws JsonProcessingException {
     
        // 真实开发,一般使用json来传递对象
        User user = new User("泥萌好", 18);
        // 将对象序列化,springboot提供了jackon功能实现
        String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user", jsonUser);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

保存成功
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第46张图片

关于对象保存:

  • 如果这里我们直接向template中保存user对象,运行时就会报异常,没有序列化,所以对象需要序列化
  • 实现对象的序列化,我们可以通过实体类实现序列化接口Serializable的方法
  • 经过序列化的对象,到了redis这里如何解析?redis默认jdk序列化,无法解析,因此我们还需要在配置类中配置redis对应的序列化方式

自定义 RedisTemplate 配置

为了解决序列化问题,redis默认template已经不能满足需要,需要我们自定义redistemplate

以下是企业常用的固定模板redistemplate配置,可以拿来即用

@Configuration
public class RedisConfig {
     
    // 自定义RedisTemplate,这是企业中常用的固定模板,拿来即用
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
     
        // redistemplate默认泛型为,我们为了使用方便,采用了
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // jackson的序列化配置
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);//object指序列化针对任意对象
        // 对象转json时涉及到转义,所以还要再配置 ObjectMapper
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // String的序列化配置
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用string的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化采用jackson
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        // hash的value序列化也采用jackson
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

使用自定义 RedisTemplate

当我们配置了自己的 redistemplate 之后,当我们再次注入 RedisTemplate 属性,此时就是我们自己配置的redistemplate

我们可以点击注入属性左侧的spring图标进入查看

由于底层也有默认的redistemplate,如果担心重名,我们也可以在属性上添加注解@Qualifier来指定名字

现在,我们拿到实体对象之后不再转义,直接将对象作为参数传递

	@Autowired
    private RedisTemplate redisTemplate;
	@Test
    void contextLoads1() throws JsonProcessingException {
     
        // 真实开发,一般使用json来传递对象
        User user = new User("泥萌好", 18);
        // 将对象序列化,springboot提供了jackon功能实现
        //String jsonUser = new ObjectMapper().writeValueAsString(user);

        // 直接传递对象
        redisTemplate.opsForValue().set("user", user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

启动测试,
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第47张图片
效果非常良好,如果使用的是jdk序列化,保存redis还会出现转义符,非常麻烦

自定义 Redis 工具类

RedisTemplate 提供的原生方法比较麻烦,还要链式编程写很多代码,通常我们不直接使用,而是自己封装一个工具类,方便使用

附上Redis工具类

@Component
public final class RedisUtil {
     
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
     
        try {
     
            if (time > 0) {
     
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
     
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
     
        try {
     
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
     
        if (key != null && key.length > 0) {
     
            if (key.length == 1) {
     
                redisTemplate.delete(key[0]);
            } else {
     
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }
    // ============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
     
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
     
        try {
     
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
     
        try {
     
            if (time > 0) {
     
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
     
                set(key, value);
            }
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
     
        if (delta < 0) {
     
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
     
        if (delta < 0) {
     
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    // ================================Map=================================
    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
     
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
     
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
     
        try {
     
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
     
        try {
     
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
     
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
     
        try {
     
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
     
        try {
     
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
     
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
     
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
     
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
     
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
     
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set<Object> sGet(String key) {
     
        try {
     
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
     
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
     
        try {
     
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
     
        try {
     
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
     
        try {
     
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
     
        try {
     
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
     
        try {
     
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================
    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
     
        try {
     
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
     
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
     
        try {
     
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
     
        try {
     
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
     
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
     
        try {
     
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
     
        try {
     
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
     
        try {
     
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }

    }
    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
     
        try {
     
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
     
        try {
     
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
     
        try {
     
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }
}

十六、分布式 Dubbo+Zookeeper

1.分布式理论

什么是分布式系统?

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

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

分布式系统(distributed system)是建立在网络之上的软件系统。

首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。

Dubbo文档

https://dubbo.apache.org/zh/docs/v2.7/user/

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。

在Dubbo的官网文档有这样一张图
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第48张图片

单一应用架构

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

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第49张图片
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。

缺点:

1、性能扩展比较难

2、协同开发问题

3、不利于升级维护

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第50张图片
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。

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

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第51张图片

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第52张图片

2.RPC

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

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第53张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第54张图片
RPC两个核心模块:通讯,序列化。

对象想要传输必须先序列化,

3.Dubbo

Dubbo 概念

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

dubbo官网 http://dubbo.apache.org/zh-cn/index.html

1.了解Dubbo的特性

2.查看官方文档
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第55张图片
这张图很重要,需要深刻理解

  • 服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
  • 服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  • 注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
  • 监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

调用关系说明

  • 服务容器负责启动,加载,运行服务提供者。
  • 服务提供者在启动时,向注册中心注册自己提供的服务。
  • 服务消费者在启动时,向注册中心订阅自己所需的服务。
  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  • 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

Dubbo环境搭建

点进dubbo官方文档,推荐我们使用Zookeeper 注册中心
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第56张图片
什么是zookeeper呢?https://zookeeper.apache.org/

Window下安装zookeeper

下载zookeeper :https://mirror.bit.edu.cn/apache/zookeeper/, 我们下载3.6.1, 最新版!解压zookeeper

下载带bin的

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第57张图片
运行/bin/zkServer.cmd ,初次运行会报错,没有zoo.cfg配置文件;

可能遇到问题:闪退 !

解决方案:编辑zkServer.cmd文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因。
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第58张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第59张图片
修改zoo.cfg配置文件

  • 将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。

  • 注意几个重要位置:
    dataDir=./ 临时数据存储的目录(可写相对路径)
    clientPort=2181 zookeeper的端口号

  • 修改完成后再次启动zookeeper
    SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第60张图片
    使用zkCli.cmd测试

ls /:列出zookeeper根下保存的所有节点
在这里插入图片描述
报错别着急,多试几次就好了

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第61张图片
create –e /kuangshen 123:创建一个kuangshen节点,值为123

在这里插入图片描述
get /kuangshen:获取/kuangshen节点的值

在这里插入图片描述
我们再来查看一下节点:ls /
在这里插入图片描述

window下安装dubbo-admin

dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。

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

我们这里来安装一下:

  1. 下载dubbo-admin

地址 :https://github.com/apache/dubbo-admin/tree/master

  1. 解压进入目录

修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址

server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
# 注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

  1. 在项目目录下打包dubbo-admin
mvn clean package -Dmaven.test.skip=true

第一次打包的过程有点慢,需要耐心等待!直到成功!

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第62张图片

  1. 执行 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;

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第63张图片
登录成功后,查看界面
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第64张图片
安装完成!

总结:

  • zookeeper :注册中心
  • dubbo-admin:是一 个监控管理后台-查看我们注册了哪些服务,哪些服务被消费了(可以不用)
  • Dubbo: jar包

4.SpringBoot + Dubbo + zookeeper

项目搭建

  1. 启动zookeeper !

  2. IDEA创建一个空项目;

  3. 创建一个模块,实现服务提供者:provider-server , 选择web依赖即可

  4. 项目创建完毕,我们写一个服务,比如卖票的服务;

编写接口

package nuc.ss.service;

public interface TicketService {
     
   public String getTicket();
}

编写实现类

package nuc.ss.service;

public class TicketServiceImpl implements TicketService {
     
    @Override
    public String getTicket() {
     
        return "《狂神说Java》";
    }
}
  1. 创建一个模块,实现服务消费者:consumer-server , 选择web依赖即可

  2. 项目创建完毕,我们写一个服务,比如用户的服务;

编写service

package nuc.ss.service;

public interface UserService {
     
   //我们需要去拿去注册中心的服务
}

需求:现在我们的用户想使用买票的服务,这要怎么弄呢 ?

服务提供者

  1. 将服务提供者注册到注册中心,我们需要整合Dubbo和zookeeper,所以需要导包

我们从dubbo官网进入github,看下方的帮助文档,找到dubbo-springboot,找到依赖包


<dependency>
   <groupId>org.apache.dubbogroupId>
   <artifactId>dubbo-spring-boot-starterartifactId>
   <version>2.7.3version>
dependency>    

zookeeper的包我们去maven仓库下载,zkclient;


<dependency>
   <groupId>com.github.sgroschupfgroupId>
   <artifactId>zkclientartifactId>
   <version>0.1version>
dependency>

【新版的坑】zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖;


<dependency>
   <groupId>org.apache.curatorgroupId>
   <artifactId>curator-frameworkartifactId>
   <version>2.12.0version>
dependency>
<dependency>
   <groupId>org.apache.curatorgroupId>
   <artifactId>curator-recipesartifactId>
   <version>2.12.0version>
dependency>
<dependency>
   <groupId>org.apache.zookeepergroupId>
   <artifactId>zookeeperartifactId>
   <version>3.4.14version>
   
   <exclusions>
       <exclusion>
           <groupId>org.slf4jgroupId>
           <artifactId>slf4j-log4j12artifactId>
       exclusion>
   exclusions>
dependency>
  1. 在springboot配置文件中配置dubbo相关属性!
server.port=8001

#当前应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#扫描指定包下服务
dubbo.scan.base-packages=nuc.ss.service
  1. 在service的实现类中配置服务注解,发布服务!注意导包问题
package nuc.ss.service;

import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;

@Service    //可以被扫描到,在项目一启动就自动注册到注册中心
@Component  //使用Dubbo后尽量不要用Service注解
public class TicketServiceImpl implements TicketService {
     
    @Override
    public String getTicket() {
     
        return "《狂神说Java》";
    }
}

逻辑理解 :应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!

  1. 运行测试

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第65张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第66张图片
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第67张图片

服务消费者

  1. 导入依赖,和之前的依赖一样;


<dependency>
   <groupId>org.apache.dubbogroupId>
   <artifactId>dubbo-spring-boot-starterartifactId>
   <version>2.7.3version>
dependency>


<dependency>
   <groupId>com.github.sgroschupfgroupId>
   <artifactId>zkclientartifactId>
   <version>0.1version>
dependency>

<dependency>
   <groupId>org.apache.curatorgroupId>
   <artifactId>curator-frameworkartifactId>
   <version>2.12.0version>
dependency>
<dependency>
   <groupId>org.apache.curatorgroupId>
   <artifactId>curator-recipesartifactId>
   <version>2.12.0version>
dependency>
<dependency>
   <groupId>org.apache.zookeepergroupId>
   <artifactId>zookeeperartifactId>
   <version>3.4.14version>
   
   <exclusions>
       <exclusion>
           <groupId>org.slf4jgroupId>
           <artifactId>slf4j-log4j12artifactId>
       exclusion>
   exclusions>
dependency>
  1. 配置参数

消费者到注册中心去拿服务

server.port=8002

#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
  1. 本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;
  2. SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第68张图片
  3. 完善消费者的服务类
package nuc.ss.service;

import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;

@Service //注入到容器中
public class UserService {
     
    // 想拿到provider-server提供的票,要去注册中心拿到服务
    @Reference //引用,Pom坐标,可以定义路径相同的接口名
            TicketService ticketService;

    public void bugTicket(){
     
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心买到"+ticket);
    }
}

直接使用@Reference注入拿不到提供者的对象,有两种办法解决

第一种,在本服务中定义一个和目标借口一样的接口,包括路径,名字,方法

  1. 测试类编写;
@SpringBootTest
public class ConsumerServerApplicationTests {
     
   @Autowired
   UserService userService;
   @Test
   public void contextLoads() {
     
       userService.bugTicket();
  }
}

启动测试

开启zookeeper

  1. 打开dubbo-admin实现监控【可以不用做】

  2. 开启服务者

  3. 消费者消费测试,结果:

SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第69张图片
监控中心 :
SpringBoot 教程详解 项目演示 资源整合 个人总结 狂神笔记(下)_第70张图片

小结

dubbo 使用步骤

1.提供者提供服务

  • 导入依赖
  • 配置注册中心地址、服务名、要扫描的包
  • 在需要被注册的服务上添加注解 @Service

2.消费者消费服务

  • 导入依赖
  • 配置注册中心的地址,配置自己的服务名
  • 从远程服务上获取服务并注入到本服务

注意:

  • 确保zookeeper已提前开启,没有注册中心,谈何远程调用
  • 这里的@Service是阿里提供的,不要和spring的混淆,否则服务不起作用
  • 服务消费者要准备和远程服务一样的接口,包括路径、方法,才能实现远程调用

富文本编辑器

狂神说-富文本编辑器

你可能感兴趣的:(Spring,系列,spring,boot,java)