在这篇文章中,我们使用Java和Spring Boot框架开发了一个微服务,然后使用DevOps管道将它与Jenkins和Docker一起部署。
序言
在本文中,将使用 Java 和 Spring 框架创建一个简单的微服务,并使用Jenkins和Docker创建一个DevOps管道。
注意:假设读者具有 Java 和 Web 技术的背景。本文不再单独介绍Spring,Jenkins,Java,Git和Docker的。
将按顺序介绍以下几点:
- 构建中的微服务
- 所需要的软件
- 使用 Jenkins 和 Docker 构建DevOps管道
微服务
可以使用以下URL从Github克隆微服务应用程序:
github.com/Microservic…
资源层
实体为Person,包含名称,电子邮件和id。开发一个管理Person实体的微服务。
package com.myapp.sample.model;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "person")
public class Person implements Serializable{
private static final long serialVersionUID = 7401548380514451401L;
public Person() {}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name = "name")
String name;
@NotNull
@Email
@Column(name = "email")
String email;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((email == null) ? 0 : email.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (email == null) {
if (other.email != null)
return false;
} else if (!email.equals(other.email))
return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", email=" + email + "]";
}
}
使用常规CRUD操作测试实体层。然后,我们检查实体是否被持久化,查询和更新
package com.myapp.sample.model;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonTest {
@Autowired
private TestEntityManager entityManager;
@Before
public void setUp() {
List<Person> list = entityManager.getEntityManager().createQuery("from Person").getResultList();
for(Person person:list) {
entityManager.remove(person);
}
}
@Test
public void testCRUD()
{
Person p1 = new Person();
p1.setName("test person 1");
p1.setEmail("test@person1.com");
entityManager.persist(p1);
List<Person> list = entityManager.getEntityManager().createQuery("from Person").getResultList();
Assert.assertEquals(1L, list.size());
Person p2 = list.get(0);
Assert.assertEquals("test person 1", p2.getName());
Assert.assertEquals("test@person1.com", p2.getEmail());
Assert.assertEquals(p2.hashCode(), p2.hashCode());
Assert.assertTrue(p2.equals(p2));
}
}
持久层
持久层由Spring Boot自动管理。 PagingAndSortingRepository接口是CrudRepository的扩展,用于提供使用分页和排序抽象检索实体的附加方法。由于使用这些接口自然会使测试覆盖率达到100%,故无需写其他测试方法。
package com.myapp.sample.repositories;
import com.myapp.sample.model.Person;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RestResource;
@RestResource(exported=false)
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
}
业务层
PersonService接口包含三个操作:保存,按ID查找,以及查找Repository层支持的多个CRUD操作的所有实例。
package com.myapp.sample.service;
import com.myapp.sample.model.Person;
import java.util.List;
public interface PersonService {
public List<Person> getAll();
public Person save(Person p);
public Person findById(Long ids);
}
PersonService接口通过调用持久层实现的并未添加任何业务服务实现。由于Spring Boot的持久层测试范围已全部覆盖,因此无需单独测试业务业务。
package com.myapp.sample.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import com.myapp.sample.model.Person;
import com.myapp.sample.repositories.PersonRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
PersonRepository personRepository;
@Override
public List<Person> getAll() {
List<Person> personList = new ArrayList<>();
personRepository.findAll().forEach(personList::add);
return personList;
}
@Override
public Person save(Person p) {
return personRepository.save(p);
}
@Override
public Person findById(Long id) {
Optional<Person> dbPerson = personRepository.findById(id);
return dbPerson.orElse(null);
}
}
REST API 层
通过将调用业务层来间接调用持久层来暴露REST API。
package com.myapp.sample.controller;
import java.util.List;
import com.myapp.sample.model.Person;
import com.myapp.sample.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PersonController {
@Autowired
PersonService personService;
@PostMapping(path = "/api/person")
public ResponseEntity<Person> register(@RequestBody Person p) {
return ResponseEntity.ok(personService.save(p));
}
@GetMapping(path = "/api/person")
public ResponseEntity<List<Person>> getAllPersons() {
return ResponseEntity.ok(personService.getAll());
}
@GetMapping(path = "/api/person/{person-id}")
public ResponseEntity<Person> getPersonById(@PathVariable(name="person-id", required=true)Long personId) {
Person person = personService.findById(personId);
if (person != null) {
return ResponseEntity.ok(person);
}
return ResponseEntity.notFound().build();
}
}
使用Spring Boot自带测试框架测试 REST API 层
package com.myapp.sample.controller;
import com.myapp.sample.model.Person;
import com.myapp.sample.repositories.PersonRepository;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PersonControllerTest {
MockMvc mockMvc;
@Mock
private PersonController personController;
@Autowired
private TestRestTemplate template;
@Autowired
PersonRepository personRepository;
@Before
public void setup() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(personController).build();
personRepository.deleteAll();
}
@Test
public void testRegister() throws Exception {
HttpEntity<Object> person = getHttpEntity(
"{\"name\": \"test 1\", \"email\": \"test10000000000001@gmail.com\"}");
ResponseEntity<Person> response = template.postForEntity(
"/api/person", person, Person.class);
Assert.assertEquals("test 1", response.getBody().getName());
Assert.assertEquals(200,response.getStatusCode().value());
}
@Test
public void testGetAllPersons() throws Exception {
HttpEntity<Object> person = getHttpEntity(
"{\"name\": \"test 1\", \"email\": \"test10000000000001@gmail.com\"}");
ResponseEntity<Person> response = template.postForEntity(
"/api/person", person, Person.class);
ParameterizedTypeReference<List<Person>> responseType = new ParameterizedTypeReference<List<Person>>(){};
ResponseEntity<List<Person>> response2 = template.exchange("/api/person", HttpMethod.GET, null, responseType);
Assert.assertEquals(response2.getBody().size(), 1);
Assert.assertEquals("test 1", response2.getBody().get(0).getName());
Assert.assertEquals(200,response2.getStatusCode().value());
}
@Test
public void testGetPersonById() throws Exception {
HttpEntity<Object> person = getHttpEntity(
"{\"name\": \"test 1\", \"email\": \"test10000000000001@gmail.com\"}");
ResponseEntity<Person> response = template.postForEntity(
"/api/person", person, Person.class);
ParameterizedTypeReference<List<Person>> responseType = new ParameterizedTypeReference<List<Person>>(){};
ResponseEntity<List<Person>> response2 = template.exchange("/api/person", HttpMethod.GET, null, responseType);
Long id = response2.getBody().get(0).getId();
ResponseEntity<Person> response3 = template.getForEntity(
"/api/person/" + id, Person.class);
Assert.assertEquals("test 1", response3.getBody().getName());
Assert.assertEquals(200,response3.getStatusCode().value());
}
@Test
public void testGetPersonByNull() throws Exception {
ResponseEntity<Person> response3 = template.getForEntity(
"/api/person/1", Person.class);
Assert.assertEquals(404,response3.getStatusCode().value());
}
private HttpEntity<Object> getHttpEntity(Object body) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<Object>(body, headers);
}
}
以上这些基本上是用于开发微服务的Java代码。
所需软件
现在看下管理微服务所需的软件。
Java
在本文的例子中,使用Java8。由于Jenkins需要Java8,所以建议使用Java8。
Git
安装最新版本的Git。
Docker
安装最新版本的Docker。由于我有一台Windows8机器,所以我使用的是Docker Toolbox for Windows。
MySQL
使用以下命令安装MySQL 5.7 Docker镜像:
docker pull mysql
docker run -d --name mysql -e MYSQL_DATABASE=person -e MYSQL_ROOT_PASSWORD=<root_password> -p 3306:3306 mysql
在Docker中使用ip命令获取Docker实例的ip并替换demo程序中resources\application.properties的ip。
Jenkins
使用以下命令安装Jenkins Blue Ocean版本:
docker pull jenkinsci/blueocean
docker run -u root --rm -d -p 8080:8080 -p 50000:50000 -v jenkins-data:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock jenkinsci/blueocean
DevOps 流水线
现在我们来看看用于构建,部署和管理Git存储库的DevOps管道。在我们理解管道之前,在Git Flow上花几分钟是很重要的。
Git Flow
Git Flow是Git的分支模型。它主要由主分支(与生产代码并行),开发分支(开发的主要分支),发布分支(用于开发)和功能分支(供开发人员使用)组成。在开发人员完成代码之后,会为团队负责人创建一个pull请求,以检查并将代码合并到开发中。创建发布分支后,错误修复将进入此分支,并在代码稳定后再次合并以开发和管理。在此模型中,标签是从主分支创建的,用于发布到生产。
可以在此处查看可视化描述:https://datasift.github.io/gitflow/IntroducingGitFlow.html
JenkinsFile
有必要在Jenkins中创建一个多分支管道。这允许Jenkins自动处理Git Flow。当提供到Git存储库的链接时,Jenkins会自动选择JenkinsFile。将阶段配置为在签入时触发构建时可视化地查看管道。在此示例中,我们使用PMD,CheckStyle和FindBugs检查代码。欢迎您尝试更成熟的工具,如Sonar,代替PMD,CheckStyle和FindBugs。在管道设置中,我们在一个步骤中构建镜像,并在另一个步骤中运行镜像,以便在主分支中发生更改时更新测试环境容器。打tag时,将使用镜像tag名称更新生产环境,如1.0.0。欢迎您尝试将此示例设置为用于生产的不同Jenkins文件和用于生产的Docker文件,这在生成镜像后是必需的。
#!/usr/bin/env groovy
pipeline {
agent any
triggers {
pollSCM('*/15 * * * *')
}
options { disableConcurrentBuilds() }
stages {
stage('Permissions') {
steps {
sh 'chmod 775 *'
}
}
stage('Cleanup') {
steps {
sh './gradlew --no-daemon clean'
}
}
stage('Check Style, FindBugs, PMD') {
steps {
sh './gradlew --no-daemon checkstyleMain checkstyleTest findbugsMain findbugsTest pmdMain pmdTest cpdCheck'
}
post {
always {
step([
$class : 'FindBugsPublisher',
pattern : 'build/reports/findbugs/*.xml',
canRunOnFailed : true
])
step([
$class : 'PmdPublisher',
pattern : 'build/reports/pmd/*.xml',
canRunOnFailed : true
])
step([
$class: 'CheckStylePublisher',
pattern: 'build/reports/checkstyle/*.xml',
canRunOnFailed : true
])
}
}
}
stage('Test') {
steps {
sh './gradlew --no-daemon check'
}
post {
always {
junit 'build/test-results/test/*.xml'
}
}
}
stage('Build') {
steps {
sh './gradlew --no-daemon build'
}
}
stage('Update Docker UAT image') {
when { branch "master" }
steps {
sh '''
docker login -u "<userid>" -p "<password>"
docker build --no-cache -t person .
docker tag person:latest amritendudockerhub/person:latest
docker push amritendudockerhub/person:latest
docker rmi person:latest
'''
}
}
stage('Update UAT container') {
when { branch "master" }
steps {
sh '''
docker login -u "<userid>" -p "<password>"
docker pull amritendudockerhub/person:latest
docker stop person
docker rm person
docker run -p 9090:9090 --name person -t -d amritendudockerhub/person
docker rmi -f $(docker images -q --filter dangling=true)
'''
}
}
stage('Release Docker image') {
when { buildingTag() }
steps {
sh '''
docker login -u "<userid>" -p "<password>"
docker build --no-cache -t person .
docker tag person:latest amritendudockerhub/person:${TAG_NAME}
docker push amritendudockerhub/person:${TAG_NAME}
docker rmi $(docker images -f “dangling=true” -q)
'''
}
}
}
}
具体描述参见:https://jenkins.io/doc/tutorials/build-a-multibranch-pipeline-project/
结语
使用Kubernetes进行部署可以改进此示例。但可以使用Docker创建一个完整的管道,但这不是本文的目标。欢迎大家提出更多好方案。
本文作者:Amritendu De
原文链接:dzone.com/articles/mi…
版权归作者所有,转载请注明出处