Java 单体服务开发指南(下)
5、分环境配置
类型
dev
test
uat
prod
环境定义:
public class EnvConstant { public static final String ENV_DEV = "dev"; public static final String ENV_TEST = "test"; public static final String ENV_UAT = "uat"; // similar to staging public static final String ENV_PROD = "prod"; }
环境配置:
// environment related configuration @Data @Builder public class EnvConfig { private String name; private boolean debug; private String externalApex; private String internalApex; private String scheme; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private static Map
开发测试环境禁用 Sentry 异常日志:
@Aspect @Slf4j public class SentryClientAspect { @Autowired EnvConfig envConfig; @Around("execution(* io.sentry.SentryClient.send*(..))") public void around(ProceedingJoinPoint joinPoint) throws Throwable { // no sentry logging in debug mode if (envConfig.isDebug()) { log.debug("no sentry logging in debug mode"); return; } joinPoint.proceed(); } }
Sentry 是统一的异常管理平台,支持异常事件的收集、展示、告警等功能。
6、异步调用处理
ThreadPoolTaskExecutor:
AsyncExecutor 配置:
Configuration @EnableAsync @Import(value = {StaffjoyRestConfig.class}) @SuppressWarnings(value = "Duplicates") public class AppConfig { public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor"; @Bean(name=ASYNC_EXECUTOR_NAME) public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setTaskDecorator(new ContextCopyingDecorator()); executor.setCorePoolSize(3); executor.setMaxPoolSize(5); executor.setQueueCapacity(100); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setThreadNamePrefix("AsyncThread-"); executor.initialize(); return executor; } }
Async 标注:
@Async(AppConfig.ASYNC_EXECUTOR_NAME) public void trackEventAsync(String userId, String eventName) { if (envConfig.isDebug()) { logger.debug("intercom disabled in dev & test environment"); return; } Event event = new Event() .setUserID(userId) .setEventName("v2_" + eventName) .setCreatedAt(Instant.now().toEpochMilli()); try { Event.create(event); } catch (Exception ex) { String errMsg = "fail to create event on Intercom"; handleException(logger, ex, errMsg); throw new ServiceException(errMsg, ex); } logger.debug("updated intercom"); }
线程上下文拷贝:
// https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executor public class ContextCopyingDecorator implements TaskDecorator { @Override public Runnable decorate(Runnable runnable) { RequestAttributes context = RequestContextHolder.currentRequestAttributes(); return () -> { try { RequestContextHolder.setRequestAttributes(context); runnable.run(); } finally { RequestContextHolder.resetRequestAttributes(); } }; } }
@Configuration @EnableAsync @Import(value = {StaffjoyRestConfig.class}) @SuppressWarnings(value = "Duplicates") public class AppConfig { public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor"; @Bean(name=ASYNC_EXECUTOR_NAME) public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // for passing in request scope context executor.setTaskDecorator(new ContextCopyingDecorator()); executor.setCorePoolSize(3); executor.setMaxPoolSize(5); executor.setQueueCapacity(100); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setThreadNamePrefix("AsyncThread-"); executor.initialize(); return executor; } }
7、Swagger 配置
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() //为当前包下controller生成API文档 .apis(RequestHandlerSelectors.basePackage("com.7d.PmsBrand.controller")) .paths(PathSelectors.any()) .build() .apiInfo(apiEndPointsInfo()) .useDefaultResponseMessages(false); } private ApiInfo apiEndPointsInfo() { return new ApiInfoBuilder().title("PmsBrand REST API") .description("7d Account REST API") .contact(new Contact("7d", "https://zuozewei.blog.csdn.net", "zuozewei@hotmail.com")) .license("The MIT License") .licenseUrl("https://opensource.org/licenses/MIT") .version("V2") .build(); } }
给 Controller 添加 Swagger 注解:
/** * 品牌管理Controller */ @Api(tags = "PmsBrandController", description = "商品品牌管理") @Controller @RequestMapping("/brand") public class PmsBrandController { @Autowired private PmsBrandService brandService; private static final Logger LOGGER = LoggerFactory.getLogger(PmsBrandController.class); @ApiOperation("获取所有品牌列表") @RequestMapping(value = "listAll", method = RequestMethod.GET) @ResponseBody public CommonResult> getBrandList() { return CommonResult.success(brandService.listAllBrand()); } @ApiOperation("添加品牌") @RequestMapping(value = "/create", method = RequestMethod.POST) @ResponseBody public CommonResult createBrand(@RequestBody PmsBrand pmsBrand) { CommonResult commonResult; int count = brandService.createBrand(pmsBrand); if (count == 1) { commonResult = CommonResult.success(pmsBrand); LOGGER.debug("createBrand success:{}", pmsBrand); } else { commonResult = CommonResult.failed("操作失败"); LOGGER.debug("createBrand failed:{}", pmsBrand); } return commonResult; } @ApiOperation("更新指定id品牌信息") @RequestMapping(value = "/update/{id}", method = RequestMethod.POST) @ResponseBody public CommonResult updateBrand(@PathVariable("id") Long id, @RequestBody PmsBrand pmsBrandDto, BindingResult result) { CommonResult commonResult; int count = brandService.updateBrand(id, pmsBrandDto); if (count == 1) { commonResult = CommonResult.success(pmsBrandDto); LOGGER.debug("updateBrand success:{}", pmsBrandDto); } else { commonResult = CommonResult.failed("操作失败"); LOGGER.debug("updateBrand failed:{}", pmsBrandDto); } return commonResult; } @ApiOperation("删除指定id的品牌") @RequestMapping(value = "/delete/{id}", method = RequestMethod.GET) @ResponseBody public CommonResult deleteBrand(@PathVariable("id") Long id) { int count = brandService.deleteBrand(id); if (count == 1) { LOGGER.debug("deleteBrand success :id={}", id); return CommonResult.success(null); } else { LOGGER.debug("deleteBrand failed :id={}", id); return CommonResult.failed("操作失败"); } } @ApiOperation("分页查询品牌列表") @RequestMapping(value = "/list", method = RequestMethod.GET) @ResponseBody public CommonResult
修改 MyBatis Generator 注释的生成规则:
CommentGenerator为MyBatis Generator的自定义注释生成器,修改addFieldComment方法使其生成Swagger的@ApiModelProperty注解来取代原来的方法注释,添加addJavaFileComment方法,使其能在import中导入@ApiModelProperty,否则需要手动导入该类,在需要生成大量实体类时,是一件非常麻烦的事。
/** * 自定义注释生成器 */ public class CommentGenerator extends DefaultCommentGenerator { private boolean addRemarkComments = false; private static final String EXAMPLE_SUFFIX="Example"; private static final String API_MODEL_PROPERTY_FULL_CLASS_NAME="io.swagger.annotations.ApiModelProperty"; /** * 设置用户配置的参数 */ @Override public void addConfigurationProperties(Properties properties) { super.addConfigurationProperties(properties); this.addRemarkComments = StringUtility.isTrue(properties.getProperty("addRemarkComments")); } /** * 给字段添加注释 */ @Override public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) { String remarks = introspectedColumn.getRemarks(); //根据参数和备注信息判断是否添加备注信息 if(addRemarkComments&&StringUtility.stringHasValue(remarks)){ // addFieldJavaDoc(field, remarks); //数据库中特殊字符需要转义 if(remarks.contains("\"")){ remarks = remarks.replace("\"","'"); } //给model的字段添加swagger注解 field.addJavaDocLine("@ApiModelProperty(value = \""+remarks+"\")"); } } /** * 给model的字段添加注释 */ private void addFieldJavaDoc(Field field, String remarks) { //文档注释开始 field.addJavaDocLine("/**"); //获取数据库字段的备注信息 String[] remarkLines = remarks.split(System.getProperty("line.separator")); for(String remarkLine:remarkLines){ field.addJavaDocLine(" * "+remarkLine); } addJavadocTag(field, false); field.addJavaDocLine(" */"); } @Override public void addJavaFileComment(CompilationUnit compilationUnit) { super.addJavaFileComment(compilationUnit); //只在model中添加swagger注解类的导入 if(!compilationUnit.isJavaInterface()&&!compilationUnit.getType().getFullyQualifiedName().contains(EXAMPLE_SUFFIX)){ compilationUnit.addImportedType(new FullyQualifiedJavaType(API_MODEL_PROPERTY_FULL_CLASS_NAME)); } } }
运行代码生成器重新生成 mbg 包中的代码:
运行com.7d.mall.tiny.mbg.Generator 的 main方法,重新生成 mbg 中的代码,可以看到 PmsBrand 类中已经自动根据数据库注释添加了@ApiModelProperty注解
8、前后端分离跨域
CORS全称Cross-Origin Resource Sharing,意为跨域资源共享。当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求。如果此时另一个资源不允许其进行跨域资源访问,那么访问的那个资源就会遇到跨域问题。
覆盖默认的CorsFilter
添加GlobalCorsConfig配置文件来允许跨域访问。
设置 SpringSecurity 允许 OPTIONS 请求访问
在SecurityConfig类的configure(HttpSecurity httpSecurity) 方法中添加如下代码。
.antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求 .permitAll()
9、统一访问日志记录
AOP 通过在 controller 层建一个切面来实现接口访问的统一日志记录。
添加日志信息封装类 WebLog
用于封装需要记录的日志信息,包括操作的描述、时间、消耗时间、url、请求参数和返回结果等信息。
/** * Controller层的日志封装类 */ public class WebLog { /** * 操作描述 */ private String description; /** * 操作用户 */ private String username; /** * 操作时间 */ private Long startTime; /** * 消耗时间 */ private Integer spendTime; /** * 根路径 */ private String basePath; /** * URI */ private String uri; /** * URL */ private String url; /** * 请求类型 */ private String method; /** * IP地址 */ private String ip; /** * 请求参数 */ private Object parameter; /** * 请求返回的结果 */ private Object result; //省略了getter,setter方法 }
添加切面类 WebLogAspect:
定义了一个日志切面,在环绕通知中获取日志需要的信息,并应用到controller层中所有的public方法中去。
/** * 统一日志处理切面 */ @Aspect @Component @Order(1) public class WebLogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class); @Pointcut("execution(public * com.dunsan.mall.tiny.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { } @AfterReturning(value = "webLog()", returning = "ret") public void doAfterReturning(Object ret) throws Throwable { } @Around("webLog()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); //获取当前请求对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //记录请求信息 WebLog webLog = new WebLog(); Object result = joinPoint.proceed(); Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method.isAnnotationPresent(ApiOperation.class)) { ApiOperation apiOperation = method.getAnnotation(ApiOperation.class); webLog.setDescription(apiOperation.value()); } long endTime = System.currentTimeMillis(); String urlStr = request.getRequestURL().toString(); webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath())); webLog.setIp(request.getRemoteUser()); webLog.setMethod(request.getMethod()); webLog.setParameter(getParameter(method, joinPoint.getArgs())); webLog.setResult(result); webLog.setSpendTime((int) (endTime - startTime)); webLog.setStartTime(startTime); webLog.setUri(request.getRequestURI()); webLog.setUrl(request.getRequestURL().toString()); LOGGER.info("{}", JSONUtil.parse(webLog)); return result; } /** * 根据方法和传入的参数获取请求参数 */ private Object getParameter(Method method, Object[] args) { List
10、打包方式
jar
docker
服务配置文件处理方式:
对于各个项目分环境部署,最麻烦的就是配置文件的问题,不同的环境需要加载不同的配置,好在 Spring Boot 框架加载配置是非常方便的,我们可以针对不同的环境分别配置不同的配置文件,这里有两个地方要注意一下:
构建镜像的时候,尽量实现一个镜像支持所有环境(即所有配置都打到一个镜像里面去),在容器启动时指定加载哪个环境配置即可,例如:在部署容器时指定 args: ["–spring.profiles.active=prod"] 参数启动。
尽量不要每个环境打出来一个镜像版本,传统方式在构建的时候指定 -D prod 配置 Profile 来指定加载哪个配置,来生成不同的产物 jar,容器化部署后不需要这样,那样后期控制各镜像版本发布会比较麻烦。
镜像可以分为基础镜像和应用镜像:
基础镜像要求体积尽量小,方便拉取,同时安装一些必要的软件,方便后期进入容器内排查问题,我们需要准备好服务运行的底层系统镜像,比如 Centos、Ubuntu 等常见 Linux 操作系统,然后基于该系统镜像,构建服务运行需要的环境镜像,比如一些常见组合:Centos + Jdk、Centos + Jdk + Tomcat、Centos + nginx 等,由于不同的服务运行依赖的环境版本不一定一致,所以还需要制作不同版本的环境镜像,例如如下基础镜像版本。
Centos6.5 + Jdk1.8: registry.docker.com/baseimg/centos-jdk:6.5_1.8
Centos7.5 + Jdk1.8: registry.docker.com/baseimg/centos-jdk:7.5_1.8
Centos7.5 + Jdk1.7: registry.docker.com/baseimg/centos-jdk:7.5_1.7
Centos7 + Tomcat8 + Jdk1.8: registry.docker.com/baseimg/centos-tomcat-jdk:7.5_8.5_1.8
Centos7 + Nginx: registry.docker.com/baseimg/centos-tomcat-jdk:7.5_1.10.2
…
这样,就可以标识该基础镜像的系统版本及软件版本,方便后边选择对应的基础镜像来构建应用镜像
有了上边的基础镜像后,就很容易构建出对应的应用镜像了,例如一个简单的应用镜像 Dockerfile 如下:
FROM registry.docker.com/baseimg/centos-jdk:7.5_1.8 COPY app-name.jar /opt/project/app.jar EXPOSE 8080 ENTRYPOINT ["/java", "-jar", "/opt/project/app.jar"]
当然,这里我建议使用另一种方式来启动服务,将启动命令放在统一 shell 启动脚本执行,例如如下Dockerfile 示例:
FROM registry.docker.com/baseimg/centos-jdk:7.5_1.8 COPY app-name.jar /opt/project/app.jar COPY entrypoint.sh /opt/project/entrypoint.sh EXPOSE 8080 ENTRYPOINT ["/bin/sh", "/opt/project/entrypoint.sh"]
将服务启动命令配置到 entrypoint.sh,这样我们可以扩展做很多事情,比如启动服务前做一些初始化操作等,还可以向容器传递参数到脚本执行一些特殊操作,而且这里变成脚本来启动,这样后续构建镜像基本不需要改 Dockerfile 了。
#!/bin/bash # do other things here java -jar $JAVA_OPTS /opt/project/app.jar $1 > /dev/null 2>&1
上边示例中,我们就注入 $JAVA_OPTS 环境变量,来优化 JVM 参数,还可以传递一个变量,这个变量大家应该就猜到了,就是服务启动加载哪个配置文件参数,例如:–spring.profiles.active=prod
十、技术选型(参考)
1、代码生成工具
MyBatis Generator
MyBatis Generator是 MyBatis 的代码生成器,支持为 MyBatis 的所有版本生成代码。非常容易及快速生成 Mybatis 的Java POJO文件及数据库 Mapping 文件。
2、核心框架
SpringBoot 2.x
SpringBoot 它使用“习惯优于配置”(项目中存在大量的配置,此外还内置一个习惯性的配置,让你无须手动进行配置)的理念让 Java 项目快速运行起来。使用 SpringBoot 很容易创建一个独立运行(运行 Jar ,内嵌 Servlet 容器)、准生产级别的基于 Spring 的框架项目,使用 SpringBoot 你可以不用或者只需要很少的 Spring 配置。
用白话来理解,就是 SpringBoot 其实不是什么新框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,SpringBoot 整合了几乎所有的框架。
官网:https://spring.io/projects/spring-boot
3、日志框架
Logback
LogBack 是 Log4j 的改良版本,比 Log4j 拥有更多的特性,同时也带来很大性能提升,同时天然支持SLF4J。
LogBack 官方建议配合 Slf4j 使用,这样可以灵活地替换底层日志框架。
官网:http://logback.qos.ch/
4、持久层框架
Mybatis 3
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
5、连接池
阿里 druid
Druid 是一个关系型数据库连接池,它是阿里巴巴的一个开源项目。Druid 支持所有 JDBC 兼容数据库,包括了Oracle、MySQL、PostgreSQL、SQL Server、H2等。
Druid 在监控、可扩展性、稳定性和性能方面具有明显的优势。通过 Druid 提供的监控功能,可以实时观察数据库连接池和SQL查询的工作情况。使用 Druid 连接池在一定程度上可以提高数据访问效率。
官网:https://druid.apache.org/
6、SQL拦截工具
P6Spy
p6spy 是一个开源项目,通常使用它来跟踪数据库操作,查看程序运行过程中执行的sql语句。
官网:https://github.com/p6spy/p6spy
7、多数据源启动器
dynamic-datasource-spring-boot-starter
dynamic-datasource-spring-boot-starter 是一个基于 springboot 的快速集成多数据源的启动器。
其支持 Jdk 1.7+, SpringBoot 1.4.x、1.5.x、 2.0.x。
官网:https://github.com/baomidou/dynamic-datasource-spring-boot-starter
8、分页插件
MyBatis PageHelper
MyBatis PageHelper 实现了通用的分页查询,其支持的数据有,mysql、Oracle、DB2、PostgreSQL等主流的数据库。
github: https://github.com/pagehelper/Mybatis-PageHelper
PageHelper.startPage(pageNum, pageSize); //之后进行查询操作将自动进行分页 List
9、API文档
swagger2.0
Swagger是一款Restful 接口的文档在线自动生成、功能测试框架。一个规范和完整的框架,用于生成、描述、调用和可视化Restful 风格的Web服务,加上Swagger-UI,可以有很好的呈现。
十一、开发环境(推荐)
1、开发插件
Lombok
Lombok 项目是一个 Java 库,它会自动插入您的编辑器和构建工具中,从而使您的Java更加生动有趣。
永远不要再写另一个 getter 或 equals 方法,带有一个注释的您的类有一个功能全面的生成器,自动化您的日志记录变量等等。
官网:https://projectlombok.org/
Hutool
Hutool 是一个小而全的Java工具类库,它帮助我们简化每一行代码,避免重复造轮子。如果你有需要用到某些工具类的时候,不妨在 Hutool 里面找找。
官网:https://www.hutool.cn/
2、JDK
SUN JDK1.8及以上
3、构建工具
Maven 3.5.4及以上
Maven 作为一个构建工具,不仅能帮我们自动化构建,还能够抽象构建过程,提供构建任务实现;它跨平台,对外提供了一致的操作接口,这一切足以使它成为优秀的、流行的构建工具。
Maven 不仅是构建工具,还是一个依赖管理工具和项目管理工具,它提供了中央仓库,能帮助我们自动下载构件。
官网:https://maven.apache.org/
4、Git 不限
5、数据库
MySQL 5.7及以上
Navicat Premium 11.2.7及以上
MySQL是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。
MySQL是一种关系数据库管理系统,关系数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
MySQL所使用的 SQL 语言是用于访问数据库的最常用标准化语言。MySQL 软件采用了双授权政策,分为社区版和商业版,由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,一般中小型网站的开发都选择 MySQL 作为网站数据库。
----- 摘抄自百度百科
官网:https://www.mysql.com/
6、IDE
IntelliJ IDEA 2020.1
推荐插件:
Free MyBatis plugin:对MyBatis的xml具有强大的提示功能,同时可以关联mapper接口和mapper.xml中的sql实现。
Lombok plugin:Lombok为Java语言添加了非常有趣的附加功能,你可以不用再为实体类手写getter,setter等方法,通过一个注解即可拥有。
MyBatis Log Plugin:把Mybatis输出的SQL日志还原成完整的SQL语句。
RestfulToolkit:一套Restful服务开发辅助工具集,提供了项目中的接口概览信息,可以根据URL跳转到对应的接口方法中去,内置了HTTP请求工具,对请求方法做了一些增强功能。
GsonFormat:这款插件可以把JSON格式的字符串转化为实体类,当我们要根据JSON字符串来创建实体类的时候用起来很方便。
Grep Console:一款帮你分析控制台日志的插件,可以对不同级别的日志进行不同颜色的高亮显示,还可以用来按关键字搜索日志内容。
Alibaba Java Coding Guidelines:阿里巴巴《Java 开发手册》配套插件,可以实时检测代码中不符合手册规约的地方,助你码出高效,码出质量。
Maven Helper:解决Maven依赖冲突的好帮手,可以快速查找项目中的依赖冲突,并予以解决
Statistic:一款代码统计工具,可以用来统计当前项目中代码的行数和大小。
Vue.js:Vue.js支持插件,可以根据模板创建.vue文件,也可以对Vue相关代码进行智能提示。
element:Element-UI支持插件,可以对Element-UI中的标签进行智能提示,有了它就不用盲写相关代码了!
7、其他工具
Postman:API接口调试工具。
https://www.postman.com/
PowerDesigner:数据库设计工具,平时用来设计数据库表,设计完成之后可以直接导出数据库表
RedisDesktop:Redis可视化工具,平时用来查看和管理Redis缓存中的数据,有时候需要清空缓存的时候就用到它了。
https://rdm.dev/
Robomongo:MongoDB可视化工具,平时用来查看和管理MongoDB中的数据。
https://robomongo.org/download
X-shell:一款强大的安全终端模拟软件,可以用来连接和管理远程linux服务器。-
https://www.netsarang.com/zh/all-downloads/
ProcessOn:作图工具,可以用来制作思维导图和流程图。
processon.com
Snipaste:一款好用的截屏工具。
www.snipaste.com
十二、代码提交规范
1、基本原则
Git 代码完整提交正确姿势,建议先 Commit,再 Pull,最后 Push;
代码提交前,保证本地编译通过;
代码提交时,保证代码、文件完整提交,不要把本地测试代码、配置提交上去了;
代码每次独立的功能、模块修改,都 Commit 到本地(不急每次都 Push);
创建本地开发分支,完成后合并到特性分支,特性分支不可 push。
2、提交注释规则
格式:[type: description] #[相关任务编号]
2.1、type
fix: 修复bug
add: 新功能
update: 更新
style : 代码格式改变
test: 增加测试代码
revert: 撤销上一次的commit
build: 构建工具或构建过程等的变动,如:gulp 换成了 webpack,webpack 升级等
2.2、description
description 是对本次提交的简短描述;
不超过50个字符;
推荐以动词开头,如: 设置、修改、增加、删减、撤销等。
3、示例
fix:修复登录正确提示不准确缺陷 #demo-1243 add:添加登录拦截校验功能 #demo-1240 update:删除登陆弹出框提示 #demo-1241 test:增加控制接口测试用例 #demo-1242
关联任务单/缺陷单编号,例如:“demo-124”;
《java开发手册》v1.7.0 嵩山版:
https://github.com/zuozewei/blog-example/blob/master/Java-api-test/12-Alibaba Java Coding Guidelines/《java开发手册》v1.7.0 嵩山版.pdf
参考资料:
[1]:《java开发手册》v1.7.0 嵩山版》
[2]:《Spring Boot & Kubernetes 云原生微服务实践》
Java
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。