Java开发实践标准

概要

本标准是本人在某Saas微服务系统的开发过程中积累而来的最佳实践,此系统及其相关项目遵守本标准。 亦可作为一般性独立项目的参考。

框架

Java 框架

Java语言每半年更新一个新的版本,2024年已经更新到了 JDK 22 ,虽然 Oracle 是收费的,但是仍有不少兼容的开源版本。比如OpenJDK,此后新项目使用 OpenJDK 17, 这是在各种操作系统中支持比较广泛的一个版本。

Spring 框架

目前 Spring 版本已经升到 3.x , 其对JPA中 对于 部分列对象的支持和 Json 字段的支持有较大变化,如果要使用 2.x 或者更早的方法,是不管用的。 这里推荐使用 3.2.1 ,因其对各种非标准操作,如Json列,自定义DTO对象,地理数据格式的支持更简单了。如果你要使用 Map 来搞定一切操作,那任何版本都没区别。

Javax与 Jakarta

从 2020年,JDK 9 开始,Javax 不再存在。由于某些历史和商业原因,Oracle 将 Javax 命名空间 托管给 Eclipse Foundation 来维护,而后者 将其重命名为 Jakarta,因此在所有相关的如 persistance(JPA)及 validation 相关的类,都应转移到 Jakarta 下。对于那些引用了 javax 的类库,基本都有其对应的 Jakarta 版本。 # 安全 对于一些临时性的中小型项目,不区分角色的登录,使用 Spring自带的 Security 组件就可以满足需要。 如果要求给不同角色分配菜单路径,并且控制其访问权限,那就适合使用 Shiro 框架。 Shiro能够控制每个接口的角色要求,如果用户角色不符合预期要求,则可以将其拦截在业务代码之外。

数据查询

基于 Spring 3.2.1(3.x) 的 JPA 与 2.5.6 (2.x) 有很大的区别,具体表达在: 1 Json列不需要在Entity上配置TypeDef注解,直接配置在属性上就能识别:

@JdbcTypeCode(SqlTypes.JSON )@Column(name = platform_roles, columnDefinition = json)private PlatformRoles platformRoles;

2 DTO 不再需要注解和复杂的配置,可以直接使用 HSQL(HyperSQL ,超级SQL语法) 支持:

@Query(value = SELECT new ShopApplyDto(u.id, u.name, u.shopName) FROM ShopApply u )
Page<shopapplydto> AllShopNames(Pageable pageable);

注意这里没有 nativeQuery = true 这样的声明,说明这是一条 HSQL 语法,由 JPA 框架解析,它可以识别 ShopApplyDto 类,前提是 ShopApplyDto 需要一个 有两个参数的构造函数。

3 复杂条件查询

复杂查询经常使用 and if(:id is not null or :id is not empty, id=:id, 1=1) 这样的查询,请注意,这是 nativeQuery = true 这样的声明,说明这不是一条 HSQL 语法,也就是它并不会被翻译成简化的 sql 语句,而是直接传输给数据库了。如果 条件过多,比如有十几二十几个and if 这样的条件,在慢查询诊断的时候是个非常复杂的问题,就连 explain 也无法给出结论。

因此,推荐使用动态拼接的方法:

if( id is not null) {  sql = sql +  and id = {id} }

动态拼接的结果是确定的 sql 语句,某个字段如果不参与查询,就不会出现在 sql 中,这有助于迅速定位慢查询的原因。因为查询条件虽然有很多种可能性,但使用拼接SQL,可以直接定义慢查询的原因可以是确定的某个条件,然后有针对性地处理就可以了。

注意:动态拼接SQL可能引起安全问题,一定要提交做好参数校验,并在防火墙中开启防SQL注入。如果无法使用防火墙,就一定要在参数校验中过滤掉像 and, select, insert 这样的关键字。

配置

1 禁止依赖项目根路径之外的文件。

当服务在机器之间迁移时,特别是当服务以容器方式运行时,会使用自动化脚本进行迁移操作。请记住,迁移操作将会变得非常普遍。并且每次重新部署,都将视为一次全新的安装,如果项目依赖根路径之外的文件,在迁移和部署时会出问题。当多实例部署时,位于绝对路径的依赖文件会相互冲突。

2 禁止使用直接字符串,而应使用常量中心代替。

常量中心是一个 类,其提供了所有字符串的常量型表达,如 const String MSG_OK = OK,使用常量表达的一个原因是,避免在代码中使用直接字符串的形式,因为字符串是无法使用工具混淆的,这很容易被反编译人员用作代码特征点来定位一段特定功能的代码。

相应地,将所有字符串集中在一起,并封装其内部实现,有利于灵活地从配置文件,或者从数据库中加载,降低代码泄漏的后果,并且如果有需要调整字符串的表达内容,只要更改一处,而不必到处搜索与替换。

接口

禁止返回Entity

禁止贪图方便,将Entity整个返回给用户。即便前端需要的数据包含了Entity的全部字段,也应将 Entity 转为 Vo 再返回给用户,因为有时对 entity 的操作,会直接同步到数据库,造成误修改。而使用 Vo 则可以放心地任意设置其值,而不必担心其与数据库的关系。

在设计返回数据内容时,应只返回目前事实上需要的数据,而不是”可能需要的数据“,要遵守”最小授权”的原则,而不是 ”为了以后方便“ 的原则。

接口形式

使用 RESTFULL 风格,GET:获取数据 , POST:创建数据 PUT:修改数据 DELETE:删除数据。RESTFULL 的特点有两个:1 充分利用HTTP谓词 ,来消减方法名 2 充分利用 url 路径参数,来消减参数名。结果是 RESTFULL 的 url 外观简洁优雅。

例如 User 对象, 获取数据,使用GET 方法,url 定义为: /user/{page}/{size} ,比如 /user/1/10,代表请求第1页,每页10条数据; 而不是 /user/get?page=1&size=10 如果要根据ID获取单个用户,应使用 /user/{id} ,比如 /user/3 ,这表示请求ID为3的数据, 而不是 /user/get?id=3

此表列出了推荐的和不推荐的常用接口url:

--- ---
接口说明 推荐的方法 不推荐的方法
请求列表 GET /user/{page}/{size}如:GET /user/1/10 GET /user/get?page=1&size=10
请求单个数据 GET /user/{id}如: GET /user/3 GET /user/get?id=3
提交数据 POST /user/ POST /user/add
修改数据 PUT /user/{id}如:PUT /user/3 POST /user/modify?id=3
删除数据 DELETE /user/{id}如:DELETE /user/3 GET /user/delete?id=3POST /user/delete?id=3

推荐写法:


@RequestMapping(user)
public class UserController {
    //GET列表
    @RequestMapping(value = {page}/{pageSize}, method = {RequestMethod.GET})
    public ApiPageResult<uservo> GetList(@PathVariable int page, @PathVariable int pageSize){
        //方法体
    }

    //GET单个对象
    @RequestMapping(value = {id}, method = {RequestMethod.GET})
    public ApiDataResult<uservo> Get(@PathVariable int id){
        //方法体
    }

    //新建对象
    @RequestMapping(value = /, method = {RequestMethod.POST}) 
    public ApiResult add(@Valid @RequestBody AddUserVo para){
        //方法体
    }

    //修改对象
    @RequestMapping(value = /, method = {RequestMethod.PUT}) 
    public ApiResult modify(@Valid @RequestBody ModifyUserVo para){
        //方法体
    }

    //删除对象
    @RequestMapping(value = /{id}, method = {RequestMethod.DELETE}) 
    public ApiResult delete(@PathVariable int id){
        //方法体
    }
}

参数验证

在以上的代码示例中,对于 POST或PUT 的参数,都有参数注解,@Valid 和 @RequestBody ,其中 @Valid 指示此参数需要进行验证,而 @RequestBody 指示此参数来源是请求体。

public class AddUserVo {

    private String loginName;

    private String password;

    @JsonProperty(name)
    @NotBlank(message = 请输入姓名)
    private String nick;

    @NotBlank(message = 必须指定角色)
    private String role;

    @NotBlank(message = 手机号不能为空)
    private String mobile;

    private String platform;

    private List<string> platforms;
}

以 AddUserVo 为例,@NotBlank 指示字符串不能为空,@JsonProperty 指示,其取自 json 的字段与代码中定义的定义不同。其他验证约束:@NotNull 指示此字段必传 @Max @Min 指示数字的最大最小值。

如果使用 get 参数,也可以使用 @Valid + @NotXXX 组合的方式,对单个get参数验证:

    //GET业务方法
    @RequestMapping(value = dosomething, method = {RequestMethod.GET})
    public ApiBaseResult<vo> DoSomething(@Valid @NotEmpty String para1, @@Valid @NotEmpty String para2){
        //方法体
    }

参数验证的生效条件是使用 GlobalExceptionHandler ,全局异常处理,对于 @Valid,抛出的异常是 ConstraintViolationException, 对于 @Valided 抛出的异常是 MethodArgumentNotValidException;如未在全局范围捕获这两个异常,验证不管用。

验证示例如下:

@RestControllerAdvice@Priority(1)public class GlobalExceptionHandler {  @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<apiresultbase> handleMethodArgumentNotValidException(MethodArgumentNotValidException e){

        log.error(ThrowableUtil.getStackTrace(e)); // 打印堆栈        StringBuffer msg = new StringBuffer();
            msg.append(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
            ApiResultBase apiError = new ApiResultBase().fail(msg.toString());
            return buildResponseEntity(apiError, OK.value());
    }
    private ResponseEntity<apiresultbase> buildResponseEntity(ApiResultBase apiError, int responseStatus) {
        return new ResponseEntity(apiError, valueOf(responseStatus));
    }
    }

GlobalExceptionHandler 说明

GlobalExceptionHandler 是参数验证实现的重要环节,其原理是处理在系统中未被捕获的异常,返回自定义消息,提示调用者发生了什么问题,让调用者去排查问题。如果不使用全局异常自定义的话,就会报 500 错误,后端去查服务器日志。

因此,将所有可以预期发生的异常写入 GlobalExceptionHandler 中处理,一是避免在业务中处理,破坏业务逻辑,也可以减轻一些业务开发工作量。

返回格式

数据的返回格式一般有三种:

1 什么也不返回 2 返回一个实体 3 返回一个列表(table) 4 返回一个对象和列表

针对第1种情况,对应的类型是 ApiResultBase,返回内容应是:

{   status: 0,   message: OK}

如果返回一个对象, 对应的返回类型是 ApiDataResult ,返回内容是:

{   status: 0,   message: OK,   data: {对象内容}}

返回列表, 对应的返回类型是 ApiListResult ,返回内容是:

{   status: 0,   message: OK,   list: []}

返回对象和列表, 对应的返回类型是 ApiDataListResult ,返回内容是:

{   status: 0,   message: OK,   data: {},   list: []}

ApiDataListResult 的情形非常少见,基本不会用到,这是为了兼容旧版本而保留的一个类。因为旧版本并不区分 data 与 list ,直接调用 setData(xx) 来设置,如果参数是 object 则设到 data 上,如果 是 list 则设置到 list 上,这种方法相当地随心所欲。

返回的数据类型一定要明确,要保证可预期性。如果只需要返回 Data 或 List 中的一种,不可使用 ApiDataListResult 作为默认类型,因为这样会让调用者搞不清返回的数据格式。

ApiPageResult 在形式上是 ApiDataResutl的一个实现,而不是 ApiDataListResult 。因为 其 Data 是一个 Page 对象,因为这些属性: total, page, pageSize, list 是平级的。

{
  status: 0,
  message: OK,
  data: {
    total: 100,
    page: 1,
    pageSize: 10,
    list: [{}, {}]  }}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇