首页 文章 精选 留言 我的

精选列表

搜索[SpringBoot],共4203篇文章
优秀的个人博客,低调大师

SpringBoot分布式 - SpringCloud

一:介绍 Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中涉及的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。 微服务(Microservices Architecture)是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每个任务代表着一个小的业务能力。 Spring Cloud 中文文档 https://springcloud.cc/spring-cloud-dalston.html Spring Cloud 官方文档 http://projects.spring.io/spring-cloud/#quick-start SpringCloud 教程PDF下载:https://download.csdn.net/download/yueshutong123/10501017 二:入门 在IDEA新建空白工程 1. 注册中心 在工程下新建模块eureka-server 1)导包 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> 2 )application.yml配置Eureka信息 server: port: 8761 eureka: instance: hostname: eureka-server #eureka实例的主机名 client: register-with-eureka: false #不把自己注册到eureka fetch-registry: false #不从eureka上获取服务的注册信息 service-url: defaultZone: http://localhost:8761/eureka/ 3)开启@EnableEurekaServer注解 package cn.zyzpp.eurekaserver; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * 开启@EnableEurekaServer注解 */ @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } } 4)开启注册中心 启动该应用,访问http://localhost:8761/ 进入Spring Eureka页面即成功。 2.服务提供者 新建模块:provider-ticket 1)导入依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> 2)application.yml配置Eureka信息 server: port: 8001 #8002 spring: application: name: provider-ticket eureka: instance: prefer-ip-address: true #注册服务的时候使用服务的Ip地址 client: service-url: defaultZone: http://localhost:8761/eureka/ 3)Service层方法 package cn.zyzpp.providerticket.service; import org.springframework.stereotype.Service; /** * Create by yster@foxmail.com 2018/6/4/004 18:37 */ @Service public class TicketService { public String getTicket(){ return "《厉害了,我的国》"; } } 4)暴露HTTP接口 /** * Create by yster@foxmail.com 2018/6/4/004 18:39 */ @RestController public class TicketControllert { //轻量级HTTP @Autowired private TicketService ticketService; @GetMapping("/ticket") public String getTicket(){ System.out.println("8001"); //8002 return ticketService.getTicket(); } } 5)然后更改端口,分别打包该模块为jar包。运行。 3.服务消费者 新建模块consumer-user 1)application.yml配置Eureka信息 server: port: 8200 spring: application: name: consumer-user eureka: instance: prefer-ip-address: true #注册服务的时候使用服务的Ip地址 client: service-url: defaultZone: http://localhost:8761/eureka/ 2)开启发现服务的功能,使用负载均衡机制(默认轮询) import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableDiscoveryClient /*开启发现服务功能*/ @SpringBootApplication public class ConsumerUserApplication { public static void main(String[] args) { SpringApplication.run(ConsumerUserApplication.class, args); } @LoadBalanced /*开启负载均衡机制*/ @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } } 3)获取服务 /** * Create by yster@foxmail.com 2018/6/4/004 19:13 */ @RestController public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping("/buy") public String getTicket(){ String s = restTemplate.getForObject("http://PROVIDER-TICKET/ticket",String.class); return "购买了 "+s; } } 启动服务消费者模块。查看服务提供者的控制台打印。 会发现第一次请求8001,第二次8002,8001,8002,,这是因为使用了负载均衡。 本文只讲解了服务的注册与发现,Spring cloud的更多内容推荐阅读:SpringCloud分布式教程

优秀的个人博客,低调大师

SpringBoot(二)_项目属性配置

修改端口 在main/resources/application.properties修改端口 server.port=8088 此时启动访问localhost:8088/hello 就会看到 Hello Spring Boot! 使用yml文件替换properties 文件 (1)在main/resources 文件下新建一个application.yml 文件 (2)在yml文件中修改端口 server: port: 8099 (3) 删除掉application.properties文件,只保留yml文件 (4) 运行程序,此时访问8099端口即可 获取配置文件的值 (1) 在application.yml 文件中,编写上其他内容 server: port: 8099 name: maomao age: 18 (2) 利用@Value 注解 @RestController public class HelloController { @Value("${name}") private String name; @Value("${age}") private int age; @RequestMapping(value = {"/hello"},method = RequestMethod.GET) public String say(){ return name+age; } } (3)访问8099端口,就获取到值 maomao 18 使用自定义配置类 如果属性很多,我们每个属性都需要写,显得有些费事,我们可以利用自定义配置类进行获取 (1) 修改yml 文件 server: port: 8099 girl: name: maomao age: 18 (2) 创建properties/GirlProperties.java @Component @ConfigurationProperties(prefix = "girl") public class GirlProperties { private String name; private int age; //get和set方法省略 } (3) 我在使用@ConfigurationProperties 这个注解时,提示找不到class,需在pom文件中引入 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> (4) 修改controller文件 @RestController public class HelloController { @Resource private GirlProperties girlProperties; @RequestMapping(value = {"/hello"},method = RequestMethod.GET) public String say(){ return girlProperties.getName(); } } (5)验证结果 maomao 开发环境和生成环境配置不同的问题 这个问题经常见,比如我们开发环境 name 是maomao ,生成环境是 毛毛,我们大部分都是修改配置文件,但是这样还是很麻烦。 (1) 复制2个yml文件,分别是application-dev.yml (开发环境) application-prod.yml(生产环境) (2) 修改application-prod.yml(生产环境)文件 server: port: 8088 girl: name: 毛毛 age: 18 (3) application-dev.yml (开发环境)文件内容 server: port: 8099 girl: name: maomao age: 18 (4)application.yml文件内容,这个就代表使用dev的配置文件 spring: profiles: active: dev (5)上篇文章讲过java -jar 的启动方式 先执行 mvn install 在执行启动 java -jar girl-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod (6)此时就是访问的prod 的配置8088,(注意我们配置的application.yml 中用的是dev 这个配置文件,但是我们启动的时候加上后面的参数就自动切换到 prod 文件上) 总结 在使用yml进行配置更加简单方便,使用java -jar 启动 加上参数,就可以避免我们来回修改配置文件,有漏掉的情况。 源码下载:github 学习不是要么0分,要么100分的。80分是收获;60分是收获;20分也是收获。有收获最重要。但是因为着眼于自己的不完美,最终放弃了,那就是彻底的0分了。

优秀的个人博客,低调大师

SpringBoot 手写拦截器

如何手写一个拦截器呢。假设我现在需要一个计时拦截器,我想把每一次调用服务锁花费的时间打印到控制台,我该怎么做呢? 拦截机制有三种: 1. 过滤器(Filter)能拿到http请求,但是拿不到处理请求方法的信息。 2. 拦截器(Interceptor)既能拿到http请求信息,也能拿到处理请求方法的信息,但是拿不到方法的参数信息。 3. 切片(Aspect)能拿到方法的参数信息,但是拿不到http请求信息。 他们三个各有优缺点,需要根据自己的业务需求来选择最适合的拦截机制。 拦截机制图 好了下面开始正文。 手写拦截器实战 /** * Time 时间拦截器(比时间过滤器准)) * Created by Fant.J. */ @Component public class TimeInterceptor implements HandlerInterceptor { //controller 调用之前被调用 @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception { System.out.println("preHandle"); System.out.println(((HandlerMethod)handler).getBean().getClass().getName()); System.out.println(((HandlerMethod)handler).getMethod().getName()); httpServletRequest.setAttribute("startTime",System.currentTimeMillis()); return true; } //controller 调用之后被调用,如果有异常则不调用 @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { System.out.println("postHandle"); long startTime = (long) httpServletRequest.getAttribute("startTime"); System.out.println("时间拦截器耗时:"+(System.currentTimeMillis() -startTime)); } //controller 调用之后被调用,有没有异常都会被调用,Exception 参数里放着异常信息 @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { System.out.println("afterCompletion"); long startTime = (long) httpServletRequest.getAttribute("startTime"); System.out.println("时间拦截器耗时:"+(System.currentTimeMillis() -startTime)); } } 代码解释: 其中,preHandle()方法再controller 调用之前被调用 postHandle()在controller 调用之后被调用,如果有异常则不调用 afterCompletion()在controller 调用之后被调用,有没有异常都会被调用,Exception 参数里放着异常信息。 但是值写这个处理拦截器还不行,还需要进一步的配置,请看下面一段代码: /** * 引入第三方过滤器 将其放入spring容器 * Created by Fant.J. */ @Configuration public class WebConfig extends WebMvcConfigurerAdapter{ // 注入Time 拦截器 @Autowired private TimeInterceptor timeInterceptor; //添加拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //往拦截器注册器里添加拦截器 registry.addInterceptor(timeInterceptor); } 首先我们继承WebMvcConfigurerAdapter类,重写它的addInterceptors()方法,该方法是添加拦截器至Spring容器中。 然后调用拦截器注册器InterceptorRegistry 进行注册。 介绍下我的所有文集: 流行框架 SpringCloudspringbootnginxredis 底层实现原理: Java NIO教程Java reflection 反射详解Java并发学习笔录Java Servlet教程jdbc组件详解Java NIO教程Java语言/版本 研究

优秀的个人博客,低调大师

SpringBoot学习之集成dubbo

一、摘自官网的一段描述 1.背景 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。 单一应用架构 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。 垂直应用架构 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。 分布式服务架构 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。 流动计算架构 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。 为什么我贴出这段话,它描述了互联网架构的演变,关键要素及dubbo存在的意义,可谓简约而不简单 官网地址,在这里关于dubbo的介绍我就不再这里阐述了,官网有中文的说明而且很详细 2、需求 在大规模服务化之前,应用可能只是通过 RMI 或 Hessian 等工具,简单的暴露和引用远程服务,通过配置服务的URL地址进行调用,通过 F5 等硬件进行负载均衡。 当服务越来越多时,服务 URL 配置管理变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。此时需要一个服务注册中心,动态的注册和发现服务,使服务的位置透明。并通过在消费方获取服务提供方地址列表,实现软负载均衡和 Failover,降低对 F5 硬件负载均衡器的依赖,也能减少部分成本。 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。这时,需要自动画出应用间的依赖关系图,以帮助架构师理清理关系。 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器?为了解决这些问题,第一步,要将服务现在每天的调用量,响应时间,都统计出来,作为容量规划的参考指标。其次,要可以动态调整权重,在线上,将某台机器的权重一直加大,并在加大的过程中记录响应时间的变化,直到响应时间到达阀值,记录此时的访问量,再以此访问量乘以机器数反推总容量。 以上是 Dubbo 最基本的几个需求。 点评:其实前半段就是描述了注册中心必要性,后半段说明了监控与分析的重要性,恰好dubbo有独特的monitor模块 3、架构 这个图不多说了,描述了一个服务注册与发现的场景 : 服务容器负责启动,加载,运行服务提供者。 服务提供者在启动时,向注册中心注册自己提供的服务。 服务消费者在启动时,向注册中心订阅自己所需的服务。 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 二、使用spring-boot快速搭建dubbo 1.项目结构图 2. 编写service-api层代码 IStudentService: package com.hzgj.lyrk.dubbo.api; import com.hzgj.lyrk.dubbo.dto.StudentDTO; public interface IStudentService { StudentDTO getStudentById(Integer id); } View Code StudentDTO:注意必须实现serializable接口 package com.hzgj.lyrk.dubbo.dto; import lombok.Data; import java.io.Serializable; @Data public class StudentDTO implements Serializable { private Integer id; private String name; } View Code 3.编写student-server模块 3.1首先添加gradle依赖项: dependencies { // testCompile group: 'junit', name: 'junit', version: '4.12' compile 'com.alibaba.boot:dubbo-spring-boot-starter:0.1.0' // https://mvnrepository.com/artifact/com.101tec/zkclient compile group: 'com.101tec', name: 'zkclient', version: '0.10' compile project(":service-api") } View Code 3.2 StudentServer: package com.hzgj.lyrk.dubbo.student.server; import com.alibaba.dubbo.config.annotation.Service; import com.hzgj.lyrk.dubbo.api.IStudentService; import com.hzgj.lyrk.dubbo.dto.StudentDTO; @Service public class StudentService implements IStudentService { @Override public StudentDTO getStudentById(Integer id) { StudentDTO studentDTO = new StudentDTO(); studentDTO.setId(id); studentDTO.setName("学号为" + id + "的学生"); return studentDTO; } } View Code 注意此处@Service要用 com.alibaba.dubbo.config.annotation.Service 3.3 编写启动类: package com.hzgj.lyrk.dubbo.student; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class StudentApplication { public static void main(String[] args) { SpringApplication.run(StudentApplication.class, args); } } View Code 3.4 application.yml文件 server: port: 8100 spring: application: name: student-server dubbo: application: name: student-server id: student-server version: 1.0 scan: base-packages: com.hzgj.lyrk.dubbo.student.server registry: address: zookeeper://localhost:2181 View Code 在这里面我们注意以下几点: 1)首先定义spring.application.name这个不多说了 遵守规范就行 2)dubbo集成的配置时通常以dubbo.xxxx打头 3)dubbo.scan.base-packages:主要是扫描dubbo的注解包 4)dubbo.registry.address:是指定注册中心的地址,这里我们使用zookeeper作为注册中心 3.5 启动成功时,我们通过zkCli能够发现在zookeeper存在如下节点: 这里面的结构为:/dubbo/接口的类全名/节点 4、编写消费端:project-portal 4.1 添加gradle依赖: dependencies { compile 'com.alibaba.boot:dubbo-spring-boot-starter:0.1.0' // https://mvnrepository.com/artifact/com.101tec/zkclient compile group: 'com.101tec', name: 'zkclient', version: '0.10' compile project(":service-api") } View Code 4.2 编写controller package com.hzgj.lyrk.dubbo.portal.controller; import com.alibaba.dubbo.config.annotation.Reference; import com.hzgj.lyrk.dubbo.api.IStudentService; import com.hzgj.lyrk.dubbo.dto.StudentDTO; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class StudentController { @Reference private IStudentService studentService; @GetMapping("/student/id/{stuId}") public StudentDTO getStudent(@PathVariable Integer stuId) { return studentService.getStudentById(stuId); } } View Code 注意:@Reference注解的使用 4.3 编写启动类 package com.hzgj.lyrk.dubbo.portal; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class PortalApplication { public static void main(String[] args) { SpringApplication.run(PortalApplication.class, args); } } View Code 4.4 编写application.yml spring: application: name: project-portal server: port: 8101 dubbo: registry: address: zookeeper://localhost:2181 application: version: 1.0 id: project-portal name: project-portal View Code 测试一下: 三 、后话- Dubbo与SpringCloud 其实,这两者都是当下微服务典型的技术解决方案,可谓一时瑜亮,只不过在国内dubbo比较流行一些,原因其实很简单: 1. dubbo官方文档详细且有中文,而且运行原理技术解决方案描述比较透彻 2.国内的架构师有许多来自于阿里,对dubbo的推广起到了不可磨灭的作用 3.由于dubbo出现的较早,当然也开源。对于当时可谓轰动一时,各大公司争先使用,即使到现在谁也不愿意将原先的项目大刀阔斧的用新技术重构。 相反,在国外的社区,dubbo的使用广度恐怕就远不及SpringCloud了。原因其实也很明了:就公司而言,dubbo出自于阿里,属于商业公司。我觉得阿里的框架肯定优先满足于自己的业务与利益。而springcloud出自于Spring的产品族,而其公司本身就是为了简化企业的开发模式,为各大企业所服务。因此他们的本身出发点就不同,我觉得这个才是必要因素。 但是有几点我在这里重点提一下: 1. 就完成的功能而言:dubbo其实是SpringCloud组件中的一部分,也就相当于netflix中的eureka+小半个Hystrix+ribbon+feign。但是SpringCloud集成的诸如:链路跟踪,分布式配置,网关路由等,目前dubbo里还没有找到,也有可能我没有发现。因此在dubbo里需要使用这些功能,我们还要借助于第三方的实现。 2. dubbo是基于rpc实现远程调用,springcloud各个服务之间调用还是经过http,就性能而言是要弱于dubbo的,毕竟dubbo是经过阿里庞大的业务产品族和并发量考验的,不过这并不代表springcloud性能会很差 3. 常用的dubbo的技术使用方案还是基于spring,因此,我还是愿意把幕后英雄归功于spring 4. spring-cloud就相当于电脑的品牌机,拿来很方便的使用,因此它绝对是中小型公司(没有过多的精力和成本去搞基础研发)福音。而dubbo就好比是组装机,我们通过其已有的实现,完整的文档装配成我们自己想要的一套微服务方案。

优秀的个人博客,低调大师

SpringBoot 项目模板:摆脱步步搭建

本文作者 | 无知者云 链接 | cnblogs.com/davenkin/p/spring-boot-template.html 前言 在我的工作中,我从零开始搭建了不少软件项目,其中包含了基础代码框架和持续集成基础设施等,这些内容在敏捷开发中通常被称为“第0个迭代”要做的事情。但是,当项目运行了一段时间之后再来反观,我总会发现一些不足的地方,要么测试分类没有分好,要么基本的编码架子没有考虑周全。 另外,我在工作中也会接触到很多既有项目,公司内部和外部的都有,多数项目的编码实践我都是不满意的。比如,我曾经新加入一个项目的时候,前前后后请教了3位同事才把该项目在本地运行起来;又比如在另一项目中,我发现前端请求对应的Java类命名规范不统一,有被后缀为Request的,也有被后缀为Command的。 再者,工作了这么多年之后,我越来越发现基础知识以及系统性学习的重要性。诚然,技术框架的发展使得我们可以快速地实现业务功能,但是当软件出了问题之后有时却需要将各方面的知识融会贯通并在大脑里综合反应才能找到解决思路。 基于以上,我希望整理出一套公共性的项目模板出来,旨在尽量多地包含日常开发之所需,减少开发者的重复性工作以及提供一些最佳实践。对于后端开发而言,我选择了当前被行业大量使用的Spring Boot,基于此整理出了一套公共的、基础性的实践方式,在结合了自己的经验以及其他项目的优秀实践之后,总结出本文以飨开发者。 本文以一个简单的电商订单系统为例,源代码请访问: git clone https://github.com/e-commerce-sample/order-backendgit checkout a443dace 所使用的技术栈主要包括:Spring Boot、Gradle、MySQL、Junit 5、Rest Assured、Docker等。 从写好README开始 一份好的README可以给人以项目全景概览,可以使新人快速上手项目,可以降低沟通成本。同时,README应该简明扼要,条理清晰,建议包含以下方面: 项目简介:用一两句话简单描述该项目所实现的业务功能; 技术选型:列出项目的技术栈,包括语言、框架和中间件等; 本地构建:列出本地开发过程中所用到的工具命令; 领域模型:核心的领域概念,比如对于示例电商系统来说有Order、Product等; 测试策略:自动化测试如何分类,哪些必须写测试,哪些没有必要写测试; 技术架构:技术架构图; 部署架构:部署架构图; 外部依赖:项目运行时所依赖的外部集成方,比如订单系统会依赖于会员系统; 环境信息:各个环境的访问方式,数据库连接等; 编码实践:统一的编码实践,比如异常处理原则、分页封装等; FAQ:开发过程中常见问题的解答。 需要注意的是,README中的信息可能随着项目的演进而改变(比如引入了新的技术栈或者加入了新的领域模型),因此也是需要持续更新的。虽然我们知道,软件文档的一个痛点便是无法与项目实际进展保持同步,但是就README这点信息来讲,还是建议开发者们不要吝啬那一点点敲键盘的时间。 此外,除了保持README的持续更新,一些重要的架构决定可以通过示例代码的形式记录在代码库中,新开发者可以通过直接阅读这些示例代码快速了解项目的通用实践方式以及架构选择,请参考: https://www.thoughtworks.com/radar/techniques/lightweight-architecture-decision-records 一键式本地构建 为了避免诸如前文中所提到的“请教了3位同事才本地构建成功”的尴尬,为了减少“懒惰”的程序员们的手动操作,也为了为所有开发者提供一种一致的开发体验,我们希望用一个命令就可以完成所有的事情。这里,对于不同的场景我总结出了以下命令: 生成IDE工程:idea.sh,生成IntelliJ工程文件并自动打开IntelliJ 本地运行:run.sh,本地启动项目,自动启动本地数据库,监听调试端口5005 本地构建:local-build.sh,只有本地构建成功才能提交代码 以上3个命令基本上可以完成日常开发之所需,此时,对于新人的开发流程大致为: 拉取代码; 运行idea.sh,自动打开IntelliJ; 编写代码,包含业务代码和自动化测试; 运行run.sh,进行本地调试或必要的手动测试(本步骤不是必需); 运行local-build.sh,完成本地构建; 再次拉取代码,保证local-build.sh成功,提交代码。 事实上,这些命令脚本的内容非常简单,比如run.sh文件内容为: #!/usr/bin/env bash./gradlew clean bootRun 然而,这种显式化的命令却可以减少新人的恐惧感,因为他们只需要知道运行这3个命令就可以搞开发了。另外,一个小小的细节:本地构建的local-build.sh命令本来可以重命名为更简单的build.sh,但是当我们在命令行中使用Tab键自动补全的时候,会发现自动补全到了build目录,而不是build.sh命令,并不方便,因此命名为了local-build.sh。 细节虽小,但是却体现了一个宗旨,即我们希望给开发者一种极简的开发体验,我把这些看似微不足道的东西称作是对程序员的“人文关怀”。 目录结构 Maven所提倡的目录结构当前已经成为事实上的行业标准,Gradle在默认情况下也采用了Maven的目录结构,这对于多数项目来说已经足够了。此外,除了Java代码,项目中还存在其他类型的文件,比如Gradle插件的配置、工具脚本和部署配置等。无论如何,项目目录结构的原则是简单而有条理,不要随意地增加多余的文件夹,并且也需要及时重构。 在示例项目中,顶层只有2个文件夹,一个是用于放置Java源代码和项目配置的src文件夹,另一个是用于放置所有Gradle配置的gradle文件夹,此外,为了方便开发人员使用,将上文提到的3个常用脚本直接放到根目录下: └── order-backend ├── gradle // 文件夹,用于放置所有Gradle配置 ├── src // 文件夹,Java源代码 ├── idea.sh //生成IntelliJ工程 ├── local-build.sh // 提交之前的本地构建 └── run.sh // 本地运行 对于gradle而言,我们刻意地将Gradle插件脚本与插件配置放到了一起,比如Checkstyle: ├── gradle│ ├── checkstyle│ │ ├── checkstyle.gradle│ │ └── checkstyle.xml 事实上,在默认情况下Checkstyle插件会从项目根目录下的config目录查找checkstyle.xml配置文件,但是这一方面增加了多余的文件夹,另一方面与该插件相关的设施分散在了不同的地方,违背了广义上的内聚原则。 基于业务分包 早年的Java分包方式通常是基于技术的,比如与domain包平级的有controller包、service包和infrastructure包等。这种方式当前并不被行业所推崇,而是应该首先基于业务分包。 比如,在订单示例项目中,有两个重要的领域对象Order和Product(在DDD中称为聚合根),所有的业务都围绕它们展开,因此分别创建order包和product包,再分别在包下创建与之相关的各个子包。此时的order包如下: ├── order│ ├── OrderApplicationService.java│ ├── OrderController.java│ ├── OrderNotFoundException.java│ ├── OrderRepository.java│ ├── OrderService.java│ └── model│ ├── Order.java│ ├── OrderFactory.java│ ├── OrderId.java│ ├── OrderItem.java│ └── OrderStatus.java 可以看到,在order包下我们直接放置了OrderController和OrderRepository等类,而没有必要再为这些类划分单独的子包。而对于领域模型Order来讲,由于包含了多个对象,因此基于内聚性原则将它们归到model包中。但是这并不是一个必须,如果业务足够简单,我们甚至可以将所有类直接放到业务包下,product包便是如此: └── product ├── Product.java ├── ProductApplicationService.java ├── ProductController.java ├── ProductId.java └── ProductRepository.java 在编码实践中,我们总是基于一个业务用例来实现代码,在技术分包场景下,我们需要在分散的各包中来回切换,增加了代码导航的成本;另外,代码提交的变更内容也是散落的,在查看代码提交历史时,无法直观的看出该次提交是关于什么业务功能的。 在业务分包下,我们只需要在单个统一的包下修改代码,减少了代码导航成本;另外一个好处是,如果哪天我们需要将某个业务迁移到另外的项目(比如识别出了独立的微服务),那么直接整体移动业务包即可。 当然,基于业务分包并不意味着所有的代码都必须囿于业务包下,这里的逻辑是:优先进行业务分包,然后对于一些不隶属于任何业务的代码可以单独分包,比如一些util类、公共配置等。比如我们依然可以创建一个common包,下面放置了Spring公共配置、异常处理框架和日志等子包: └── common ├── configuration ├── exception ├── loggin └── utils 自动化测试分类 在当前的微服务和前后端分离的开发模式下,后端项目仅提供纯粹的业务API,而不包含UI逻辑,因此后端项目不会再包含诸如WebDriver的重量级端到端测试。同时,后端项目作为向外提供业务功能的独立运行单元,在API级别也应该有相应的测试。 此外,程序中有些框架性代码,要么是诸如Controller之类的技术性框架代码,要么是基于某种架构风格的代码(比如DDD实践中的ApplicationService),这些代码一方面并不包含业务逻辑,一方面是很薄的一个抽象层(即实现相对简单),用单元测试来覆盖显得没有必要,因此笔者的观点是可以不为此编写单独的单元测试。欢迎关注公众号"Java学习之道",查看更多干货! 再者,程序中有些重要的组件性代码,比如访问数据库的Repository或者分布式锁,使用单元测试实际上“测不到点上”,而使用API测试又显得在分类逻辑上不合理,为此我们可以专门创建一种测试类型谓之组件测试。 基于以上,我们可以对自动化测试做个分类: 单元测试:核心的领域模型,包括领域对象(比如Order类),Factory类,领域服务类等; 组件测试:不适合写单元测试但是又必须测试的类,比如Repository类,在有些项目中,这种类型测试也被称为集成测试; API测试:模拟客户端测试各个API接口,需要启动程序。 Gradle在默认情况下只提供src/test/java目录用于测试,对于以上3种类型的测试,我们需要将它们分开以便于管理(也是职责分离的体现)。为此,可以通过Gradle提供的SourceSets对测试代码进行分类: sourceSets { componentTest { compileClasspath += sourceSets.main.output + sourceSets.test.output runtimeClasspath += sourceSets.main.output + sourceSets.test.output } apiTest { compileClasspath += sourceSets.main.output + sourceSets.test.output runtimeClasspath += sourceSets.main.output + sourceSets.test.output }} 到此,3种类型的测试可以分别编写在以下目录: 单元测试:src/test/java 组件测试:src/componentTest/java API测试:src/apiTest/java 需要注意的是,这里的API测试更多强调的是对业务功能的测试,有些项目中可能还会存在契约测试和安全测试等,虽然从技术上讲都是对API的访问,但是这些测试都是单独的关注点,因此建议分开对待。 值得一提的是,由于组件测试和API测试需要启动程序,也即需要准备好本地数据库,我们采用了Gradle的docker-compose插件(或者jib插件),该插件会在运行测试之前自动运行Docker容器(比如MySQL): apply plugin: 'docker-compose'dockerCompose { useComposeFiles = ['docker/mysql/docker-compose.yml']}bootRun.dependsOn composeUpcomponentTest.dependsOn composeUpapiTest.dependsOn composeUp 更多的测试分类配置细节,比如JaCoCo测试覆盖率配置等,请参考本文的示例项目代码。对Gradle不熟悉的读者可以参考: https://www.cnblogs.com/CloudTeng/p/3417762.html 日志处理 在日志处理中,除了完成基本配置外,还有2个需要考虑的点: 1、在日志中加入请求标识,便于链路追踪。在处理一个请求的过程中有时会输出多条日志,如果每条日志都共享统一的请求ID,那么在日志追踪时会更加方便。此时,可以使用Logback原生提供的MDC(Mapped Diagnostic Context)功能,创建一个RequestIdMdcFilter: protectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain) throws ServletException, IOException { //request id in header may come from Gateway, eg. Nginx String headerRequestId = request.getHeader(HEADER_X_REQUEST_ID); MDC.put(REQUEST_ID, isNullOrEmpty(headerRequestId) ? newUuid() : headerRequestId); try { filterChain.doFilter(request, response); } finally { clearMdc(); } } 2、集中式日志管理,在多节点部署的场景下,各个节点的日志是分散的,为此可以引入诸如ELK之类的工具将日志统一输出到ElasticSearch中。本文的示例项目使用了RedisAppender将日志输出到Logstash: <appender name="REDIS" class="com.cwbase.logback.RedisAppender"> <tags>ecommerce-order-backend-${ACTIVE_PROFILE}</tags> <host>elk.yourdomain.com</host> <port>6379</port> <password>whatever</password> <key>ecommerce-ordder-log</key> <mdc>true</mdc> <type>redis</type></appender> 当然,统一日志的方案还有很多,比如Splunk和Graylog等。 异常处理 在设计异常处理的框架时,需要考虑以下几点: 向客户端提供格式统一的异常返回 异常信息中应该包含足够多的上下文信息,最好是结构化的数据以便于客户端解析 不同类型的异常应该包含唯一标识,以便客户端精确识别 异常处理通常有两种形式,一种是层级式的,即每种具体的异常都对应了一个异常类,这些类最终继承自某个父异常;另一种是单一式的,即整个程序中只有一个异常类,再以一个字段来区分不同的异常场景。 层级式异常的好处是能够显式化异常含义,但是如果层级设计不好可能导致整个程序中充斥着大量的异常类;单一式的好处是简单,而其缺点在于表意性不够。 本文的示例项目使用了层级式异常,所有异常都继承自一个AppException: public abstract class AppException extends RuntimeException { private final ErrorCode code; private final Map<String, Object> data = newHashMap();} 这里,ErrorCode枚举中包含了异常的唯一标识、HTTP状态码以及错误信息;而data字段表示各个异常的上下文信息。 在示例系统中,在没有找到订单时抛出异常: public class OrderNotFoundException extends AppException { public OrderNotFoundException(OrderId orderId) { super(ErrorCode.ORDER_NOT_FOUND, ImmutableMap.of("orderId", orderId.toString())); }} 在返回异常给客户端时,通过一个ErrorDetail类来统一异常格式: public final class ErrorDetail { private final ErrorCode code; private final int status; private final String message; private final String path; private final Instant timestamp; private final Map<String, Object> data = newHashMap();} 最终返回客户端的数据为: { requestId: "d008ef46bb4f4cf19c9081ad50df33bd", error: { code: "ORDER_NOT_FOUND", status: 404, message: "没有找到订单", path: "/order", timestamp: 1555031270087, data: { orderId: "123456789" } }} 可以看到,ORDER_NOT_FOUND与data中的数据结构是一一对应的,也即对于客户端来讲,如果发现了ORDER_NOT_FOUND,那么便可确定data中一定存在orderId字段,进而完成精确的结构化解析。 后台任务与分布式锁 除了即时完成客户端的请求外,系统中通常会有一些定时性的例行任务,比如定期地向用户发送邮件或者运行数据报表等;另外,有时从设计上我们会对请求进行异步化处理。此时,我们需要搭建后台任务相关基础设施。Spring原生提供了任务处理(TaskExecutor)和任务计划(TaskSchedulor)机制;而在分布式场景下,还需要引入分布式锁来解决并发冲突,为此我们引入一个轻量级的分布式锁框架ShedLock。 启用Spring任务配置如下: @Configuration@EnableAsync@EnableSchedulingpublic class SchedulingConfiguration implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(newScheduledThreadPool(10)); } @Bean(destroyMethod = "shutdown") @Primary public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(5); executor.setQueueCapacity(10); executor.setTaskDecorator(new LogbackMdcTaskDecorator()); executor.initialize(); return executor; }} 然后配置Shedlock: @Configuration@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")public class DistributedLockConfiguration { @Bean public LockProvider lockProvider(DataSource dataSource) { return new JdbcTemplateLockProvider(dataSource); } @Bean public DistributedLockExecutor distributedLockExecutor(LockProvider lockProvider) { return new DistributedLockExecutor(lockProvider);}} 实现后台任务处理: @Scheduled(cron = "0 0/1 * * * ?") @SchedulerLock(name = "scheduledTask", lockAtMostFor = THIRTY_MIN, lockAtLeastFor = ONE_MIN) public void run() { logger.info("Run scheduled task."); }为了支持代码直接调用分布式锁,基于Shedlock的LockProvider创建DistributedLockExecutor:public class DistributedLockExecutor { private final LockProvider lockProvider; public DistributedLockExecutor(LockProvider lockProvider) { this.lockProvider = lockProvider; } public <T> T executeWithLock(Supplier<T> supplier, LockConfiguration configuration) { Optional<SimpleLock> lock = lockProvider.lock(configuration); if (!lock.isPresent()) { throw new LockAlreadyOccupiedException(configuration.getName()); } try { return supplier.get(); } finally { lock.get().unlock(); } }} 使用时在代码中直接调用: public String doBusiness() { return distributedLockExecutor.executeWithLock(() -> "Hello World.", new LockConfiguration("key", Instant.now().plusSeconds(60))); } 本文的示例项目使用了基于JDBC的分布式锁,事实上任何提供原子操作的机制都可用于分布式锁,Shedlock还提供基于Redis、ZooKeeper和Hazelcast等的分布式锁实现机制。 统一代码风格 除了Checkstyle统一代码格式之外,项目中有些通用的公共的编码实践方式也需要在整个开发团队中进行统一,包括但不限于以下方面: 客户端的请求数据类统一使用相同后缀,比如Command 返回给客户端的数据统一使用相同后缀,比如Represetation 统一对请求处理的流程框架,比如采用传统的3层架构或者DDD战术模式 提供一致的异常返回(请参考“异常处理”小节) 提供统一的分页结构类 明确测试分类以及统一的测试基础类(请参考“自动化测试分类”小节) 静态代码检查 静态代码检查主要包含以下Gradle插件,具体配置请参考本文示例代码: Checkstyle:用于检查代码格式,规范编码风格 Spotbugs:Findbugs的继承者 Dependency check:OWASP提供的Java类库安全性检查 Sonar:用于代码持续改进的跟踪 健康检查 健康检查主要用于以下场景: 我们希望初步检查程序是否运行正常 有些负载均衡软件会通过一个健康检查URL判断节点的可达性 此时,可以实现一个简单的API接口,该接口不受权限管控,可以公开访问。如果该接口返回HTTP的200状态码,便可初步认为程序运行正常。此外,我们还可以在该API中加入一些额外的信息,比如提交版本号、构建时间、部署时间等。 启动本文的示例项目: ./run.sh 然后访问健康检查API:http://localhost:8080/about,结果如下: { requestId: "698c8d29add54e24a3d435e2c749ea00", buildNumber: "unknown", buildTime: "unknown", deployTime: "2019-04-11T13:05:46.901+08:00[Asia/Shanghai]", gitRevision: "unknown", gitBranch: "unknown", environment: "[local]"} 以上接口在示例项目中用了一个简单的Controller实现,事实上Spring Boot的Acuator框架也能够提供相似的功能。 API文档 软件文档的难点不在于写,而在于维护。多少次,当我对照着项目文档一步一步往下走时,总得不到正确的结果,问了同事之后得到回复“哦,那个已经过时了”。本文示例项目所采用的Swagger在一定程度上降低了API维护的成本,因为Swagger能自动识别代码中的方法参数、返回对象和URL等信息,然后自动地实时地创建出API文档。 配置Swagger如下: @Configuration@EnableSwagger2@Profile(value = {"local", "dev"})public class SwaggerConfiguration { @Bean public Docket api() { return new Docket(SWAGGER_2) .select() .apis(basePackage("com.ecommerce.order")) .paths(any()) .build(); }} 启动本地项目,访问http://localhost:8080/swagger-ui.html: 数据库迁移 在传统的开发模式中,数据库由专门的运维团队或者DBA来维护,要对数据库进行修改需要向DBA申请,告之迁移内容,最后由DBA负责数据库变更实施。在持续交付和DevOps运动中,这些工作逐步提前到开发过程,当然并不是说不需要DBA了,而是这些工作可以由开发者和运维人员一同完成。 另外,在微服务场景下,数据库被包含在单个服务的边界之内,因此基于内聚性原则(咦,这好像是本文第三次提到内聚原则了,可见其在软件开发中的重要性),数据库的变更最好也与项目代码一道维护在代码库中。 本文的示例项目采用了Flyway作为数据库迁移工具,加入了Flyway依赖后,在src/main/sources/db/migration目录下创建迁移脚本文件即可: resources/├── db│ └── migration│ ├── V1__init.sql│ └── V2__create_product_table.sql 迁移脚本的命名需要遵循一定的规则以保证脚本执行顺序,另外迁移文件生效之后不要任意修改,因为Flyway会检查文件的checksum,如果checksum不一致将导致迁移失败。 多环境构建 在软件的开发流程中,我们需要将软件部署到多个环境,经过多轮验证后才能最终上线。在不同的阶段中,软件的运行态可能是不一样的,比如本地开发时可能将所依赖的第三方系统stub掉;持续集成构建时可能使用的是测试用的内存数据库等等。为此,本文的示例项目推荐采用以下环境: local:用于开发者本地开发 ci:用于持续集成 dev:用于前端开发联调 qa:用于测试人员 uat:类生产环境,用于功能验收(有时也称为staging环境) prod:正式的生产环境 CORS 在前后端分离的系统中,前端单独部署,有时连域名都和后端不同,此时需要进行跨域处理。传统的做法可以通过JSONP,但这是一种比较“trick”的做法,当前更通用的实践是采用CORS机制,在Spring Boot项目中,启用CORS配置如下: @Configurationpublic class CorsConfiguration { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**"); } }; }} 对于使用Spring Security的项目,需要保证CORS工作于Spring Security的过滤器之前,为此Spring Security专门提供了相应配置: @EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // by default uses a Bean by the name of corsConfigurationSource .cors().and() ... } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("https://example.com")); configuration.setAllowedMethods(Arrays.asList("GET","POST")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; }} 常用第三方类库: 这里列出一些比较常见的第三方库,开发者们可以根据项目所需引入: Guava:来自Google的常用类库 Apache Commons:来自Apache的常用类库 Mockito:主要用于单元测试的mock DBUnit:测试中管理数据库测试数据 Rest Assured:用于Rest API测试 Jackson 2:Json数据的序列化和反序列化 jjwt:Jwt token认证 Lombok:自动生成常见Java代码,比如equals()方法,getter和setter等; Feign:声明式Rest客户端 Tika:用于准确检测文件类型 itext:生成Pdf文件等 zxing:生成二维码 Xstream:比Jaxb更轻量级的XML处理库 总结 本文通过一个示例项目谈及到了项目之初开发者搭建后端工程的诸多方面,其中的绝大多数实践均在笔者的项目中真实落地。读完本文之后你可能会发现,文中的很多内容都是很基础很简单的。 没错,的确没有什么难的东西,但是要系统性地搭建好后端项目的基础框架却不见得是每个开发团队都已经做到的事情,而这恰恰是本文的目的。 最后,需要提醒的是,本文提到的实践方式只是一个参考,一方面依然存在考虑不周的地方,另一方面示例项目中用到的技术工具还存在其他替代方案,请根据自己项目的实际情况进行取舍。 END 我是武哥,最后给大家 免费分享我写的 10 万字 Spring Boot 学习笔记(带完整目录)以及对应的源码 。这是我之前在 CSDN 开的一门课,所以笔记非常详细完整,我准备将资料分享出来给大家免费学习,相信大家看完一定会有所收获( 下面有下载方式 )。 可以看出,我当时备课非常详细,目录非常完整,读者可以手把手跟着笔记,结合源代码来学习。现在免费分享出来,有需要的读者可以下载学习,就在我公众号回复:笔记,就行。如有文章对你有帮助,在看和转发是对我最大的支持!关注Java开发宝典每天学习技术干货点赞是最大的支持 本文分享自微信公众号 - 武哥聊编程(eson_15)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册