博客

  • 号称 Redis Plus,来看看 KeyDB 性能有多炸裂!

    快乐分享,Java干货及时送达👇

    KeyDB项目是从redis fork出来的分支。众所周知redis是一个单线程的kv内存存储系统,而KeyDB在100%兼容redis API的情况下将redis改造成多线程。
    • 项目git地址:https://github.com/JohnSully/KeyDB

    网上公开的技术细节比较少,本文基本是通过阅读源码总结出来的,如有错漏之处欢迎指正。

    多线程架构

    线程模型

    KeyDB将redis原来的主线程拆分成了主线程和worker线程。每个worker线程都是io线程,负责监听端口,accept请求,读取数据和解析协议。如图所示:

    KeyDB使用了SO_REUSEPORT特性,多个线程可以绑定监听同个端口。

    每个worker线程做了cpu绑核,读取数据也使用了SO_INCOMING_CPU特性,指定cpu接收数据。

    解析协议之后每个线程都会去操作内存中的数据,由一把全局锁来控制多线程访问内存数据。

    主线程其实也是一个worker线程,包括了worker线程的工作内容,同时也包括只有主线程才可以完成的工作内容。在worker线程数组中下标为0的就是主线程。

    主线程的主要工作在实现serverCron,包括:

    • 处理统计
    • 客户端链接管理
    • db数据的resize和reshard
    • 处理aof
    • replication主备同步
    • cluster模式下的任务

    链接管理

    在redis中所有链接管理都是在一个线程中完成的。在KeyDB的设计中,每个worker线程负责一组链接,所有的链接插入到本线程的链接列表中维护。链接的产生、工作、销毁必须在同个线程中。每个链接新增一个字段

    int iel; /* the event loop index we're registered with */

    用来表示链接属于哪个线程接管。

    KeyDB维护了三个关键的数据结构做链接管理:

    • clients_pending_write:线程专属的链表,维护同步给客户链接发送数据的队列
    • clients_pending_asyncwrite:线程专属的链表,维护异步给客户链接发送数据的队列
    • clients_to_close:全局链表,维护需要异步关闭的客户链接

    分成同步和异步两个队列,是因为redis有些联动api,比如pub/sub,pub之后需要给sub的客户端发送消息,pub执行的线程和sub的客户端所在线程不是同一个线程,为了处理这种情况,KeyDB将需要给非本线程的客户端发送数据维护在异步队列中。同步发送的逻辑比较简单,都是在本线程中完成,以下图来说明如何同步给客户端发送数据:

    如上文所提到的,一个链接的创建、接收数据、发送数据、释放链接都必须在同个线程执行。异步发送涉及到两个线程之间的交互。KeyDB通过管道在两个线程中传递消息:

    int fdCmdWrite; //写管道
    int fdCmdRead; //读管道

    本地线程需要异步发送数据时,先检查client是否属于本地线程,非本地线程获取到client专属的线程ID,之后给专属的线程管到发送AE_ASYNC_OP::CreateFileEvent的操作,要求添加写socket事件。专属线程在处理管道消息时将对应的请求添加到写事件中,如图所示:

    redis有些关闭客户端的请求并非完全是在链接所在的线程执行关闭,所以在这里维护了一个全局的异步关闭链表。

    锁机制

    KeyDB实现了一套类似spinlock的锁机制,称之为fastlock。

    fastlock的主要数据结构有:

    struct ticket
    {
        uint16_t m_active;  //解锁+1
        uint16_t m_avail;  //加锁+1
    };
    struct fastlock
    {
        volatile struct ticket m_ticket;

        volatile int m_pidOwner; //当前解锁的线程id
        volatile int m_depth; //当前线程重复加锁的次数
    };

    使用原子操作__atomic_load_2__atomic_fetch_add__atomic_compare_exchange来通过比较m_active=m_avail判断是否可以获取锁。

    fastlock提供了两种获取锁的方式:

    • try_lock:一次获取失败,直接返回
    • lock:忙等,每1024 * 1024次忙等后使用sched_yield 主动交出cpu,挪到cpu的任务末尾等待执行。

    在KeyDB中将try_lock和事件结合起来,来避免忙等的情况发生。每个客户端有一个专属的lock,在读取客户端数据之前会先尝试加锁,如果失败,则退出,因为数据还未读取,所以在下个epoll_wait处理事件循环中可以再次处理。

    Active-Replica

    KeyDB实现了多活的机制,每个replica可设置成可写非只读,replica之间互相同步数据。主要特性有:

    • 每个replica有个uuid标志,用来去除环形复制
    • 新增加rreplay API,将增量命令打包成rreplay命令,带上本地的uuid
    • key,value加上时间戳版本号,作为冲突校验,如果本地有相同的key且时间戳版本号大于同步过来的数据,新写入失败。采用当前时间戳向左移20位,再加上后44位自增的方式来获取key的时间戳版本号。

    作者:羽洵

    来源:developer.aliyun.com/article/705239

    
    

  • 面试官:SpringBoot 循环依赖,如何解决?

    什么是循环依赖?

    循环依赖是指在Spring Boot 应用程序中,两个或多个类之间存在彼此依赖的情况,形成一个循环依赖链。

    在这种情况下,当一个类在初始化时需要另一个类的实例,而另一个类又需要第一个类的实例时,就会出现循环依赖问题。这会导致应用程序无法正确地初始化和运行,因为Spring Boot 无法处理这种循环依赖关系。

    问题及症状

    在2.6.0之前,Spring Boot会自动处理循环依赖的问题。2.6.0及之后的版本会默认检查循环依赖,存在该问题则会报错。

    ComponentA类注入ComponentB类,ComponentB类注入ComponentA类,就会发生循环依赖的问题。

    ComponentA

    import org.springframework.stereotype.Service;
    import javax.annotation.Resource;
     
    @Service
    public class ComponentA {

        @Resource
        private ComponentB componentB;
     
    }

    ComponentB

    import org.springframework.stereotype.Service;
    import javax.annotation.Resource;
     
    @Service
    public class ComponentB {
     
        @Resource
        private ComponentA componentA;
     
    }

    错误

    现在,2.6.0 这个版本已经默认禁止 Bean 之间的循环引用, 则基于上面的代码,会报错:

    ***************************
    APPLICATION FAILED TO START
    ***************************

    Description:

    The dependencies of some of the beans in the application context form a cycle:

    ┌─────┐
    |  componentA
    ↑     ↓
    |  componentB
    └─────┘


    Action:

    Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

    解决方法

    循环依赖是指两个或更多的组件之间存在着互相依赖的关系。在Spring Boot应用程序中,循环依赖通常是由以下几种情况引起的:

    • 构造函数循环依赖: 两个或更多的组件在它们的构造函数中互相依赖。
    • 属性循环依赖: 两个或更多的组件在它们的属性中互相依赖。
    • 方法循环依赖: 两个或更多的组件在它们的方法中互相依赖。

    Spring Boot提供了一些解决循环依赖的方法:

    • 构造函数注入: 在构造函数中注入依赖项,而不是在属性中注入。
    • Setter注入: 使用setter方法注入依赖项,而不是在构造函数中注入。
    • 延迟注入: 使用@Lazy注解延迟加载依赖项。
    • @Autowired注解的required属性:required属性设置为false,以避免出现循环依赖问题。
    • @DependsOn注解: 使用@DependsOn注解指定依赖项的加载顺序,以避免出现循环依赖问题

    构造器注入的案例

    假设有以下两个类:

    public class A {
        private B b;

        public A() {
            // ...
        }

        public void setB(B b) {
            this.b = b;
        }
    }

    public class B {
        private A a;

        public B() {
            // ...
        }

        public void setA(A a) {
            this.a = a;
        }
    }

    通过构造函数注入可以避免循环依赖,改造后的代码如下:

    public class A {
        private B b;

        public A(B b) {
            this.b = b;
        }
    }

    public class B {
        private A a;

        public B(A a) {
            this.a = a;
        }
    }

    这样,在创建 A 实例时,只需要将 B 实例传递给 A 的构造函数即可,不需要再通过 setter 方法将 B 实例注入到 A 中。同理,在创建 B 实例时,只需要将 A 实例传递给 B 的构造函数即可,不需要再通过 setter 方法将 A 实例注入到 B 中。这样可以避免循环依赖。

    延迟注入的案例

    假设有如下情景:

    类A依赖于类B,同时类B也依赖于类A。这样就形成了循环依赖。

    为了解决这个问题,可以使用@Lazy注解,将类A或类B中的其中一个延迟加载。

    例如,我们可以在类A中使用@Lazy注解,将类A延迟加载,这样在启动应用程序时,Spring容器不会立即加载类A,而是在需要使用类A的时候才会进行加载。这样就避免了循环依赖的问题。

    示例代码如下:

    @Component
    public class A {

        private final B b;

        public A(@Lazy B b) {
            this.b = b;
        }

        //...
    }

    @Component
    public class B {

        private final A a;

        public B(A a) {
            this.a = a;
        }

        //...
    }

    在类A中,我们使用了@Lazy注解,将类B延迟加载。这样在启动应用程序时,Spring容器不会立即加载类B,而是在需要使用类B的时候才会进行加载。

    这样就避免了类A和类B之间的循环依赖问题。

    接口隔离的案例

    假设有两个类A和B,它们之间存在循环依赖:

    public class A {
        private final B b;
        public A(B b) {
            this.b = b;
        }
    }

    public class B {
        private final A a;
        public B(A a) {
            this.a = a;
        }
    }

    这时候,如果直接在Spring Boot中注入A和B,就会出现循环依赖的问题。为了解决这个问题,可以使用接口隔离。

    首先,定义一个接口,包含A和B类中需要使用的方法:

    public interface Service {
        void doSomething();
    }

    然后,在A和B类中分别注入Service接口:

    public class A {
        private final Service service;
        public A(Service service) {
            this.service = service;
        }
    }

    public class B {
        private final Service service;
        public B(Service service) {
            this.service = service;
        }
    }

    最后,在Spring Boot中注入Service实现类:

    @Service
    public class ServiceImpl implements Service {
        private final A a;
        private final B b;
        public ServiceImpl(A a, B b) {
            this.a = a;
            this.b = b;
        }
        @Override
        public void doSomething() {
            // ...
        }
    }

    通过这种方式,A和B类不再直接依赖于彼此,而是依赖于同一个接口。同时,Spring Boot也能够正确地注入A、B和ServiceImpl,避免了循环依赖的问题。

    作者:熊猫Jay

    来源:jayliu.blog.csdn.net/article/

    details/129972669

  • 简化 Hello World:Java 新写法要来了!!

    快乐分享,Java干货及时送达👇

    出品:OSC开源社区
    ID:oschina2013

    OpenJDK 的 JEP 445 提案正在努力简化 Java 的入门难度。这个提案主要是引入 “灵活的 Main 方法和匿名 Main 类” ,希望 Java 的学习过程能更平滑,让学生和初学者能更好地接受 Java 。

    提案的作者 Ron Pressler 解释:现在的 Java 语言非常适合开发和维护大型复杂应用程序,但学校在教编程时往往会从变量、函数和子程序等基本的小型编程概念开始,在这个阶段,往往不需要类、包和模块的大型编程概念。但现在的 Java 对初学者不太友好,比如经典的 Hello, World! 入门程序 :

    public class HelloWorld 
        public static void main(String[] args) 
            System.out.println("Hello, World!");
        }
    }

    Ron 认为,作为新手入门的第一个程序,这段代码太复杂了。比如 class 声明和强制性的 public 访问修饰符是大型编程结构,它们在封装具有定义良好的外部组件接口的代码单元时很有用,但在这个入门小示例中毫无意义。String [] args 参数用于将代码与外部组件连接起来,但在这段代码里面不会被使用。static 修饰符是 Java 类和对象模型的一部分,但在新手村出现也为时尚早。

    作为优化,该提案首先增强了启动 Java 程序的协议灵活性:

    • 允许已启动类的 main 方法具有 public 、 protected 或默认(即包)访问权限。
    • 如果启动的类不包含带 String [] 参数的 static main 方法,但包含不带参数的 static main 方法,则调用该方法。
    • 如果启动的类没有 static main 方法,但有一个非 private 零参数构造函数(即 public 、 protected 或包访问)和一个非 private 实例 main 方法,然后构造该类的一个实例。如果该类有一个带 String [] 参数的实例 main 方法,则调用该方法;否则,不带参数调用实例 main 方法。

    如此一来便允许省略 main 方法的 String[] 参数,并允许 main 方法既不是 public 也不是 static 。可以稍微简化 Hello, World! :

    class HelloWorld 
        void main() 
            System.out.println("Hello, World!");
        }
    }

    此外,还要引入匿名 Main 类来隐式声明 class :

    void main() {
        System.out.println("Hello, World!");
    }

    到这里 Java 的 Hello, World!  入门程序已经到了简化,但在 Ron 眼里,该 JEP 提供的优化只是使 Java 更易于学习的第一步,像 System.out.println 这种长方法 / 函数也是需要简化的地方,不过这些问题需要在未来的 JEP 提案中逐步解决。

    该优化属于预览语言功能,默认禁用。要在 JDK 21 中尝试该示例,必须启用预览功能:用 javac --release 21 --enable-preview Main.java 编译程序,用 java --enable-preview Main 运行;或者使用源代码启动器时,使用 java --source 21 --enable-preview Main.java 运行程序。

    有关灵活的启动协议和匿名 main 类的更多信息,可以在提案正文中详阅(https://openjdk.org/jeps/445)。

    提案目标

    • 为 Java 提供平滑的入口,方便教育工作者可以循序渐进地介绍编程的概念。
    • 帮助学生以简洁的方式编写基本入门程序,并随着其 Java 技能增长,优雅地扩展代码。
    • 减少编写简单程序(例如脚本和命令行实用程序)的仪式。
    • 不要单独介绍 Java 的初学者方言。
    • 不引入单独的初学者工具链;编译和运行学生的程序,使用的工具应该和生产环境的 Java 程序相同。

    相关链接

    https://openjdk.org/jeps/445

    
    

  • 使用 IDEA 远程 Debug 调试(一篇懂所有)

    背景

    有时候我们需要进行远程的debug,本文研究如何进行远程debug,以及使用 IDEA 远程debug的过程中的细节。看完可以解决你的一些疑惑。

    配置

    远程debug的服务,以springboot微服务为例(springcloud的应该差不多,我没研究过)。首先,启动springboot需要加上特定的参数。

    1、IDEA设置

    高低版本的 IDEA 的设置可能界面有点不一样,我用2020.1.1的。大致上差不多,自行摸索。

    IDEA打开远程启动的springboot应用程序所对应的

    1.选择 Edit Configuration

    2.如图,点击加号,选择Remote

    3.配置,详细步骤见图

    注意:注意端口别被占用。后续这个端口是用来跟远程的java进程通信的。

    可以注意到:切换不同的jdk版本,生成的脚本不一样

    选择 jdk1.4,则为

    -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=50055

    这就是你为什么搜其他博客,会有这种配置的原因,其实这个配置也是可行的。但更准确应该按照下面jdk5-8的配置

    选择 jdk 5-8,则为

    -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=50055

    选择 jdk9以上,则为

    -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:50055

    据说因为jdk9变得安全了,远程调试只允许本地,如果要远程,则需要在端口前配置*

    2、启动脚本改造

    使用第一步得到的 Command line arguments for remote JVM 即可,即-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=50055

    改造后的启动脚本如下

    nohup java 
    -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=50055 
    -jar remote-debug-0.0.1-SNAPSHOT.jar &

    注意在windows中用 ^ 来进行换行,例如

    java ^
    -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=50055 ^
    -jar remote-debug-0.0.1-SNAPSHOT.jar

    说明:

    1、端口可随意自己定,未被占用的都行,但是要和IDEA里的remote中设置的端口一致!其他参数照抄。详细的参数解释可以参照附录或自己搜

    2、remote-debug-0.0.1-SNAPSHOT.jar 改成给你自己的 jar 包名字

    3、我给的脚本是后台运行的,如不需要后台运行,自行去掉 nohup&

    3、启动springboot,启动IDEA里的

    IDEA 远程调试的细节

    1、细节1:停在本地断点,关闭程序后会继续执行吗

    如果远程调试在自己的断点处停下来了,此时关闭IDEA中的项目停止运行,则还会继续运行执行完剩下的逻辑吗?会的,这点比较不容易记住

    以下面的代码为例,在第一行停住了。然后IDEA中停掉,发现停掉之后控制台还是打印了剩下的日志。

    2、细节2:jar包代码和本地不一致会怎么样?

    IDEA 里的代码如果不和jar包的一致,会怎么样。

    结论:要保证和远程启动的代码一致。

    否则你debug的时候的行数会对不上。报错抛异常倒是不会。像这种还是能对得上行数的

    比如你调试test1方法,test2方法在test1下面,在test2里加代码,这样并不影响test1中的行号,这种是可以在调试的时候准确反应行号的

    3、细节3:日志打印在哪里?

    日志不会打印在IDEA的控制台上。即System.out 以及 log.info 还是打印在远程的。

    @GetMapping("/test1")
    public String test1() {
        System.out.println("第一行");
        System.out.println("第二行");
        log.info("log 第一行");
        log.info("log 第二行");
        return "ok";
    }

    4、细节4:调试时其他人会不会卡住?

    远程调试的时候,打了断点,停住后会不会导致页面的请求卡住。

    比如你使用远程调试,别的QA在测试这个页面,结果他们看到的结果是怎么样的?会卡住吗?会的,已经实际遇到过这种情况了。

    5、细节5:本地代码修复bug远程调用的时候

    如果在远程调试过程自己发现了bug,本地改好后重新启动IDEA里的项目,再到页面调用一次,能修复吗?不能,运行的还是远程部署的jar中的代码

    这个直接击碎了远程页面点一点触发本地代码进行debug的梦想。如果可以的话那调试代码就方便太多。

    6、细节6:这个不算远程调试的问题,是dropframe的问题,放在这里一起讲了

    关于drop frame的问题,如果drop frame了重新进行调试,会不会插入2条记录?

    如图 userMapper.insert(eo) ,本方法没有使用 @Transactional 修饰,mapper方法执行过后事务会被立即提交,则库表里多了一行记录,如果drop frame后,再次进行调试,再次执行这代码,于是又插入了一条记录。

    如果加上 @Transational 就不会有两条记录了,dropframe的时候事务没被提交,再次执行该插入代码也不会插入2条。

    关于什么是drop frame

    7、细节7:跟上面一样,是dropframe问题

    如果把上述插入数据库的逻辑,换成调用远程的接口,在dropframe后,再次执行相同的代码,会不会导致远程接口被执行了2次?会的。

    总结

    好像感觉远程调试的用处也不是那么大,不能作为长期使用的调试工具。只能作为临时调试的手段。

    难点有几个:

    • 难保证本地代码和远程一致,而且你也很难判断是否一致
    • 通过远程调试发现了bug,但是又不能立即修复后继续调试,只能修复后部署后继续远程调试

    作者:石头wang

    来源:blog.csdn.net/w8y56f/article/

    details/116493681

  • 用 Java 写个爬虫,So Easy !

    快乐分享,Java干货及时送达👇

    背景

    随着后疫情时代的到来,在过去的2022年,全国的经济情况,想必是很多学者和研究对象都非常关心的事。而这些数据在国家统计局网站上都有相应的记录。通过分析这些数据,可以从某一个角度来验证和观察当下的经济情况。

    全国共计有1279个县级单位已经披露了2022年GDP和一般公共预算收入数据情况,企业预警通根据这些数据整理出中国百强县gdp排行榜和百强县一般公共预算收入排行榜。其中昆山市以5006.66亿元GDP蝉联榜单榜首,江阴市、晋江市位列百强县第二三位,长沙县是湖南省唯一进入全国前十的(Top7)。

    第一张图是以图片的形式发布的,第二种是采用Html的表格(table)形式展示的。在离线分析使用数据的时候非常不方便。作为程序猿,这一定难不倒您。我们可以采用网页抓取的技术对数据进行整理。

    本文将以Java语言为编程语言,讲解使用Jsoup对Web网页知识进行爬取,文中给出了详细的示例代码,希望对大家有帮助。

    一、初识Jsoup抓取

    1、网页结构分析

    在使用Jsoup对页面进行抓取时,需要对网页的结构进行初步的分析,便于制定相对应的抓取策略。首先打开浏览器,输入目标网站的地址,同时打开F12进入调试,找到目标网页的元素。

    打开上面gdp百强榜表格中的div下table表格,找到如下的数据

    同理,对于一般公共预算收入的数据处理也是同样的处理办法,在此不再赘述。

    二、Java开发Jsoup抓取

    1、引用Jsoup相关依赖包

    这里我们采用Maven的jar进行包的依赖处理管理。因此先定义Pom.xml,关键代码如下所示:

    project xmlns="http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     modelVersion>4.0.0modelVersion>
     groupId>com.yelanggroupId>
     artifactId>jsoupdemoartifactId>
     version>0.0.1-SNAPSHOTversion>
     
     dependencies>
      dependency>
       groupId>org.jsoupgroupId>
       artifactId>jsoupartifactId>
       version>1.11.3version>
      dependency>
     
      dependency>
       groupId>com.alibabagroupId>
       artifactId>easyexcelartifactId>
       version>3.0.5version>
      dependency>
     dependencies>
     
    project>

    2、 信息实体类的处理

    对比发现两个表格处理具体的指标不一样,前面的排名和县名称,所在省份名称都是一样的。因此我们采用面向对象的设计方法对信息处理的类进行开发。相应的类图如下所示:

    3、数据采集实体

    package com.yelang.entity;
     
    import java.io.Serializable;
    import com.alibaba.excel.annotation.ExcelProperty;
    public class CountyBase implements Serializable {
     private static final long serialVersionUID = -1760099890427975758L;
     
     @ExcelProperty(value= {"序号"},index = 1)
     private Integer index;
     
     @ExcelProperty(value= {"县级地区"},index = 2)
     private String name;
     
     @ExcelProperty(value= {"所属省"},index = 3)
     private String province;
     
     public Integer getIndex() {
      return index;
     }
     
     public void setIndex(Integer index) {
      this.index = index;
     }
     
     public String getName() {
      return name;
     }
     
     public void setName(String name) {
      this.name = name;
     }
     
     public String getProvince() {
      return province;
     }
     
     public void setProvince(String province) {
      this.province = province;
     }
     
     public CountyBase(Integer index, String name, String province) {
      super();
      this.index = index;
      this.name = name;
      this.province = province;
     }
     
     public CountyBase() {
      super();
     }
     
    }

    在上面的代码中,将排序、县级地区、省作为父类抽象出来,设计两个子类:GDP类和一般公共收入类。这里需要注意的是,由于这里我们需要将采集的数据保存到本地的Excel表格中,这里我们采用EasyExcel作为技术生成组件。@ExcelProperty这个属性中,我们定义了写入的Excel表头以及具体的排序。

    package com.yelang.entity;
     
    import java.io.Serializable;
    import com.alibaba.excel.annotation.ExcelProperty;
    public class Gdp extends CountyBase implements Serializable {
     
     private static final long serialVersionUID = 5265057372502768147L;
     
     @ExcelProperty(value= {"GDP(亿元)"},index = 4)
     private String gdp;
     
     public String getGdp() {
      return gdp;
     }
     
     public void setGdp(String gdp) {
      this.gdp = gdp;
     }
     
     public Gdp(Integer index, String name, String province, String gdp) {
      super(index,name,province);
      this.gdp = gdp;
     }
     
     public Gdp(Integer index, String name, String province) {
      super(index, name, province);
     }
     
    }
    package com.yelang.entity;
     
    import java.io.Serializable;
     
    import com.alibaba.excel.annotation.ExcelProperty;
     
    public class Gpbr extends CountyBase implements Serializable {
     
     private static final long serialVersionUID = 8612514686737317620L;
     
     @ExcelProperty(value= {"一般公共预算收入(亿元)"},index = 4)
     private String gpbr;// General public budget revenue
     
     public String getGpbr() {
      return gpbr;
     }
     
     public void setGpbr(String gpbr) {
      this.gpbr = gpbr;
     }
     
     public Gpbr(Integer index, String name, String province, String gpbr) {
      super(index, name, province);
      this.gpbr = gpbr;
     }
     
     public Gpbr(Integer index, String name, String province) {
      super(index, name, province);
     }
    }

    4、实际爬取

    下面是处理GDP数据的转换代码,如果不熟悉Jsoup可以先熟悉下相关语法,如果有类似Jquery的开发经验,对于Jsoup上手非常快。

    static void grabGdp() {
      String target = "https://www.maigoo.com/news/665462.html";
      try {
                Document doc = Jsoup.connect(target)
                        .ignoreContentType(true)
                        .userAgent(FetchCsdnCookie.ua[1])
                        .timeout(300000)
                        .header("referer","https://www.maigoo.com")
                        .get();
                Elements elements = doc.select("#t_container > div:eq(3) table tr");
                List list = new ArrayList();
                for(int i = 1;i             Element tr = elements.get(i);//获取表头
                 Elements tds = tr.select("td");
                 Integer index = Integer.valueOf(tds.get(0).text());
                 String name = tds.get(1).text();
                 String province = tds.get(2).text();
                 String gdp = tds.get(3).text();
                 Gdp county = new Gdp(index, name, province, gdp);
                 list.add(county);
                }
                String fileName = "E:/gdptest/2023全国百强县GDP排行榜 .xlsx";
                EasyExcel.write(fileName, Gdp.class).sheet("GDP百强榜").doWrite(list);
                System.out.println("完成...");
      } catch (Exception e) {
       System.out.println(e.getMessage());
       System.out.println("发生异常,继续下一轮循环");
      }
     }

    这里需要注意的是在jsoup中如何进行网页的元素定位及抓取。在上面这里,我们使用类似jquery的Dom获取方法。

     Elements elements = doc.select("#t_container > div:eq(3) table tr");

    通过这一行去获取表格下的每一个tr,然后再循环每个td就可以获取对应的数据。

    三、过程分析及结果

    1、采集过程分析

    这里采用对源程序进行debug的方法对网页进行抽丝剥茧的分析。使用jsou进行网页模拟访问

    采用select(xxx)的方法获取页面元素,

    获取tr下的td单元格数据,

    2、运行结果

    上述代码运行完成后,在目的磁盘可以看到以下两个文件,

    打开上述两个excel文件可以看到想要采集的数据已经采集完毕,数据的顺序也是完全按照网页上的顺序来进行生成的。

    总结

    以上就是本文的主要内容。本文将以Java语言为编程语言,详细讲解了如何使用Jsoup对Web网页知识进行爬取,结合EasyExcel将网页表格转换成Excel表格,同时文中给出了详细的示例代码。由于行文仓促,难免有误,欢迎批评指正交流。

    来源:blog.csdn.net/yelangkingwuzuhu/

    article/details/130901172

  • SpringBoot怎么设计业务操作日志功能?

    快乐分享,Java干货及时送达👇

    来源:blog.csdn.net/fox9916/article/details/130175379
    • 前言
    • 需求描述与分析
    • 设计思路
    • 实现方案
    • 测试
    • 总结

    前言

    很久以前都想写这篇文章,一直没有空,但直到现在我对当时的情景还有印象,之所以有印象是因为需求很简单,业务操作日志的记录与查询的功能,但是具体实现真的很烂,具体的烂法会在反面示例里细说,领导以及客户层面很认可,一系列迷之操作,让我印象深刻。

    需求描述与分析

    客户侧提出需求很简单:要对几个关键的业务功能进行操作日志记录,即什么人在什么时间操作了哪个功能,操作前的数据报文是什么、操作后的数据报文是什么,必要的时候可以一键回退。

    日志在业务系统中是必不可少的一个功能,常见的有系统日志、操作日志等:

    系统日志

    这里的系统日志是指的是程序执行过程中的关键步骤,根据实际场景输出的debug、info、warn、error等不同级别的程序执行记录信息,这些一般是给程序员或运维看的,一般在出现异常问题的时候,可以通过系统日志中记录的关键参数信息和异常提示,快速排除故障。

    操作日志

    操作日志,是用户实际业务操作行为的记录,这些信息一般存储在数据库里,如什么时间哪个用户点了某个菜单、修改了哪个配置等这类业务操作行为,这些日志信息是给普通用户或系统管理员看到。

    通过对需求的分析,客户想要是一个业务操作日志管理的功能:

    1、记录用户的业务操作行为,记录的字段有:操作人、操作时间、操作功能、日志类型、操作内容描述、操作内容报文、操作前内容报文

    2、提供一个可视化的页面,可以查询用户的业务操作行为,对重要操作回溯;

    3、提供一定的管理功能,必要的时候可以对用户的误操作回滚;

    反面实现

    明确需求后,就是怎么实现的问题了,这里先上一个反面的实现案例,也是因为这一个反面案例,才让我对这个简单的需求印象深刻。

    这里我以一个人员管理的功能为例还原一下,当时的具体实现:

    1、每个接口里都加一段记录业务操作日志的记录;

    2、每个接口里都要捕获一下异常,记录异常业务操作日志;

    下面是伪代码:

    @RestController
    @Slf4j
    @BusLog(name = "人员管理")
    @RequestMapping("/person")
    public class PersonController2 {
        @Autowired
        private IPersonService personService;
        @Autowired
        private IBusLogService busLogService;
        //添加人员信息
        @PostMapping
        public Person add(@RequestBody Person person) {
           try{
               //添加信息信息
            Person result = this.personService.registe(person);
            //保存业务日志
            this.saveLog(person);
            log.info("//增加person执行完成");        
           }catch(Exception e){
               //保存异常操作日志
               this.saveExceptionLog(e);       
           }
            return result;
        }
    }

    这种通过硬编码实现的业务操作日志管理功能,最大的问题就是业务操作日志收集与业务逻辑耦合严重,和代码重复,新开发的接口在完成业务逻辑后要织入一段业务操作日志保存的逻辑,已开发上线的接口,还要重新再修改织入业务操作日志保存的逻辑并测试,且每个接口需要织入的业务操作日志保存的逻辑是一样的。

    设计思路

    如果对AOP有一些印象的话,最好的方法就是使用aop实现:

    1、定义业务操作日志注解,注解内可以定义一些属性,如操作功能名称、功能的描述等;

    2、把业务操作日志注解标记在需要进行业务操作记录的方法上(在实际业务中,一些简单的业务查询行为通常没有必要记录);

    3、定义切入点,编写切面:切入点就是标记了业务操作日志注解的目标方法;切面的主要逻辑就是保存业务操作日志信息;

    Spring AOP

    AOP (Aspect Orient Programming),直译过来就是 面向切面编程,AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向切面编程,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术,AOP可以拦截指定的方法并且对方法增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离;

    而SpringAOP,则是AOP的一种具体实现,Spring内部对SpringAOP的应用最经典的场景就是Spring的事务,通过事务注解的配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略;与过滤器、拦截器相比,更加重要的是其适用范围不再局限于SpringMVC项目,可以在任意一层定义一个切点,织入相应的操作,并且还可以改变返回值;

    图片

    Filter和HandlerInterceptor

    之所以没有选择Filter和HandlerInterceptor,而是AOP来实现业务操作日志功能,是因为Filter和HandlerInterceptor自身的一些局限性:

    过滤器

    过滤器(Filter)是与servlet相关联的一个接口,主要适用于java web项目中,依赖于Servlet容器,是利用java的回调机制来实现过滤拦截来自浏览器端的http请求,可以拦截到访问URL对应的方法的请求和响应(ServletRequest request, ServletResponse response),但是不能对请求和响应信息中的值进行修改;一般用于设置字符编码、鉴权操作等;

    如果想要做到更细一点的类和方法或者是在非servlet环境中使用,则是做不到的;所以凡是依赖Servlet容器的环境,过滤器都可以使用,如Struts2、SpringMVC;

    图片
    拦截器

    拦截器的(HandlerInterceptor)使用范围以及功能和过滤器很类似,但是也是有区别的。首先,拦截器(HandlerInterceptor)适用于SpringMVC中,因为HandlerInterceptor接口是SpringMVC相关的一个接口,而实现java Web项目,SpringMVC是目前的首选选项,但不是唯一选项,还有struts2等;因此,如果是非SpingMVC的项目,HandlerInterceptor无法使用的;

    其次,和过滤器一样,拦截器可以拦截到访问URL对应的方法的请求和响应(ServletRequest request, ServletResponse response),但是不能对请求和响应信息中的值进行修改;一般用于设置字符编码、鉴权操作等;如果想要做到更细一点的类和方法或者是在非servlet环境中使用,则也是是做不到的;

    总之,过滤器和拦截器的功能很类似,但是拦截器的适用范围比过滤器更小;

    图片
    SpringAOP、过滤器、拦截器对比

    在匹配中同一目标时,过滤器、拦截器、SpringAOP的执行优先级是:过滤器>拦截器>SpringAOP,执行顺序是先进后出,具体的不同则体现在以下几个方面:

    1、作用域不同

    • 过滤器依赖于servlet容器,只能在 servlet容器,web环境下使用,对请求-响应入口处进行过滤拦截;
    • 拦截器依赖于springMVC,可以在SpringMVC项目中使用,而SpringMVC的核心是DispatcherServlet,而DispatcherServlet又属于Servlet的子类,因此作用域和过滤器类似;
    • SpringAOP对作用域没有限制,只要定义好切点,可以在请求-响应的入口层(controller层)拦截处理,也可以在请求的业务处理层(service层)拦截处理;

    2、颗粒度的不同

    • 过滤器的控制颗粒度比较粗,只能在doFilter()中对请求和响应进行过虑和拦截处理;
    • 拦截器提供更精细颗粒度的控制,有preHandle()、postHandle()、afterCompletion(),可以在controller对请求处理之前、请求处理后、请求响应完毕织入一些业务操作;
    • SpringAOP,提供了前置通知、后置通知、返回后通知、异常通知、环绕通知,比拦截器更加精细化的颗粒度控制,甚至可以修改返回值;

    实现方案

    环境配置

    • jdk版本:1.8开发工具:Intellij iDEA 2020.1
    • springboot:2.3.9.RELEASE
    • mybatis-spring-boot-starter:2.1.4

    依赖配置


        org.springframework.boot
        spring-boot-starter-aop

    表结构设计

    create table if not exists bus_log
    (
       id bigint auto_increment comment '自增id'
          primary key,
       bus_name varchar(100) null comment '业务名称',
       bus_descrip varchar(255) null comment '业务操作描述',
       oper_person varchar(100) null comment '操作人',
       oper_time datetime null comment '操作时间',
       ip_from varchar(50) null comment '操作来源ip',
       param_file varchar(255) null comment '操作参数报文文件'
    )
    comment '业务操作日志' default charset ='utf8';

    代码实现

    1、定义业务日志注解@BusLog,可以作用在控制器或其他业务类上,用于描述当前类的功能;也可以用于方法上,用于描述当前方法的作用;

    /**
     * 业务日志注解
     * 可以作用在控制器或其他业务类上,用于描述当前类的功能;
     * 也可以用于方法上,用于描述当前方法的作用;
     */
    @Target({ElementType.METHOD,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface BusLog {
     
     
        /**
         * 功能名称
         * @return
         */
        String name() default "";
     
        /**
         * 功能描述
         * @return
         */
        String descrip() default "";
     
    }

    2、把业务操作日志注解BusLog标记在PersonController类和方法上;

    @RestController
    @Slf4j
    @BusLog(name = "人员管理")
    @RequestMapping("/person")
    public class PersonController {
        @Autowired
        private IPersonService personService;
        private Integer maxCount=100;
     
        @PostMapping
        @NeedEncrypt
        @BusLog(descrip = "添加单条人员信息")
        public Person add(@RequestBody Person person) {
            Person result = this.personService.registe(person);
            log.info("//增加person执行完成");
            return result;
        }
        @PostMapping("/batch")
        @BusLog(descrip = "批量添加人员信息")
        public String addBatch(@RequestBody List personList){
            this.personService.addBatch(personList);
            return String.valueOf(System.currentTimeMillis());
        }
     
        @GetMapping
        @NeedDecrypt
        @BusLog(descrip = "人员信息列表查询")
        public PageInfo list(Integer page, Integer limit, String searchValue) {
           PageInfo pageInfo = this.personService.getPersonList(page,limit,searchValue);
            log.info("//查询person列表执行完成");
            return pageInfo;
        }
        @GetMapping("/{loginNo}")
        @NeedDecrypt
        @BusLog(descrip = "人员信息详情查询")
        public Person info(@PathVariable String loginNo,String phoneVal) {
            Person person= this.personService.get(loginNo);
            log.info("//查询person详情执行完成");
            return person;
        }
        @PutMapping
        @NeedEncrypt
        @BusLog(descrip = "修改人员信息")
        public String edit(@RequestBody Person person) {
             this.personService.update(person);
            log.info("//查询person详情执行完成");
            return String.valueOf(System.currentTimeMillis());
        }
        @DeleteMapping
        @BusLog(descrip = "删除人员信息")
        public String edit(@PathVariable(name = "id") Integer id) {
             this.personService.delete(id);
            log.info("//查询person详情执行完成");
            return String.valueOf(System.currentTimeMillis());
        }
    }

    3、编写切面类BusLogAop,并使用@BusLog定义切入点,在环绕通知内执行过目标方法后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述, 把方法的参数报文写入到文件中,最后保存业务操作日志信息;

    @Component
    @Aspect
    @Slf4j
    public class BusLogAop implements Ordered {
        @Autowired
        private BusLogDao busLogDao;
     
        /**
         * 定义BusLogAop的切入点为标记@BusLog注解的方法
         */
        @Pointcut(value = "@annotation(com.fanfu.anno.BusLog)")
        public void pointcut() {
        }
     
        /**
         * 业务操作环绕通知
         *
         * @param proceedingJoinPoint
         * @retur
         */
        @Around("pointcut()")
        public Object around(ProceedingJoinPoint proceedingJoinPoint) {
            log.info("----BusAop 环绕通知 start");
            //执行目标方法
            Object result = null;
            try {
                result = proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            //目标方法执行完成后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述
            Object target = proceedingJoinPoint.getTarget();
            Object[] args = proceedingJoinPoint.getArgs();
            MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
            BusLog anno1 = target.getClass().getAnnotation(BusLog.class);
            BusLog anno2 = signature.getMethod().getAnnotation(BusLog.class);
            BusLogBean busLogBean = new BusLogBean();
            String logName = anno1.name();
            String logDescrip = anno2.descrip();
            busLogBean.setBusName(logName);
            busLogBean.setBusDescrip(logDescrip);
            busLogBean.setOperPerson("fanfu");
            busLogBean.setOperTime(new Date());
            JsonMapper jsonMapper = new JsonMapper();
            String json = null;
            try {
                json = jsonMapper.writeValueAsString(args);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            //把参数报文写入到文件中
            OutputStream outputStream = null;
            try {
                String paramFilePath = System.getProperty("user.dir") + File.separator + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN) + ".log";
                outputStream = new FileOutputStream(paramFilePath);
                outputStream.write(json.getBytes(StandardCharsets.UTF_8));
                busLogBean.setParamFile(paramFilePath);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (outputStream != null) {
                    try {
                        outputStream.flush();
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
     
                }
            }
            //保存业务操作日志信息
            this.busLogDao.insert(busLogBean);
            log.info("----BusAop 环绕通知 end");
            return result;
        }
     
        @Override
        public int getOrder() {
            return 1;
        }
    }

    测试

    调试方法

    平时后端调试接口,一般都是使用postman,这里给大家安利一款工具,即Intellij IDEA的Test RESTful web service,功能和使用和postman差不多,唯一的好处就是不用在电脑上再额外装个postman,功能入口:工具栏的Tools–>http client–>Test RESTful web

    图片

    另外还有一种用法,我比较喜欢用这种,简单几句就可以发起一个http请求,还可以一次批量执行;

    图片

    图片

    验证结果

    图片

    总结

    业务操作日志记录中包含了用户操作的功能名称、功能描述、操作人、操作时间和操作的参数报文,参数报文之所以选择存储在文件中,是因为正常情况下,是不需要知道具体的参数报文,只有在回滚操作的时候才会用到,可以根据上一次的参数报文逆向操作。

    
    

  • 学会这招,来给你的 SpringBoot 工程部署的 jar 包瘦瘦身吧!

    快乐分享,Java干货及时送达👇

    SpringBoot 为我们快速开发提供了很好的架子,使得我们只需要少量配置就能开始我们的开发工作,但是当我们需要打包上传部署时,却是很神伤的一个问题,因为打出来的 Jar 包少则十几兆,多则一百来兆,我们需要上传至公网服务器时,是非常慢的,这就引出了今天的主题,SpringBoot项目Jar包如何瘦身部署

    思路

    分析 jar,我们可以看出,jar 包里面分为以下三个模块

    分为 BOOT-INFMETA-INF,org 三个部分,打开 BOOT-INF

    可以看到有 classes,lib 两个文件夹,我们编译好的代码是放在 classes 里面的,而我们所依赖的 jar 包都是放在 lib 文件夹下

    classes 部分是非常小的(我的是3M左右),lib部分是非常大的(我的是70M左右),所以上传很慢

    那我们可以将我们自己写的代码部分与所依赖的 maven jar 包部分拆开上传,每次只需要上传我们自己写的代码部分即可

    瘦身部署

    1、正常打包

    首先,我们项目的 pom.xml 文件中的打包方式如下:

    build>
        plugins>
            plugin>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

    这是 SpringBoot 中默认的打包方式,我们先按照这种方式打包出来,得到一个 jar 包,我们将 jar 包解压,如果不能直接解压,则将后缀改为 zip 再进行解压,我们只需要拿到 BOOT-INF 中的 lib 目录即可

    2、改变打包方式

    我们对 SpringBoot 中默认的打包方式做一些配置

    build>
        plugins>
            plugin>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-maven-pluginartifactId>
                configuration>
                    mainClass>com.zyxx.DeclareApplicationmainClass>
                    layout>ZIPlayout>
                    includes>
                        include>
                            groupId>nothinggroupId>
                            artifactId>nothingartifactId>
                        include>
                    includes>
                configuration>
                executions>
                    execution>
                        goals>
                            goal>repackagegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>
    • mainClass,我们指定了项目的启动类
    • layout,我们指定了打包方式为 ZIP,注意:一定是大写的
    • includes,有自己的依赖 jar,可以在此导入
    • repackage,剔除其它的依赖,只需要保留最简单的结构

    3、再次打包

    我们再次点击 maven package,得到一个 jar 包,可以看到此时的 jar 包只有几兆了

    上传启动

    我们将 lib 目录,以及最后打包的瘦身项目 jar 包,上传至服务器,目录如下

    使用命令

    nohup java -Dloader.path=./lib -jar ./sbm-0.0.1-SNAPSHOT.jar &
    • -Dloader.path,告诉它所依赖的 maven jar 包位置
    • sbm-0.0.1-SNAPSHOT.jar,项目 jar 包的名字
    • nohup、&,使得 jar 包在服务后台运行

    总结

    使用瘦身部署,方便每次的迭代更新,不用每次都上传一个很大的 jar 包,从而节省部署时间。

  • SpringBoot 3.1 正式发布,学不动了!!

    快乐分享,Java干货及时送达👇

    Spring Boot 3.1.0 现已发布,此版本添加了大量新功能和改进。

    Spring Boot 各个版本对 Java 版本的要求如下:

    亮点

    • Spring Authorization Server 的自动配置
    • 支持在开发时使用 Testcontainers 和 Docker Compose 管理外部服务
    • 简化集成测试中 Testcontainers 的配置
    • 支持 Mockito 单元测试

    升级 HttpClient 5

    在 Spring Framework 6 中,推荐使用 Apache HttpClient 5,因此已经移除了对 Apache HttpClient 4 与 RestTemplate 的支持。Spring Boot 3.0 包含了 HttpClient 4 和 5 的依赖管理。但如果使用了 HttpClient 4 仍会导致使用 RestTemplate 时出现难以诊断的错误。

    Spring Boot 3.1 为了鼓励用户改用 HttpClient 5 取代 HttpClient 4,移除了 HttpClient 4 的依赖管理。

    Servet 和 Filter 注册优化

    如果 ServletRegistrationBean 和 FilterRegistrationBean 类的注册失败,将抛出 IllegalStateException 异常而不是记录警告。如果需要保持旧有的行为,则需要在注册 bean 上调用 setIgnoreRegistrationFailure(true)方法。

    Git Commit ID Maven 插件版本属性

    覆盖 io.github.git-commit-id:git-commit-id-maven-plugin 版本的属性已更新以匹配其 artifact 名称。适应此更改,在 pom.xml 文件中需要将 git-commit-id-plugin.version 替换为 git-commit-id-maven-plugin.version。

    Spring Kafka 重试主题自动配置

    如果使用了 Apache Kafka 的自动配置重试主题(spring.kafka.retry.topic.enabled: true),并使用具有最大延迟的指数回退,则所有最大延迟水平的重试现在都发送到相同的主题。以前,即使超过了最大延迟,每个重试都会使用一个单独的主题。

    Testcontainers 的依赖管理

    Spring Boot 的依赖管理现在包括 Testcontainers。如果需要,可以使用 testcontainers.version 属性覆盖由 Spring Boot 管理的版本。

    Hibernate 6.2

    Spring Boot 3.1 升级到 Hibernate 6.2。请参阅 Hibernate 6.2 迁移指南,了解此更改可能对应用程序产生的影响。

    Jackson 2.15

    Spring Boot 3.1 升级到 Jackson 2.15。请参阅 Jackson Wiki,了解此更改可能对应用程序产生的影响。

    2.15 中显着的更改是引入了处理限制。要调整这些约束,请定义一个类似于以下内容的 Jackson2ObjectMapperBuilderCustomizer:

    @Bean
    Jackson2ObjectMapperBuilderCustomizer customStreamReadConstraints() {
        return (builder) -> builder.postConfigurer((objectMapper) -> objectMapper.getFactory()
            .setStreamReadConstraints(StreamReadConstraints.builder().maxNestingDepth(2000).build()));
    }

    升级 Mockito 5

    Spring Boot 3.1 升级到 Mockito 5,具体是 5.3。请参阅 Mockito 发布说明,了解 5.x 系列中的显着更改。

    Health Groups 默认验证健康状态

    健康组成员的配置现在将在启动时进行验证。如果包含或排除了不存在的健康指标,则启动将失败。设置 management.endpoint.health.validate-group-membership 为 false 可以禁用此验证,恢复早期版本的行为。

    服务连接

    引入了新的服务连接概念,由 ConnectionDetails bean 在应用程序中表示。这些 bean 提供了建立与远程服务连接所需的必要详细信息,而 Spring Boot 的自动配置已更新为消耗 ConnectionDetails bean。当这些 bean 可用时,它们将优先于任何与连接相关的配置属性。控制连接池大小和行为等与连接本身无关的配置属性仍将使用。

    这项低级功能旨在作为自动配置服务连接的其他更高级特性的构建块,通过定义 ConnectionDetails bean 来实现。。

    Docker Compose

    新模块spring-boot-docker-compose提供了与 Docker Compose 的集成。当应用程序启动时,Docker Compose 集成将在当前工作目录中查找配置文件。支持以下文件:

    • compose.yaml
    • compose.yml
    • docker-compose.yaml
    • docker-compose.yml

    要使用非标准文件,请设置 spring.docker.compose.file 属性。

    默认情况下,配置文件中声明的服务将使用 docker-compose up 启动,并将这些服务的连接详细信息 bean 添加到应用程序上下文中,以便可以在不进行任何进一步配置的情况下使用这些服务。当应用程序停止时,将使用 docker-compose down 关闭服务。可以使用 spring.docker.compose.lifecycle-management、spring.docker.compose.startup.command 和 spring.docker.compose.shutdown.command 配置属性自定义此生命周期管理及用于启动和关闭服务的命令。

    请参阅参考文档以获取更多详细信息,包括目前支持的服务列表。

    SSL 配置

    现在可以使用属性配置 SSL 信任材料,例如 Java KeyStores 和 PEM 编码的证书,并以更一致的方式应用于各种类型的连接,例如嵌入式 Web 服务器、数据服务、RestTemplate 和 WebClient。

    请参阅参考文档以获取更多信息。

    Spring Authorization Server 自动配置

    此版本中新增了对 Spring Authorization Server 项目的支持,同时提供了新的spring-boot-starter-oauth2-authorization-server启动器。有关详细信息,请参阅 Spring Boot 参考文档中的 Authorization Server 部分。

    Docker 镜像构建

    镜像创建日期和时间

    spring-boot:build-image Maven 目标和 bootBuildImage Gradle 任务现在具有 createdDate 配置选项,可用于将生成图像元数据中的 Created 字段的值设置为用户指定的日期或 now 以使用当前日期和时间。有关详细信息,请参阅 Gradle 和 Maven 插件文档。

    镜像应用程序目录

    spring-boot:build-image Maven 目标和 bootBuildImage Gradle 任务现在具有 applicationDirectory 配置选项,可用于设置在构建包消耗之前将应用程序内容上传到构建器映像中的位置。这也将是生成的映像中应用程序内容的位置。有关详细信息,请参阅 Gradle 和 Maven 插件文档。

  • 公司规定所有接口都用 post 请求,这是为什么?

    快乐分享,Java干货及时送达👇

    文章来源:https://www.zhihu.com/question/336797348


    目录
    • 背景

    • get 与 post 的区别

    • 所有接口都用 post 请求?


    背景


    最近在逛知乎的时候发现一个有趣的问题:公司规定所有接口都用 post 请求,这是为什么?

    看到这个问题的时候其实我也挺有感触的,因为我也曾经这样问过我自己。在上上一家公司的时候接到一个项目是从零开始搭建一个微服务,当时就有了解过接口的一些规范,比如耳熟能详的 Restful 规范,就被应用到这个微服务项目中。


    get 与 post 的区别


    今天再次看到这个问题,我也有了一些新的理解和感触,临时回顾了一下 get 与 post 的请求的一些区别。

    如下:

    • post 更安全(不会作为 url 的一部分,不会被缓存、保存在服务器日志、以及浏览器浏览记录中)
    • post 发送的数据更大(get 有 url 长度限制)
    • post 能发送更多的数据类型(get 只能发送 ASCII 字符)
    • post 比 get 慢
    • post 用于修改和写入数据,get 一般用于搜索排序和筛选之类的操作
    • get 请求的是静态资源,则会缓存,如果是数据,则不会缓存

    查看上面的区别,就会发现 post 在发送数据量大的请求时优势很显示,get 则更适合获取静态资源、简单的查询等接口。

    我个人在开发接口的时候也会注意,将简单的查询请求使用 get 方法,其他增、删、改、复杂的查询请求都可以使用 post,但不会像题主的公司一样全部使用 post。


    所有接口都用 post 请求?


    | 网友程墨 Morgan

    网友程墨 Morgan 提出如果是自己会按照『业界最佳实践』制定规范:

    | 网友苏莉安

    另外一个知友提出:就是为了迁就低水平不思进取的架构师和前后端程序员们。

    | 网友大宽宽

    大宽宽的回答:我打算跳出技术的范畴,从 ROI 的角度讨论下如果一个架构风格(比如  Restful)真的那么好,为啥应用上没有那么广泛?

    首先要明确,不管你多么喜欢技术,无论是这里说的一个 http 的 method,又或者是编程语言的一些用法、架构设计方法、甚至是 OKR 这样的管理和沟通的方法。这一切,都是为了满足企业对市场的需求。

    简单来说,公司给你发工资,不是为了让你遵守规范的,而是为了能在成本可接受的情况下,让业务落地。而其中,一般情况下,接口的形式是个微不足道的局部问题。

    对于企业来讲,技术团队要解决的更重要的问题:

    • 是理解业务模型,形成业务架构和可以稳定跑的系统;

    • 是面对大量涌入用户对系统可用性的要求对系统不会卡顿挂机的扩展性保障;

    • 是不会动不动抽疯一下,丢条数据或者数据冲突的稳定性要求,以及为了达成这些要求给监控体系的各种便利。

    但一定要纠结下 POST/GET,以及 Restful。好吧,Restful 能明确列出来的好处,就那么几点(如果有疏漏的请在评论区里补充)。

    如下:

    • 表达不同的业务动作语义:GET/POST/PATCH/PUT/DELETE……,
    • 表达“资源”的概念利用
    • url path,querystring,header,status code 等来表达很多接口功能
    • 以上两条可以达成一种“统一”的接口表达形式,以至于可以围绕这个形式实现接口维护的工具,比如 swagger。
    • Get 资源可以利用缓存

    但代价是什么?

    ①强行的统一,让本来天然不是资源的业务概念也一定要强行“资源“一下,引发了更多的理解不一致和沟通困难。

    当然,事物总是和可以“抽象”一下,业务概念抽象为“资源”很多时候都是可行的。但这这么做的收益除了证明“一个人聪明,有不错的抽象能力“,以及“更容易利用上 swagger 一类的工具“之外,我看不到啥额外的短期或者长期收益。

    乱折腾 path,querysting 等东西,让横切面治理抓取关键信息更难了。比如监控时抓一个 path 里带变量的 url 是非常恶心的事情。

    又或者看到一个 404 的报警,却根本搞不清楚到底是服务部署有问题;还是服务正常,但用户不存在;又或者是用户存在,但用户订单不存在。带来的问题是运营工具编写困难,线上问题响应能力会被降低。


    即使使用 swagger,还是需要写说明和文档来说明其业务语义。接口工具应该提供的“好理解,接口改了后文档自动生成”等好处,只有在接口反应的资源刚好和后台数据表/视图能够对应上才有效。

    也就是说只适合接口层级低的场景下有用,而对高层接口意义不大。结果开发者既要用 swagger 这样的工具,同时还是要看常规文档。本来用一套机制可以解决的问题要改成两套。

    Cache 虽好,但最怕的是管控不到位让用户拿到了过期数据。对于 Cache,业务上一般会区分动态接口和静态接口。

    前者默认不应该有 cache,所以用了 Get 之后为了防范,还得手工在大部分动态接口上加 Cache-Control: no-cache,或者动态产生 ETag(浪费 CPU)。而后者一般会采用 CDN,这一套针对 cache 做了很精巧的设计。

    使用形式各异的 method 和 url path,querystring 上做各种奇怪的拼接,会给前端带来巨大的困扰。

    因为本来一个函数调用,还得翻译一遍,活生生的弄出来一个接口翻译层。妥妥的降低人效。如果是 web,iOS,Android 三套前端,就得弄 3 个接口翻译层。

    非 GET 和 POST 之外的 method 有可能会被不恰当的网关转发规则给干掉。为此 Restful 还是搞出了 method override 这样的招数……

    所以到底适不适合,落地时听骂声和吵架声就知道了。

    有人举了 Google S3 运用 Restful 接口的例子来说明其正确性。但 S3 是干什么的大家都懂,S3 天然就是用来存取“资源“的。

    一个工具用在了恰当场景,当然是“正确“的。S3 用的好的东西,只能说明类似的阿里云 OSS,腾讯云 COS 也可以这么干。但无法证明电商业务、社交业务、I 医疗业务、政企办公协同……这些业务也适合这么干。

    而作为技术负责人,如果他搞出了一套接口方案(也许其中一条就是所有 http 接口都用 post),提高了开发效率,降低了沟通成本,降低了运维和错误定位成本,为企业真正做到了降本增效。

    把瞎折腾的成本,投入到了其他比如业务架构设计,测试体系,线上监控,容灾降级等领域上。

    最终让企业(用户需求得到满足,收入增加)和员工得到了收益(因为公司收入增加而涨薪)。

    我会评价这样的人为“真正懂架构,懂技术,善于用技术解决实际问题。水平不知道高到哪里去了”。

    如果一个技术负责人只知道遵守一个书上写的,但从没验证过在自己的环境有效的方案,以至于让企业的核心目标无法达成。他就是赵括,该马上卷铺盖卷走人。

    至于我司,使用的规范是:对于动态业务接口,只有一个接口 POST/action,在 Header 里给 X-Action 给出具体的接口名称交给网关路由,session 表示用户登录身份,以及用于推荐、防重、染色、安全用到的各种 token/签名。

    所有的业务请求参数都以 PB 编码后放在请求体里,并和后端的 gRPC 体系衔接。接口除了防重试之外,不提供常规意义上的 Cache。

    而对于静态接口,走 CDN,做多级 Cache。该用 Get 用 Get。如果一个动态接口也想利用 http 层 Cache,可以向网关申请和配置。有没有 Cache,cache 多久是网关和端上自己实施的,完全自己管控。

    各位读者可以参考看看,并根据自己所处的业务场景和前后端交互思考下“我们目前用的技术规范是性价比最高的吗,是最合适的吗?“

    如果是你来设计公司的 API 规范,会规定所有接口都用 post 请求吗,这是为什么?


  • 加密后的敏感字段还能进行模糊查询吗?该如何实现?

    快乐分享,Java干货及时送达👇

    前言

    有一个问题不知道大家想过没?敏感字段数据是加密存储在数据库的表中,如果需要对这些敏感字段进行模模糊查询,还用原来的通过sql的where从句的like来模糊查询的方式肯定是不行的,那么应该怎么实现呢?这篇文章就来解决这个问题。

    场景分析

    假如有类似这样的一个场景:有一个人员管理的功能,人员信息列表的主要字段有姓名、性别、用户账号、手机号码、身份证号码、家庭住址、注册日期等,可以对任意一条数据进行增、删、改、查,其中姓名、身份证号码、手机号码字段要支持模糊查询。

    简单分析一个场景,可以知道:手机号码、身份证号码、家庭人址字段数据是敏感数据,这些字段的数据是要加密存储在数据库里,在页面上展示的时候需要进行脱敏处理的。

    如果用户想要查询真实姓名是包含有“张三”的所有人员信息,可以在页面上输入一个关键字,如“张三”,点击开始查询后,这个参数会传递到后台,后台会执行一条sql,如“select * from sys_person where real_name like ‘%张三%’”,执行结果中包含了所有用户真实姓名包含有“张三”的所有数据记录,如“张三”,“张三丰”等。

    如果用户要查询手机号码尾号是“0537”的用户,后台执行类似与姓名模糊查询的sql,”select * from sys_person where phone like '%0537'“,肯定是得不到正确的结果的,因为手机号码字段在数据库中的数据是加密后的结果,而‘0537’是明文。身份证号码、家庭住址等其他敏感字段在模糊查询的时候也都有类似这样的问题,这也是敏感字段模糊查询的痛点,即模糊查询关键字与实际存储的数据不一致。

    实现方案

    下面分享几种解决方案:

    第一种,先解密再查询

    查询出目标表内所有的数据,在内存中对要模糊查询的敏感字段的加密数据进行解密,然后再遍历解密后的数据,与模糊查询关键字进行比较,筛选出包含有模糊查询关键字的数据行。

    这种方法是最容易想到的,但有一个比较明显的问题是,模糊查询的过程是在内存中进行的,如果数据量特别大,很容易导致内存溢出,因此不推荐在生产中使用这种方法;

    第二种,明文映射表

    新建一张映射表,存储敏感字段解密后的数据与目标表主键的映射表,需要模糊查询的时候,先对明文映射表进行模糊查询,得到符合条件的目标数据的主键,再返回来根据主键查询目标表;

    这种方法,实际上是有点掩耳盗铃的感觉,敏感字段加密存储的字段主要是考虑到安全性,使用明文映射表来存储解密后的敏感字段,实际上相当于敏感字段没有加密存储,与最被要对敏感字段加密的初衷相违背,因此不推荐在生产中使用这种方法;

    第三种,数据库层面进行解密查询

    后台在执行查询sql时对敏感字段先解密,然后再执行like,以上面的人员管理列表模糊查询为例,即对sql的改造为:“select * from sys_person where AES_DECRYPT(phone,'key') like '%0537'”;

    这种方法的优点是,成本比较小,容易实现,但是缺点很明显,该字段无法通过数据库索引来优化查询,另外有一些数据库无法保证数据库的加解密算法与程序的加解密算法一致,可能会导致可以程序中加密,但是无法在数据库中解密的或者可以在数据库加密无法在程序中解密的问题,因此不推荐在生产中使用这种方法;

    第四种,分词密文映射表

    这种方法是对第二种思路的基础上进行延伸优化,也是主流的方法。新建一张分词密文映射表,在敏感字段数据新增、修改的后,对敏感字段进行分词组合,如“15503770537”的分词组合有“155”、“0377”、“0537”等,再对每个分词进行加密,建立起敏感字段的分词密文与目标数据行主键的关联关系;在处理模糊查询的时候,对模糊查询关键字进行加密,用加密后的模糊查询关键字,对分词密文映射表进行like查询,得到目标数据行的主键,再以目标数据行的主键为条件返回目标表进行精确查询。

    图片一:分组组合加密前

    图片二:分组组合加密后

    淘宝、阿里、拼多、京东等大厂对用户敏感数据加密后支持模糊查询都是这样的原理,下面是几个大厂的敏感字段模糊查询方案说明,有兴趣可以了解一下:

    淘宝密文字段检索方案

    • https://open.taobao.com/docV3.htm?docId=106213&docType=1

    阿里巴巴文字段检索方案

    • https://jaq-doc.alibaba.com/docs/doc.htm?treeId=1&articleId=106213&docType=1

    拼多多密文字段检索方案

    • https://open.pinduoduo.com/application/document/browse?idStr=3407B605226E77F2

    京东密文字段检索方案

    • https://jos.jd.com/commondoc?listId=345

    这种方法的优点就是原理简单,实现起来也不复杂,但是有一定的局限性,算是一个对性能、业务相折中的一个方案,相比较之下,在能想的方法中,比较推荐这种方法,但是要特别注意的是,对模糊查询的关键字的长度,要在业务层面进行限制;以手机号为例,可以要求对模糊查询的关键字是四位或者是五位,具体可以再根据具体的场景进行详细划分。

    为什么要增加这样的限制呢?因为明文加密后长度为变长,有额外的存储成本和查询性能成本,分词组合越多,需要的存储空间以及所消耗的查询性能成本也就更大,并且分词越短,被硬破解的可能性也就越大,也会在一定程度上导致安全性降低;

    环境配置

    • jdk版本:1.8开发工具:Intellij iDEA 2020.1
    • springboot:2.3.9.RELEASE
    • mybatis-spring-boot-starter:2.1.4

    依赖配置

    示例主要用到了SpringAop,加密是对称加密,用到了hutool工具包里的加密解密工具类,也可以使用自己封装的加密解密工具类。


        org.springframework.boot
        spring-boot-starter-aop


        cn.hutool
        hutool-all
        5.3.3

    代码实现

    1、新建分词密文映射表;

    如果是多个模糊查询的字段,可以共用在一张分词密文映射表中扩展多个字段,以示例中的人员管理功能为例,新建sys_person_phone_encrypt表(人员的手机号码分词密文映射表),用于存储人员id与分词组合密文的映射关系

    create table if not exists sys_person_phone_encrypt
    (
       id bigint auto_increment comment '主键' primary key,
       person_id int not null comment '关联人员信息表主键',
       phone_key varchar(500) not null comment '手机号码分词密文'
    )
    comment '人员的手机号码分词密文映射表';

    2、敏感字段数据在保存入库的时候,对敏感字段进行分词组合并加密码,存储在分词密文映射表;

    在注册人员信息的时候,先取出通过AOP进行加密过的手机号码进行解密;手机号码解密之后,对手机号码按照连续四位进行分词组合,并对每一个手机号码的分词进行加密,最后把所有的加密后手机号码分词拼接成一个字符串,与人员id一起保存到人员的手机号码分词密文映射表;

    public Person registe(Person person) {
        this.personDao.insert(person);
        String phone = this.decrypt(person.getPhoneNumber());
        String phoneKeywords = this.phoneKeywords(phone);
        this.personDao.insertPhoneKeyworkds(person.getId(),phoneKeywords);
        return person;
    }
    private String phoneKeywords(String phone) {
        String keywords = this.keywords(phone, 4);
        System.out.println(keywords.length());
        return keywords;
    }
     
    //分词组合加密
    private String keywords(String word, int len) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i         int start = i;
            int end = i + len;
            String sub1 = word.substring(start, end);
            sb.append(this.encrypt(sub1));
            if (end == word.length()) {
                break;
            }
        }
        return sb.toString();
    }
    public String encrypt(String val) {
        //这里特别注意一下,对称加密是根据密钥进行加密和解密的,加密和解密的密钥是相同的,一旦泄漏,就无秘密可言,
        //“fanfu-csdn”就是我自定义的密钥,这里仅作演示使用,实际业务中,这个密钥要以安全的方式存储;
        byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
        SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
        String encryptValue = aes.encryptBase64(val);
        return encryptValue;
    }
    public String decrypt(String val) {
        //这里特别注意一下,对称加密是根据密钥进行加密和解密的,加密和解密的密钥是相同的,一旦泄漏,就无秘密可言,
        //“fanfu-csdn”就是我自定义的密钥,这里仅作演示使用,实际业务中,这个密钥要以安全的方式存储;
        byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
        SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
        String encryptValue = aes.decryptStr(val);
        return encryptValue;
    }

    3、模糊查询的时候,对模糊查询关键字进行加密,以加密后的关键字密文为查询条件,查询密文映射表,得到目标数据行的id,再以目标数据行id为查询条件,查询目标数据表;

    根据手机号码的四位进行模糊查询的时候,以加密后模糊查询的关键字为条件,查询sys_person_phone_encrypt表(人员的手机号码分词密文映射表),得到人员信息id;再以人员信息id,查询人员信息表;

    public List getPersonList(String phoneVal) {
        if (phoneVal != null) {
           return this.personDao.queryByPhoneEncrypt(this.encrypt(phoneVal));
        }
        return this.personDao.queryList(phoneVal);
    }

    图片

    示例完整代码:

    • https://gitcode.net/fox9916/fanfu-web.git

    来源:blog.csdn.net/fox9916/article/details/129997442