框架

SpringBoot整合MyBatis

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

1.背景介绍

Spring Boot是一个用于构建Spring应用程序的快速开始点,它提供了一些默认配置,使得开发人员可以快速地开始编写代码,而不需要关心复杂的配置。Spring Boot的目标是简化Spring应用程序的开发,使其更加易于部署和扩展。

MyBatis是一个基于Java的持久层框架,它提供了一个简单的API,使得开发人员可以更容易地访问数据库。MyBatis支持多种数据库,包括MySQL、Oracle、SQL Server等。

Spring Boot整合MyBatis是一种将Spring Boot与MyBatis框架结合使用的方法,以便更简单地访问数据库。这种整合方式可以让开发人员更容易地编写数据库访问代码,并且可以提高代码的可读性和可维护性。

在本文中,我们将讨论如何将Spring Boot与MyBatis整合,以及如何编写数据库访问代码。我们将讨论MyBatis的核心概念,以及如何使用MyBatis进行数据库操作。我们还将讨论如何使用Spring Boot的依赖管理功能,以及如何配置MyBatis的数据源。

2.核心概念与联系

在本节中,我们将介绍Spring Boot和MyBatis的核心概念,以及它们之间的联系。

2.1 Spring Boot

Spring Boot是一个用于构建Spring应用程序的快速开始点,它提供了一些默认配置,使得开发人员可以快速地开始编写代码,而不需要关心复杂的配置。Spring Boot的目标是简化Spring应用程序的开发,使其更加易于部署和扩展。

Spring Boot提供了一些内置的功能,例如:

  • 自动配置:Spring Boot可以自动配置大量的Spring组件,例如数据源、缓存、日志等。
  • 依赖管理:Spring Boot可以自动管理依赖关系,例如数据库驱动程序、Web框架等。
  • 嵌入式服务器:Spring Boot可以自动启动嵌入式服务器,例如Tomcat、Jetty等。
  • 健康检查:Spring Boot可以自动检查应用程序的健康状态,例如内存使用、CPU使用等。

2.2 MyBatis

MyBatis是一个基于Java的持久层框架,它提供了一个简单的API,使得开发人员可以更容易地访问数据库。MyBatis支持多种数据库,包括MySQL、Oracle、SQL Server等。

MyBatis的核心概念包括:

  • SQL映射:MyBatis提供了一种称为SQL映射的机制,用于将SQL查询映射到Java对象。
  • 动态SQL:MyBatis提供了一种称为动态SQL的机制,用于生成动态SQL查询。
  • 缓存:MyBatis提供了一种称为缓存的机制,用于存储查询结果,以便在后续查询中重用。

2.3 Spring Boot与MyBatis的联系

Spring Boot与MyBatis之间的联系是,Spring Boot可以轻松地整合MyBatis,以便更简单地访问数据库。通过使用Spring Boot的依赖管理功能,开发人员可以轻松地添加MyBatis的依赖关系。通过使用Spring Boot的自动配置功能,开发人员可以轻松地配置MyBatis的数据源。

3.核心算法原理和具体操作步骤以及数学模型公式详细讲解

在本节中,我们将详细讲解MyBatis的核心算法原理,以及如何使用MyBatis进行数据库操作。

3.1 SQL映射

MyBatis提供了一种称为SQL映射的机制,用于将SQL查询映射到Java对象。SQL映射是MyBatis中最重要的概念之一,它允许开发人员将SQL查询与Java对象进行关联,以便在执行查询时,MyBatis可以自动将查询结果映射到Java对象。

SQL映射是通过XML文件或注解来定义的。XML文件是MyBatis中的一个重要组件,用于定义SQL映射。通过使用XML文件,开发人员可以将SQL查询与Java对象进行关联,以便在执行查询时,MyBatis可以自动将查询结果映射到Java对象。

以下是一个简单的SQL映射示例:

<select id="selectUser" resultType="User">
  SELECT id, name, email FROM users WHERE id = #{id}
</select>

在上述示例中,我们定义了一个名为”selectUser”的SQL映射,它将查询结果映射到”User”类型的Java对象。通过使用#{id},我们可以将查询参数映射到Java对象的属性。

3.2 动态SQL

MyBatis提供了一种称为动态SQL的机制,用于生成动态SQL查询。动态SQL是MyBatis中的另一个重要概念,它允许开发人员根据查询参数生成动态SQL查询。

动态SQL可以通过以下方式来实现:

  • 使用if语句:通过使用if语句,开发人员可以根据查询参数生成动态SQL查询。
  • 使用choose语句:通过使用choose语句,开发人员可以根据查询参数选择不同的SQL查询。
  • 使用trim语句:通过使用trim语句,开发人员可以根据查询参数生成动态SQL查询的子句。

以下是一个简单的动态SQL示例:

<select id="selectUser" resultType="User">
  SELECT id, name, email FROM users WHERE 1=1
  <if test="id != null">
    AND id = #{id}
  </if>
</select>

在上述示例中,我们定义了一个名为”selectUser”的动态SQL查询,它根据查询参数生成动态SQL查询。通过使用标签,我们可以根据查询参数生成动态的AND子句。

3.3 缓存

MyBatis提供了一种称为缓存的机制,用于存储查询结果,以便在后续查询中重用。缓存是MyBatis中的一个重要概念,它可以提高查询性能,减少数据库访问次数。

MyBatis支持多种类型的缓存,包括:

  • 一级缓存:一级缓存是MyBatis的内部缓存,它存储在当前会话中。一级缓存可以存储查询结果,以便在后续查询中重用。
  • 二级缓存:二级缓存是MyBatis的外部缓存,它存储在应用程序的上下文中。二级缓存可以存储查询结果,以便在不同的会话中重用。

以下是一个简单的缓存示例:

<select id="selectUser" resultType="User" useCache="true">
  SELECT id, name, email FROM users WHERE id = #{id}
</select>

在上述示例中,我们定义了一个名为”selectUser”的查询,并使用useCache属性启用了缓存。通过使用useCache属性,我们可以启用一级缓存,以便在后续查询中重用查询结果。

4.具体代码实例和详细解释说明

在本节中,我们将通过一个具体的代码实例来演示如何使用Spring Boot整合MyBatis。

4.1 创建Spring Boot项目

首先,我们需要创建一个新的Spring Boot项目。我们可以使用Spring Initializr(https://start.spring.io/)来创建一个新的Spring Boot项目。在创建项目时,我们需要选择”Web”和”JPA”作为项目的依赖项。

4.2 添加MyBatis依赖

接下来,我们需要添加MyBatis的依赖关系。我们可以使用Maven或Gradle来管理依赖关系。以下是使用Maven添加MyBatis依赖关系的示例:

<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.1.4</version>
</dependency>

4.3 配置数据源

接下来,我们需要配置数据源。我们可以使用Spring Boot的内置数据源来配置数据源。以下是使用内置数据源配置MySQL数据源的示例:

spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?useSSL=false
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

4.4 创建实体类

接下来,我们需要创建实体类。实体类是MyBatis中的一个重要概念,它用于表示数据库表。以下是一个简单的实体类示例:

public class User {
  private int id;
  private String name;
  private String email;

  // getter and setter methods
}

4.5 创建Mapper接口

接下来,我们需要创建Mapper接口。Mapper接口是MyBatis中的一个重要概念,它用于定义数据库操作。以下是一个简单的Mapper接口示例:

public interface UserMapper {
  List<User> selectUsers();
}

4.6 创建Mapper实现类

接下来,我们需要创建Mapper实现类。Mapper实现类是MyBatis中的一个重要概念,它用于实现Mapper接口。以下是一个简单的Mapper实现类示例:

public class UserMapperImpl implements UserMapper {
  @Autowired
  private SqlSession sqlSession;

  @Override
  public List<User> selectUsers() {
    return sqlSession.selectList("com.example.UserMapper.selectUsers");
  }
}

4.7 使用Mapper实现类

最后,我们需要使用Mapper实现类。我们可以通过使用Spring的依赖注入来注入Mapper实现类。以下是使用Mapper实现类的示例:

@Autowired
private UserMapper userMapper;

public List<User> getUsers() {
  return userMapper.selectUsers();
}

5.未来发展趋势与挑战

在本节中,我们将讨论Spring Boot与MyBatis的未来发展趋势和挑战。

5.1 未来发展趋势

Spring Boot与MyBatis的未来发展趋势包括:

  • 更好的集成:Spring Boot和MyBatis之间的集成将会越来越好,以便更简单地访问数据库。
  • 更好的性能:Spring Boot和MyBatis的性能将会越来越好,以便更快地访问数据库。
  • 更好的可用性:Spring Boot和MyBatis的可用性将会越来越好,以便更多的开发人员可以使用它们。

5.2 挑战

Spring Boot与MyBatis的挑战包括:

  • 学习曲线:Spring Boot和MyBatis的学习曲线可能会比其他框架和技术更陡峭,需要更多的时间和精力来学习。
  • 性能问题:Spring Boot和MyBatis的性能可能会比其他框架和技术更差,需要更多的优化工作来提高性能。
  • 兼容性问题:Spring Boot和MyBatis可能会与其他框架和技术存在兼容性问题,需要更多的工作来解决这些问题。

6.附录常见问题与解答

在本节中,我们将讨论Spring Boot与MyBatis的常见问题和解答。

6.1 问题1:如何配置数据源?

答案:我们可以使用Spring Boot的内置数据源来配置数据源。以下是使用内置数据源配置MySQL数据源的示例:

spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?useSSL=false
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

6.2 问题2:如何创建实体类?

答案:实体类是MyBatis中的一个重要概念,它用于表示数据库表。以下是一个简单的实体类示例:

public class User {
  private int id;
  private String name;
  private String email;

  // getter and setter methods
}

问题3:如何创建Mapper接口?

答案:Mapper接口是MyBatis中的一个重要概念,它用于定义数据库操作。以下是一个简单的Mapper接口示例:

public interface UserMapper {
  List<User> selectUsers();
}

问题4:如何创建Mapper实现类?

答案:Mapper实现类是MyBatis中的一个重要概念,它用于实现Mapper接口。以下是一个简单的Mapper实现类示例:

public class UserMapperImpl implements UserMapper {
  @Autowired
  private SqlSession sqlSession;

  @Override
  public List<User> selectUsers() {
    return sqlSession.selectList("com.example.UserMapper.selectUsers");
  }
}

问题5:如何使用Mapper实现类?

答案:我们可以通过使用Spring的依赖注入来注入Mapper实现类。以下是使用Mapper实现类的示例:

@Autowired
private UserMapper userMapper;

public List<User> getUsers() {
  return userMapper.selectUsers();
}

7.总结

在本文中,我们详细介绍了如何将Spring Boot与MyBatis整合,以及如何编写数据库访问代码。我们介绍了MyBatis的核心概念,以及如何使用MyBatis进行数据库操作。我们还讨论了Spring Boot与MyBatis的未来发展趋势和挑战。最后,我们回顾了Spring Boot与MyBatis的常见问题和解答。我们希望这篇文章对您有所帮助。

Shiro安全框架【快速入门】就这一篇!

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

Shiro 简介

照例又去官网扒了扒介绍:

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro™是一个强大且易用的Java安全框架,能够用于身份验证、授权、加密和会话管理。Shiro拥有易于理解的API,您可以快速、轻松地获得任何应用程序——从最小的移动应用程序到最大的网络和企业应用程序。

简而言之,Apache Shiro 是一个强大灵活的开源安全框架,可以完全处理身份验证、授权、加密和会话管理。

Shiro能到底能做些什么呢?

  • 验证用户身份
  • 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
  • 在非 Web 或 EJB 容器的环境下可以任意使用Session API
  • 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
  • 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
  • 支持单点登录(SSO)功能
  • 支持提供“Remember Me”服务,获取用户关联信息而无需登录
    ···

为什么是 Shiro?

使用 Shiro 官方给了许多令人信服的原因,因为 Shiro 具有以下几个特点:

  • 易于使用——易用性是项目的最终目标。应用程序安全非常令人困惑和沮丧,被认为是“不可避免的灾难”。如果你让它简化到新手都可以使用它,它就将不再是一种痛苦了。
  • 全面——没有其他安全框架的宽度范围可以同Apache Shiro一样,它可以成为你的“一站式”为您的安全需求提供保障。
  • 灵活——Apache Shiro可以在任何应用程序环境中工作。虽然在网络工作、EJB和IoC环境中可能并不需要它。但Shiro的授权也没有任何规范,甚至没有许多依赖关系。
  • Web支持——Apache Shiro拥有令人兴奋的web应用程序支持,允许您基于应用程序的url创建灵活的安全策略和网络协议(例如REST),同时还提供一组JSP库控制页面输出。
  • 低耦合——Shiro干净的API和设计模式使它容易与许多其他框架和应用程序集成。你会看到Shiro无缝地集成Spring这样的框架, 以及Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin…等。
  • 被广泛支持——Apache Shiro是Apache软件基金会的一部分。项目开发和用户组都有友好的网民愿意帮助。这样的商业公司如果需要Katasoft还提供专业的支持和服务。

有兴趣的可以去仔细看看官方的文档:【传送门】

Apache Shiro Features 特性

Apache Shiro是一个全面的、蕴含丰富功能的安全框架。下图为描述Shiro功能的框架图:

Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:

  • Authentication(认证):用户身份识别,通常被称为用户“登录”
  • Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
  • Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
  • Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。

还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:

  • Web支持:Shiro的Web支持API有助于保护Web应用程序。
  • 缓存:缓存是Apache Shiro API中的第一级,以确保安全操作保持快速和高效。
  • 并发性:Apache Shiro支持具有并发功能的多线程应用程序。
  • 测试:存在测试支持,可帮助您编写单元测试和集成测试,并确保代码按预期得到保障。
  • “运行方式”:允许用户承担另一个用户的身份(如果允许)的功能,有时在管理方案中很有用。
  • “记住我”:记住用户在会话中的身份,所以用户只需要强制登录即可。

注意: Shiro不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro

High-Level Overview 高级概述

在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。

  • Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
  • Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。

我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。

Shiro 认证过程

上图展示了 Shiro 认证的一个重要的过程,为了加深我们的印象,我们来自己动手来写一个例子,来验证一下,首先我们新建一个Maven工程,然后在pom.xml中引入相关依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

新建一个【AuthenticationTest】测试类:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;

public class AuthenticationTest {

    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before // 在方法开始前添加一个用户
    public void addUser() {
        simpleAccountRealm.addAccount("wmyskxz", "123456");
    }

    @Test
    public void testAuthentication() {

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登录

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true

        subject.logout(); // 登出

        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出false
    }
}

运行之后可以看到预想中的效果,先输出isAuthenticated:true表示登录认证成功,然后再输出isAuthenticated:false表示认证失败退出登录,再来一张图加深一下印象:

流程如下:

  1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
  2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

Shiro 授权过程

跟认证过程大致相似,下面我们仍然通过代码来熟悉一下过程(引入包类似这里节约篇幅就不贴出来了):

public class AuthenticationTest {

    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before // 在方法开始前添加一个用户,让它具备admin和user两个角色
    public void addUser() {
        simpleAccountRealm.addAccount("wmyskxz", "123456", "admin", "user");
    }

    @Test
    public void testAuthentication() {

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登录

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
        // 判断subject是否具有admin和user两个角色权限,如没有则会报错
        subject.checkRoles("admin","user");
//        subject.checkRole("xxx"); // 报错
    }
}

运行测试,能够正确看到效果。

自定义 Realm

从上面我们了解到实际进行权限信息验证的是我们的 Realm,Shiro 框架内部默认提供了两种实现,一种是查询.ini文件的IniRealm,另一种是查询数据库的JdbcRealm,这两种来说都相对简单,感兴趣的可以去【这里】瞄两眼,我们着重就来介绍介绍自定义实现的 Realm 吧。

有了上面的对认证和授权的理解,我们先在合适的包下创建一个【MyRealm】类,继承 Shirot 框架的 AuthorizingRealm 类,并实现默认的两个方法:

package com.wmyskxz.demo.realm;

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.*;

public class MyRealm extends AuthorizingRealm {

    /**
     * 模拟数据库数据
     */
    Map<String, String> userMap = new HashMap<>(16);

    {
        userMap.put("wmyskxz", "123456");
        super.setName("myRealm"); // 设置自定义Realm的名称,取什么无所谓..
    }

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 从数据库获取角色和权限数据
        Set<String> roles = getRolesByUserName(userName);
        Set<String> permissions = getPermissionsByUserName(userName);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 模拟从数据库中获取权限数据
     *
     * @param userName
     * @return
     */
    private Set<String> getPermissionsByUserName(String userName) {
        Set<String> permissions = new HashSet<>();
        permissions.add("user:delete");
        permissions.add("user:add");
        return permissions;
    }

    /**
     * 模拟从数据库中获取角色数据
     *
     * @param userName
     * @return
     */
    private Set<String> getRolesByUserName(String userName) {
        Set<String> roles = new HashSet<>();
        roles.add("admin");
        roles.add("user");
        return roles;
    }

    /**
     * 认证
     *
     * @param authenticationToken 主体传过来的认证信息
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1.从主体传过来的认证信息中,获得用户名
        String userName = (String) authenticationToken.getPrincipal();

        // 2.通过用户名到数据库中获取凭证
        String password = getPasswordByUserName(userName);
        if (password == null) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");
        return authenticationInfo;
    }

    /**
     * 模拟从数据库取凭证的过程
     *
     * @param userName
     * @return
     */
    private String getPasswordByUserName(String userName) {
        return userMap.get(userName);
    }
}

然后我们编写测试类,来验证是否正确:

import com.wmyskxz.demo.realm.MyRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class AuthenticationTest {

    @Test
    public void testAuthentication() {

        MyRealm myRealm = new MyRealm(); // 实现自己的 Realm 实例

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(myRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登录

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
        // 判断subject是否具有admin和user两个角色权限,如没有则会报错
        subject.checkRoles("admin", "user");
//        subject.checkRole("xxx"); // 报错
        // 判断subject是否具有user:add权限
        subject.checkPermission("user:add");
    }
}

运行测试,完美。

Shiro 加密

在之前的学习中,我们在数据库中保存的密码都是明文的,一旦数据库数据泄露,那就会造成不可估算的损失,所以我们通常都会使用非对称加密,简单理解也就是不可逆的加密,而 md5 加密算法就是符合这样的一种算法。

如上面的 123456 用 Md5 加密后,得到的字符串:e10adc3949ba59abbe56e057f20f883e,就无法通过计算还原回 123456,我们把这个加密的字符串保存在数据库中,等下次用户登录时我们把密码通过同样的算法加密后再从数据库中取出这个字符串进行比较,就能够知道密码是否正确了,这样既保留了密码验证的功能又大大增加了安全性,但是问题是:虽然无法直接通过计算反推回密码,但是我们仍然可以通过计算一些简单的密码加密后的 Md5 值进行比较,推算出原来的密码

比如我的密码是 123456,你的密码也是,通过 md5 加密之后的字符串一致,所以你也就能知道我的密码了,如果我们把常用的一些密码都做 md5 加密得到一本字典,那么就可以得到相当一部分的人密码,这也就相当于“破解”了一样,所以其实也没有我们想象中的那么“安全”。

加盐 + 多次加密

既然相同的密码 md5 一样,那么我们就让我们的原始密码再加一个随机数,然后再进行 md5 加密,这个随机数就是我们说的盐(salt),这样处理下来就能得到不同的 Md5 值,当然我们需要把这个随机数盐也保存进数据库中,以便我们进行验证。

另外我们可以通过多次加密的方法,即使黑客通过一定的技术手段拿到了我们的密码 md5 值,但它并不知道我们到底加密了多少次,所以这也使得破解工作变得艰难。

在 Shiro 框架中,对于这样的操作提供了简单的代码实现:

String password = "123456";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
int times = 2;  // 加密次数:2
String alogrithmName = "md5";   // 加密算法

String encodePassword = new SimpleHash(alogrithmName, password, salt, times).toString();

System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodePassword);

输出:

原始密码是 123456 , 盐是: f5GQZsuWjnL9z585JjLrbQ==, 运算次数是: 2, 运算出来的密文是:55fee80f73537cefd6b3c9a920993c25 

SpringBoot 简单实例

通过上面的学习,我们现在来着手搭建一个简单的使用 Shiro 进行权限验证授权的一个简单系统

第一步:新建SpringBoot项目,搭建基础环境

pom包:

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

application.properties文件:

#thymeleaf 配置
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
#缓存设置为false, 这样修改之后马上生效,便于调试
spring.thymeleaf.cache=false

#数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
#显示SQL语句
spring.jpa.show-sql=true
#不加下面这句则不会默认创建MyISAM引擎的数据库
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#自己重写的配置类,默认使用utf8编码
spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.shiro.config.MySQLConfig

第二步:新建实体类

新建一个【entity】包,在下面创建以下实体:

用户信息:

@Entity
public class UserInfo {
    @Id
    @GeneratedValue
    private Long id; // 主键.
    @Column(unique = true)
    private String username; // 登录账户,唯一.
    private String name; // 名称(匿名或真实姓名),用于UI显示
    private String password; // 密码.
    private String salt; // 加密密码的盐
    @JsonIgnoreProperties(value = {"userInfos"})
    @ManyToMany(fetch = FetchType.EAGER) // 立即从数据库中进行加载数据
    @JoinTable(name = "SysUserRole", joinColumns = @JoinColumn(name = "uid"), inverseJoinColumns = @JoinColumn(name = "roleId"))
    private List<SysRole> roles; // 一个用户具有多个角色

    /** getter and setter */
}

角色信息:

@Entity
public class SysRole {
    @Id
    @GeneratedValue
    private Long id; // 主键.
    private String name; // 角色名称,如 admin/user
    private String description; // 角色描述,用于UI显示

    // 角色 -- 权限关系:多对多
    @JsonIgnoreProperties(value = {"roles"})
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "permissionId")})
    private List<SysPermission> permissions;

    // 用户 -- 角色关系:多对多
    @JsonIgnoreProperties(value = {"roles"})
    @ManyToMany
    @JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
    private List<UserInfo> userInfos;// 一个角色对应多个用户

    /** getter and setter */
}

权限信息:

@Entity
public class SysPermission {
    @Id
    @GeneratedValue
    private Long id; // 主键.
    private String name; // 权限名称,如 user:select
    private String description; // 权限描述,用于UI显示
    private String url; // 权限地址.
    @JsonIgnoreProperties(value = {"permissions"})
    @ManyToMany
    @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "permissionId")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
    private List<SysRole> roles; // 一个权限可以被多个角色使用

    /** getter and setter */
}

注意:这里有一个坑,还缠了我蛮久感觉,就是当我们想要使用RESTful风格返回给前台JSON数据的时候,这里有一个关于多对多无限循环的坑,比如当我们想要返回给前台一个用户信息时,由于一个用户拥有多个角色,一个角色又拥有多个权限,而权限跟角色也是多对多的关系,也就是造成了 查用户→查角色→查权限→查角色→查用户… 这样的无限循环,导致传输错误,所以我们根据这样的逻辑在每一个实体类返回JSON时使用了一个@JsonIgnoreProperties注解,来排除自己对自己无线引用的过程,也就是打断这样的无限循环。

根据以上的代码会自动生成user_info(用户信息表)、sys_role(角色表)、sys_permission(权限表)、sys_user_role(用户角色表)、sys_role_permission(角色权限表)这五张表,为了方便测试我们给这五张表插入一些初始化数据:

INSERT INTO `user_info` (`id`,`name`,`password`,`salt`,`username`) VALUES (1, '管理员','951cd60dec2104024949d2e0b2af45ae', 'xbNIxrQfn6COSYn1/GdloA==', 'wmyskxz');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (1,'查询用户','userInfo:view','/userList');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (2,'增加用户','userInfo:add','/userAdd');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (3,'删除用户','userInfo:delete','/userDelete');
INSERT INTO `sys_role` (`id`,`description`,`name`) VALUES (1,'管理员','admin');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);

第三步:配置 Shiro

新建一个【config】包,在下面创建以下文件:

MySQLConfig:

public class MySQLConfig extends MySQL5InnoDBDialect {
    @Override
    public String getTableTypeString() {
        return "ENGINE=InnoDB DEFAULT CHARSET=utf8";
    }
}

这个文件关联的是配置文件中最后一个配置,是让 Hibernate 默认创建 InnoDB 引擎并默认使用 utf-8 编码

MyShiroRealm:

public class MyShiroRealm extends AuthorizingRealm {
    @Resource
    private UserInfoService userInfoService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 能进入这里说明用户已经通过验证了
        UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (SysRole role : userInfo.getRoles()) {
            simpleAuthorizationInfo.addRole(role.getName());
            for (SysPermission permission : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permission.getName());
            }
        }
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户输入的账户
        String username = (String) authenticationToken.getPrincipal();
        System.out.println(authenticationToken.getPrincipal());
        // 通过username从数据库中查找 UserInfo 对象
        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        UserInfo userInfo = userInfoService.findByUsername(username);
        if (null == userInfo) {
            return null;
        }

        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                userInfo, // 用户名
                userInfo.getPassword(), // 密码
                ByteSource.Util.bytes(userInfo.getSalt()), // salt=username+salt
                getName() // realm name
        );
        return simpleAuthenticationInfo;
    }
}

自定义的 Realm ,方法跟上面的认证授权过程一致

ShiroConfig:

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 拦截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/static/**", "anon");
        // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");
        // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        filterChainDefinitionMap.put("/**", "authc");
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");

        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2); // 散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedCredentialsMatcher;
    }

    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }


    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean(name = "simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver
    createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError"); // 数据库异常处理
        mappings.setProperty("UnauthorizedException", "403");
        r.setExceptionMappings(mappings);  // None by default
        r.setDefaultErrorView("error");    // No default
        r.setExceptionAttribute("ex");     // Default is "exception"
        //r.setWarnLogCategory("example.MvcLogger");     // No default
        return r;
    }
}

Apache Shiro 的核心通过 Filter 来实现,就好像 SpringMvc 通过 DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。

Filter Chain定义说明:

  • 1、一个URL可以配置多个Filter,使用逗号分隔
  • 2、当设置多个过滤器时,全部验证通过,才视为通过
  • 3、部分过滤器可指定参数,如perms,roles

Shiro内置的FilterChain,因表格显示问题,麻烦移步简书观看完整版..

  • anon:所有url都都可以匿名访问
  • authc: 需要认证才能进行访问
  • user:配置记住我或认证通过可以访问

第四步:准备 DAO 层和 Service 层

新建【dao】包,在下面创建【UserInfoDao】接口:

public interface UserInfoDao extends JpaRepository<UserInfo, Long> {
    /** 通过username查找用户信息*/
    public UserInfo findByUsername(String username);
}

新建【service】包,创建【UserInfoService】接口:

public interface UserInfoService {
    /** 通过username查找用户信息;*/
    public UserInfo findByUsername(String username);
}

并在该包下再新建一个【impl】包,新建【UserInfoServiceImpl】实现类:

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Resource
    UserInfoDao userInfoDao;

    @Override
    public UserInfo findByUsername(String username) {
        return userInfoDao.findByUsername(username);
    }
}

第五步:controller层

新建【controller】包,然后在下面创建以下文件:

HomeController:

@Controller
public class HomeController {

    @RequestMapping({"/","/index"})
    public String index(){
        return"/index";
    }

    @RequestMapping("/login")
    public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
        System.out.println("HomeController.login()");
        // 登录失败从request中获取shiro处理的异常信息。
        // shiroLoginFailure:就是shiro异常类的全类名.
        String exception = (String) request.getAttribute("shiroLoginFailure");
        System.out.println("exception=" + exception);
        String msg = "";
        if (exception != null) {
            if (UnknownAccountException.class.getName().equals(exception)) {
                System.out.println("UnknownAccountException -- > 账号不存在:");
                msg = "UnknownAccountException -- > 账号不存在:";
            } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                System.out.println("IncorrectCredentialsException -- > 密码不正确:");
                msg = "IncorrectCredentialsException -- > 密码不正确:";
            } else if ("kaptchaValidateFailed".equals(exception)) {
                System.out.println("kaptchaValidateFailed -- > 验证码错误");
                msg = "kaptchaValidateFailed -- > 验证码错误";
            } else {
                msg = "else >> "+exception;
                System.out.println("else -- >" + exception);
            }
        }
        map.put("msg", msg);
        // 此方法不处理登录成功,由shiro进行处理
        return "/login";
    }

    @RequestMapping("/403")
    public String unauthorizedRole(){
        System.out.println("------没有权限-------");
        return "403";
    }
}

这里边的地址对应我们在设置 Shiro 时设置的地址

UserInfoController:

@RestController
public class UserInfoController {

    @Resource
    UserInfoService userInfoService;

    /**
     * 按username账户从数据库中取出用户信息
     *
     * @param username 账户
     * @return
     */
    @GetMapping("/userList")
    @RequiresPermissions("userInfo:view") // 权限管理.
    public UserInfo findUserInfoByUsername(@RequestParam String username) {
        return userInfoService.findByUsername(username);
    }

    /**
     * 简单模拟从数据库添加用户信息成功
     *
     * @return
     */
    @PostMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String addUserInfo() {
        return "addUserInfo success!";
    }

    /**
     * 简单模拟从数据库删除用户成功
     *
     * @return
     */
    @DeleteMapping("/userDelete")
    @RequiresPermissions("userInfo:delete")
    public String deleteUserInfo() {
        return "deleteUserInfo success!";
    }
}

第六步:准备页面

新建三个页面用来测试:

index.html:首页

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
index - 首页
</body>
</html>

login.html:登录页

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
错误信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
    <p>账号:<input type="text" name="username" value="wmyskxz"/></p>
    <p>密码:<input type="text" name="password" value="123456"/></p>
    <p><input type="submit" value="登录"/></p>
</form>
</body>
</html>

403.html:没有权限的页面

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>403错误页</title>
</head>
<body>
错误页面
</body>
</html>

第七步:测试

  1. 编写好程序后就可以启动,首先访问http://localhost:8080/userList?username=wmyskxz页面,由于没有登录就会跳转到我们配置好的http://localhost:8080/login页面。登陆之后就会看到正确返回的JSON数据,上面这些操作时候触发MyShiroRealm.doGetAuthenticationInfo()这个方法,也就是登录认证的方法。
  2. 登录之后,我们还能访问http://localhost:8080/userAdd页面,因为我们在数据库中提前配置好了权限,能够看到正确返回的数据,但是我们访问http://localhost:8080/userDelete时,就会返回错误页面.

注意:以上测试需要在REST工具中测试,因为在Controller层中配置了方法,大家也可以不用REST风格来测试一下看看!


参考地址:https://zhuanlan.zhihu.com/p/54176956

解决分布式事务,Seata真香!

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

一、分布式事务基本概念

在开始介绍Seata分布式框架之前,我们先来了解一下,什么是分布式事务?

在传统的单体应用中,事务是由单个数据库管理的,一个事务中的所有操作要么全部成功,要么全部失败。但是,在分布式系统中,一个事务可能涉及多个数据库,这些数据库可能位于不同的服务器上。因此,需要一种机制来协调这些数据库之间的事务。

分布式事务是指一个事务中的操作分布在多个节点上,需要保证这些操作要么全部成功,要么全部失败。在分布式事务中,有几个基本概念:

  1. 事务管理器(Transaction Manager):负责管理事务的整个生命周期,包括事务的开始、提交和回滚。
  2. 资源管理器(Resource Manager):负责管理本地资源(如数据库),并与事务管理器进行交互。
  3. 参与者(Participant):参与到分布式事务中的节点。
  4. 事务(Transaction):一个事务是由一个或多个操作组成的逻辑单元。
  5. 分支(Branch):一个事务中的每个操作都被称为一个分支。
  6. 全局事务(Global Transaction):一个包含多个分支的事务被称为全局事务。

二、Seata 是什么

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

  1. AT 模式(Auto Commit Transaction):是一种基于两阶段提交协议的自动事务提交模式。在这种模式下,Seata 框架会自动管理事务的提交和回滚过程,以确保数据的一致性。
  2. TCC 模式(Try-Confirm-Cancel):是一种基于补偿机制的事务处理模式。在这种模式下,事务中的每个操作都被拆分成了 try、confirm 和 cancel 三个阶段。如果所有的 try 操作都成功了,那么事务就会被提交;否则,事务就会被回滚。
  3. SAGA 模式(Long Running Transaction):是一种基于事件驱动的事务处理模式。在这种模式下,事务中的每个操作都是一个独立的事务,它们之间通过事件进行协调。如果一个操作失败了,那么它会发送一个补偿事件来撤销之前的操作。
  4. XA 模式(eXtended Architecture):是一种基于 XA 协议的分布式事务处理模式。在这种模式下,事务中的每个操作都需要与事务管理器进行交互,以确保数据的一致性。

官方4大模式解释:https://seata.apache.org/zh-cn/docs/user/mode/at

从Seata的使用角度来说,AT模式和XA模式,在用法上面是一样的,只是说数据库配置不一样而已。

三、Seata服务器

Seata分布式事务的实现,可以分为client和serve两大部分。client就是每个业务服务本身,只需要引入Seata相关的依赖即可,serve是一个独立运行的服务,需要我们独立部署,独立于业务之外。下面给大家介绍如何基于Nacos作为配置中心和注册中心,来使用Seata框架。

3.1 Nacos安装和配置

首先下载安装Nacos安装包,教程可以参照:Window环境下Nacos安装教程

在Configuration Management模块中配置Seata所需要用到的配置,如下所示:

#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none

#Transaction routing rules configuration, only for the client
service.vgroupMapping.default_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

client.metadataMaxAgeMs=30000
#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.rm.sqlParserType=druid
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
# You can choose from the following options: fastjson, jackson, gson
tcc.contextJsonParserType=fastjson

#Log rule configuration, for client and server
log.exceptionRate=100

#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
store.publicKey=

#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true&useSSL=false
store.db.user=seata
store.db.password=seata
store.db.minConn=5
store.db.maxConn=10
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
store.redis.mode=single
store.redis.type=pipeline
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.sentinel.sentinelPassword=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100

#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=true
server.enableParallelHandleBranch=false

server.raft.cluster=127.0.0.1:7091,127.0.0.1:7092,127.0.0.1:7093
server.raft.snapshotInterval=600
server.raft.applyBatch=32
server.raft.maxAppendBufferSize=262144
server.raft.maxReplicatorInflightMsgs=256
server.raft.disruptorBufferSize=16384
server.raft.electionTimeoutMs=2000
server.raft.reporterEnabled=false
server.raft.reporterInitialDelay=60
server.raft.serialization=jackson
server.raft.compressor=none
server.raft.sync=true



#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

上述文件来自seata\seata-server-2.0.0\seata\script\config-center\config.txt,我们也可以通过Linux脚本或者Python脚本,将配置自动同步Nacos中,也可以我们自己手动复制上去。

主要事项:其中store.db.url部分需要修改为自己的数据库地址,否则Seata启动过程中会造成连接数据库失败。

3.2 初始化store.db脚本

如果想要Seata进行存储共享,需要将store.mode=db,默认是file,也就是本地文件存储,这种模式是无法进行数据共享的,脚本如下所示:

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

3.3 修改application.yml配置

修改\seata\seata-server-2.0.0\seata\confapplication.yml配置,将注册中心和配置中心修改为Nacos地址。

#  Copyright 1999-2019 Seata.io Group.
#
#  Licensed 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.

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${user.home}/logs/seata}
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata
seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: "nacos"
    nacos:
      server-addr: "127.0.0.1:8848"
      namespace: ""
      group: "SEATA_GROUP"
      username: "nacos"
      password: "nacos"
      # context-path: /nacos
      ##if use MSE Nacos with auth, mutex with username/password attribute
      # access-key:
      # secret-key:
      data-id: "seataServer.properties"
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: "nacos"
    nacos:
      application: "seata-server"
      server-addr: "127.0.0.1:8848"
      namespace: ""
      group: "SEATA_GROUP"
      cluster: "default"
      username: "nacos"
      password: "nacos"
      # context-path: /nacos
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**

上述Nacos地址需要修改为自己的地址,否则会拉取不到配置,导致启动失败。

3.4 启动Seata服务

如果是window环境,只需要双击seata-server.bat文件,即可启动Seata服务。

地址:http://127.0.0.1:7091/#/transaction/list

账号:seata

密码:seata

Seata服务启动成功之后,可以进入Seata控制台,查看当前正在执行的事务和全局锁信息,如果当前没有服务在运行,那列表就是空的。

最后我们可以进入Nacos控制台,查看在线服务列表,如果可以看到如上所示的服务信息,就说明Seata服务已经成功注册到Nacos上面了。

四、基于nacos实现分布式事务步骤

Seata使用教程我们可以去官网中查看,例如:https://seata.apache.org/zh-cn/docs/user/quickstart

但是我推荐大家还是基于GitHub上面的demo项目来探索Seata的用法,因为官网的教程不是很详细,GitHub地址:https://github.com/apache/incubator-seata-samples

下面来给大家介绍以下使用Nacos作为注册中心和配置中心,来实现Seata分布式事务的详细步骤。

4.1 找到springcloud-nacos-seata项目

在incubator-seata-samples父项目中,找到springcloud-nacos-seata的子项目,如下图所示:

4.2 修改项目配置

在application.properties中,将spring.cloud.nacos.discovery.server-addr修改为我们自己的Nacos地址,将spring.datasource.url修改为我们自己的数据库地址。

spring.application.name=order-service
server.port=9091
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
logging.level.io.seata=debug
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata_order?allowMultiQueries=true&useSSL=false
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=seata
spring.datasource.password=seata

spring.application.name=stock-service
server.port=9092
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
logging.level.io.seata=debug
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata_stock?allowMultiQueries=true&useSSL=false
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=seata
spring.datasource.password=seata

在registry.conf中,将配置中心和注册中心的Nacos地址更新为自己的地址。

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    # order和stock中application名称需要进行区分,不然注册过去的应用名称就重复了。
    application = "seata-server-order"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

注意springcloud-nacos-seata含有order和stock两个项目模块,两个模块的application.properties和registry.conf都需要修改。

4.3 初始化order和stock的db脚本

-- 创建 order库、业务表、undo_log表
create database seata_order;
use seata_order;

DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `undo_log`
(
  `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT,
  `branch_id`     BIGINT(20)   NOT NULL,
  `xid`           VARCHAR(100) NOT NULL,
  `context`       VARCHAR(128) NOT NULL,
  `rollback_info` LONGBLOB     NOT NULL,
  `log_status`    INT(11)      NOT NULL,
  `log_created`   DATETIME     NOT NULL,
  `log_modified`  DATETIME     NOT NULL,
  `ext`           VARCHAR(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;


-- 创建 stock库、业务表、undo_log表
create database seata_stock;
use seata_stock;

DROP TABLE IF EXISTS `stock_tbl`;
CREATE TABLE `stock_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `undo_log`
(
  `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT,
  `branch_id`     BIGINT(20)   NOT NULL,
  `xid`           VARCHAR(100) NOT NULL,
  `context`       VARCHAR(128) NOT NULL,
  `rollback_info` LONGBLOB     NOT NULL,
  `log_status`    INT(11)      NOT NULL,
  `log_created`   DATETIME     NOT NULL,
  `log_modified`  DATETIME     NOT NULL,
  `ext`           VARCHAR(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

-- 初始化库存模拟数据
INSERT INTO seata_stock.stock_tbl (id, commodity_code, count) VALUES (1, 'product-1', 9999999);
INSERT INTO seata_stock.stock_tbl (id, commodity_code, count) VALUES (2, 'product-2', 0);

脚本的位置在README.md文件中,官网demo并没有将DDL单独独立一份文件出来,所以脚本的位置需要注意一下。

4.4 调用接口验证分布式事务

分别启动order和stock服务,如下所示就代表启动成功了。

确定Nacos服务列表中包含启动成功的order和stock两个服务,如下所示:

调用http://127.0.0.1:9091/order/placeOrder/commit接口,该接口会先扣减库存,然后再创建订单,但是创建订单后会抛出一个异常,如果分布式事务执行成功,应该是库存没扣减成功,订单也没有创建成功,所以现在我们来验证一下是不是这样子。

order服务代码:

@RequestMapping("/placeOrder/commit")
public Boolean placeOrderCommit() {
    orderService.placeOrder("1", "product-1", 1);
    return true;
}

@GlobalTransactional
@Transactional(rollbackFor = Exception.class)
public void placeOrder(String userId, String commodityCode, Integer count) {
    stockFeignClient.deduct(commodityCode, count);
    BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
    Order order = new Order().setUserId(userId).setCommodityCode(commodityCode).setCount(count).setMoney(
        orderMoney);
    orderDAO.insert(order);
    if (commodityCode.equals("product-1")) {
        throw new RuntimeException("异常:模拟业务异常:stock branch exception");
    }
}

stock服务代码:

@RequestMapping(path = "/deduct")
public Boolean deduct(String commodityCode, Integer count) {
    stockService.deduct(commodityCode, count);
    return true;
}

/** * 减库存 * * @param commodityCode * @param count */
@Transactional(rollbackFor = Exception.class)
public void deduct(String commodityCode, int count) {
    QueryWrapper<Stock> wrapper = new QueryWrapper<>();
    wrapper.setEntity(new Stock().setCommodityCode(commodityCode));
    Stock stock = stockDAO.selectOne(wrapper);
    stock.setCount(stock.getCount() - count);
    stockDAO.updateById(stock);
}

在开始执行接口之前,我们来看一下stock_tbl和order_tbl的数据,如下所示我们可以得到product-1产品的库存为9999999,order_tbl表数据为空。

利用postman来执行order/placeOrder/commit接口,可以看到返回值是500,如下所示:

执行接口后,我们重新刷新一下stock_tbl和order_tbl表,发现库表数据没有发生任何变动,说明分布式事务生效了,stock_tbl和order_tbl的数据都成功回滚了。

如果要从0创建seata的客户端,可以将demo中的pom相关依赖引用进去即可,其它的步骤和上述差不多。

五、总结

Seata 是一种强大而简单的分布式事务解决方案,它提供了一种高性能、易于使用和可扩展的方式来管理分布式事务。通过使用 Seata,你可以轻松地实现分布式事务,确保数据的一致性和可靠性。

但是Seata也有它的局限性,如果非分布式事务中,MySQL事务默认隔离机制是REPEATABLE-READ,但是Seata的隔离级别是Read Uncommitted,如果对数据非常敏感的系统,要注意因为分布式事务造成的数据问题。

分布式调度平台你会选择谁?我会选择“它”

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

相信很多后端的童靴,或多或少都听过XXL-JOB的大名,但是如果没有大量task任务的项目,可能出于架构复杂度考虑,也不会使用到XXL-JOB这个框架,但是作为技术开发人员,与时俱进不断学习新的框架,是必然的选择。

所以今天林老师带着童靴们,玩一遍XXL-JOB这个框架,在开始之前,我们要先知道,我们为什么要用XXL-JOB框架,它解决了我什么问题,我在什么情况下,可以不用它。

一、什么时候要使用XX-JOB?

因为现在大部分项目都是采用SpringBoot框架来搭建,所以我们先来介绍一下SpringBoot中最常见的定时任务是如何实现的呢?下面是一个使用 SpringBoot 实现定时任务的简单示例:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class MyScheduledTask {
    @Scheduled(cron = "0 0 1 * * *")  // 每天凌晨 1 点执行
    public void doSomething() {
        // 执行定时任务逻辑
        System.out.println("执行定时任务逻辑...");
    }
}

在上面的代码中,我们使用了@Scheduled注解来定义一个定时任务。cron属性指定了任务执行的时间表达式,这里表示每天凌晨 1 点执行。doSomething()方法是定时任务的执行逻辑,你可以在这里编写你需要执行的任务代码。

从上面的案例可以看出,SpringBoot实现定时任务,非常简单快速,基本没有任何学习成本。那这么好,为什么还需要升级为XXL-JOB呢?因为@Scheduled的缺点也非常多,比如:

  1. 不支持分布式@Scheduled定时任务是基于Spring框架的,它只能在单节点上执行定时任务。如果你需要在多个节点上执行定时任务,那么你需要使用其他的分布式定时任务框架,比如XXL-JOB
  2. 不支持高可用@Scheduled定时任务没有提供高可用的解决方案。如果你的应用程序需要在高可用的环境中运行,那么你需要使用其他的高可用定时任务框架,比如XXL-JOB
  3. 不支持动态调整任务@Scheduled定时任务不能动态地调整任务的执行时间和频率。如果你需要动态地调整任务的执行时间和频率,那么你需要使用其他的定时任务框架,比如XXL-JOB
  4. 不支持监控和告警@Scheduled定时任务没有提供监控和告警的功能。如果你需要监控和告警定时任务的执行情况,那么你需要使用其他的监控和告警框架,比如XXL-JOB

所以如果你的项目有上述几点需求,或者系统任务数达到一定规模时,为了系统调度任务稳定+可靠+可监控,就必须升级为XXL-JOB。

二、那XXL-JOB要怎么使用呢?他的架构长啥样子呢?

  1. 架构图

乍一看这个架构图会觉得非常复杂,应该难度特别大,但是实际上很简单,我们从项目的目录结构可以看出来,分为admin、core、executor三大模块,其中:

  1. core是核心依赖,不能单独部署运行。
  2. admin是调度管理平台,有自己的web管理页面,是前后端一体的项目。
  3. executor是执行器,纯后端服务,没有管理页面。
  1. 快速入门

本来我想一步步的介绍每一个步骤,但是奈何官网教程实在是太强大了,为了不引起歧义,下面给出官网的快速入门教程。

2.1 初始化“调度数据库”

请下载项目源码并解压,获取 “调度数据库初始化SQL脚本” 并执行即可。

“调度数据库初始化SQL脚本” 位置为:

/xxl-job/doc/db/tables_xxl_job.sql

调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例;

如果mysql做主从,调度中心集群节点务必强制走主库;

2.2 编译源码

解压源码,按照maven格式将源码导入IDE, 使用maven进行编译即可,源码结构如下:

xxl-job-admin:调度中心
xxl-job-core:公共依赖
xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器)
    :xxl-job-executor-sample-springboot:Springboot版本,通过Springboot管理执行器,推荐这种方式;
    :xxl-job-executor-sample-frameless:无框架版本;

2.3 配置部署“调度中心”

调度中心项目:xxl-job-admin作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。

步骤一:调度中心配置:

调度中心配置文件地址:

/xxl-job/xxl-job-admin/src/main/resources/application.properties

调度中心配置内容说明:

### 调度中心JDBC链接:链接地址请保持和 2.1章节 所创建的调度数据库的地址一致
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root_pwd
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

### 报警邮箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=

### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文;
xxl.job.i18n=zh_CN

## 调度线程池最大线程配置【必填】
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100

### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=30

步骤二:部署项目:

如果已经正确进行上述配置,可将项目编译打包部署。

调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址)

默认登录账号 “admin/123456”, 登录后运行界面如下图所示。

至此“调度中心”项目已经部署成功。

步骤三:调度中心集群(可选):

调度中心支持集群部署,提升调度系统容灾和可用性。

调度中心集群部署时,几点要求和建议:

  • DB配置保持一致;
  • 集群机器时钟保持一致(单机集群忽视);
  • 建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。

2.4 配置部署“执行器项目”

“执行器”项目:xxl-job-executor-sample-springboot (提供多种版本执行器供选择,现以 springboot 版本为例,可直接使用,也可以参考其并将现有项目改造成执行器)作用:负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。

步骤一:maven依赖

确认pom文件中引入了 “xxl-job-core” 的maven依赖;

步骤二:执行器配置

执行器配置,配置文件地址:

/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties

执行器配置,配置内容说明:

### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=

### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30

步骤三:执行器组件配置

执行器组件,配置文件地址:

/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java

执行器组件,配置内容说明:

@Beanpublic XxlJobSpringExecutor xxlJobExecutor() {    logger.info(">>>>>>>>>>> xxl-job config init.");    XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();    xxlJobSpringExecutor.setAdminAddresses(adminAddresses);    xxlJobSpringExecutor.setAppname(appname);    xxlJobSpringExecutor.setIp(ip);    xxlJobSpringExecutor.setPort(port);    xxlJobSpringExecutor.setAccessToken(accessToken);    xxlJobSpringExecutor.setLogPath(logPath);    xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);    return xxlJobSpringExecutor;}

步骤四:部署执行器项目:

如果已经正确进行上述配置,可将执行器项目编译打部署,系统提供多种执行器Sample示例项目,选择其中一个即可,各自的部署方式如下。

xxl-job-executor-sample-springboot:项目编译打包成springboot类型的可执行JAR包,命令启动即可;xxl-job-executor-sample-frameless:项目编译打包成JAR包,命令启动即可;

至此“执行器”项目已经部署结束。

步骤五:执行器集群(可选):

执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。

执行器集群部署时,几点要求和建议:

  • 执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。
  • 同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。

2.5 开发第一个任务“Hello World”

本示例以新建一个 “GLUE模式(Java)” 运行模式的任务为例。更多有关任务的详细配置,请查看“章节三:任务详解”。

( “GLUE模式(Java)”的执行代码托管到调度中心在线维护,相比“Bean模式任务”需要在执行器项目开发部署上线,更加简便轻量)

前提:请确认“调度中心”和“执行器”项目已经成功部署并启动;

步骤一:新建任务:

登录调度中心,点击下图所示“新建任务”按钮,新建示例任务。然后,参考下面截图中任务的参数配置,点击保存。

步骤二:“GLUE模式(Java)” 任务开发:

请点击任务右侧 “GLUE” 按钮,进入 “GLUE编辑器开发界面” ,见下图。“GLUE模式(Java)” 运行模式的任务默认已经初始化了示例任务代码,即打印Hello World。 ( “GLUE模式(Java)” 运行模式的任务实际上是一段继承自IJobHandler的Java类代码,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务,详细介绍请查看第三章节)

步骤三:触发执行:

请点击任务右侧 “执行” 按钮,可手动触发一次任务执行(通常情况下,通过配置Cron表达式进行任务调度触发)。

步骤四:查看日志:

请点击任务右侧 “日志” 按钮,可前往任务日志界面查看任务日志。

在任务日志界面中,可查看该任务的历史调度记录以及每一次调度的任务调度信息、执行参数和执行信息。运行中的任务点击右侧的“执行日志”按钮,可进入日志控制台查看实时执行日志。

在日志控制台,可以Rolling方式实时查看任务在执行器一侧运行输出的日志信息,实时监控任务进度;

三、XX-JOB提供了哪些强大的功能?

1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手;

2、动态:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效;

3、调度中心HA(中心式):调度采用中心式设计,“调度中心”自研调度组件并支持集群部署,可保证调度中心HA;

4、执行器HA(分布式):任务分布式执行,任务”执行器”支持集群部署,可保证任务执行HA;

5、注册中心: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。同时,也支持手动录入执行器地址;

6、弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务;

7、触发策略:提供丰富的任务触发策略,包括:Cron触发、固定间隔触发、固定延时触发、API(事件)触发、人工触发、父子任务触发;

8、调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略、立即补偿触发一次等;

9、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度;

10、任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务;

11、任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试;

12、任务失败告警;默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式;

13、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;

14、分片广播任务:执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务;

15、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。

16、故障转移:任务路由策略选择”故障转移”情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求。

17、任务进度监控:支持实时监控任务进度;

18、Rolling实时日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志;

19、GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持30个版本的历史版本回溯。

20、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python、NodeJS、PHP、PowerShell等类型脚本;

21、命令行任务:原生提供通用命令行任务Handler(Bean任务,”CommandJobHandler”);业务方只需要提供命令行即可;

22、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔;

23、一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行;

24、自定义任务参数:支持在线配置调度任务入参,即时生效;

25、调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞;

26、数据加密:调度中心和执行器之间的通讯进行数据加密,提升调度信息安全性;

27、邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件;

28、推送maven中央仓库: 将会把最新稳定版推送到maven中央仓库, 方便用户接入和使用;

29、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等;

30、全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行;

31、跨语言:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。除此之外,还提供了 “多任务模式”和“httpJobHandler”等其他跨语言方案;

32、国际化:调度中心支持国际化设置,提供中文、英文两种可选语言,默认为中文;

33、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用;

34、线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入”Slow”线程池,避免耗尽调度线程,提高系统稳定性;

35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色;

36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作;

一句话就是强大,基本完全可以覆盖我们项目中的需求,因为完全开源,我们甚至可以在框架的基础上,进行二次改造升级,实现定制化开发。

error: Content is protected !!
滚动至顶部