Java 中代码优化的 30 个小技巧(中)

news2024/11/28 3:43:57

11 位运算效率更高

如果你读过 JDK 的源码,比如 ThreadLocal、HashMap 等类,你就会发现,它们的底层都用了位运算。

为什么开发 JDK 的大神们,都喜欢用位运算?

答:因为位运算的效率更高。

在 ThreadLocal 的 get、set、remove 方法中都有这样一行代码:

int i = key.threadLocalHashCode & (len-1);

通过 key 的 hashCode 值,与数组的长度减 1。其中 key 就是 ThreadLocal 对象,与数组的长度减 1,相当于除以数组的长度减 1,然后取模。

这是一种 hash 算法。

接下来给大家举个例子:假设 len=16,key.threadLocalHashCode=31,

于是:int i = 31 & 15 = 15

相当于:int i = 31 % 16 = 15

计算的结果是一样的,但是使用与运算效率跟高一些。

为什么与运算效率更高?

答:因为 ThreadLocal 的初始大小是 16,每次都是按 2 倍扩容,数组的大小其实一直都是 2 的 n 次方。

这种数据有个规律就是高位是 0,低位都是 1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是 0。只需考虑低位的与运算,所以效率更高。

12 巧用第三方工具类

在 Java 的庞大体系中,其实有很多不错的小工具,也就是我们平常说的轮子。

如果在我们的日常工作当中,能够将这些轮子用户,再配合一下 IDEA 的快捷键,可以极大得提升我们的开发效率。

如果你引入 com.google.guava 的 pom 文件,会获得很多好用的小工具。这里推荐一款 com.google.common.collect 包下的集合工具 Lists。

它是在太好用了,让我爱不释手。

如果你想将一个大集合分成若干个小集合。

之前我们是这样做的:


List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);

List<List<Integer>> partitionList = Lists.newArrayList();
int size = 0;
List<Integer> dataList = Lists.newArrayList();
for(Integer data : list) {
   if(size >= 2) {
      dataList = Lists.newArrayList();
      size = 0;
   } 
   size++;
   dataList.add(data);
}

将 list 按 size=2 分成多个小集合,上面的代码看起来比较麻烦。

如果使用 Lists 的 partition 方法,可以这样写代码:


List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);
List<List<Integer>> partitionList = Lists.partition(list, 2);
System.out.println(partitionList);

执行结果:

[[1, 2], [3, 4], [5]]

这个例子中,list 有 5 条数据,我将list集合按大小为 2,分成了 3 页,即变成 3 个小集合。这个是我最喜欢的方法之一,经常在项目中使用。

比如有个需求:现在有 5000 个 id,需要调用批量用户查询接口,查出用户数据。但如果你直接查 5000 个用户,单次接口响应时间可能会非常慢。如果改成分页处理,每次只查 500 个用户,异步调用 10 次接口,就不会有单次接口响应慢的问题。

13 用同步代码块代替同步方法

在某些业务场景中,为了防止多个线程并发修改某个共享数据,造成数据异常。

为了解决并发场景下,多个线程同时修改数据,造成数据不一致的情况。通常情况下,我们会加锁。

但如果锁加得不好,导致锁的粒度太粗,也会非常影响接口性能。

在 Java 中提供了 synchronized 关键字给我们的代码加锁。

通常有两种写法:在方法上加锁 和 在代码块上加锁。

先看看如何在方法上加锁:


public synchronized doSave(String fileUrl) {
    mkdir();
    uploadFile(fileUrl);
    sendMessage(fileUrl);
}

这里加锁的目的是为了防止并发的情况下,创建了相同的目录,第二次会创建失败,影响业务功能。

但这种直接在方法上加锁,锁的粒度有点粗。因为 doSave 方法中的上传文件和发消息方法,是不需要加锁的。只有创建目录方法,才需要加锁。

我们都知道文件上传操作是非常耗时的,如果将整个方法加锁,那么需要等到整个方法执行完之后才能释放锁。显然,这会导致该方法的性能很差,变得得不偿失。

这时,我们可以改成在代码块上加锁了,具体代码如下:

 


public void doSave(String path,String fileUrl) {
    synchronized(this) {
      if(!exists(path)) {
          mkdir(path);
       }
    }
    uploadFile(fileUrl);
    sendMessage(fileUrl);
}

这样改造之后,锁的粒度一下子变小了,只有并发创建目录功能才加了锁。而创建目录是一个非常快的操作,即使加锁对接口的性能影响也不大。

最重要的是,其他的上传文件和发送消息功能,任然可以并发执行。

14 不用的数据及时清理

在 Java 中保证线程安全的技术有很多,可以使用 synchroized、Lock 等关键字给代码块加锁。

但是它们有个共同的特点,就是加锁会对代码的性能有一定的损耗。

其实,在jdk中还提供了另外一种思想即用空间换时间。

没错,使用 ThreadLocal 类就是对这种思想的一种具体体现。

ThreadLocal 为每个使用变量的线程提供了一个独立的变量副本,这样每一个线程都能独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal 的用法大致是这样的:

1) 先创建一个 CurrentUser 类,其中包含了 ThreadLocal 的逻辑。


public class CurrentUser {
    private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();

    public static void set(UserInfo userInfo) {
        THREA_LOCAL.set(userInfo);
    }

    public static UserInfo get() {
       THREA_LOCAL.get();
    }

    public static void remove() {
       THREA_LOCAL.remove();
    }
}

2) 在业务代码中调用 CurrentUser 类。


public void doSamething(UserDto userDto) {
   UserInfo userInfo = convert(userDto);
   CurrentUser.set(userInfo);
   ...

   //业务代码
   UserInfo userInfo = CurrentUser.get();
   ...
}

在业务代码的第一行,将 userInfo 对象设置到 CurrentUser,这样在业务代码中,就能通过 CurrentUser.get() 获取到刚刚设置的 userInfo 对象。特别是对业务代码调用层级比较深的情况,这种用法非常有用,可以减少很多不必要传参。

但在高并发的场景下,这段代码有问题,只往 ThreadLocal 存数据,数据用完之后并没有及时清理。

ThreadLocal 即使使用了 WeakReference(弱引用)也可能会存在内存泄露问题,因为  entry 对象中只把 key(即 threadLocal 对象)置成了弱引用,但是 value 值没有。

那么,如何解决这个问题呢?


public void doSamething(UserDto userDto) {
   UserInfo userInfo = convert(userDto);

   try{
     CurrentUser.set(userInfo);
     ...

     //业务代码
     UserInfo userInfo = CurrentUser.get();
     ...
   } finally {
      CurrentUser.remove();
   }
}

需要在 finally 代码块中,调用 remove 方法清理没用的数据。

15 用 equals 方法比较是否相等

不知道你在项目中有没有见过,有些同事对 Integer 类型的两个参数使用==号比较是否相等?

反正我见过的,那么这种用法对吗?

我的回答是看具体场景,不能说一定对,或不对。

有些状态字段,比如 orderStatus有:-1(未下单)、0(已下单),1(已支付),2(已完成)、3(取消)、5种状态。

这时如果用 == 判断是否相等:


Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1 == orderStatus2);

返回结果会是 true 吗?

答案:是 false。

有些同学可能会反驳,Integer 中不是有范围是 -128 ~ 127 的缓存吗?

为什么是 false?

先看看 Integer 的构造方法:


public Integer(int value) {
  this.value = value;
}

它其实并没有用到缓存。那么缓存是在哪里用的?

答案在 valueOf 方法中:


public static Integer valueOf(int i) {
  if (i >= IntegerCache.low && i <= IntegerCache.high) {
    return IntegerCache.cache[i + (-IntegerCache.low)];
  }
  return new Integer(i);
}

如果上面的判断改成这样:


String orderStatus1 = new String("1");
String orderStatus2 = new String("1");
System.out.println(Integer.valueOf(orderStatus1) == Integer.valueOf(orderStatus2));

返回结果会是 true 吗?

答案:还真是 true。

我们要养成良好编码习惯,尽量少用 == 判断两个 Integer 类型数据是否相等,只有在上述非常特殊的场景下才相等。

而应该改成使用 equals 方法判断:

Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1.equals(orderStatus2));

运行结果为 true。

16 避免创建大集合

很多时候,我们在日常开发中,需要创建集合。比如为了性能考虑,从数据库查询某张表的所有数据,一次性加载到内存的某个集合中,然后做业务逻辑处理。

例如:


List<User> userList = userMapper.getAllUser();
for(User user:userList) {
   doSamething();
}

从数据库一次性查询出所有用户,然后在循环中,对每个用户进行业务逻辑处理。

如果用户表的数据量非常多时,这样 userList 集合会很大,可能直接导致内存不足,而使整个应用挂掉。

针对这种情况,必须做分页处理。

例如:


private static final int PAGE_SIZE = 500;

int currentPage = 1;
RequestPage page = new RequestPage();
page.setPageNo(currentPage);
page.setPageSize(PAGE_SIZE);

Page<User> pageUser = userMapper.search(page);

while(pageUser.getPageCount() >= currentPage) {
    for(User user:pageUser.getData()) {
       doSamething();
    }
   page.setPageNo(++currentPage);
   pageUser = userMapper.search(page);
}

通过上面的分页改造之后,每次从数据库中只查询 500 条记录,保存到 userList 集合中,这样 userList 不会占用太多的内存。

这里特别说明一下,如果你查询的表中的数据量本来就很少,一次性保存到内存中,也不会占用太多内存,这种情况也可以不做分页处理。

此外,还有中特殊的情况,即表中的记录数并算不多,但每一条记录,都有很多字段,单条记录就占用很多内存空间,这时也需要做分页处理,不然也会有问题。

整体的原则是要尽量避免创建大集合,导致内存不足的问题,但是具体多大才算大集合。目前没有一个唯一的衡量标准,需要结合实际的业务场景进行单独分析。

17 状态用枚举

在我们建的表中,有很多状态字段,比如订单状态、禁用状态、删除状态等。

每种状态都有多个值,代表不同的含义。

比如订单状态有:

  • 1:表示下单

  • 2:表示支付

  • 3:表示完成

  • 4:表示撤销

如果没有使用枚举,一般是这样做的:


public static final int ORDER_STATUS_CREATE = 1;
public static final int ORDER_STATUS_PAY = 2;
public static final int ORDER_STATUS_DONE = 3;
public static final int ORDER_STATUS_CANCEL = 4;
public static final String ORDER_STATUS_CREATE_MESSAGE = "下单";
public static final String ORDER_STATUS_PAY = "下单";
public static final String ORDER_STATUS_DONE = "下单";
public static final String ORDER_STATUS_CANCEL = "下单";

需要定义很多静态常量,包含不同的状态和状态的描述。

使用枚举定义之后,代码如下:


public enum OrderStatusEnum {  
     CREATE(1, "下单"),  
     PAY(2, "支付"),  
     DONE(3, "完成"),  
     CANCEL(4, "撤销");  

     private int code;  
     private String message;  

     OrderStatusEnum(int code, String message) {  
         this.code = code;  
         this.message = message;  
     }  

     public int getCode() {  
        return this.code;  
     }  

     public String getMessage() {  
        return this.message;  
     }  

     public static OrderStatusEnum getOrderStatusEnum(int code) {  
        return Arrays.stream(OrderStatusEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);  
     }  
}

使用枚举改造之后,职责更单一了。

而且使用枚举的好处是:

  • 代码的可读性变强了,不同的状态,有不同的枚举进行统一管理和维护。

  • 枚举是天然单例的,可以直接使用 == 号进行比较。

  • code 和 message 可以成对出现,比较容易相关转换。

  • 枚举可以消除 if...else 过多问题。

18 把固定值定义成静态常量

不知道你在实际的项目开发中,有没有使用过固定值?

例如:


if(user.getId() < 1000L) {
   doSamething();
}

或者:


if(Objects.isNull(user)) {
   throw new BusinessException("该用户不存在");
}

其中 1000L 和该用户不存在是固定值,每次都是一样的。

既然是固定值,我们为什么不把它们定义成静态常量呢?

这样语义上更直观,方便统一管理和维护,更方便代码复用。

代码优化为:


private static final int DEFAULT_USER_ID = 1000L;
...
if(user.getId() < DEFAULT_USER_ID) {
   doSamething();
}

或者:


private static final String NOT_FOUND_MESSAGE = "该用户不存在";
...
if(Objects.isNull(user)) {
   throw new BusinessException(NOT_FOUND_MESSAGE);
}

使用 static final 关键字修饰静态常量,static 表示静态的意思,即类变量,而 final 表示不允许修改。

两个关键字加在一起,告诉 Java 虚拟机这种变量,在内存中只有一份,在全局上是唯一的,不能修改,也就是静态常量。

19. 避免大事务

很多小伙伴在使用 Spring 框架开发项目时,为了方便,喜欢使用 @Transactional 注解提供事务功能。

没错,使用 @Transactional 注解这种声明式事务的方式提供事务功能,确实能少写很多代码,提升开发效率。

但也容易造成大事务,引发其他的问题。

下面用一张图看看大事务引发的问题。

 

从图中能够看出,大事务问题可能会造成接口超时,对接口的性能有直接的影响。

我们该如何优化大事务呢?

  • 少用 @Transactional 注解

  • 将查询 (select) 方法放到事务外

  • 事务中避免远程调用

  • 事务中避免一次性处理太多数据

  • 有些功能可以非事务执行

  • 有些功能可以异步处理

20 消除过长的 if...else

我们在写代码的时候,if...else 的判断条件是必不可少的。不同的判断条件,走的代码逻辑通常会不一样。

废话不多说,先看看下面的代码。

public interface IPay {  
    void pay();  
}  

@Service
public class AliaPay implements IPay {  
     @Override
     public void pay() {  
        System.out.println("===发起支付宝支付===");  
     }  
}  

@Service
public class WeixinPay implements IPay {  
     @Override
     public void pay() {  
         System.out.println("===发起微信支付===");  
     }  
}  

@Service
public class JingDongPay implements IPay {  
     @Override
     public void pay() {  
        System.out.println("===发起京东支付==="); 
     }  
}  

@Service
public class PayService {  
     @Autowired
     private AliaPay aliaPay;  
     @Autowired
     private WeixinPay weixinPay;  
     @Autowired
     private JingDongPay jingDongPay;  

     public void toPay(String code) {  
         if ("alia".equals(code)) {  
             aliaPay.pay();  
         } elseif ("weixin".equals(code)) {  
              weixinPay.pay();  
         } elseif ("jingdong".equals(code)) {  
              jingDongPay.pay();  
         } else {  
              System.out.println("找不到支付方式");  
         }  
     }
}

PayService 类的 toPay 方法主要是为了发起支付,根据不同的 code,决定调用用不同的支付类(比如 aliaPay)的 pay 方法进行支付。

这段代码有什么问题呢?也许有些人就是这么干的。

试想一下,如果支付方式越来越多,比如又加了百度支付、美团支付、银联支付等等,就需要改 toPay 方法的代码,增加新的 else...if 判断,判断多了就会导致逻辑越来越多?

很明显,这里违法了设计模式六大原则的:开闭原则和单一职责原则。

开闭原则:对扩展开放,对修改关闭。就是说增加新功能要尽量少改动已有代码。

单一职责原则:顾名思义,要求逻辑尽量单一,不要太复杂,便于复用。

那么,如何优化 if...else 判断呢?

答:使用策略模式+工厂模式。

策略模式定义了一组算法,把它们一个个封装起来, 并且使它们可相互替换。工厂模式用于封装和管理对象的创建,是一种创建型模式。


public interface IPay {
    void pay();
}

@Service
public class AliaPay implements IPay {
    @PostConstruct
    public void init() {
        PayStrategyFactory.register("aliaPay", this);
    }

    @Override
    public void pay() {
        System.out.println("===发起支付宝支付===");
    }
}

@Service
public class WeixinPay implements IPay {
    @PostConstruct
    public void init() {
        PayStrategyFactory.register("weixinPay", this);
    }

    @Override
    public void pay() {
        System.out.println("===发起微信支付===");
    }
}

@Service
public class JingDongPay implements IPay {
    @PostConstruct
    public void init() {
        PayStrategyFactory.register("jingDongPay", this);
    }

    @Override
    public void pay() {
        System.out.println("===发起京东支付===");
    }
}

public class PayStrategyFactory {
    private static Map<String, IPay> PAY_REGISTERS = new HashMap<>();

    public static void register(String code, IPay iPay) {
        if (null != code && !"".equals(code)) {
            PAY_REGISTERS.put(code, iPay);
        }
    }

    public static IPay get(String code) {
        return PAY_REGISTERS.get(code);
    }
}

@Service
public class PayService3 {
    public void toPay(String code) {
        PayStrategyFactory.get(code).pay();
    }
}

这段代码的关键是 PayStrategyFactory 类,它是一个策略工厂,里面定义了一个全局的 map,在所有 IPay 的实现类中注册当前实例到 map 中,然后在调用的地方通过 PayStrategyFactory 类根据 code 从 map 获取支付类实例即可。

如果加了一个新的支付方式,只需新加一个类实现 IPay 接口,定义 init 方法,并且重写 pay 方法即可,其他代码基本上可以不用动。

当然,消除又臭又长的 if...else 判断,还有很多方法,比如使用注解、动态拼接类名称、模板方法、枚举等等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/3793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数码相机raw照片编辑Capture One Pro中文

怎么编辑数码相机拍摄的raw格式的照片&#xff1f;Capture One Pro 22是一款专业、强大、易于使用的图像编辑软件&#xff0c;与主流相机型号兼容&#xff0c;直接导入照片进行编辑操作&#xff0c;包括佳能、尼康、索尼、富士等。将所有必备工具和高端性能融于一体、使您在一套…

riscv引导程序及仿真记录

1.riscv基本的寄存器列表 这里只关注32个通用寄存器x0-x31 2.引导程序代码 # 1 "iriscvboot.casm" # 1 "<built-in>" # 1 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 1 "<command-line>&qu…

【Linux】进程间通信

文章目录1.进程间通信基础2.管道2.1匿名管道2.1.1匿名管道的原理2.2匿名管道的特点2.3匿名管道函数2.3.1用例2.3.2实现ps -ajx | grep bash指令2.4匿名管道的特点2.5管道的大小2.6管道的生命周期2.7进程池3.命名管道FIFO3.1命名管道的接口3.2命名管道和匿名管道的区别3.3用FIFO…

大数据面试重点之kafka(七)

大数据面试重点之kafka(七) Kafka的分区器、拦截器、序列化器&#xff1f; 问过的一些公司&#xff1a;ebay 参考答案&#xff1a; Kafka中&#xff0c;先执行拦截器对消息进行相应的定制化操作&#xff0c;然后执行序列化器将消息序列化&#xff0c;最后执行分 区器选择对应分…

python:基础知识

环境&#xff1a; window11python 3.10.6vscodejavascript、c/c/java/c#基础&#xff08;与这些语言对比&#xff09; 注释 一、数据类型 基础六大数据类型&#xff0c;可以使用 type()查看&#xff0c;如下图&#xff1a; 1.1 数字&#xff08;Number&#xff09; 支持 整…

联邦学习--记录

简介 联邦学习&#xff08;Federated Learning&#xff09;是一种新兴的人工智能基础技术&#xff0c;其设计目标是在保障大数据交换时的信息安全、保护终端数据和个人数据隐私、保证合法合规的前提下&#xff0c;在多参与方或多计算结点之间开展高效率的机器学习。其中&#…

【机器学习大杀器】Stacking堆叠模型-English

1. Introduction The stacking model is very common in Kaglle competitions. Why? 【机器学习大杀器】Stacking堆叠模型&#xff08;English&#xff09; 1. Introduction 2. Model 3: Stacking model 2.1 description of the algorithms: 2.2 interpretation of the es…

浅谈Vue中 ref、reactive、toRef、toRefs、$refs 的用法

&#x1f4ad;&#x1f4ad; ✨&#xff1a; 浅谈ref、reactive、toRef、toRefs、$refs   &#x1f49f;&#xff1a;东非不开森的主页   &#x1f49c;: 技术需沉淀&#xff0c;不要浮躁&#x1f49c;&#x1f49c;   &#x1f338;: 如有错误或不足之处&#xff0c;希望可…

Redhat(3)-Bash-Shell-正则表达式

1.bash脚本 2.bash变量、别名、算术扩展 3.控制语句 4.正则表达式 1.bash脚本 #!/bin/bash#this is basic bash script<< BLOCK This is the basic bash script BLOKC: This is the basic bash script echo "hello world!" 双引号、单引号只有在变量时才有区…

健身房信息管理系统/健身房管理系统

21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存储达到…

VCS 工具学习笔记(1)

目录 引言 平台说明 关于VCS 能力 Verilog 仿真事件队列 准备 VCS工作介绍 工作步骤 支持 工作机理 编译命令格式 编译选项 示例 仿真命令格式 仿真选项 示例 库调用 -y 总结 实践 设计文件 仿真文件 编译 仿真 关于增量编译 日志文件记录 编译仿真接续进…

链接脚本和可执行文件

几个重要的概念 摘取自知乎内容&#xff1a; 链接器与链接脚本 - 知乎 linker 链接器 链接器(linker) 是一个程序&#xff0c;这个程序主要的作用就是将目标文件(包括用到的标准库函数目标文件)的代码段、数据段以及符号表等内容搜集起来并按照 ELF或者EXE 等格式组合成一个…

【C++学习】string的使用

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《C学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; string的使用&#x1f640;模板&#x1f639;函数模板&#x1f639;类模板&#x1f640;string模板简…

【菜鸡读论文】Former-DFER: Dynamic Facial Expression Recognition Transformer

Former-DFER: Dynamic Facial Expression Recognition Transformer 哈喽&#xff0c;大家好呀&#xff01;本菜鸡又来读论文啦&#xff01;先来个酷炫小叮当作为我们的开场&#xff01; 粉红爱心泡泡有没有击中你的少女心&#xff01;看到这么可爱的小叮当陪我们一起读论文&am…

有了PySnooper,不用print、不用debug轻松查找问题所在!

PySnooper是一个非常方便的调试器&#xff0c;它是通过python注解的方式来对函数的执行过程进行监督的。 应用起来比较简单&#xff0c;不用一步一步的去走debug来查找问题所在&#xff0c;并且将运行过程中函数的变量值打印出来结果一目了然&#xff0c;相当于替代了print函数…

Boundary Loss 原理与代码解析

paper&#xff1a;Boundary loss for highly unbalanced segmentation Introduction 在医学图像分割中任务中通常存在严重的类别不平衡问题&#xff0c;目标前景区域的大小常常比背景区域小几个数量级&#xff0c;比如下图中前景区域比背景区域小500倍以上。 分割通常采用的交…

SpringBoot实践(三十三):Maven使用及POM详解

文章目录maven是什么maven怎么装settings.xml本地仓库地址&#xff1a;localRepository远程镜像&#xff1a;mirrorsJDK 版本&#xff1a;profile私服配置POM.xml中的常用标签projectmodelVersiongroupIdartifactIdversionpropertiesdependenciesbuild和pluginsresourcesdepend…

【学生管理系统】用户登录三种验证方式—图片验证、短信验证、邮件验证

目录 一、页面需求展示 二、验证方式—按钮组件 三、手机短信验证 四、邮件验证 五、图片验证邮件验证 &#x1f49f; 创作不易&#xff0c;不妨点赞&#x1f49a;评论❤️收藏&#x1f499;一下 一、页面需求展示 二、验证方式—按钮组件 2.1前端 <el-form-item labe…

【Linux】第十章 进程间通信(管道+system V共享内存)

&#x1f3c6;个人主页&#xff1a;企鹅不叫的博客 ​ &#x1f308;专栏 C语言初阶和进阶C项目Leetcode刷题初阶数据结构与算法C初阶和进阶《深入理解计算机操作系统》《高质量C/C编程》Linux ⭐️ 博主码云gitee链接&#xff1a;代码仓库地址 ⚡若有帮助可以【关注点赞收藏】…

工作流的例子

工作流的例子目录概述需求&#xff1a;设计思路实现思路分析1.配置bean2.examples3.no bean4.activiti-api-basic-process-example5.taskspringweb参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c…