平台网站定制,临沂网站建设哪家好,青岛seo青岛黑八网络最强,百度秒收录神器戳蓝字“CSDN云计算”关注我们哦#xff01; 作者 | Toni Kukurin责编 | 刘丹错误一#xff1a;太过关注底层我们正在解决这个常见错误#xff0c;是因为 “非我所创” 综合症在软件开发领域很是常见。症状包括经常重写一些常见的代码#xff0c;很多开发人员都有这种症状… 戳蓝字“CSDN云计算”关注我们哦 作者 | Toni Kukurin责编 | 刘丹错误一太过关注底层我们正在解决这个常见错误是因为 “非我所创” 综合症在软件开发领域很是常见。症状包括经常重写一些常见的代码很多开发人员都有这种症状。虽然理解特定库的内部结构及其实现在很大程度上是好的并且很有必要的也可以是一个很好的学习过程但作为软件工程师不断地处理相同的底层实现细节对个人的开发生涯是有害的。像 Spring 这种抽象框架的存在是有原因的它将你从重复地手工劳作中解放出来并允许你专注于更高层次的细节 —— 领域对象和业务逻辑。因此接受抽象。下次面对特定问题时首先进行快速搜索确定解决该问题的库是否已被集成到 Spring 中现在你可能找到一个合适的现成解决方案。比如一个很有用的库在本文的其他部分我将在示例中使用 Project Lombok 注解。Lombok 被用作样板代码生成器希望懒惰的开发人员在熟悉这个库时不会遇到问题。错误二内部结构 “泄露”公开你的内部结构从来都不是一个好主意因为它在服务设计中造成了不灵活性从而促进了不好的编码实践。“泄露” 的内部机制表现为使数据库结构可以从某些 API 端点访问。例如下面的 POJO“Plain Old Java Object”类表示数据库中的一个表Entity
NoArgsConstructor
Getter
public class TopTalentEntity { Id GeneratedValue private Integer id; Column private String name; public TopTalentEntity(String name) { this.name name; } }假设存在一个端点他需要访问 TopTalentEntity 数据。返回 TopTalentEntity 实例可能很诱人但更灵活的解决方案是创建一个新的类来表示 API 端点上的 TopTalentEntity 数据。AllArgsConstructor
NoArgsConstructor
Getter
public class TopTalentData { private String name;
}这样对数据库后端进行更改将不需要在服务层进行任何额外的更改。考虑下在 TopTalentEntity 中添加一个 “password” 字段来存储数据库中用户密码的 Hash 值 —— 如果没有 TopTalentData 之类的连接器忘记更改服务前端将会意外地暴露一些不必要的秘密信息。错误三缺乏关注点分离随着程序规模的增长逐渐地代码组织成为一个越来越重要的问题。讽刺的是大多数好的软件工程原则开始在规模上崩溃 —— 特别是在没有太多考虑程序体系结构设计的情况下。开发人员最常犯的一个错误就是混淆代码关注点这很容易做到通常打破 关注点分离 的是将新功能简单地 “倒” 在现有类中。当然这是一个很好的短期解决方案对于初学者来说它需要更少的输入但它也不可避免地会在将来成为一个问题无论是在测试期间、维护期间还是介于两者之间。考虑下下面的控制器它将从数据库返回 TopTalentData。RestController
public class TopTalentController { private final TopTalentRepository topTalentRepository; RequestMapping(/toptal/get) public ListTopTalentData getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }起初这段代码似乎没什么特别的问题它提供了一个从 TopTalentEntity 实例检索出来的 TopTalentData 的 List。然而仔细观察下我们可以看到 TopTalentController 实际上在此做了些事情也就是说它将请求映射到特定端点从数据库检索数据并将从 TopTalentRepository 接收的实体转换为另一种格式。一个“更干净” 的解决方案是将这些关注点分离到他们自己的类中。看起来可能是这个样子的RestController
RequestMapping(/toptal)
AllArgsConstructor
public class TopTalentController { private final TopTalentService topTalentService; RequestMapping(/get) public ListTopTalentData getTopTalent() { return topTalentService.getTopTalent(); }
} AllArgsConstructor
Service
public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public ListTopTalentData getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); }
} Component
public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); }
}这种层次结构的另一个优点是它允许我们通过检查类名来确定将功能驻留在何处。此外在测试期间如果需要我们可以很容易地用模拟实现来替换任何类。错误四缺乏异常处理或处理不当一致性的主题并非是 Spring或 Java所独有的但仍然是处理 Spring 项目时需要考虑的一个重要方面。虽然编码风格可能存在争议通常团队或整个公司内部已达成一致但拥有一个共同的标准最终会极大地提高生产力。对多人团队尤为如此一致性允许交流发生而不需要花费很多资源在手把手交接上也不需要就不同类的职责提供冗长的解释。考虑一个包含各种配置文件、服务和控制器的 Spring 项目。在命名时保持语义上的一致性可以创建一个易于搜索的结构任何新的开发人员都可以按照自己的方式管理代码例如将 Config 后缀添加到配置类服务层以 Service 结尾以及控制器用 Controller 结尾。与一致性主题密切相关服务器端的错误处理值得特别强调。如果你曾经不得不处理编写很差的 API 的异常响应那你可能知道原因 —— 正确解析异常会是一件痛苦的事情而确定这些异常最初发生的原因则更为痛苦。作为一名 API 开发者理想情况下你希望覆盖所有面向用户的端点并将他们转换为常见的错误格式。这通常意味着有一个通用的错误代码和描述而不是逃避解决问题a) 返回一个 “500 Internal Server Error”信息。b) 直接返回异常的堆栈信息给用户。实际上这些都应该不惜一切代价地去避免因为除了客户端难以处理以外它还暴露了你的内部信息。例如常见错误响应格式可能长这样Value
public class ErrorResponse { private Integer errorCode; private String errorMessage; }与此类似的事情在大多数流行的 API 中也经常遇到由于可以容易且系统地记录效果往往很不错。将异常转换为这种格式可以通过向方法提供 ExceptionHandler 注解来完成。错误五多线程处理不当不管是桌面应用还是 Web 应用无论是 Spring 还是 No Spring多线程都是很难破解的。由并行执行程序所引起的问题是令人毛骨悚然且难以捉摸的而且常常难以调试 —— 实际上由于问题的本质一旦你意识到你正在处理一个并行执行问题你可能就不得不完全放弃调试器了并 “手动” 检查代码直到找到根本上的错误原因。不幸的是这类问题并没有千篇一律的解决方案根据具体场景来评估情况然后从你认为最好的角度来解决问题。当然理想情况下你也希望完全避免多线程错误。同样不存在那种一刀切的方法但这有一些调试和防止多线程错误的实际考虑因素避免全局状态首先牢记 “全局状态” 问题。如果你正创建一个多线程应用那么应该密切关注任何可能全局修改的内容如果可能的话将他们全部删掉。如果某个全局变量有必须保持可修改的原因请仔细使用 synchronization并对程序性能进行跟踪以确定没有因为新引入的等待时间而导致系统性能降低。避免可变性这点直接来自于 函数式编程并且适用于 OOP声明应该避免类和状态的改变。简而言之这意味着放弃 setter 方法并在所有模型类上拥有私有的 final 字段。它们的值唯一发生变化的时间是在构造期间。这样你可以确定不会出现争用问题且访问对象属性将始终提供正确的值。记录关键数据评估你的程序可能会在何处发生异常并预先记录所有关键数据。如果发生错误你将很高兴可以得到信息说明收到了哪些请求并可更好地了解你的应用程序为什么会出现错误。需要再次注意的是日志记录引入了额外的文件 I/O可能会严重影响应用的性能因此请不要滥用日志。复用现存实现每当你需要创建自己的线程时例如向不同的服务发出异步请求复用现有的安全实现来代替创建自己的解决方案。这在很大程度上意味着要使用 ExecutorServices 和 Java 8 简洁的函数式 CompletableFutures 来创建线程。Spring 还允许通过 DeferredResult 类来进行异步请求处理。错误六不使用基于注解的验证假设我们之前的 TopTalent 服务需要一个端点来添加新的 TopTalent。此外假设基于某些原因每个新名词都需要为 10 个字符长度。执行此操作的一种方法可能如下RequestMapping(/put)
public void addTopTalent(RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name - name.length() 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData);
}然而上面的方法除了构造很差以外并不是一个真正 “干净” 的解决办法。我们正检查不止一种类型的有效性即 TopTalentData 不得为空TopTalentData.name 不得为空且 TopTalentData.name 为 10 个字符长度以及在数据无效时抛出异常。通过在 Spring 中集成 Hibernate validator数据校验可以更干净地进行。让我们首先重构 addTopTalent 方法来支持验证RequestMapping(/put)
public void addTopTalent(Valid NotNull RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData);
} ExceptionHandler
ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception
}
// 此外我们还必须指出我们想要在 TopTalentData 类中验证什么属性
public class TopTalentData { Length(min 10, max 10) NotNull private String name;
}现在Spring 将在调用方法之前拦截其请求并对参数进行验证 —— 无需使用额外的手工测试。另一种实现相同功能的方法是创建我们自己的注解。虽然你通常只在需要超出 Hibernate的内置约束集 时才使用自定义注解本例中我们假设 Length 不存在。你可以创建两个额外的类来验证字符串长度一个用于验证一个用于对属性进行注解Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
Retention(RetentionPolicy.RUNTIME)
Documented
Constraint(validatedBy { MyAnnotationValidator.class })
public interface MyAnnotation { String message() default String length does not match expected; Class?[] groups() default {}; Class? extends Payload[] payload() default {}; int value(); } Component
public class MyAnnotationValidator implements ConstraintValidatorMyAnnotation, String { private int expectedLength; Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength myAnnotation.value(); } Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return s null || s.length() this.expectedLength; }
}请注意这些情况下关注点分离的最佳实践要求在属性为 null 时将其标记为有效isValid 方法中的 s null如果这是属性的附加要求则使用 NotNull 注解。public class TopTalentData { MyAnnotation(value 10) NotNull private String name;
}错误七依旧使用基于xml的配置虽然之前版本的 Spring 需要 XML但如今大部分配置均可通过 Java 代码或注解来完成XML 配置只是作为附加的不必要的样板代码。本文及其附带的 GitHub 仓库均使用注解来配置 SpringSpring 知道应该连接哪些 Bean因为待扫描的顶级包目录已在 SpringBootApplication 复合注解中做了声明如下所示SpringBootApplication
public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }
}复合注解可通过 Spring 文档 了解更多信息只是向 Spring 提示应该扫描哪些包来检索 Bean。在我们的案例中这意味着这个顶级包 co.kukurin将用于检索Component (TopTalentConverter, MyAnnotationValidator)RestController (TopTalentController)Repository (TopTalentRepository)Service (TopTalentService) 类如果我们有任何额外的 Configuration 注解类它们也会检查基于 Java 的配置。错误八忽略 profile在服务端开发中经常遇到的一个问题是区分不同的配置类型通常是生产配置和开发配置。在每次从测试切换到部署应用程序时不要手动替换各种配置项更有效的方法是使用 profile。考虑这么一种情况你正在使用内存数据库进行本地开发而在生产环境中使用 MySQL 数据库。本质上这意味着你需要使用不同的 URL 和 希望如此 不同的凭证来访问这两者。让我们看看可以如何做到这两个不同的配置文件APPLICATION.YAML 文件# set default profile to dev
spring.profiles.active: dev # production database details
spring.datasource.url: jdbc:mysql://localhost:3306/toptal
spring.datasource.username: root
spring.datasource.password:
8.2. APPLICATION-DEV.YAML 文件
spring.datasource.url: jdbc:h2:mem:
spring.datasource.platform: h2假设你不希望在修改代码时意外地对生产数据库进行任何操作因此将默认配置文件设为 dev 是很有意义的。然后在服务器上你可以通过提供 -Dspring.profiles.activeprod 参数给 JVM 来手动覆盖配置文件。另外还可将操作系统的环境变量设置为所需的默认 profile。错误九无法接受依赖项注入正确使用 Spring 的依赖注入意味着允许其通过扫描所有必须的配置类来将所有对象连接在一起这对于解耦关系非常有用也使测试变得更为容易而不是通过类之间的紧耦合来做这样的事情Misko Hevery 的 Google talk 深入解释了依赖注入的 “为什么”所以让我们看看它在实践中是如何使用的。在关注点分离常见错误 #3一节中我们创建了一个服务和控制器类。假设我们想在 TopTalentService 行为正确的前提下测试控制器。我们可以通过提供一个单独的配置类来插入一个模拟对象来代替实际的服务实现Configuration
public class SampleUnitTestConfig { Bean public TopTalentService topTalentService() { TopTalentService topTalentService Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of(Mary, Joel).map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; }
}然后我们可以通过告诉 Spring 使用 SampleUnitTestConfig 作为它的配置类来注入模拟对象之后我们就可以使用上下文配置将 Bean 注入到单元测试中。错误十缺乏测试或测试不当尽管单元测试的概念已经存在很长时间了但很多开发人员似乎要么 “忘记” 做这件事特别是如果它不是 “必需” 的时候要么只是在事后把它添加进来。这显然是不可取的因为测试不仅应该验证代码的正确性还应该作为程序在不同场景下应如何表现的文档。在测试 Web 服务时很少只进行 “纯” 单元测试因为通过 HTTP 进行通信通常需要调用 Spring 的 DispatcherServlet并查看当收到一个实际的 HttpServletRequest 时会发生什么使它成为一个 “集成” 测试处理验证、序列化等。REST Assured一个用于简化测试REST服务的 Java DSL在 MockMVC 之上已经被证明提供了一个非常优雅的解决方案。考虑以下带有依赖项注入的代码片段RunWith(SpringJUnit4Cla***unner.class)
ContextConfiguration(classes { Application.class, SampleUnitTestConfig.class
})
public class RestAssuredTestDemonstration { Autowired private TopTalentController topTalentController; Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response givenRestAssuredSpecification.when().get(/toptal/get); // then response.then().statusCode(200); response.then().body(name, hasItems(Mary, Joel)); } }SampleUnitTestConfig 类将 TopTalentService 的模拟实现连接到 TopTalentController 中而所有的其他类都是通过扫描应用类所在包的下级包目录来推断出的标准配置。RestAssuredMockMvc 只是用来设置一个轻量级环境并向 /toptal/get 端点发送一个 GET 请求。福利扫描添加小编微信备注“姓名公司职位”入驻【CSDN博客】加入【云计算学习交流群】和志同道合的朋友们共同打卡学习推荐阅读漫画什么是希尔排序一次失败的面试复习一次一致性哈希算法Pandas中第二好用的函数 | 优雅的Apply程序员因接外包坐牢 456 天两万字揭露心酸经历限时早鸟票 | 2019 中国大数据技术大会BDTC超豪华盛宴抢先看阿里开源物联网操作系统 AliOS Things 3.0 发布集成平头哥 AI 芯片架构雷声大雨点小Bakkt「见光死」了吗真香朕在看了