【译】用Java,Spring Boot,Git Flow,Jenkins和Docker构建微服务和DevOps|窗纸儿吧

429 阅读7分钟
原文链接: blog.chuangzhi8.cn

在这篇文章中,我们使用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…
版权归作者所有,转载请注明出处