「Java学习 + 面试指南」: javaguide.cn (一份涵盖大部分 Java 程序员所须要把握的外围常识)
这篇文章我会总结一些实用的有助于进步代码品质的倡议,内容较多,倡议珍藏!
内容概览:
提取通用解决逻辑
注解、反射和动静代理是 Java 语言中的利器,应用切当的话,能够大大简化代码编写,并进步代码的可读性、可维护性和可扩展性。
咱们能够利用 注解 + 反射 和 注解+动静代理 来提取类、类属性或者类办法通用解决逻辑,进而防止反复的代码。尽管可能会带来一些性能损耗,但与其带来的益处相比还是十分值得的。
通过 注解 + 反射 这种形式,能够在运行时动静地获取类的信息、属性和办法,并对它们进行通用解决。比如说在通过 Spring Boot 中通过注解验证接口输出的数据就是这个思维的使用,咱们通过注解来标记须要验证的参数,而后通过反射获取属性的值,并进行相应的验证。
@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;}
相干浏览:一坨一坨的 if/else 参数校验,终于被 SpringBoot 参数校验组件整洁净了! 。
通过 注解 + 动静代理 这种形式,能够在运行时生成代理对象,从而实现通用解决逻辑。比如说 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> { ...}
缓存:
@Repositorypublic class UserRedis { @Autowired private RedisTemplate<String, String> redisTemplate; public User save(User user) { }}
音讯队列:
// 勾销订单音讯生产者public class CancelOrderProducer{ ...}// 勾销订单音讯消费者public class CancelOrderConsumer{ ...}
不要把业务代码放在 Controller 中
这个是陈词滥调了,最根本的标准。肯定不要把业务代码应该放在 Controller 中,业务代码就是要交给 Service 解决。
业务代码放到 Service 的益处 :
- 防止 Controller 的代码过于臃肿,进而难以保护和扩大。
- 形象业务解决逻辑,不便复用比方给用户减少积分的操作可能会有其余的 Service 用到。
- 防止一些小问题比方 Controller 层通过
@Value
注入值会失败。 - 更好的进行单元测试。如果将业务代码放在 Controller 中,会减少测试难度和不确定性。
谬误案例:
@RestControllerpublic 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; }}
善用现有的工具类库
Java 的一大劣势就是生态特地好, 蕴含了许多好用的工具类库和框架,简直笼罩了所有的需要场景。很多事件咱们齐全不须要本人从头开始做,利用现有的稳固牢靠的工具类库能够大大提高开发效率。
比方 Excel 文档解决,你能够思考上面这几个开源的工具类库:
- easyexcel :疾速、简略防止 OOM 的 Java 解决 Excel 工具。
- excel-streaming-reader:Excel 流式代码格调读取工具(只反对读取 XLSX 文件),基于 Apache POI 封装,同时保留规范 POI API 的语法。
- myexcel:一个集导入、导出、加密 Excel 等多项性能的工具包。
再比方 PDF 文档解决:
- pdfbox :用于解决 PDF 文档的开放源码 Java 工具。该我的项目容许创立新的 PDF 文档、对现有文档进行操作以及从文档中提取内容。PDFBox 还包含几个命令行实用程序。PDFBox 是在 Apache 2.0 版许可下公布的。
- OpenPDF:OpenPDF 是一个收费的 Java 库,用于应用 LGPL 和 MPL 开源许可创立和编辑 PDF 文件。OpenPDF 基于 iText 的一个分支。
- itext7:iText 7 代表了想要利用利用好 PDF 的开发人员的更高级别的 sdk。iText 7 装备了更好的文档引擎、高级和低级编程性能以及创立、编辑和加强 PDF 文档的能力,简直对每个工作流都有益处。
- FOP :Apache FOP 我的项目的次要的输入指标是 PDF。
我的网站上总结了 Java 开发罕用的一些工具类库,能够作为参考:https://javaguide.cn/open-source-project/tool-library.html 。
善用设计模式
理论开发我的项目的过程中,咱们应该正当地应用现有的设计模式来优化咱们的代码。不过,切忌为了应用设计模式而应用。
新来了个共事,设计模式用的是真优雅呀!这篇文章中介绍了 9 种在源码中十分常见的设计模式:
- 工厂模式(Factory Pattern) :通过定义一个工厂办法来创建对象,从而将对象的创立和应用解耦,实现了“开闭准则”。
- 建造者模式(Builder Pattern) :通过链式调用和流式接口的形式,创立一个简单对象,而不须要间接调用它的构造函数。
- 单例模式(Singleton Pattern) :确保一个类只有一个实例,并且提供一个全局的拜访点,比方常见的 Spring Bean 单例模式。
- 原型模式(Prototype Pattern) :通过复制现有的对象来创立新的对象,从而防止了对象的创立老本和复杂度。
- 适配器模式(Adapter Pattern) :将一个类的接口转换成客户端所冀望的接口,从而解决了接口不兼容的问题。
- 桥接模式(Bridge Pattern) :将形象局部与实现局部拆散开来,从而使它们能够独立变动。
- 装璜器模式(Decorator Pattern) :动静地给一个对象增加一些额定的职责,比方 Java 中的 IO 流解决。
- 代理模式(Proxy Pattern) :为其余对象提供一种代理以管制对这个对象的拜访,比方常见的 Spring AOP 代理模式。
- 观察者模式(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(double income) { if (income <= 10000) { return CALCULATOR_MAP.get("first"); } else if (income <= 30000) { return CALCULATOR_MAP.get("second"); } else if (income <= 60000) { return CALCULATOR_MAP.get("third"); } else { return CALCULATOR_MAP.get("fourth"); } }}
这样,就能够通过 InsuranceCalculatorFactory
类手动获取相应的策略对象了。
double income = 40000;// 获取第三级保险费用计算器InsuranceCalculator calculator = InsuranceCalculatorFactory.getCalculator(income);double insurance = calculator.calculate(income);System.out.println("保险费用为:" + insurance);
这种形式容许咱们在运行时依据须要抉择不同的策略,而无需在代码中硬编码条件语句。
相干浏览:Replace Conditional Logic with Strategy Pattern - IDEA 。
除了策略模式之外,Map+函数式接口也能实现相似的成果,代码个别还要更简洁一些。
上面是应用Map+函数式接口重构后的代码:
首先,在 InsuranceCalculatorFactory
类中,将 getCalculator
办法的返回类型从 InsuranceCalculator
改为 Function<Double, Double>
,示意该办法返回一个将 double
类型的 income
映射到 double
类型的 insurance
的函数。
import java.util.HashMap;import java.util.Map;import java.util.function.Function;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(double income) { if (income <= 10000) { return CALCULATOR_MAP.get("first"); } else if (income <= 30000) { return CALCULATOR_MAP.get("second"); } else if (income <= 60000) { return CALCULATOR_MAP.get("third"); } else { return CALCULATOR_MAP.get("fourth"); } }}
而后,在调用工厂办法时,能够应用 Lambda 表达式或办法援用来代替实现策略接口的类。
double income = 40000;Function<Double, Double> calculator = InsuranceCalculatorFactory.getCalculator(income);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、创立事件发布者公布事件。
@Componentpublic 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、创立监听器监听并处理事件(反对异步处理事件的形式,须要配置线程池)。
@Componentpublic 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 格局的数据进行校验。
通过以上实现,咱们能够通过继承形象父类并实现其中的形象办法,来定义本人的数据导入流程。另外,因为形象父类曾经定义了整个流程的构造和大部分默认实现,因而子类只须要关注定制化的逻辑即可,从而进步了代码的可复用性和可维护性。
相干浏览:21 | 代码反复:搞定代码反复的三个绝招 - Java 业务开发常见谬误 100 例 。
善用 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
相互转换。
@Mapperpublic 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 转 EmployeeEmployee employee = EmployeeMapper.INSTANT.employeeToEmployeeDTO(employee);// Employee 转 EmployeeDTOEmployeeDTO employeeDTO = EmployeeMapper.INSTANT.employeeDTOtoEmployee(employeeDTO);
相干浏览:
- MapStruct,升高无用代码的神器 - 大淘宝技术 - 2022 (举荐):对于 MapStruct 的各种操作介绍的更具体一些,波及到一对多字段互转、为转换加缓存、 利用 Spring 进行依赖注入等高级用法。
- 辞别 BeanUtils,Mapstruct 从入门到精通 - 大淘宝技术 - 2022 :次要和 Spring 的 BeanUtils 做了简略比照,介绍的绝对比较简单。
标准日志打印
1、不要随便打印日志,确保本人打印的日志是前面能用到的。
打印太多无用的日志不光影响问题排查,还会影响性能,减轻磁盘累赘。
2、打印日志中的敏感数据比方身份证号、电话号、明码须要进行脱敏。相干浏览:Spring Boot 3 步实现日志脱敏,简略实用!!
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);}
相干浏览:15 个日志打印的实用倡议 。
标准异样解决
阿里巴巴 Java 异样解决规约如下:
对立异样解决
所有的异样都应该由最上层捕捉并解决,这样代码更简洁,还能够防止反复输入异样日志。 如果咱们都在业务代码中应用try-catch
或者try-catch-finally
解决的话,就会让业务代码中冗余太多异样解决的逻辑,对于同样的异样咱们还须要反复编写代码解决,还可能会导致反复输入异样日志。这样的话,代码可维护性、可浏览性都十分差。
Spring Boot 应用程序能够借助 @RestControllerAdvice
和 @ExceptionHandler
实现全局对立异样解决。
@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result businessExceptionHandler(HttpServletRequest request, BusinessException e){ ... return Result.faild(e.getCode(), e.getMessage()); } ...}
应用 try-with-resource 敞开资源
- 适用范围(资源的定义): 任何实现
java.lang.AutoCloseable
或者java.io.Closeable
的对象 - 敞开资源和 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;}
对立接口返回值
接口返回的数据肯定要对立格局,遮掩更方面对接前端开发的同学以及其余调用该接口的开发。
通常来说,上面这些信息是必备的:
- 状态码和状态信息:能够通过枚举定义状态码和状态信息。状态码标识申请的后果,状态信息属于提示信息,提醒胜利信息或者错误信息。
- 申请数据:申请该接口理论要返回的数据比方用户信息、文章列表。
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 格局返回 。
须要留神的是,这种形式在 Spring Cloud OpenFeign 的继承模式下是有侵入性,解决办法见:SpringBoot 无侵入式 API 接口对立格局返回,在 Spring Cloud OpenFeign 继承模式具备了侵入性 。
理论我的项目中,其实应用比拟多的还是上面这种比拟间接的形式:
public class PostController { @GetMapping("/list") public R<List<SysPost>> getPosts() { ... return R.ok(posts); }}
下面介绍的无侵入的形式,个别革新旧我的项目的时候用的比拟多。
近程调用设置超时工夫
开发过程中,第三方接口调用、RPC 调用以及服务之间的调用倡议设置一个超时工夫。
咱们平时接触到的超时能够简略分为上面 2 种:
- 连贯超时(ConnectTimeout) :客户端与服务端建设连贯的最长等待时间。
- 读取超时(ReadTimeout) :客户端和服务端曾经建设连贯,客户端期待服务端解决完申请的最长工夫。理论我的项目中,咱们关注比拟多的还是读取超时。
一些连接池客户端框架中可能还会有获取连贯超时和闲暇连贯清理超时。
如果没有设置超时的话,就可能会导致服务端连接数爆炸和大量申请沉积的问题。这些沉积的连贯和申请会耗费系统资源,影响新收到的申请的解决。重大的状况下,甚至会拖垮整个零碎或者服务。
我之前在理论我的项目就遇到过相似的问题,整个网站无奈失常解决申请,服务器负载间接快被拉满。前面发现起因是我的项目超时设置谬误加上客户端申请解决异样,导致服务端连接数间接靠近 40w+,这么多沉积的连贯间接把零碎干趴了。
相干浏览:超时&重试详解 。
正确应用线程池
在 10 个线程池最佳实际和坑! 这篇文章中,我总结了 10 个应用线程池的注意事项:
- 线程池必须手动通过
ThreadPoolExecutor
的构造函数来申明,防止应用Executors
类创立线程池,会有 OOM 危险。 - 监测线程池运行状态。
- 倡议不同类别的业务用不同的线程池。
- 别忘记给线程池命名。
- 正确配置线程池参数。
- 别忘记敞开线程池。
- 线程池尽量不要放耗时工作。
- 防止反复创立线程池。
- 应用 Spring 外部线程池时,肯定要手动自定义线程池,配置正当的参数,不然会呈现生产问题(一个申请创立一个线程)
- 线程池和
ThreadLocal
共用,可能会导致线程从ThreadLocal
获取到的是旧值/脏数据。
敏感数据处理
- 返回前端的敏感数据比方身份证号、电话、地址信息要依据业务需要进行脱敏解决,示例:
163****892
。 - 保留在数据库中的明码须要加盐之后应用哈希算法(比方 BCrypt)进行加密。
- 保留在数据库中的银行卡号、身份号这类敏感数据须要应用对称加密算法(比方 AES)保留。
- 网络传输的敏感数据比方银行卡号、身份号须要用 HTTPS + 非对称加密算法(如 RSA)来保障传输数据的安全性。
- 对于密码找回性能,不能明文存储用户明码。能够采纳重置明码的形式,让用户通过验证身份后从新设置明码。
- 在代码中不应该明文写入密钥、口令等敏感信息。能够采纳配置文件、环境变量等形式来动静加载这些信息。
- 定期更新敏感数据的加密算法和密钥,以保障加密算法和密钥的安全性和有效性。