这篇文章我会总结一些实用的有助于提高代码质量的建议,内容较多,建议收藏!
内容概览:
注解、反射和动态代理是 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;
}
相关阅读:一坨一坨的 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> {
...
}
缓存:
@Repository
public class UserRedis {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public User save(User user) {
}
}
消息队列:
// 取消订单消息生产者
public class CancelOrderProducer{
...
}
// 取消订单消息消费者
public class CancelOrderConsumer{
...
}
这个是老生常谈了,最基本的规范。一定不要把业务代码应该放在 Controller 中,业务代码就是要交给 Service 处理。
业务代码放到 Service 的好处 :
@Value
注入值会失败。错误案例:
@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;
}
}
Java 的一大优势就是生态特别好, 包含了许多好用的工具类库和框架,几乎覆盖了所有的需求场景。很多事情我们完全不需要自己从头开始做,利用现有的稳定可靠的工具类库可以大大提高开发效率。
比如 Excel 文档处理,你可以考虑下面这几个开源的工具类库:
再比如 PDF 文档处理:
我的网站上总结了 Java 开发常用的一些工具类库,可以作为参考:http://javaguide.cn/open-source-project/tool-library.html 。
实际开发项目的过程中,我们应该合理地使用现有的设计模式来优化我们的代码。不过,切忌为了使用设计模式而使用。
新来了个同事,设计模式用的是真优雅呀!这篇文章中介绍了 9 种在源码中非常常见的设计模式:
策略模式是一种常见的优化条件逻辑的方法。当代码中有一个包含大量条件逻辑(即 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、创建事件发布者发布事件。
@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 格式的数据进行校验。
通过以上实现,我们可以通过继承抽象父类并实现其中的抽象方法,来定义自己的数据导入流程。另外,由于抽象父类已经定义了整个流程的结构和大部分默认实现,因此子类只需要关注定制化的逻辑即可,从而提高了代码的可复用性和可维护性。
相关阅读:21 | 代码重复:搞定代码重复的三个绝招 - Java 业务开发常见错误 100 例 。
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) { }
我们经常在代码中会对一个数据结构封装成 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、打印日志中的敏感数据比如身份证号、电话号、密码需要进行脱敏。相关阅读:Spring Boot 3 步完成日志脱敏,简单实用!!
3、选择合适的日志打印级别。最常用的日志级别有四个: 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
实现全局统一异常处理。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result businessExceptionHandler(HttpServletRequest request, BusinessException e){
...
return Result.faild(e.getCode(), e.getMessage());
}
...
}
java.lang.AutoCloseable
或者 java.io.Closeable
的对象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 种:
一些连接池客户端框架中可能还会有获取连接超时和空闲连接清理超时。
如果没有设置超时的话,就可能会导致服务端连接数爆炸和大量请求堆积的问题。这些堆积的连接和请求会消耗系统资源,影响新收到的请求的处理。严重的情况下,甚至会拖垮整个系统或者服务。
我之前在实际项目就遇到过类似的问题,整个网站无法正常处理请求,服务器负载直接快被拉满。后面发现原因是项目超时设置错误加上客户端请求处理异常,导致服务端连接数直接接近 40w+,这么多堆积的连接直接把系统干趴了。
相关阅读:超时&重试详解 。
在 10 个线程池最佳实践和坑! 这篇文章中,我总结了 10 个使用线程池的注意事项:
ThreadPoolExecutor
的构造函数来声明,避免使用 Executors
类创建线程池,会有 OOM 风险。ThreadLocal
共用,可能会导致线程从 ThreadLocal
获取到的是旧值/脏数据。163****892
。大家好,又见面了,我是你们的朋友全栈君。查看Python安装路径方法在使用python的时候,有时候会需要找到python包的安装位置,怎么办?对于Windows平台,打开cmd使用命令py-0p【其中0是零】显示已安装的python版本且带路径的列表,参见下图:其中带星号*的为默认版本。其它还有一、对于Windows平台,打开cmd输入命令wherePython,回车(即按下Enter键)可输出Python的安装路径。参见下图:【提示:若安装了python虚拟环境virtualenv,其路径也将显示。】【如何清除命令行窗口(cmd)内容 输入cls回车】二、在IDLE(Python自带的简洁的集成开发环境)中先输入importsys回车再输入sys.path回车其中红线标识的就是路径,将其中的\\替换为\即可。但这种方式只能看到当前运行的Python的安装路径。三、以操作系统是Windows10计算机为例,右键此电脑,属性->高级系统设置->环境变量,在“用户变量”(对当前用户起作用)或“系统变量”(对此计算机上所有的用户起作用)找到path行点击选中,点击“编辑”找到带
如何在matomo中启用代码跟踪?当你问到这个问题的时候,我们确定自你应该对Matomo比较了解,并且知道Matomo是干什么的了。Matomo的前身是Piwik,好像是最近才开始改名的,是个非常出名的开源网站统计分析程序,提供了顶级关键字和搜索引擎,网站,社交媒体网站,首页网址,页面标题,用户国家,提供商,操作系统,浏览器市场份额,屏幕分辨率。更加主要的是,你将会获得所有用户访问的数据,能够有效避免因为网站屏蔽给你带来的问题,比如说GA在很多时候就访问不了。Matomo是基于PHP和MySql平台的,搭建也非常容易。我们这里就主要讲如何在Discourse中嵌入进去。首先你需要为你的主题安装主题组件,安装的方法请参考:Discourse如何安装一个主题组件或者主题组件页面中的内容。我们使用的仓库地址为:https://github.com/ossez-com/discourse-matomo-analytics,这个Fork仓库地址https://github.com/discourse/discourse-matomo-analytics.git的内容的。我们使用自己的仓库地址主要
点击上方“算法与数据之美”,选择“置顶公众号”更多精彩等你来!之前我们分享了2017年的冠军图像分类模型SENet,今天给大家带来的这篇2018年发表在ECCV上的论文不仅考虑到了不同特征通道的重要性不一,还考虑到了同一个特征通道的不同位置的重要性程度。也就是说既包含通道的注意力机制,又包含空间的注意力机制。相比于只关注通道的SENet,ConvolutionalBlockAttentionModule(CBAM)取得了更好的效果。上图为整个CBAM的示意图,先是通过注意力机制模块,然后是空间注意力模块,对于两个模块先后顺序对模型性能的影响,本文作者也给出了实验的数据对比,先通道再空间要比先空间再通道以及通道和空间注意力模块并行的方式效果要略胜一筹。那么这个通道注意力模块和空间注意力模块又是如何实现的呢?通道注意力模块这个部分大体上和SENet的注意力模块相同,主要的区别是CBAM在S步采取了全局平均池化以及全局最大池化,两种不同的池化意味着提取的高层次特征更加丰富。接着在E步同样通过两个全连接层和相应的激活函数建模通道之间的相关性,合并两个输出得到各个特征通道的权重。最后,得到特征通
导读:自2008年成立以来,StackOverflow一直在拯救所有类型的开发人员。自那时以来,开发人员提出了数百万个关于开发领域的问题。但是,迫使开发者转向StackOverflow的问题都是什么呢?作者:NickRoberts编译/来源:AI科技大本营(ID:rgznai100)我们选择了11种最流行的编程语言(以StackOverflow标签的频率来衡量),并进行了一项研究,旨在揭示这些问题中的某些共性和差异。但在这之前,让我们先瞧瞧如下所示的11种语言。就所提问题的数量而言,JavaScript是自StackOverflow成立以来最常被问到的编程语言。这可能是由于其在众多不同应用和服务中无处不在:无论你以任何方式在网络上工作,都可能需要了解一些JavaScript。但是,尽管JavaScript可能是整体排名最高的编程语言,但当我们按时间划分数据时,我们发现需要一个新的王冠。2011年,《哈佛商业评论》将数据科学家标记为“21世纪最性感的工作(SexiestJobofthe21stCentury)”。从那以后,数据科学家常用的Python语言的受欢迎程度一直在增长……以至于到
AWS一直是纯粹的公有云供应商,即使它已经对混合云做出了点头,但未曾有过其他大的动作。AWS尚未采用混合云的最重要步骤将成为许多企业用户欢迎的举措,但可能会引起其他人的担忧。11月28日,AWS与VMware合作推出了新的云服务:AWSOutposts。AWSOutposts是由AWS硬件和软件组成的计算和存储机架,将允许需要本地基础结构的企业客户在自己的数据中心中运行AWS云基础架构。Outposts源起于什么?该服务首先将基于AWS自己的硬件,提供两种软件:VMwareCloudonAWS:可直接使用VMware控制面板;AWSNative:允许客户使用AWS云中相同的AWSAPI在本地进行计算和存储。VMwareCEOGelsinger表示,VMware不仅在AWSOutposts上提供VMwareCloud选项,还在其他产品中构建集成点,支持新形式的基础架构。AWSCEOAndyJassy表示,AWS不仅会提供装载其软件服务的硬件机架,还会为客户安装和维护这些硬件,它将在2019年开始发布。Jassy同时强调,AWSOutposts的推出是应广大企业客户的呼声。企业客户都长期使
前面我们讲了《基于宝塔搭建SS-Panel教程》、《【图文】使用SS-Panel以及怎么部署SSR后端教程》、《SS-Panel魔改面板.config.php文件详解》三篇教程,接下来给各位几个非常实用的审计规则,可以有效防止滥用和避免开机场后遇到的一些麻烦。首先是屏蔽SPAM,防止有恶意用户使用你的酸酸服务器进行邮件滥发:(Subject|HELO|SMTP)复制如图填写即可:接着是禁止BT下载,防止有些逗比用户用你的酸酸服务器BT下载版权内容或者是踩到蜜灌被版权方投诉,导致商家把你的服务器停止。BitTorrentprotocol复制如图填写即可:还有这些规则就不一一截图啦禁用BT防止版权争议BitTorrentprotocol复制数据包明文匹配禁止百度高精度定位防止IP与客户端地理位置被记录(api|ps|sv|offnavi|newvector|ulog\.imap|newloc)(\.map|)\.(baidu|n\.shifen)\.com复制数据包明文匹配禁止360有毒服务屏蔽360(.+\.|^)(360|so)\.(cn|com)复制数据包明文匹配禁止邮件滥发防止垃圾
选自Medium作者:ShaoboGUAN机器之心编译参与:王淑婷、路、张倩基于描述生成逼真图像是一项比较困难的任务。本文介绍了一项新研究TransparentLatent-spaceGAN(TL-GAN),它使用英伟达的pg-GAN模型,利用潜在空间中的特征轴,轻松完成图像合成和编辑任务。使用TL-GAN模型进行受控人脸图像合成的示例。教电脑根据描述生成照片判别任务VS生成任务描述一张图像对人类来说相当容易,我们在很小的时候就能做到。在机器学习中,这项任务是一个判别分类/回归问题,即从输入图像预测特征标签。随着最近ML/AI技术(尤其是深度学习模型)的进步,它们开始在这些任务中脱颖而出,有时会达到甚至超过人类的表现,如视觉目标识别(例如,从AlexNet到ResNet在ImageNet分类任务上的表现)和目标检测/分割(如从RCNN到YOLO在COCO数据集上的表现)等场景中展示的一样。然而,另一方面,基于描述生成逼真图像却要困难得多,需要多年的平面设计训练。在机器学习中,这是一项生成任务,比判别任务难多了,因为生成模型必须基于更小的种子输入产出更丰富的信息(如具有某些细节和变化的完
常规问题Q1:什么是非SDK接口?A:非SDK接口指不在官方AndroidSDK涵盖范围内的Java字段和方法。此类接口是SDK的内部实现细节,可能随时会被修改,且不对开发者另行通知。常规问题Q2:AndroidP在非SDK接口使用限制方面采取了哪些举措?A:谷歌正在逐步限制非SDK接口的使用:针对不同接口采取不同形式的限制(详情请参照条目“应用运行时,我应该如何检测非SDK接口的使用?”)。若您正在使用非SDK接口进行开发,请特别注意限制对应用行为造成的影响。常规问题Q3:如果我正在使用非SDK接口,我应该如何提交请求,申请重新评估该接口?A:请提交申请并提供需要使用此接口的详细原因。常规问题Q4:非SDK接口都有哪些不同管控名单?且不同名单下接口的限制又有何不同?A:以下为各名单的具体说明:白名单:SDK本身浅灰名单:仍允许调用的非SDK方法和字段深灰名单若应用的targetSDK低于AndroidP(即targetSdkVersion<28):允许调用深灰名单中的接若应用的targetSDK为AndroidP或更高(即targetSdkVersion>=28):深灰名
入门知识对于我们新手入门学习hadoop的朋友来说,首先了解一下云计算和云计算技术是有必要的。下面先是介绍云计算和云计算技术的:云计算,是一种基于互联网的计算方式,通过这种方式,共享的软硬件资源和信息可以按需求提供给计算机和其他设备,主要是基于互联网的相关服务地增加、使用和交付模式,通常涉及通过互联网来提供动态易扩展且经常是虚拟化的资源。云是网络、互联网的一种比喻说法。过去在图中往往用云来表示电信网,后来也用来表示互联网和底层基础设施的抽象。狭义云计算指IT基础设施的交付和使用模式,指通过网络以按需、易扩展的方式获得所需资源;广义云计算指服务地交付和使用模式,指通过网络以按需、易扩展的方式获得所需服务。这种服务可以是IT和软件、互联网相关,也可是其他服务。它意味着计算也可作为一种商品通过互联网进行流通。什么是云计算?什么是云计算技术?在世界上云计算已经大面流行,有很流行的Google、Drive、SkyDrive、Dropbox、亚马逊云服务等等。在国内百度云存储、360云存储都是比较流行的。我们接下来就应该会想到大数据存储,目前开源市场上最流行的应该是hadoop分布式存储,已经有大
1login.php <?php //$conn=mysql_connect("localhost","root","root")ordie("Couldnotconnectmysql!".msyql_error()."<br/>"); //mysql_query("createdatabasecookie_user")ordie("Createdatabasefailed!".mysql_error()."<br/>"); //mysql_select_db("cookie_user"); /*$sql="createtableuser( idint(10)notnullauto_increment, namevarchar(50)defaultnull, passwordvarchar(50)defaultnull, ageint(5)defaul
Jedis实例不是线程安全的,所以不可以多个线程共用一个Jedis实例,但是创建太多的实现也不好因为这意味着会建立很多sokcet连接。 JedisPool是一个线程安全的网络连接池。可以用JedisPool创建一些可靠Jedis实例,可以从池中获取Jedis实例,使用完后再把Jedis实例还回JedisPool。这种方式可以避免创建大量socket连接并且会实现高效的性能.JedisPool初始化JedisPoolConfigjedisPoolConfig=newJedisPoolConfig(); //设置最大10个连接 jedisPoolConfig.setMaxTotal(10); pool=newJedisPool(jedisPoolConfig,"localhost");复制使用JedisPool1.JedisPool#getResource()方法从连接池中取得一个Jedis实例, 2.使用Jedis实例进行正常的数据操作 3.Jedis实例使用完后要把它再放回连接池。资源释放关于如何将使用完后的Jedis实例还回连接池,网上看到的大部分文章都是建议用
背景上周分享了一篇《一个线程罢工的诡异事件》,最近也在公司内部分享了这个案例。无独有偶,在内部分享的时候也有小伙伴问了之前分享时所提出的一类问题:这其实是一类共性问题,我认为主要还是两个原因:我自己确实也没讲清楚,之前画的那张图还需要再完善,有些误导。第二还是大家对线程池的理解不够深刻,比如今天要探讨的内容。线程池的工作原理首先还是来复习下线程池的基本原理。我认为线程池它就是一个调度任务的工具。众所周知在初始化线程池会给定线程池的大小,假设现在我们有1000个线程任务需要运行,而线程池的大小为10~20,在真正运行任务的过程中他肯定不会创建这1000个线程同时运行,而是充分利用线程池里这10~20个线程来调度这1000个任务。而这里的10~20个线程最后会由线程池封装为ThreadPoolExecutor.Worker对象,而这个Worker是实现了Runnable接口的,所以他自己本身就是一个线程。深入分析这里我们来做一个模拟,创建了一个核心线程、最大线程数、阻塞队列都为2的线程池。这里假设线程池已经完成了预热,也就是线程池内部已经创建好了两个线程Worker。当我们往一个线程池丢一
<Image style={{height:'25px'}} //src="../../../../../title1.png" //src={require("./resource/title.jpg").default} src={require('design/assets/images/resource/title.jpg').default} //src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" />复制 background:url('../images/resource/proxyBg.png')no-repeat!important; /*背景图垂直、水平均居中*/ background-position:centercenter!important; /*让背景图基于容器大小伸缩*/ background-size:cover!important;
客户使用的MAC系统,自己没有,单独买一台又不划算,虚拟机就成了很好的选择。 因为之前有使用Kali,虚拟机已经安装过了,主要是按照MAC系统。 1、下载unlocker-master,解压后以管理员方式运行“win-install.cmd”,否则没有MAC系统的安装选项。 2、MAC系统镜像,下载的dmg镜像,安装失败,使用UltraISO转格式为ISO,还是安装失败,下载CDR格式镜像,进入界面,但是卡在苹果界面,提示“客户机操作系统已禁用CPU,请关闭或者重置虚拟机”。 3、在虚拟机安装的MAC系统所在文件夹中找到VMX文件,记事本格式打开,添加cpuid.1.eax="0000:0000:0000:0001:0000:0110:1010:0101" 再次打开安装,顺利安装完成
=============================== ---@Import(AutoConfigurationImportSelector.class) 只要是@Import+DeferredImportSelector实现类都可以注入到dbregistry(可以仿照springboot的AutoConfigurationImportSelector的写法) ConfigurationClassParser ConfigurationClassParserparser=newConfigurationClassParser( this.metadataReaderFactory,this.problemReporter,this.environment, this.resourceLoader,this.componentScanBeanNameGenerator,registry); parser也注入了registry,但是此时还没有注入到registry -processDeferredImportSelectors(){...}; AutoConfigurationI
今天用c写服务器后端时,调用一些函数的时候出了bug。 我函数传参传的是数组、结构体等 然后在函数中用sizeof的时候我惊奇的发现居然很怪。 举个例子 #include<iostream> #include<typeinfo> #include<stdio.h> #include<string.h> usingnamespacestd; //charstr[]="hello"; voidtest(charc[]) { printf("%s\n",c); printf("函数内大小:%d\n",sizeof(c));//8 printf("函数内长度:%d\n",strlen(c));//14 //这么神奇 } intmain() { test("recv_useless_s"); charc[]="recv_useless_s"; printf("函数外大小:%d\n",sizeof(c)); //printf("char大小:%d\n",sizeof(char*)); return0; }复制 &n
基于jQuery扁平多颜色选项卡切换代码,支持自动轮播切换,鼠标滑过切换的jQuery特效。效果图如下: 在线预览 源码下载 实现的代码。 html代码: <divclass="wrap"> <divclass="part1clearfix"> <divclass="slide"> <divclass="slide_cont"> <ulclass="clearfix"> <listyle="background-color:#4ab9f3;"><imgsrc="images/banner_01.png"/></li> <listyle="background-color:#4fc34e;"><imgsrc="images/banner_02.png"/></li> <listyle="background-color:#ff852a;"><imgsrc="images/banner_03.
常用镜像仓库 官方仓库:hub.docker.com 自己的私有仓库:Harbor 阿里云私有仓库:registry.cn-hangzhou.aliyuncs.com 复制 登录镜像仓库 #格式 dockerlogin 注:默认情况下,dockerlogin登录的是官方仓库,如果登录其他镜像仓库则需要指定镜像仓库的URL连接。 #镜像仓库申请地址: https://cr.console.aliyun.com/cn-shanghai/instances/repositories #实例 [root@Centos7~]#dockerloginregistry.cn-hangzhou.aliyuncs.com Username:xxxxxxx Password: WARNING!Yourpasswordwillbestoredunencryptedin/root/.docker/config.json. Configureacredentialhelpertoremovethiswarning.See https://docs.docker.com/engine/reference/