你见过哪些优雅的 Java 代码优化技巧?

Java Servlet 代码优化的实用技巧 Servlet 代码冗余、逻辑混乱怎么办?高并发下 Servlet 性能下降如何优化?如何避免 Servlet 中的线程安全问题?如何优雅管理数据库连接、缓存等资源?本文将从“为什么需要优化”入手,通过“餐厅服务员”的生活类比引出核心问题,再分模块讲解 8 个实用优化技巧,最后结合“用户登录”实战案例演示优化全过程。Servlet 生命周期:单例模式,需注意线程安全。线程安全:避免成员变量,用局部变量或线程安全容器。资源管理+ 连接池,避免泄漏。 阅读详情

今天分享一些实用的有助于提高代码质量的建议,内容较多,建议收藏!

内容概览:

提取通用处理逻辑

注解、反射和动态代理是 Java 语言中的利器,使用得当的话,可以大大简化代码编写,并提高代码的可读性、可维护性和可扩展性。

大家可以利用 注解 + 反射 和 注解+动态代理 来提取类、类属性或者类方法通用处理逻辑,进而避免重复的代码。虽然可能会带来一些性能损耗,但与其带来的好处相比还是非常值得的。

通过 注解 + 反射 这种方式,可以在运行时动态地获取类的信息、属性和方法,并对它们进行通用处理。比如说在通过 Spring Boot 中通过注解验证接口输入的数据就是这个思想的运用,通过注解来标记需要验证的参数,然后通过反射获取属性的值,并进行相应的验证。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {

    @NotNull(message = "classId 不能为空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能为空")
    private String name;

    @Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围")
    @NotNull(message = "sex 不能为空")
    private String sex;

    @Region
    private String region;

    @PhoneNumber(message = "phoneNumber 格式不正确")
    @NotNull(message = "phoneNumber 不能为空")
    private String phoneNumber;

}

通过 注解 + 动态代理 这种,可以在运行时生成代理对象,从而实现通用处理逻辑。比如说 Spring 框架中,AOP 模块正是利用了这种思想,通过在目标类或方法上添加注解,动态生成代理类,并在代理类中加入相应的通用处理逻辑,比如事务管理、日志记录、缓存处理等。同时,Spring 也提供了两种代理实现方式,即基于 JDK 动态代理和基于 CGLIB 动态代理(JDK 动态代理底层基于反射,CGLIB 动态代理底层基于字节码生成),用户可以根据具体需求选择不同的实现方式。

@LogRecord(content = "修改了订单的配送地址:从“#oldAddress”, 修改到“#request.address”",
        bizNo="#request.deliveryOrderNo")
public void modifyAddress(updateDeliveryRequest request){
    // 查询出原来的地址是什么
    LogRecordContext.putVariable("oldAddress", DeliveryService.queryOldAddress(request.getDeliveryOrderNo()));
    // 更新派送信息 电话,收件人、地址
    doUpdate(request);
}

避免炫技式单行代码

代码没必要一味追求“短”,是否易于阅读和维护也非常重要。像炫技式的单行代码就非常难以理解、排查和修改起来都比较麻烦且耗时。

反例:

if (response.getData() != null && CollectionUtils.isNotEmpty(response.getData().getShoppingCartDTOList())) {
      cartList = response.getData().getShoppingCartDTOList().stream().map(CartResponseBuilderV2::buildCartList).collect(Collectors.toList());
}

正例:

T data = response.getData();
if (data != null && CollectionUtils.isNotEmpty(data.getShoppingCartDTOList())) {
  cartList = StreamUtil.map(data.getShoppingCartDTOList(), CartResponseBuilderV2::buildCartList);
}

基于接口编程提高扩展性

基于接口而非实现编程是一种常用的编程范式,也是一种非常好的编程习惯,一定要牢记于心!

基于接口编程可以让代码更加灵活、更易扩展和维护,因为接口可以为不同的实现提供相同的方法签名(方法的名称、参数类型和顺序以及返回值类型)和契约(接口中定义的方法的行为和约束,即方法应该完成的功能和要求),这使得实现类可以相互替换,而不必改变代码的其它部分。另外,基于接口编程还可以帮助避免过度依赖具体实现类,降低代码的耦合性,提高代码的可测试性和可重用性。

就比如说在编写短信服务、邮箱服务、存储服务等常用第三方服务的代码时,可以先先定义一个接口,接口中抽象出具体的方法,然后实现类再去实现这个接口。

public interface SmsSender {
    SmsResult send(String phone, String content);
    SmsResult sendWithTemplate(String phone, String templateId, String[] params);
}

/*
 * 阿里云短信服务
 */
public class AliyunSmsSender implements SmsSender {
  ...
}

/*
 * 腾讯云短信服务
 */
public class TencentSmsSender implements SmsSender {
  ...
}

拿短信服务这个例子来说,如果需要新增一个百度云短信服务,直接实现 SmsSender 即可。如果想要替换项目中使用的短信服务也比较简单,修改的代码非常少,甚至说可以直接通过修改配置无需改动代码就能轻松更改短信服务。

操作数据库、缓存、中间件的代码单独抽取一个类

尽量不要将操作数据库、缓存、中间件的代码和业务处理代码混合在一起,而是要单独抽取一个类或者封装一个接口,这样代码更清晰易懂,更容易维护,一些通用逻辑也方便统一维护。

数据库:

public interface UserRepository extends JpaRepository<User, Long> {
  ...
}

缓存:

@Repository
public class UserRedis {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public User save(User user) {
    }
}

消息队列:

// 取消订单消息生产者
public class CancelOrderProducer{
 ...
}
// 取消订单消息消费者
public class CancelOrderConsumer{
 ...
}

不要把业务代码放在 Controller 中

这个是老生常谈了,最基本的规范。一定不要把业务代码应该放在 Controller 中,业务代码就是要交给 Service 处理。

业务代码放到 Service 的好处 :

  1. 避免 Controller 的代码过于臃肿,进而难以维护和扩展。

  2. 抽象业务处理逻辑,方便复用比如给用户增加积分的操作可能会有其他的 Service 用到。

  3. 避免一些小问题比如 Controller 层通过 @Value注入值会失败。

  4. 更好的进行单元测试。如果将业务代码放在 Controller 中,会增加测试难度和不确定性。

错误案例:

@RestController
public class UserController {
    @Autowired
    private UserRepository userRepository;

    @GetMapping("/users/{id}")
    public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        User user = repository.findById(id)
                  .orElseThrow(() -> new UserNotFoundException(id));
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(user, userVO);//演示使用
        // 可能还有其他业务操作
        ...
        return Result.success(userVO);
    }
    ...
}

静态函数放入工具类

静态函数/方法不属于某个特定的对象,而是属于这个类。调用静态函数无需创建对象,直接通过类名即可调用。

静态函数最适合放在工具类中定义,比如文件操作、格式转换、网络请求等。

/**
 * 文件工具类
 */
public class FileUtil extends PathUtil {

    /**
     * 文件是否为空<br>
     * 目录:里面没有文件时为空 文件:文件大小为0时为空
     *
     * @param file 文件
     * @return 是否为空,当提供非目录时,返回false
     */
    public static boolean isEmpty(File file) {
        // 文件为空或者文件不存在直接返回 true
        if (null == file || false == file.exists()) {
            return true;
        }
        if (file.isDirectory()) {
            // 文件是文件夹的情况
            String[] subFiles = file.list();
            return ArrayUtil.isEmpty(subFiles);
        } else if (file.isFile()) {
            // 文件不是文件夹的情况
            return file.length() <= 0;
        }

        return false;
    }
}

善用设计模式

实际开发项目的过程中,应该尽量多地使用现有的设计模式来优化代码。有 9 种在源码中非常常见的设计模式:

  1. 工厂模式(Factory Pattern) :通过定义一个工厂方法来创建对象,从而将对象的创建和使用解耦,实现了“开闭原则”。

  2. 建造者模式(Builder Pattern) :通过链式调用和流式接口的方式,创建一个复杂对象,而不需要直接调用它的构造函数。

  3. 单例模式(Singleton Pattern) :确保一个类只有一个实例,并且提供一个全局的访问点,比如常见的 Spring Bean 单例模式。

  4. 原型模式(Prototype Pattern) :通过复制现有的对象来创建新的对象,从而避免了对象的创建成本和复杂度。

  5. 适配器模式(Adapter Pattern) :将一个类的接口转换成客户端所期望的接口,从而解决了接口不兼容的问题。

  6. 桥接模式(Bridge Pattern) :将抽象部分与实现部分分离开来,从而使它们可以独立变化。

  7. 装饰器模式(Decorator Pattern) :动态地给一个对象添加一些额外的职责,比如 Java 中的 IO 流处理。

  8. 代理模式(Proxy Pattern) :为其他对象提供一种代理以控制对这个对象的访问,比如常见的 Spring AOP 代理模式。

  9. 观察者模式(Observer Pattern) :定义了对象之间一种一对多的依赖关系,从而当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。

策略模式替换条件逻辑

策略模式是一种常见的优化条件逻辑的方法。当代码中有一个包含大量条件逻辑(即 if 语句)的方法时,你应该考虑使用策略模式对其进行优化,这样代码更加清晰,同时也更容易维护。

假设有这样一段代码:

public class IfElseDemo {

    public double calculateInsurance(double income) {
        if (income <= 10000) {
            return income*0.365;
        } else if (income <= 30000) {
            return (income-10000)*0.2+35600;
        } else if (income <= 60000) {
            return (income-30000)*0.1+76500;
        } else {
            return (income-60000)*0.02+105600;
        }

    }
}

下面是使用策略+工厂模式重构后的代码:

首先定义一个接口 InsuranceCalculator,其中包含一个方法 calculate(double income),用于计算保险费用。

public interface InsuranceCalculator {
    double calculate(double income);
}

然后,分别创建四个类来实现这个接口,每个类代表一个保险费用计算方式。

public class FirstLevelCalculator implements InsuranceCalculator {
    public double calculate(double income) {
        return income * 0.365;
    }
}

public class SecondLevelCalculator implements InsuranceCalculator {
    public double calculate(double income) {
        return (income - 10000) * 0.2 + 35600;
    }
}

public class ThirdLevelCalculator implements InsuranceCalculator {
    public double calculate(double income) {
        return (income - 30000) * 0.1 + 76500;
    }
}

public class FourthLevelCalculator implements InsuranceCalculator {
    public double calculate(double income) {
        return (income - 60000) * 0.02 + 105600;
    }
}

最后,可以为每个策略类添加一个唯一的标识符,例如字符串类型的 name 属性。然后,在工厂类中创建一个 Map 来存储策略对象和它们的标识符之间的映射关系(也可以用 switch 来维护映射关系)。

import java.util.HashMap;
import java.util.Map;

public class InsuranceCalculatorFactory {
    private static final Map<String, InsuranceCalculator> CALCULATOR_MAP = new HashMap<>();

    static {
        CALCULATOR_MAP.put("first", new FirstLevelCalculator());
        CALCULATOR_MAP.put("second", new SecondLevelCalculator());
        CALCULATOR_MAP.put("third", new ThirdLevelCalculator());
        CALCULATOR_MAP.put("fourth", new FourthLevelCalculator());
    }

    public static InsuranceCalculator getCalculator(String name) {
        return CALCULATOR_MAP.get(name);
    }
}

这样,就可以通过 InsuranceCalculatorFactory 类手动获取相应的策略对象了。

double income = 40000;
// 获取第二级保险费用计算器
InsuranceCalculator calculator = InsuranceCalculatorFactory.getCalculator("second");
double insurance = calculator.calculate(income);
System.out.println("保险费用为:" + insurance);

这种方式允许在运行时根据需要选择不同的策略,而无需在代码中硬编码条件语句。

相关阅读:Replace Conditional Logic with Strategy Pattern - IDEA[3] 。

除了策略模式之外,Map+函数式接口也能实现类似的效果,代码一般还要更简洁一些。

下面是使用 Map+函数式接口重构后的代码:

首先,在 InsuranceCalculatorFactory 类中,将 getCalculator 方法的返回类型从 InsuranceCalculator 改为 Function<Double, Double>,表示该方法返回一个将 double 类型的 income 映射到 double 类型的 insurance 的函数。

public class InsuranceCalculatorFactory {
    private static final Map<String, Function<Double, Double>> CALCULATOR_MAP = new HashMap<>();

    static {
        CALCULATOR_MAP.put("first", income -> income * 0.365);
        CALCULATOR_MAP.put("second", income -> (income - 10000) * 0.2 + 35600);
        CALCULATOR_MAP.put("third", income -> (income - 30000) * 0.1 + 76500);
        CALCULATOR_MAP.put("fourth", income -> (income - 60000) * 0.02 + 105600);
    }

    public static Function<Double, Double> getCalculator(String name) {
        return CALCULATOR_MAP.get(name);
    }
}

然后,在调用工厂方法时,可以使用 Lambda 表达式或方法引用来代替实现策略接口的类。

double income = 40000;
Function<Double, Double> calculator = InsuranceCalculatorFactory.getCalculator("second");
double insurance = calculator.apply(income);
System.out.println("保险费用为:" + insurance);

复杂对象使用建造者模式

复杂对象的创建可以使用建造者模式优化。

使用 Caffeine 创建本地缓存的代码示例:

Caffeine.newBuilder()
                // 设置最后一次写入或访问后经过固定时间过期
                .expireAfterWrite(60, TimeUnit.DAYS)
                // 初始的缓存空间大小
                .initialCapacity(100)
                // 缓存的最大条数
                .maximumSize(500)
                .build();

链式处理优先使用责任链模式

责任链模式在实际开发中还是挺实用的,像 MyBatis、Netty、OKHttp3、SpringMVC、Sentinel 等知名框架都大量使用了责任链模式。

如果一个请求需要进过多个步骤处理的话,可以考虑使用责任链模式。

责任链模式下,存在多个处理者,这些处理者之间有顺序关系,一个请求被依次传递给每个处理者(对应的是一个对象)进行处理。处理者可以选择自己感兴趣的请求进行处理,对于不感兴趣的请求,转发给下一个处理者即可。如果满足了某个条件,也可以在某个处理者处理完之后直接停下来。

责任链模式下,如果需要增加新的处理者非常容易,符合开闭原则。

Netty 中的 ChannelPipeline 使用责任链模式对数据进行处理。我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler (一个数据或者事件可能会被多个 Handler 处理) 。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。

ChannelPipeline pipeline = ch.pipeline()
      // 添加一个用于对 HTTP 请求和响应报文进行编解码的 ChannelHandler
      .addLast(HTTP_CLIENT_CODEC, new HttpClientCodec())
       // 添加一个对 gzip 或者 deflate 格式的编码进行解码的 ChannelHandler
      .addLast(INFLATER_HANDLER, new HttpContentDecompressor())
       // 添加一个用于处理分块传输编码的 ChannelHandler
      .addLast(CHUNKED_WRITER_HANDLER, new ChunkedWriteHandler())
       // 添加一个处理 HTTP 请求并响应的 ChannelHandler
      .addLast(AHC_HTTP_HANDLER, new HttpHandler);

Tomcat 中的请求处理是通过一系列过滤器(Filter)来完成的,这同样是责任连模式的运用。每个过滤器都可以对请求进行处理,并将请求传递给下一个过滤器,直到最后一个过滤器将请求转发到相应的 Servlet 或 JSP 页面。

public class CompressionFilter implements Filter {
    // ...
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 检查是否支持压缩
        if (isCompressable(request, response)) {
            // 创建一个自定义的响应对象,用于在压缩数据时获取底层输出流
            CompressionServletResponseWrapper wrappedResponse = new CompressionServletResponseWrapper(
                    (HttpServletResponse) response);
            try {
                // 将请求转发给下一个过滤器或目标 Servlet/JSP 页面
                chain.doFilter(request, wrappedResponse);
                // 压缩数据并写入原始响应对象的输出流
                wrappedResponse.finishResponse();
            } catch (IOException e) {
                log.warn(sm.getString("compressionFilter.compressFailed"), e); //$NON-NLS-1$
                handleIOException(e, wrappedResponse);
            }
        } else {
            // 不支持压缩,直接将请求转发给下一个过滤器或目标 Servlet/JSP 页面
            chain.doFilter(request, response);
        }
    }
    // ...
}

使用观察者模式解耦

观察者模式也是解耦的利器。当对象之间存在一对多关系,可以使用观察者模式,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,会通知所有观察者,观察者收到通知之后可以根据通知的内容去针对性地做一些事情。

Spring 事件就是基于观察者模式实现的。

1、定义一个事件。

public class CustomSpringEvent extends ApplicationEvent {
    private String message;

    public CustomSpringEvent(Object source, String message) {
        super(source);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

2、创建事件发布者发布事件。

@Component
public class CustomSpringEventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publishCustomEvent(final String message) {
        System.out.println("Publishing custom event. ");
        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
        applicationEventPublisher.publishEvent(customSpringEvent);
    }
}

3、创建监听器监听并处理事件(支持异步处理事件的方式,需要配置线程池)。

@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        System.out.println("Received spring custom event - " + event.getMessage());
    }
}

抽象父类利用模板方法模式定义流程

多个并行的类实现相似的代码逻辑。我们可以考虑提取相同逻辑在父类中实现,差异逻辑通过抽象方法留给子类实现。

对于相同的流程和逻辑,我们还可以借鉴模板方法模式将其固定成模板,保留差异的同时尽可能避免代码重复。

下面是一个利用模板方法模式定义流程的示例代码:

public abstract class AbstractDataImporter {
    private final String filePath;

    public AbstractDataImporter(String filePath) {
        this.filePath = filePath;
    }

    public void importData() throws IOException {
        List<String> data = readDataFromFile();
        validateData(data);
        saveDataToDatabase(data);
    }

    protected abstract List<String> readDataFromFile() throws IOException;

    protected void validateData(List<String> data) {
        // 若子类没有实现该方法,则不进行数据校验
    }

    protected abstract void saveDataToDatabase(List<String> data);

    protected String getFilePath() {
        return filePath;
    }
}

在上面的代码中,AbstractDataImporter 是一个抽象类。该类提供了一个 importData() 方法,它定义了导入数据的整个流程。具体而言,该方法首先从文件中读取原始数据,然后对数据进行校验,最后将数据保存到数据库中。

其中,readDataFromFile() 和 saveDataToDatabase() 方法是抽象的,由子类来实现。validateData() 方法是一个默认实现,可以通过覆盖来定制校验逻辑。getFilePath() 方法用于获取待导入数据的文件路径。

子类继承 AbstractDataImporter 后,需要实现 readDataFromFile() 和 saveDataToDatabase() 方法,并覆盖 validateData() 方法(可选)。例如,下面是一个具体的子类 CsvDataImporter 的实现:

public class CsvDataImporter extends AbstractDataImporter {
    private final char delimiter;

    public CsvDataImporter(String filePath, char delimiter) {
        super(filePath);
        this.delimiter = delimiter;
    }

    @Override
    protected List<String> readDataFromFile() throws IOException {
        List<String> data = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(getFilePath()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                data.add(line);
            }
        }
        return data;
    }

    @Override
    protected void validateData(List<String> data) {
        // 对 CSV 格式的数据进行校验,例如检查是否每行都有相同数量的字段等
    }

    @Override
    protected void saveDataToDatabase(List<String> data) {
        // 将 CSV 格式的数据保存到数据库中,例如将每行解析为一个对象,然后使用 JPA 保存到数据库中
    }
}

在上面的代码中,CsvDataImporter 继承了 AbstractDataImporter 类,并实现了 readDataFromFile() 和 saveDataToDatabase() 方法。它还覆盖了 validateData() 方法,以支持对 CSV 格式的数据进行校验。

通过以上实现,我们可以通过继承抽象父类并实现其中的抽象方法,来定义自己的数据导入流程。另外,由于抽象父类已经定义了整个流程的结构和大部分默认实现,因此子类只需要关注定制化的逻辑即可,从而提高了代码的可复用性和可维护性。

善用 Java 新特性

Java 版本在更新迭代过程中会增加很多好用的特性,一定要善于使用 Java 新特性来优化自己的代码,增加代码的可阅读性和可维护性。

就比如火了这么多年的 Java 8 在增强代码可读性、简化代码方面,相比 Java 7 增加了很多功能,比如 Lambda、Stream 流操作、并行流(ParallelStream)、Optional 可空类型、新日期时间类型等。

Lambda 优化排序代码示例:

// 匿名内部类实现数组从小到大排序
Integer[] scores = {89, 100, 77, 90,  86};
Arrays.sort(scores,new Comparator<Integer>(){
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});
for(Integer score:scores){
    System.out.print(score);
}
// 使用 Lambda 优化
Arrays.sort(scores,(o1,o2)->o1.compareTo(o2) );
// 还可以像下面这样写
Arrays.sort(scores,Comparator.comparing(Integer::intValue));

Optional 优化代码示例:

private Double calculateAverageGrade(Map<String, List<Integer>> gradesList, String studentName)
  throws Exception {
 return Optional.ofNullable(gradesList.get(studentName))// 创建一个Optional对象,传入参数为空时返回Optional.empty()
   .map(list -> list.stream().collect(Collectors.averagingDouble(x -> x)))// 对 Optional 的值进行操作
   .orElseThrow(() -> new NotFoundException("Student not found - " + studentName));// 当值为空时,抛出指定的异常
}

再比如 Java 17 中转正的密封类(Sealed Classes) ,Java 16 中转正的记录类型(record关键字定义)、instanceof 模式匹配等新特性。

record关键字优化代码示例:

/**
 * 这个类具有两个特征
 * 1. 所有成员属性都是final
 * 2. 全部方法由构造方法,和两个成员属性访问器组成(共三个)
 * 那么这种类就很适合使用record来声明
 */
final class Rectangle implements Shape {
    final double length;
    final double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double length() { return length; }
    double width() { return width; }
}
/**
 * 1. 使用record声明的类会自动拥有上面类中的三个方法
 * 2. 在这基础上还附赠了equals(),hashCode()方法以及toString()方法
 * 3. toString方法中包括所有成员属性的字符串表示形式及其名称
 */
record Rectangle(float length, float width) { }

使用 Bean 自动映射工具

我们经常在代码中会对一个数据结构封装成 DO、DTO、VO 等,而这些 Bean 中的大部分属性都是一样的,所以使用属性拷贝类工具可以帮助我们节省大量的 set 和 get 操作。

常用的 Bean 映射工具有:Spring BeanUtils、Apache BeanUtils、MapStruct、ModelMapper、Dozer、Orika、JMapper 。

由于 Apache BeanUtils 、Dozer 、ModelMapper 性能太差,所以不建议使用。MapStruct 性能更好而且使用起来比较灵活,是一个比较不错的选择。

这里以 MapStruct 为例,简单演示一下转换效果。

1、定义两个类 Employee 和 EmployeeDTO。

public class Employee {
    private int id;
    private String name;
    // getters and setters
}

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    // getters and setters
}

2、定义转换接口让 Employee 和 EmployeeDTO互相转换。

@Mapper
public interface EmployeeMapper {
    // Spring 项目可以将 Mapper 注入到 IoC 容器中,这样就可以像 Spring Bean 一样调用了
    EmployeeMapper INSTANT = Mappers.getMapper(EmployeeMapper.class);

    @Mapping(target="employeeId", source="entity.id")
    @Mapping(target="employeeName", source="entity.name")
    EmployeeDTO employeeToEmployeeDTO(Employee entity);

    @Mapping(target="id", source="dto.employeeId")
    @Mapping(target="name", source="dto.employeeName")
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

3、实际使用。

//  EmployeeDTO 转  Employee
Employee employee = EmployeeMapper.INSTANT.employeeToEmployeeDTO(employee);

//  Employee 转  EmployeeDTO
EmployeeDTO employeeDTO = EmployeeMapper.INSTANT.employeeDTOtoEmployee(employeeDTO);

规范日志打印

1、不要随意打印日志,确保自己打印的日志是后面能用到的。

打印太多无用的日志不光影响问题排查,还会影响性能,加重磁盘负担。

2、打印日志中的敏感数据比如身份证号、电话号、密码需要进行脱敏。

3、选择合适的日志打印级别。最常用的日志级别有四个:DEBUG、INFO、WARN、ERROR。

  • DEBUG(调试):开发调试日志,主要开发人员开发调试过程中使用,生产环境禁止输出 DEBUG 日志。

  • INFO(通知):正常的系统运行信息,一些外部接口的日志,通常用于排查问题使用。

  • WARN(警告):警告日志,提示系统某个模块可能存在问题,但对系统的正常运行没有影响。

  • ERROR(错误):错误日志,提示系统某个模块可能存在比较严重的问题,会影响系统的正常运行。

4、生产环境禁止输出 DEBUG 日志,避免打印的日志过多(DEBUG 日志非常多)。

5、应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

Spring Boot 应用程序可以直接使用内置的日志框架 Logback,Logback 就是按照 SLF4J API 标准实现的。

6、异常日志需要打印完整的异常信息。

反例:

try {
    //读文件操作
    readFile();
} catch (IOException e) {
    // 只保留了异常消息,栈没有记录
    log.error("文件读取错误, {}", e.getMessage());
}

正例:

try {
    //读文件操作
    readFile();
} catch (IOException e) {
    log.error("文件读取错误", e);
}

7、避免层层打印日志。

举个例子:method1 调用 method2,method2 出现 error 并打印 error 日志,method1 也打印了 error 日志,等同于一个错误日志打印了 2 遍。

8、不要打印日志后又将异常抛出。

反例:

try {
    ...
} catch (IllegalArgumentException e) {
    log.error("出现异常啦", e);
    throw e;
}

在日志中会对抛出的一个异常打印多条错误信息。

正例:

try {
    ...
} catch (IllegalArgumentException e) {
    log.error("出现异常啦", e);
}
// 或者包装成自定义异常之后抛出
try {
    ...
} catch (IllegalArgumentException e) {
    throw new MyBusinessException("一段对异常的描述信息.", e);
}

规范异常处理

阿里巴巴 Java 异常处理规约如下:

阿里巴巴Java异常处理规约

统一异常处理

所有的异常都应该由最上层捕获并处理,这样代码更简洁,还可以避免重复输出异常日志。 如果我们都在业务代码中使用try-catch或者try-catch-finally处理的话,就会让业务代码中冗余太多异常处理的逻辑,对于同样的异常我们还需要重复编写代码处理,还可能会导致重复输出异常日志。这样的话,代码可维护性、可阅读性都非常差。

Spring Boot 应用程序可以借助 @RestControllerAdvice 和 @ExceptionHandler 实现全局统一异常处理。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result businessExceptionHandler(HttpServletRequest request, BusinessException e){
        ...
        return Result.faild(e.getCode(), e.getMessage());
    }
    ...
}

使用 try-with-resource 关闭资源

  1. 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象

  2. 关闭资源和 finally 块的执行顺序: 在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

《Effective Java》中明确指出:

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让人更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

Java 中类似于InputStream、OutputStream 、Scanner 、PrintWriter等的资源都需要调用close()方法来手动关闭,一般情况下都是通过try-catch-finally语句来实现这个需求,如下:

//读取文本文件的内容
Scanner scanner = null;
try {
    scanner = new Scanner(new File("D://read.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}

使用 Java 7 之后的 try-with-resources 语句改造上面的代码:

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。

通过使用分号分隔,可以在try-with-resources块中声明多个资源。

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
}
catch (IOException e) {
    e.printStackTrace();
}

不要把异常定义为静态变量

不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,都需要手动 new 一个异常对象抛出。

// 错误做法
public class Exceptions {
    public static BusinessException ORDEREXISTS = new BusinessException("订单已经存在", 3001);
...
}

其他异常处理注意事项

  • 抛出完整具体的异常信息(避免 throw new BIZException(e.getMessage()这种形式的异常抛出),尽量自定义异常,而不是直接使用 RuntimeException或Exception。

  • 优先捕获具体的异常类型。

  • 捕获了异常之后一定要处理,避免直接吃掉异常。

  • ......

接口不要直接返回数据库对象

接口不要直接返回数据库对象(也就是 DO),数据库对象包含类中所有的属性。

// 错误做法
public UserDO getUser(Long userId) {
  return userService.getUser(userId);
}

原因:

  • 如果数据库查询不做字段限制,会导致接口数据庞大,浪费用户的宝贵流量。

  • 如果数据库查询不做字段限制,容易把敏感字段暴露给接口,导致出现数据的安全问题。

  • 如果修改数据库对象的定义,接口返回的数据紧跟着也要改变,不利于维护。

建议的做法是单独定义一个类比如 VO(可以看作是接口返回给前端展示的对象数据)来对接口返回的数据进行筛选,甚至是封装和组合。

public UserVo getUser(Long userId) {
  UserDO userDO = userService.getUser(userId);
  UserVO userVO = new UserVO();
  BeanUtils.copyProperties(userDO, userVO);//演示使用
  return userVO;
}

统一接口返回值

接口返回的数据一定要统一格式,遮掩更方面对接前端开发的同学以及其他调用该接口的开发。

通常来说,下面这些信息是必备的:

  1. 状态码和状态信息:可以通过枚举定义状态码和状态信息。状态码标识请求的结果,状态信息属于提示信息,提示成功信息或者错误信息。

  2. 请求数据:请求该接口实际要返回的数据比如用户信息、文章列表。

public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源");

    private Integer code;
    private String message;
    ...
}

public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    ...
    public static <T> Result<T> success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }

    public static Result<?> failed() {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
    }
    ...
}

对于 Spring Boot 项目来说,可以使用 @RestControllerAdvice 注解+ ResponseBodyAdvic接口统一处理接口返回值,实现代码无侵入。篇幅问题这里就不贴具体实现代码了,比较简单,具体实现方式可以参考这篇文章:Spring Boot 无侵入式 实现 API 接口统一 JSON 格式返回[6] 。

需要注意的是,这种方式在 Spring Cloud OpenFeign 的继承模式下是有侵入性,解决办法见:SpringBoot 无侵入式 API 接口统一格式返回,在 Spring Cloud OpenFeign 继承模式具有了侵入性[7] 。

实际项目中,其实使用比较多的还是下面这种比较直接的方式:

public class PostController {

 @GetMapping("/list")
 public R<List<SysPost>> getPosts() {
  ...
  return R.ok(posts);
 }
}

上面介绍的无侵入的方式,一般改造旧项目的时候用的比较多。

远程调用设置超时时间

开发过程中,第三方接口调用、RPC 调用以及服务之间的调用建议设置一个超时时间。

平时接触到的超时可以简单分为下面 2 种:

  • 连接超时(ConnectTimeout) :客户端与服务端建立连接的最长等待时间。

  • 读取超时(ReadTimeout) :客户端和服务端已经建立连接,客户端等待服务端处理完请求的最长时间。实际项目中,关注比较多的还是读取超时。

一些连接池客户端框架中可能还会有获取连接超时和空闲连接清理超时。

如果没有设置超时的话,就可能会导致服务端连接数爆炸和大量请求堆积的问题。这些堆积的连接和请求会消耗系统资源,影响新收到的请求的处理。严重的情况下,甚至会拖垮整个系统或者服务。

我之前在实际项目就遇到过类似的问题,整个网站无法正常处理请求,服务器负载直接快被拉满。后面发现原因是项目超时设置错误加上客户端请求处理异常,导致服务端连接数直接接近 40w+,这么多堆积的连接直接把系统干趴了。

正确使用线程池

 10 个使用线程池的注意事项:

  1. 线程池必须手动通过 ThreadPoolExecutor 的构造函数来声明,避免使用 Executors 类创建线程池,会有 OOM 风险。

  2. 监测线程池运行状态。

  3. 建议不同类别的业务用不同的线程池。

  4. 别忘记给线程池命名。

  5. 正确配置线程池参数。

  6. 别忘记关闭线程池。

  7. 线程池尽量不要放耗时任务。

  8. 避免重复创建线程池。

  9. 使用 Spring 内部线程池时,一定要手动自定义线程池,配置合理的参数,不然会出现生产问题(一个请求创建一个线程)

  10. 线程池和 ThreadLocal 共用,可能会导致线程从 ThreadLocal 获取到的是旧值/脏数据。

敏感数据处理

  1. 返回前端的敏感数据比如身份证号、电话、地址信息要根据业务需求进行脱敏处理,示例:163****892。

  2. 保存在数据库中的密码需要加盐之后使用哈希算法(比如 BCrypt)进行加密。

  3. 保存在数据库中的银行卡号、身份号这类敏感数据需要使用对称加密算法(比如 AES)保存。

  4. 网络传输的敏感数据比如银行卡号、身份号需要用 HTTPS + 非对称加密算法(如 RSA)来保证传输数据的安全性。

  5. 对于密码找回功能,不能明文存储用户密码。可以采用重置密码的方式,让用户通过验证身份后重新设置密码。

  6. 在代码中不应该明文写入密钥、口令等敏感信息。可以采用配置文件、环境变量等方式来动态加载这些信息。

  7. 定期更新敏感数据的加密算法和密钥,以保证加密算法和密钥的安全性和有效性。

参考资料

[1]美团技术团队:如何优雅地记录操作日志?: https://tech.meituan.com/2021/09/16/operational-logbook.html

[2]一个较重的代码坏味:“炫技式”的单行代码: https://www.cnblogs.com/lovesqcc/p/16559923.html

[3]Replace Conditional Logic with Strategy Pattern - IDEA: https://www.jetbrains.com/help/idea/replace-conditional-logic-with-strategy-pattern.html

[4]聊一聊责任链模式: https://juejin.cn/post/7160840952855134239

[5]21 | 代码重复:搞定代码重复的三个绝招 - Java 业务开发常见错误 100 例 : https://time.geekbang.org/column/article/228964

[6]Spring Boot 无侵入式 实现 API 接口统一 JSON 格式返回: https://blog.youkuaiyun.com/qq_34347620/article/details/102239179

[7]SpringBoot 无侵入式 API 接口统一格式返回,在 Spring Cloud OpenFeign 继承模式具有了侵入性: https://blog.youkuaiyun.com/qq_34347620/article/details/124295302

[8]超时&重试详解: https://javaguide.cn/high-availability/timeout-and-retry.html

[9]10 个线程池最佳实践和坑!: https://javaguide.cn/java/concurrent/java-thread-pool-best-practices.html

 

基于DE2-115红外解码实验 基于DE2-115红外解码实验   一、   功能描述 在同一个遥控电路中通常要使用实现不同的遥控功能或区分不同的机器类型, 这样就要求信号按一定的编码传送,编码则会由编码芯片或电路完成。对应于编码芯片通常会有相配对的解码芯片或包含解码模块的应用芯片。在实际的产品设计或业余电子制作中,编码芯片并一定能完成我们要求的功能,这时我们就需要了解所使用的编码芯片到底是如何编码的。只有知道编码方式,我... 阅读详情

相关推荐

STM32F103C8T6键盘DIY全攻略:从电路设计到固件烧录(附避坑指南)

本文详细介绍了基于STM32F103C8T6微控制器的DIY键盘制作全流程,涵盖硬件电路设计、按键矩阵实现、USB HID通信及固件烧录等关键步骤。特别针对常见问题提供避坑指南,帮助开发者高效完成从电路设计到功能实现的完整过程,打造个性化瀚文键盘。

soda5的博客 770

如何编写高效的Java代码?常用的优化技巧

代码结构优化是提高Java程序性能的一种重要方式。代码结构的优化包括优化算法、数据结构和设计模式等。以下是一些优化技巧。

2301_76607156的博客 1082

Python爬虫实战:构建药监局药品批准信息本地查询系统

在医药信息化时代,药品批准信息的快速查询对于医药从业者、研究人员和普通消费者都具有重要意义。国家药品监督管理局(NMPA)提供了官方的药品查询系统,但有时我们需要建立本地数据库以便批量分析或离线查询。本文将详细介绍如何使用Python爬虫技术,结合最新的异步爬虫框架和技术栈,构建一个完整的药监局药品信息本地查询系统。本系统将实现以下功能:自动化爬取药监局官网的药品批准信息数据清洗和结构化存储建立本地查询接口数据定期更新机制pythonimport os"""药品类型枚举"""

2201_76125261的博客 726

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

我之前写过两篇关于优化相关的问题《聊聊sql优化的15个小技巧》和《聊聊接口性能优化的11个小技巧》,发表之后,在全网受到广大网友的好评。今天接着优化这个话题,我们一起聊聊Java中代码优化的30个小技巧,希望会对你有所帮助。不知道你在实际的项目开发中,有没有使用过固定值?}thrownewBusinessException("该用户不存在");}其中1000L和该用户不存在是固定值,每次都是一样的。既然是固定值,我们为什么不把它们定义成静态常量呢?}使用关键字修饰静态常量,表示静态。...

4336

一篇文章足够你学习Bluez,提供史上最全的Bluez教程,让你告别Bluez从入门到放弃

Bluez是真的难,说“入门到放弃”真的毫不夸张,Bluez的学习曲线陡峭,要求对Linux 系统/驱动概念、D-Bus 通信和蓝牙协议都有要求,加上网络上其实没有一个完整的介绍Bluez系列的文档,所以不管是蓝牙初学者还是蓝牙从业人员,都有不小的难度。

朝气蓬勃 2532

高德地图API如何使用

通过以上步骤,您可以成功接入并使用高德地图API来为您的应用添加丰富的地图功能。

QQ_778132974的博客 4783

Java代码优化的30个小技巧

Java代码优化的30个小技巧

weixin_45985053的博客 661

把 LangGraph 当“工作流引擎”用:与传统 BPM 的差异与融合策略

首先,我们来回顾一下有限状态机(FSM)的基本定义——一个 FSM 可以用一个五元组MQΣδq0FMQΣδq0​FQQQ有限状态集合,包含 FSM 所有可能的状态;Σ\SigmaΣ有限输入字母表,包含 FSM 所有可能的输入;δ\deltaδ状态转移函数,定义了 FSM 从当前状态q∈Qq \in Qq∈Q接收输入σ∈Σσ∈Σ后,转移到的下一个状态q′∈Qq' \in Qq′∈Q,即δQ×Σ→QδQ×Σ→Q。

AI 原生应用开发的博客 227

掌握这35个 Java 优化小技巧,让你代码水平提升

代码优化 ,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的。代码优化的目标是减小代码的体积。

奔走的蚂蚁的博客 2999

Rcpp简介

在数据分析时,代码的运行速率常常是我们所关心的话题之一。小Q往期的文章中也有介绍通过并行计算提升R运行速率的文章。然而R代码自身的运行速率偏慢,具有某种“先天缺陷”。因此,并行R函数的运行时间有时仍然不尽人意。本期小Q与大家分享R的另一提速方式:Rcpp之效率瓶颈突破。 1 什么是Rcpp 在往期的文章中曾讲过,我们在R中尽量采用向量化或内建函数运算,避免自编循环带来的额外时间消耗。然而,算法中有时难以避免循环,若循环部分耗时严重,直接并行含有该循环的算法显得力不从心,此时又该如何?稍有编程知识的读者可

Mrrunsen的博客 2259

Joule 终于会说 ABAP 了:从 ADT 提效到 ABAP AI SDK + ISLM 自建企业级 AI 场景的落地路线

生成结构化摘要(故障现象、初步原因、处理动作、备件消耗)自动给出建议分类(电气、机械、软件、网络)把摘要回写到工单抬头,方便后续统计与知识沉淀如果你用 CAP 来做这类服务,通常会在 Node.js 层封装一个对 AI Hub 的调用,再通过 OData 或 Event 把结果写回 SAP。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 342

JAVA的16 个实用代码优化小技巧

而使用 KeySet 方法,获得的是key 的集合,需要再进行一次 get 操作,多了一个操作步骤,所以更推荐使用 EntrySet 方式遍历 Map。当这些对象存入 HashMap 时,性能就会非常低,因为 HashMap 是通过 HashCode 定位到 Hash 槽,有冲突的时候,才会使用链表或者红黑树组织节点,固定地返回 0,相当于把 Hash 寻址功能无效了。由于堆资源是多线程共享的,是垃圾回收器工作的主要区域,过多的对象会造成 GC 压力,可以通过局部变量的方式,将变量在栈上分配。

Java后端技术栈 1234

【知识分享】单端到混合S参数转换指南

信号完整性(SI)工程师几乎总是必须使用S参数,因为许多的高速接口和行业标准都正在转向串行链路架构,并使用基于S参数测量的频域限制指标,了解其工作过程将会很有意义。

一只豌豆象的博客 1576

如何写出优雅、高效、逼格高的Java代码(代码优化、Java新特性、代码规范)

尽量多的收录了一些优雅、高效、逼格高的java代码 欢迎大家补充,共同学习进步~

Meteors.的博客 6763

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

优雅写代码的45个小技巧

比如在项目中不同的类型的业务可能需要上传各种各样的附件,此时就可以定义好不同的一个附件的枚举,来区分不同业务的附件。不要在代码中直接写死,不定义枚举,代码阅读起来非常困难,直接看到数字都是懵逼的。。

LoveSummer 2万+

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

优雅又实用的 Java 代码优化技巧

不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。vbnet复制代码// 错误做法public class Exceptions { public static BusinessException ORDEREXISTS = new BusinessException("订单已经存在", 3001);...}

BASK2312的博客 520

你见过哪些目瞪口呆的 Java 代码技巧?

你见过哪些目瞪口呆的 Java 代码技巧? https://juejin.im/post/5d8db248f265da5b81793861 如何让java代码性能"更高、更优雅、远离BUG"? https://juejin.im/post/5d8c5a3ee51d457815708599 ...

qq_37065641的博客 180

Java优雅编码(持续更新)

在类的每个对象实例中,每个成员变量都有一份副本,而成员静态常量只有一份实例......在老版JDK中,建议“尽量不要在循环体内定义变量”,但是在新版的JDK中已经做了优化,根据“ 局部变量作用域最小化 ”原则,变量定义在循环体内更科学更便于维护,避免了延长大对象生命周期导致延缓回收问题......多一个类就需要多一份类加载,所以尽量避免定义不必要的子类});

2401_87300124的博客 1088

优雅、高效、逼格高的Java代码指南

优雅、高效、逼格高的Java代码指南 去发现同类优质开源项目:https://gitcode.com/ 本仓库提供了一个资源文件,详细介绍了如何写出优雅、高效、逼格高的Java代码。内容涵盖了代码优化、Java新特性以及代码规范等方面。 内容概述 代码优化:包括if-else语句的优化、使用枚举和映射简化条件判断、利用函数式编程和Stream API处理集合、异常处理的优化(如try-with-...

gitblog_06560的博客 446

Java中的代码优化

大家好,我是城南。Java中的代码优化是一门技术与艺术相结合的学问,就像精雕细琢一件艺术品,需要的不仅是技巧,更是对细节的执着追求。今天,我们来深入探讨一下如何在Java中进行代码优化,让我们的程序更高效、更优雅。

weixin_46372265的博客 838

Java 代码优化技巧

通过 注解 + 反射 这种方式,可以在运行时动态地获取类的信息、属性和方法,并对它们进行通用处理。比如说在通过 Spring Boot 中通过注解验证接口输入的数据就是这个思想的运用,我们通过注解来标记需要验证的参数,然后通过反射获取属性的值,并进行相应的验证。我们可以利用 注解 + 反射 和 注解+动态代理 来提取类、类属性或者类方法通用处理逻辑,进而避免重复的代码。注解、反射和动态代理是 Java 语言中的利器,使用得当的话,可以大大简化代码编写,并提高代码的可读性、可维护性和可扩展性。

Blue_Pepsi_Cola的博客 373

Java编程中十个让你代码更优雅的Lambda表达式实战技巧

传统的集合遍历通常需要显式使用迭代器或增强for循环,而Lambda表达式结合Stream API可以使代码更加简洁。通过andThen或compose方法可以将多个Lambda表达式组合成复杂的操作链,提高代码的模块化和可重用性。相比传统的匿名内部类方式,Lambda表达式减少了模板代码,使并发编程更加简洁。方法引用通过::运算符直接指向方法,避免了重复编写参数列表,使代码更加简洁。这种方式将条件判断抽象为可重用的函数对象,提高了代码的可维护性。这避免了创建冗长的匿名内部类,使事件处理代码更加紧凑。

lovesco的博客 1482
上一篇: Linus亲自修改英特尔的“屎山”代码
下一篇: Transformer开山论文惊天「翻车」?图与代码不一致,神秘bug看傻了
IT界那些事儿
博客等级 码龄6年 2万+粉丝 22原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值