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

一些实用的有助于提高代码质量的建议。

提取通用处理逻辑

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructorpublic
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 动态代理底层基于字节码生成),用户可以根据具体需求选择不同的实现方式。

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

避免炫技式单行代码

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

反例:

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

正例:

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

基于接口编程提高扩展性

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

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

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

1
2
3
4
5
6
7
8
9
10
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 即可。如果想要替换项目中使用的短信服务也比较简单,修改的代码非常少,甚至说可以直接通过修改配置无需改动代码就能轻松更改短信服务。

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

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

数据库:

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

缓存:

1
2
3
4
5
6
7
@Repository
public class UserRedis {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public User save(User user) {
}
}

消息队列:

1
2
3
4
5
6
7
8
// 取消订单消息生产者
public class CancelOrderProducer{
...
}
// 取消订单消息消费者
public class CancelOrderConsumer{
...
}

不要把业务代码放在 Controller 中

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

业务代码放到 Service 的好处

  1. 避免 Controller 的代码过于臃肿,进而难以维护和扩展。
  2. 抽象业务处理逻辑,方便复用比如给用户增加积分的操作可能会有其他的 Service 用到。
  3. 避免一些小问题比如 Controller 层通过 @Value注入值会失败。
  4. 更好的进行单元测试。如果将业务代码放在 Controller 中,会增加测试难度和不确定性。

错误案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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);
}
...
}

静态函数放入工具类

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 文件工具类
public class FileUtil extends PathUtil {
/**
* 文件是否为空
* 目录:里面没有文件时为空 文件:文件大小为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 语句)的方法时,你应该考虑使用策略模式对其进行优化,这样代码更加清晰,同时也更容易维护。

假设我们有这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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),用于计算保险费用。

1
2
3
public interface InsuranceCalculator {
double calculate(double income);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 来维护映射关系)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 类手动获取相应的策略对象了。

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 表达式或方法引用来代替实现策略接口的类。

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

复杂对象使用建造者模式

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

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

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

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

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

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

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

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

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

1
2
3
4
5
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 页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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、定义一个事件。

1
2
3
4
5
6
7
8
9
10
11
12
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、创建事件发布者发布事件。

1
2
3
4
5
6
7
8
9
10
11
@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、创建监听器监听并处理事件(支持异步处理事件的方式,需要配置线程池)。

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 优化排序代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 匿名内部类实现数组从小到大排序
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 优化代码示例:

1
2
3
4
5
6
7
8
9
10
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()
// 对 Optional 的值进行操作
.collect(Collectors.averagingDouble(x -> x)))
// 当值为空时,抛出指定的异常
.orElseThrow(() -> new NotFoundException("Student not found - " + studentName));
}

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

record关键字优化代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/** 
* 这个类具有两个特征
* 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、定义两个类 EmployeeEmployeeDTO

1
2
3
4
5
6
7
8
9
10
11
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、定义转换接口让 EmployeeEmployeeDTO互相转换。

1
2
3
4
5
6
7
8
9
10
11
@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、实际使用。

1
2
3
4
//  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、异常日志需要打印完整的异常信息。

反例:

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

正例:

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

7、避免层层打印日志。

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

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

反例:

1
2
3
4
5
6
try {
...
} catch (IllegalArgumentException e) {
log.error("出现异常啦", e);
throw e;
}

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

正例:

1
2
3
4
5
6
7
8
9
10
11
try {
...
} catch (IllegalArgumentException e) {
log.error("出现异常啦", e);
}
// 或者包装成自定义异常之后抛出
try {
...
} catch (IllegalArgumentException e) {
throw new MyBusinessException("一段对异常的描述信息.", e);
}

本文标题:优雅又实用的 Java 代码优化技巧

文章作者:LiJing

发布时间:2023年08月27日 - 11:55:06

最后更新:2023年08月27日 - 12:01:38

原始链接:https://blog-next.xiaojingge.com/posts/4082488636.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------------本文结束 感谢您的阅读-------------------