摘要:本篇文章探讨了大数据技术之Java编写,希望阅读本篇文章以后大家有所收获,帮助大家对相关内容的理解更加深入。
本篇文章探讨了大数据技术之Java编写,希望阅读本篇文章以后大家有所收获,帮助大家对相关内容的理解更加深入。
"
本文不是一个吹嘘的文章,不会讲很多高深的架构,相反,会讲解很多基础的问题和写法问题,如果读者自认为基础问题和写法问题都是不是问题,那请忽略这篇文章,节省出时间去做一些有意义的事情。
不知道有多少”老”程序员还在使用 Eclipse,这些程序员们要不就是因循守旧,要不就是根本就不知道其他好的开发工具的存在,Eclipse 吃内存卡顿的现象以及各种偶然莫名异常的出现,都告知我们是时候寻找新的开发工具了。
根本就不想多解释要换什么样的 IDE,如果你想成为一个优秀的 Java 程序员,请更换 IntelliJ IDEA。使用 IDEA 的好处,请搜索谷歌。
更换 IDE 不在我本文的重点内容中,所以不想用太多的篇幅去写为什么更换IDE。在这里,我只能告诉你,更换 IDE 只为了更好、更快的写好 Java 代码。原因略。
别告诉我快捷键不好用,请尝试新事物。
bean 使我们使用最多的模型之一,我将以大篇幅去讲解 bean,希望读者好好体会。
根据很多 Java 程序员的”经验”来看,一个数据库表则对应着一个 domain 对象,所以很多程序员在写代码时,包名则使用:com.xxx.domain ,这样写好像已经成为了行业的一种约束,数据库映射对象就应该是 domain。但是你错了,domain 是一个领域对象,往往我们再做传统 Java 软件 Web 开发中,这些 domain 都是贫血模型,是没有行为的,或是没有足够的领域模型的行为的,所以,以这个理论来讲,这些 domain 都应该是一个普通的 entity 对象,并非领域对象,所以请把包名改为:com.xxx.entity。
如果你还不理解我说的话,请看一下 Vaughn Vernon 出的一本叫做《IMPLEMENTING DOMAIN-DRIVEN DESIGN》(实现领域驱动设计)这本书,书中讲解了贫血模型与领域模型的区别,相信你会受益匪浅。
数据传输我们应该使用 DTO 对象作为传输对象,这是我们所约定的,因为很长时间我一直都在做移动端 API 设计的工作,有很多人告诉我,他们认为只有给手机端传输数据的时候(input or output),这些对象成为 DTO 对象。请注意!这种理解是错误的,只要是用于网络传输的对象,我们都认为他们可以当做是 DTO 对象,比如电商平台中,用户进行下单,下单后的数据,订单会发到 OMS 或者 ERP 系统,这些对接的返回值以及入参也叫 DTO 对象。
我们约定某对象如果是 DTO 对象,就将名称改为 XXDTO,比如订单下发OMS:OMSOrderInputDTO。
正如我们所知,DTO 为系统与外界交互的模型对象,那么肯定会有一个步骤是将 DTO 对象转化为 BO 对象或者是普通的 entity 对象,让 service 层去处理。
比如添加会员操作,由于用于演示,我只考虑用户的一些简单数据,当后台管理员点击添加用户时,只需要传过来用户的姓名和年龄就可以了,后端接受到数据后,将添加创建时间和更新时间和默认密码三个字段,然后保存数据库。
@RequestMapping(""/v1/api/user"")@RestControllerpublic class UserApi { @Autowired private UserService userService; @PostMapping public User addUser(UserInputDTO userInputDTO){ User user = new User(); user.setUsername(userInputDTO.getUsername()); user.setAge(userInputDTO.getAge()); return userService.addUser(user); } }
我们只关注一下上述代码中的转化代码,其他内容请忽略:
User user = new User(); user.setUsername(userInputDTO.getUsername()); user.setAge(userInputDTO.getAge());
上边的代码,从逻辑上讲,是没有问题的,只是这种写法让我很厌烦,例子中只有两个字段,如果有 20 个字段,我们要如何做呢? 一个一个进行 set 数据吗?当然,如果你这么做了,肯定不会有什么问题,但是,这肯定不是一个最优的做法。
网上有很多工具,支持浅拷贝或深拷贝的 Utils。举个例子,我们可以使用 org.springframework.beans.BeanUtils#copyProperties 对代码进行重构和优化:
@PostMappingpublic User addUser(UserInputDTO userInputDTO){ User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return userService.addUser(user); }
BeanUtils.copyProperties 是一个浅拷贝方法,复制属性时,我们只需要把 DTO 对象和要转化的对象两个的属性值设置为一样的名称,并且保证一样的类型就可以了。如果你在做 DTO 转化的时候一直使用 set 进行属性赋值,那么请尝试这种方式简化代码,让代码更加清晰!
上边的转化过程,读者看后肯定觉得优雅很多,但是我们再写 Java 代码时,更多的需要考虑语义的操作,再看上边的代码:
User user = new User(); BeanUtils.copyProperties(userInputDTO,user);
虽然这段代码很好的简化和优化了代码,但是他的语义是有问题的,我们需要提现一个转化过程才好,所以代码改成如下:
@PostMapping public User addUser(UserInputDTO userInputDTO){ User user = convertFor(userInputDTO); return userService.addUser(user); } private User convertFor(UserInputDTO userInputDTO){ User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; }
这是一个更好的语义写法,虽然他麻烦了些,但是可读性大大增加了,在写代码时,我们应该尽量把语义层次差不多的放到一个方法中,比如:
User user = convertFor(userInputDTO);return userService.addUser(user);
这两段代码都没有暴露实现,都是在讲如何在同一个方法中,做一组相同层次的语义操作,而不是暴露具体的实现。
如上所述,是一种重构方式,读者可以参考 Martin Fowler 的《Refactoring Imporving the Design of Existing Code》(重构 改善既有代码的设计) 这本书中的 Extract Method 重构方式。
当实际工作中,完成了几个 API 的 DTO 转化时,我们会发现,这样的操作有很多很多,那么应该定义好一个接口,让所有这样的操作都有规则的进行。
如果接口被定义以后,那么 convertFor 这个方法的语义将产生变化,它将是一个实现类。
看一下抽象后的接口:
public interface DTOConvert<S,T> { T convert(S s); }
虽然这个接口很简单,但是这里告诉我们一个事情,要去使用泛型,如果你是一个优秀的 Java 程序员,请为你想做的抽象接口,做好泛型吧。
我们再来看接口实现:
public class UserInputDTOConvert implements DTOConvert {@Overridepublic User convert(UserInputDTO userInputDTO) { User user = new User(); BeanUtils.copyProperties(userInputDTO,user);return user; } }
我们这样重构后,我们发现现在的代码是如此的简洁,并且那么的规范:
@RequestMapping(""/v1/api/user"")@RestControllerpublic class UserApi { @Autowired private UserService userService; @PostMapping public User addUser(UserInputDTO userInputDTO){ User user = new UserInputDTOConvert().convert(userInputDTO); return userService.addUser(user); } }
如果你是一个优秀的 Java 程序员,我相信你应该和我一样,已经数次重复 review 过自己的代码很多次了。
我们再看这个保存用户的例子,你将发现,API 中返回值是有些问题的,问题就在于不应该直接返回 User 实体,因为如果这样的话,就暴露了太多实体相关的信息,这样的返回值是不安全的,所以我们更应该返回一个 DTO 对象,我们可称它为 UserOutputDTO:
@PostMappingpublic UserOutputDTO addUser(UserInputDTO userInputDTO){ User user = new UserInputDTOConvert().convert(userInputDTO); User saveUserResult = userService.addUser(user); UserOutputDTO result = new UserOutDTOConvert().convertToUser(saveUserResult); return result; }
这样你的 API 才更健全。
不知道在看完这段代码之后,读者有是否发现还有其他问题的存在,作为一个优秀的 Java 程序员,请看一下这段我们刚刚抽象完的代码:
User user = new UserInputDTOConvert().convert(userInputDTO);
你会发现,new 这样一个 DTO 转化对象是没有必要的,而且每一个转化对象都是由在遇到 DTO 转化的时候才会出现,那我们应该考虑一下,是否可以将这个类和 DTO 进行聚合呢,看一下我的聚合结果:
public class UserInputDTO {private String username;private int age; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public User convertToUser(){ UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert(); User convert = userInputDTOConvert.convert(this); return convert; } private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> { @Override public User convert(UserInputDTO userInputDTO) { User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; } } }
然后 API 中的转化则由:
User user = new UserInputDTOConvert().convert(userInputDTO);User saveUserResult = userService.addUser(user);
变成了:
User user = userInputDTO.convertToUser();User saveUserResult = userService.addUser(user);
我们再 DTO 对象中添加了转化的行为,我相信这样的操作可以让代码的可读性变得更强,并且是符合语义的。
再来看 DTO 内部转化的代码,它实现了我们自己定义的 DTOConvert 接口,但是这样真的就没有问题,不需要再思考了吗?
我觉得并不是,对于 Convert 这种转化语义来讲,很多工具类中都有这样的定义,这中 Convert 并不是业务级别上的接口定义,它只是用于普通 bean 之间转化属性值的普通意义上的接口定义,所以我们应该更多的去读其他含有 Convert 转化语义的代码。
我仔细阅读了一下 GUAVA 的源码,发现了 com.google.common.base.Convert 这样的定义:
public abstract class Converter<A, B> implements Function<A, B> { protected abstract B doForward(A a); protected abstract A doBackward(B b); //其他略}
从源码可以了解到,GUAVA 中的 Convert 可以完成正向转化和逆向转化,继续修改我们 DTO 中转化的这段代码:
private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> { @Override public User convert(UserInputDTO userInputDTO) { User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; } }
修改后:
private static class UserInputDTOConvert extends Converter<UserInputDTO, User> { @Override protected User doForward(UserInputDTO userInputDTO) { User user = new User(); BeanUtils.copyProperties(userInputDTO,user); return user; } @Override protected UserInputDTO doBackward(User user) { UserInputDTO userInputDTO = new UserInputDTO(); BeanUtils.copyProperties(user,userInputDTO); return userInputDTO; } }
看了这部分代码以后,你可能会问,那逆向转化会有什么用呢?其实我们有很多小的业务需求中,入参和出参是一样的,那么我们变可以轻松的进行转化,我将上边所提到的 UserInputDTO 和 UserOutputDTO 都转成 UserDTO 展示给大家。
**DTO:**public class UserDTO { private String username; private int age; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public User convertToUser(){ UserDTOConvert userDTOConvert = new UserDTOConvert(); User convert = userDTOConvert.convert(this); return convert; } public UserDTO convertFor(User user){ UserDTOConvert userDTOConvert = new UserDTOConvert(); UserDTO convert = userDTOConvert.reverse().convert(user); return convert; } private static class UserDTOConvert extends Converter<UserDTO, User> { @Override protected User doForward(UserDTO userDTO) { User user = new User(); BeanUtils.copyProperties(userDTO,user); return user; } @Override protected UserDTO doBackward(User user) { UserDTO userDTO = new UserDTO(); BeanUtils.copyProperties(user,userDTO); return userDTO; } } }
API:
@PostMapping public UserDTO addUser(UserDTO userDTO){ User user = userDTO.convertToUser(); User saveResultUser = userService.addUser(user); UserDTO result = userDTO.convertFor(saveResultUser); return result; }
当然,上述只是表明了转化方向的正向或逆向,很多业务需求的出参和入参的 DTO 对象是不同的,那么你需要更明显的告诉程序:逆向是无法调用的:
private static class UserDTOConvert extends Converter<UserDTO, User> { @Override protected User doForward(UserDTO userDTO) { User user = new User(); BeanUtils.copyProperties(userDTO,user); return user; } @Override protected UserDTO doBackward(User user) { throw new AssertionError(""不支持逆向转化方法!""); } }
看一下 doBackward 方法,直接抛出了一个断言异常,而不是业务异常,这段代码告诉代码的调用者,这个方法不是准你调用的,如果你调用,我就”断言”你调用错误了。
如果你认为我上边写的那个添加用户 API 写的已经非常完美了,那只能说明你还不是一个优秀的程序员。我们应该保证任何数据的入参到方法体内都是合法的。
很多人会告诉我,如果这些 API 是提供给前端进行调用的,前端都会进行验证啊,你为什还要验证?
其实答案是这样的,我从不相信任何调用我 API 或者方法的人,比如前端验证失败了,或者某些人通过一些特殊的渠道(比如 Charles 进行抓包),直接将数据传入到我的 API,那我仍然进行正常的业务逻辑处理,那么就有可能产生脏数据!
“对于脏数据的产生一定是致命”,这句话希望大家牢记在心,再小的脏数据也有可能让你找几个通宵!
hibernate 提供的 jsr 303 实现,我觉得目前仍然是很优秀的,具体如何使用,我不想讲,因为谷歌上你可以搜索出很多答案!
再以上班的 API 实例进行说明,我们现在对 DTO 数据进行检查:
public class UserDTO { @NotNull private String username; @NotNull private int age; //其他代码略}
API 验证:
@PostMapping public UserDTO addUser(@Valid UserDTO userDTO){ User user = userDTO.convertToUser(); User saveResultUser = userService.addUser(user); UserDTO result = userDTO.convertFor(saveResultUser); return result; } 我们需要将验证结果传给前端,这种异常应该转化为一个 api 异常(带有错误码的异常)。@PostMappingpublic UserDTO addUser(@Valid UserDTO userDTO, BindingResult bindingResult){ checkDTOParams(bindingResult); User user = userDTO.convertToUser(); User saveResultUser = userService.addUser(user); UserDTO result = userDTO.convertFor(saveResultUser); return result; }private void checkDTOParams(BindingResult bindingResult){ if(bindingResult.hasErrors()){ //throw new 带验证码的验证错误异常 } }
BindingResult 是 Spring MVC 验证 DTO 后的一个结果集,可以参考spring 官方文档(//spring.io/)。
上边的 DTO 代码,已经让我看的很累了,我相信读者也是一样,看到那么多的 Getter 和 Setter 方法,太烦躁了,那时候有什么方法可以简化这些呢。
请拥抱 lombok,它会帮助我们解决一些让我们很烦躁的问题
其实这个标题,我不太想说,因为网上太多,但是因为很多人告诉我,他们根本就不知道 lombok 的存在,所以为了让读者更好的学习,我愿意写这样一个例子:
@Setter@Getterpublic class UserDTO { @NotNull private String username; @NotNull private int age; public User convertToUser(){ UserDTOConvert userDTOConvert = new UserDTOConvert(); User convert = userDTOConvert.convert(this); return convert; } public UserDTO convertFor(User user){ UserDTOConvert userDTOConvert = new UserDTOConvert(); UserDTO convert = userDTOConvert.reverse().convert(user); return convert; } private static class UserDTOConvert extends Converter<UserDTO, User> { @Override protected User doForward(UserDTO userDTO) { User user = new User(); BeanUtils.copyProperties(userDTO,user); return user; } @Override protected UserDTO doBackward(User user) { throw new AssertionError(""不支持逆向转化方法!""); } } }
看到了吧,烦人的 Getter 和 Setter 方法已经去掉了。
但是上边的例子根本不足以体现 lombok 的强大。我希望写一些网上很难查到,或者很少人进行说明的 lombok 的使用以及在使用时程序语义上的说明。
比如:@Data,@AllArgsConstructor,@NoArgsConstructor..这些我就不进行一一说明了,请大家自行查询资料。
什么是链式风格?我来举个例子,看下面这个 Student 的 bean:
public class Student { private String name; private int age; public String getName() { return name; } public Student setName(String name) { this.name = name; return this; } public int getAge() { return age; } public Student setAge(int age) { return this; } }
仔细看一下 set 方法,这样的设置便是 chain 的 style,调用的时候,可以这样使用:
Student student = new Student() .setAge(24) .setName(""zs"");
相信合理使用这样的链式代码,会更多的程序带来很好的可读性,那看一下如果使用 lombok 进行改善呢,请使用 @Accessors(chain = true),看如下代码:
@Accessors(chain = true)@Setter@Getterpublic class Student { private String name; private int age; }
这样就完成了一个对于 bean 来讲很友好的链式操作。
静态构造方法的语义和简化程度真的高于直接去 new 一个对象。比如 new 一个 List 对象,过去的使用是这样的:
List<String> list = new ArrayList<>();
看一下 guava 中的创建方式:
List<String> list = Lists.newArrayList();
Lists 命名是一种约定(俗话说:约定优于配置),它是指 Lists 是 List 这个类的一个工具类,那么使用 List 的工具类去产生 List,这样的语义是不是要比直接 new 一个子类来的更直接一些呢,答案是肯定的,再比如如果有一个工具类叫做 Maps,那你是否想到了创建 Map 的方法呢:
HashMap<String, String> objectObjectHashMap = Maps.newHashMap();
好了,如果你理解了我说的语义,那么,你已经向成为 Java 程序员更近了一步了。
再回过头来看刚刚的 Student,很多时候,我们去写 Student 这个 bean 的时候,他会有一些必输字段,比如 Student 中的 name 字段,一般处理的方式是将 name 字段包装成一个构造方法,只有传入 name 这样的构造方法,才能创建一个 Student 对象。
接上上边的静态构造方法和必传参数的构造方法,使用 lombok 将更改成如下写法(@RequiredArgsConstructor 和 @NonNull):
@Accessors(chain = true)@Setter@Getter@RequiredArgsConstructor(staticName = ""ofName"")public class Student { @NonNull private String name; private int age; }
测试代码:
Student student = Student.ofName(""zs"");
这样构建出的 bean 语义是否要比直接 new 一个含参的构造方法(包含 name 的构造方法)要好很多。
当然,看过很多源码以后,我想相信将静态构造方法 ofName 换成 of 会先的更加简洁:
@Accessors(chain = true)@Setter@Getter@RequiredArgsConstructor(staticName = ""of"")public class Student { @NonNull private String name; private int age; }
测试代码:
Student student = Student.of(""zs"");
当然他仍然是支持链式调用的:
Student student = Student.of(""zs"").setAge(24);
这样来写代码,真的很简洁,并且可读性很强。
Builder 模式我不想再多解释了,读者可以看一下《Head First》(设计模式) 的建造者模式。
今天其实要说的是一种变种的 builder 模式,那就是构建 bean 的 builder 模式,其实主要的思想是带着大家一起看一下 lombok 给我们带来了什么。
看一下 Student 这个类的原始 builder 状态:
public class
您输入的评论内容中包含违禁敏感词
我知道了
请输入正确的手机号码
请输入正确的验证码
您今天的短信下发次数太多了,明天再试试吧!
我们会在第一时间安排职业规划师联系您!
您也可以联系我们的职业规划师咨询:
版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
沪公网安备 31011502005948号