分类: springboot

  • 面试官: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

  • 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 插件文档。

  • SpringBoot + 规则引擎 URule,真的很强!

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

    没有规则,不成方圆;

    一、背景

    前段时间,在做项目重构的时候,遇到很多地方需要做很多的条件判断。当然可以用很多的if-else判断去解决,但是当时也不清楚怎么回事,就想玩点别的。于是乎,就去调研了规则引擎。

    当然,市面上有很多成熟的规则引擎,功能很多,性能很好。但是,就是想玩点不一样的(大家做技术选型别这样,这个是反面教材)。最终一款URule的规则引擎吸引了我,主要还是采用浏览器可直接配置,不需要过多安装,可视化规则也做的不错。经过一系列调研,后面就把它接入了项目中,顺便记录下调研的结果。

    二、介绍

    规则引擎其实是一种组件,它可以嵌入到程序当中。将程序复杂的判断规则从业务代码中剥离出来,使得程序只需要关心自己的业务,而不需要去进行复杂的逻辑判断;简单的理解是规则接受一组输入的数据,通过预定好的规则配置,再输出一组结果。

    当然,市面上有很多成熟的规则引擎,如:Drools、Aviator、EasyRules等等。但是URule,它可以运行在Windows、Linux、Unix等各种类型的操作系统之上,采用纯浏览器的编辑模式,不需要安装工具,直接在浏览器上编辑规则和测试规则。

    当然这款规则引擎有开源和pro版本的区别,至于pro版是啥,懂的都懂,下面放个表格,了解下具体的区别

    特性 PRO版 开源版
    向导式决策集
    脚本式决策集
    决策树
    决策流
    决策表
    交叉决策表
    复杂评分卡
    文件名、项目名重构
    参数名、变量常量名重构
    Excel决策表导入
    规则集模版保存与加载
    中文项目名和文件名支持
    服务器推送知识包到客户端功能的支持
    知识包优化与压缩的支持
    客户端服务器模式下大知识包的推拉支持
    规则集中执行组的支持
    规则流中所有节点向导式条件与动作配置的支持
    循环规则多循环单元支持
    循环规则中无条件执行的支持
    导入项目自动重命名功能
    规则树构建优化
    对象查找索引支持
    规则树中短路计算的支持
    规则条件冗余计算缓存支持
    基于方案的批量场景测试功能
    知识包调用监控
    更为完善的文件读写权限控制
    知识包版本控制
    SpringBean及Java类的热部署
    技术支持

    三、安装使用

    实际使用时,有四种使用URule Pro的方式,分别是嵌入式模式、本地模式、分布式计算模式以及独立服务模式。

    但是我们这里不考虑URule Pro,咱自己整个开源版,在开源版集成springboot的基础上做一个二次开发,搜了一圈,其实就有解决方案。大致的项目模块如下:

    图片

    自己创建个空数据库,只需要在edas-rule-server服务中修改下数据库的配置,然后启动服务即可。第一次启动完成,数据库中会创建表。

    properties
    复制代码spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/urule-data?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=mysql

    上面说过,它是纯用浏览器进行编辑,配置规则的,只需要打开浏览器,输入地址:http://localhost:8090/urule/frame,看到这个界面,就说明启动成功了。

    图片

    四、基础概念

    3.1整体介绍

    先说下URule它的构成部分,主要是两部分:1、设计器部分 2、规则执行引擎。设计器部分主要是库文件和规则文件构成。下面看下整体的结构图

    图片

    3.2库文件

    如上图介绍的,库文件有4种,包括变量库,参数库,常量库和动作库。其实类似于Java开发的系统中的实体对象,枚举,常量以及方法。

    上面说过,规则都是可视化配置的。在配置规则的过程中,就需要引入各种已经定义好的库文件,再结合业务需求,从而配置出符合业务场景的业务规则,所以哪里都有库文件的身影。

    3.2.1变量库文件

    在业务开发中,我们会创建很多Getter和Setter的Java类,比如PO、VO、BO、DTO、POJO等等,其实这些类new对象后主要起到的作用就是数据的载体,用来传输数据。

    在URule中,变量库就是用来映射这些对象,然后可以在规则中使用,最终完成业务和规则的互动。最后上一张图,用来创建变量库

    图片

    对了,上面废话了这么多可视化配置,这才是第一次展示配置界面,惭愧惭愧。

    上图一目了然,在“库”这个菜单底下右键,然后点击添加变量库即可,最后定义自己喜欢的变量库名,当然名字只支持中文或者英文,其他字符不可用。

    图片

    创建完变量库后,就可以对变量库进行编辑,可以认为就是给POJO添加属性

    图片

    也不弯弯绕绕讲什么术语,就个人理解。图左边是创建类,其中名称是它的别名,配置规则用它代替这个类。图右边是类的属性,我这里随便写了几个,估计看了懂得都懂。

    最后在业务系统中创建对应的类,注意全限定名和配置变量库的类路径一致。

    package com.cicada;

    import com.bstek.urule.model.Label;
    import lombok.Data;

    /**
     * @author 往事如风
     * @version 1.0
     * @date 2023/3/3 15:38
     * @description
     */
    @Data
    public class Stu {

        @Label("姓名")
        private String name;

        @Label("年龄")
        private int age;

        @Label("班级")
        private String classes;
    }

    最后说下这个@Label注解,这个是由URule提供的注解,主要是描述字段的属性,跟变量库的标题一栏一致就行。听官方介绍可以通过这个注解,实现POJO属性和变量库属性映射。就是POJO写好,然后对应规则的变量库就不需要重新写,可以直接生成。反正就有这个功能,这里就直接一笔带过了。

    3.2.2常量库文件

    说到常量库,这个就可以认为是我们Java系统中的常量,枚举。比如性别,要定义枚举吧;比如对接的机构,也可以定义一个枚举吧。

    当然,类似于变量库,常量库也可以实现和系统中的枚举相互映射,这样做的好处可以避免我们手动输入,防止输入错误。创建常量库也比较简单,直接在“库”这个菜单下右键,“添加常量库”。

    创建好常量库文件后,也会出现如下页面:

    图片

    3.2.3参数库文件

    参数库,就是URule规则中的临时变量,变量的类型和数量不固定。可以认为类似于Map,实际上存储参数库的也就是个Map。

    同样的套路,直接在“库”这个菜单下右键,“添加参数库”。

    可以看到,参数库已经少了左边分类这一项,直接添加参数,选择类型就是干,相对简单了很多。“名称”这列我这里用了英文,就是Map中的key,而“标题”这列就是在配置规则时候显示用的,中文看着比较直观。

    当然还需要注意的点是,定义的名称要保证唯一,因为Map中的key是唯一的,不然就会存在覆盖的情况。

    3.2.4动作库文件

    动作库可以对配置在spring中的bean方法进行映射,然后可以在规则中直接调用这批方法。惯用套路,还是在“库”菜单下右键,点击“添加动作库”。

    图片

    然后我在系统中添加了一个类Action,然后在类上标记@Component注解,将该类交给spring的bean容器管理。该类中添加一些方法,在方法上标记@ExposeAction注解,该注解是URule定义的,说明被标记的方法都会被动作库读取到。

    package com.bstek.urule.cicada;

    import com.bstek.urule.action.ActionId;
    import com.bstek.urule.model.ExposeAction;
    import org.springframework.stereotype.Component;

    import java.text.SimpleDateFormat;
    import java.util.Date;

    /**
     * @author 往事如风
     * @version 1.0
     * @date 2023/3/10 13:59
     * @description
     */
    @Component("action")
    public class Action {

        @ActionId("Hello")
        public String hello(){
            return "hello";
        }

        @ExposeAction(value="方法1")
        public boolean evalTest(String username){
            if(username==null){
                return false;
            }else if(username.equals("张三")){
                return true;
            }
            return false;
        }

        @ExposeAction(value="测试Int")
        public int testInt(int a,int b){
            return a+b;
        }

        @ExposeAction(value="打印内容")
        public void printContent(String username, Date birthday){
            SimpleDateFormat sd=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            if(birthday!=null){
                System.out.println(username+"今年已经"+sd.format(birthday)+"岁了!");
            }else{
                System.out.println("Hello "+username+"");
            }
        }
        
        @ExposeAction(value="打印Stu")
        public void printUser(Stu m){
            System.out.println("Hello "+m.getName()+", is age:"+m.getAge());
        }
    }

    最后在动作库页面上添加bean,“Bean Id”一列输入对应的spring bean的名称,这里输入action。然后点击操作列中的小手按钮,就会弹出刚在Action类中标记了ExposeAction注解的方法。选择一个指定的方法添加进来,最后看到方法对应的参数也会被自动加载进去。

    最后,变量库、参数库、动作库、常量库这些库文件定义好后,各种规则文件配置的时候就可以导入他们。但是一旦这些库文件被某个规则文件使用,就不要随意修改库文件了。

    我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!

    3.3规则集

    说到规则集,顾名思义,就是配置规则了。前面定义的库文件就需要导入到规则集中去配置使用。它是使用频率最高的一个业务规则实现方式。

    规则集说的是规则的集合,由三个部分规则组成:如果、那么、否则。

    在规则集的定义的方式上,URule由向导式和脚本式两种;

    • 向导式规则集:就是在页面上通过鼠标点点点,高度的可视化配置,不是开发都能懂,这也是这个规则引擎的亮点所在。
    • 脚本式规则集:听名字就知道了,这玩意要写脚本的。拉高配置门槛,需要懂点编码的人来编写。

    3.3.1向导式规则集

    还是一样,首先新建。这次是在“决策集”菜单上右键,点击“添加向导式决策集”,这样就创建好一个规则集了。

    图片

    在配置规则前,可以先导入前面定义好的库文件。我这里导入变量库文件,页面上点击“变量库”,然后选择指定的变量库文件即可。如图所示;

    图片

    最后,可以愉快的配置规则了,向导式没什么好讲的,都是可视化界面,点点点即可。下面是我配置的一个简单的规则集;

    图片

    可以看到由三部分组成:如果、那么、否则;

    1. 如果:配置规则的条件;
    2. 那么:配置满足条件后执行的动作,一般配置变量赋值比较多
    3. 否则:配置不满足条件执行的动作

    最后,附上添加完规则后,通过代码去执行规则;

    package com.cicada;

    import cn.hutool.core.bean.BeanUtil;
    import com.Result;
    import com.bstek.urule.Utils;
    import com.bstek.urule.runtime.KnowledgePackage;
    import com.bstek.urule.runtime.KnowledgeSession;
    import com.bstek.urule.runtime.KnowledgeSessionFactory;
    import com.bstek.urule.runtime.service.KnowledgeService;
    import com.cicada.req.StuReq;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.io.IOException;

    /**
     * @author 往事如风
     * @version 1.0
     * @date 2023/3/10 16:47
     * @description
     */
    @RestController
    @RequestMapping("/rule")
    public class RuleDataController {

        @PostMapping("/stu")
        public Result rule(@RequestBody StuReq stuReq) throws IOException {
            KnowledgeService knowledgeService = (KnowledgeService) Utils.getApplicationContext().getBean(KnowledgeService.BEAN_ID);
            KnowledgePackage knowledgePackage = knowledgeService.getKnowledge("xxx/xxx");
            KnowledgeSession knowledgeSession = KnowledgeSessionFactory.newKnowledgeSession(knowledgePackage);
            Stu stu = BeanUtil.copyProperties(stuReq, Stu.class);
            knowledgeSession.insert(stu);
            knowledgeSession.fireRules();
            return Result.success(stu.getTeacher());
        }
    }

    请求接口,最终参数符合配置的条件,返回“那么”中配置的输出结果。

    3.3.2脚本式规则集

    脚本式的规则集,各种原理都是和向导式一模一样,无非就是拉高门槛,用写脚本的方式去实现配置的规则。这里不做过多的介绍了。

    3.4决策表

    再聊下决策表,其实它就是规则集的另一种展示形式,比较相对规则集,我更喜欢用决策表去配置规则,应为它呈现的更加直观,更便于理解。但是本质和规则集没啥区别。

    也不展开过多的赘述,这里我就放一张配置过的决策表;

    图片

    3.5其他

    当然,还有其他的概念和功能,这里也不一一介绍了,因为上面说的已经是最常用的了,想了解的可以自行去了解。其他功能包括:交叉决策表、评分卡、复杂评分卡、决策树、规则流;当然,其中有些是Pro版的功能。

    我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!

    四、运用场景

    最近在开发一期大版本的需求,其中就有个场景,具体如下;参与购买订单的用户都会有自己的一个职级,也可以说是角色。每个用户都会有三个职位:普通用户、会员、精英会员。

    然后,每个月初都会对用户进行一次晋升处理,普通用户达到要求,就会晋升为会员,会员达到要求就会晋升为精英会员。

    当然,普通用户晋升会员,会员晋升精英会员,都会有不同的规则;

    1. 普通用户->会员:3个月内帮注册人数达到3人;3个月内自己和底下团队的人,下单金额超过1万;个人的订单继续率超过80%。
    2. 会员->精英会员:3个月内帮注册人数达到6人;3个月内自己和底下团队的人,下单金额超过5万;个人的订单继续率超过90%。
    3. 不能跨级晋升,普通用户最多只能到会员,达到会员了才能晋升到精英会员。

    当然,这只是做过简化的一部分需求,我做过稍许的改动,真实的需求场景并没有这么简单。

    下面,我对这个需求做一个规则的配置,这里用一个决策表进行配置;在配置规则前,我添加一个变量库文件和常量库;

    图片

    图片

    最后,添加一个决策表,并进行规则配置;

    图片

    可以看到,表格一共五列,其中前四列是规则,最后一列是满足规则后输出的信息。这样看着就很清晰,即使并不是技术人员,也可以轻松看懂其中的规则。

    五、总结

    规则引擎对于我们的系统而言可用可不用,它可以锦上添花,帮助我们剥离出业务中需要进行大量判断的场景。但是,这种规则的剥离,需要我们开发人员对需求进行理解,在理解的基础上进行抽象概念的具化。这,也是整个编程的必经之路

    六、参考源码

    • https://gitee.com/cicadasmile/butte-java-note
    • https://gitee.com/cicadasmile/butte-flyer-parent

    来源:juejin.cn/post/7210194936276680759