Springboot2快速集成

  1. 通过一个实例快速搭建SSM。
  2. 通过实例来看看各个环节的最佳实践。
  3. 如何使用业务异常。
  4. 如何设计一个对前端友好的接口。
  5. 通过单元测试使你的工作更加轻松和安全。
  6. 简单聊聊代码规范。
  7. 利用CI/CD来帮助你处理繁杂的重复性工作。

源码地址 https://github.com/bestaone/Mybatis4Springboot


Spring5、springboot2近况


  • spring5

    最大的亮点是 Spring webflux。Spring webflux 是一个新的非堵塞函数式 Reactive Web 框架,可以用来建立异步的,非阻塞,事件驱动的服务,并且扩展性非常好。

  • springboot

    集成了大量常用的第三方库配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),Spring Boot应用中这些第三方库几乎可以零配置的开箱即用(out-of-the-box),大部分的Spring Boot应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。


一分钟helloworld看看新姿势


  • 创建文件

    新建项目Demo

    创建文件 pom.xml

    src/main/java、src/main/resources、src/test/java

    创建包 hello


pom.xml




    4.0.0
    com.caad.springboot.test
    Demo
    jar
    1.0-SNAPSHOT

    
        org.springframework.boot
        spring-boot-starter-parent
        2.0.0.M7
    
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
    
    
    
        
            spring-milestones
            Spring Milestones
            https://repo.spring.io/libs-milestone
            
                false
            
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

    
        
            spring-snapshots
            Spring Snapshots
            http://repo.spring.io/snapshot
            
                true
            
        
        
            spring-milestones
            Spring Milestones
            http://repo.spring.io/milestone
            
                false
            
        
        
            spring-releases
            Spring Releases
            http://repo.spring.io/release
            
                false
            
        
    



创建启动类 SampleController.java

package hello;

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;

@Controller
@EnableAutoConfiguration
public class SampleController {

    @RequestMapping("/")
    @ResponseBody
    String home() {
        return "Hello World!";
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(SampleController.class, args);
    }
    
}
  • 运行
mvn install
mvn spring-boot:run
  • 测试
http://localhost:8080/


标准的三层模型


  • 目录结构
- com.caad.springboot.test
    - controller
        - UserController.java
    - service
        - UserService.java
    - dao
        - UserDao.java
    - domain
        - enums
            - GenderType.java
        - User.java
    - Application.java

为什么service不使用interface了

  • 代码实现
public class User {

    private Long id;
    private String name;
    private GenderType gender;
    private Date createTime;

}

public enum GenderType {

    MALE,
    FEMALE,
    UNKNOW,
    OTHER;

}
@RestController
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/find/{id}")
    public User find(@PathVariable("id") Long id) {
        User user = userService.findById(id);
        return user;
    }

}
@Service
public class UserService {

    @Autowired
    private UserDao dao;

    public User findById(Long id) {
        return dao.findById(id);
    }

}
@Repository
public class UserDao {

    public User findById(Long id) {
        User user = new User();
        user.setId(123L);
        user.setName("test");
        user.setGender(GenderType.UNKNOW);
        user.setCreateTime(new Date());
        return user;
    }

}
  • 测试
    http://localhost:8080/user/find/1


集成mybatis


  • 添加依赖maven依赖

    mysql
    mysql-connector-java



    org.mybatis.spring.boot
    mybatis-spring-boot-starter
    1.3.1

  • 添加springboot配置文件 application.yml
server.port: 8888

spring.datasource:
  driverClassName: com.mysql.jdbc.Driver
  url: jdbc:mysql://172.16.2.154:3307/aqs_test?useUnicode=true&characterEncoding=utf-8
  username: 
  password: r5rD6a8NBnWP9NGs

mybatis:
  config-locations: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
  type-aliases-package: com.caad.springboot.test.domain

  • 添加mybatis配置文件 mybatis-config.xml


    
    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    


  • 修改DAO注解
@Mapper
public interface UserDao {

    @Select("SELECT id, name, gender, createTime FROM User where id=#{id}")
    public User findById(Long id);

}
  • 初始化数据库
CREATE TABLE `User` (
  `id` bigint(20) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  `gender` varchar(20) DEFAULT NULL,
  `createTime` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • CRUD
@Insert("INSERT INTO User(id,name,gender,createTime) VALUES(#{id}, #{name}, #{gender}, #{createTime})")
void insert(User user);

@Delete("DELETE FROM User WHERE id = #{id}")
void delete(Long id);

@Update("UPDATE User SET name=#{name},gender=#{gender},createTime=#{createTime} WHERE id =#{id}")
void update(User user);

@Select("SELECT id, name, gender, createTime FROM User")
List findAll();
@Service
public class UserService {

    @Autowired
    private UserDao dao;

    public User findById(Long id) {
        return dao.findById(id);
    }

    public User save(User user){
        if(user==null) return null;
        if(user.getId()==null){
            dao.insert(user);
        }else {
            dao.update(user);
        }
        return user;
    }

    public void remove(Long id){
        dao.delete(id);
    }

    public List findAll(){
        return dao.findAll();
    }

}
  • 添加单元测试依赖

    org.springframework.boot
    spring-boot-starter-test

  • 编写测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserServiceTest {

    @Autowired
    public UserService service;

    @Test
    public void CRUDTest() {

        //CREATE
        User o = new User();
        o.setCreateTime(new Date());
        o.setName("CRUDTest");
        service.save(o);
        Assert.assertNotNull(o.getId());

        //READ
        o = service.findById(o.getId());
        Assert.assertNotNull(o.getId());

        //UPDATE
        o.setName("CRUDTest1");
        service.save(o);
        o = service.findById(o.getId());
        Assert.assertTrue(o.getName().equals("CRUDTest1"));

        //DELETE
        service.remove(o.getId());
        o = service.findById(o.getId());
        Assert.assertNull(o);

    }
    
}
  • 引入主键生成器 IdGenerator

  • XML方式实现DAO接口

List findByName(String name);

  • 添加mapper文件(文件名需要和接口名一致)





    
        
        
        
        
    

    
        id, name, gender, createTime
    

    


  • 添加logback.xml配置,查看sql



    
    
    
    
    
    

    
    
    
    
    

    
    
        
            ${LOG_PATTERN}
        
    

    
        
            ${LOG_PATTERN}
        
        ${LOG_FILE}
        
            ${LOG_FILE}.%i.zip
            1
            10
        
        
            10MB
        
    

    
    
        
        
    


  • 添加测试代码
List list = service.findByName(o.getName());
Assert.assertNotNull(list.size()>0);
  • 几次失误,导致了脏数据,映入测试回滚
@Transactional
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserServiceTest {
  • 添加controller端findAll接口
@RequestMapping(value = "/getAll")
public List getAll() {
    return userService.findAll();
}
  • 引入分页插件

    com.github.pagehelper
    pagehelper-spring-boot-starter
    1.2.3


    
        
        
        
        
        
    

@RequestMapping(value = "/pageAll")
public PageInfo pageAll() {
    PageHelper.startPage(1, 5);
    List list = userService.findAll();
    return new PageInfo(list);
}


如何写一个对用户友好的接口


  • 问题

    用户有哪些,测试人员、开发人员、浏览器、调试工具、客户端程序等

    开发人员拿到没有统一格式的数据,没办法分层处理

    为了方便框架解析、分层控制,有必要规范输入输出格式

  • 事例 getUser
{
    "id": 4354523,
    "name":"张三"
}

{
    "errorCode":-10000,
    "message":"未登录"
}

{
    "bizCode":-1,
    "message":"所查用户不存在"
}
{
    "code": 1,
    "message":"",
    "data":{
        "id": 4354523,
        "name":"张三"
    }
}

{
    "code": -10000,
    "message":"未登录",
    "data":{
    }
}

{
    "code": -1,
    "message":"所查用户不存在",
    "data":{
    }
}
  • 引入ViewData
public class ViewData implements Serializable{

    private static final long serialVersionUID = 7408790903212368997L;

    private Integer code = 1;

    private String message;

    private T data;

    public ViewData(){}

    public ViewData(T obj) {
        this.data = obj;
    }

    public ViewData(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public ViewData(Integer code, String message, T obj) {
        this.code = code;
        this.message = message;
        this.data = obj;
    }

    public static  ViewData ok() {
        return new ViewData<>();
    }

    public static  ViewData ok(T obj) {
        return new ViewData<>(obj);
    }

    public static  ViewData error(String msg) {
        return new ViewData<>(-1, msg);
    }

    public static  ViewData error(Integer code, String msg) {
        return new ViewData<>(code, msg);
    }

}
  • 对输出数据进行格式化
package com.caad.springboot.test.api.resp;

public class UserResp {

    private Long id;
    private String username;
    private Date createTime;
    private GenderType gender;

}
@RequestMapping(value = "/find/{id}")
public ViewData find(@PathVariable("id") Long id) {
    User user = userService.findById(id);
    UserResp resp = new UserResp();
    resp.setCreateTime(user.getCreateTime());
    resp.setGender(user.getGender());
    resp.setUsername(user.getName());
    resp.setId(user.getId());
    return ViewData.ok(resp);
}
  • 使用@RequestBody对输入参数格式化
package com.caad.springboot.test.api.requ;

public class UserRequ {

    private Long id;
    private String name;
    private String gender;

    public UserRequ() { }

    public UserRequ(Long id, String name, String gender) {
        this.id = id;
        this.name = name;
        this.gender = gender;
    }

}

@RequestMapping(value = "/update")
public ViewData update(@RequestBody UserRequ userRequ) {
    User user = userService.findById(userRequ.getId());
    user.setName(userRequ.getName());
    user.setGender(GenderType.valueOf(userRequ.getGender()));
    userService.save(user);
    return ViewData.ok(user);
}
http://localhost:8080/user/update
{
    "id":1,
    "name":"hi boy",
    "gender":"MALE"
}

  • 自定义数据转换
@Component
public class JsonDataSerializer extends JsonSerializer {

    @Override
    public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        if(o==null) return;
        if(o instanceof Long){
            Long data = (Long) o;
            jsonGenerator.writeString(data.toString());
        } if(o instanceof Date){
            Date date = (Date) o;
            jsonGenerator.writeString(date.getTime() + "");
        }
    }

}

要装换的字段使用 @JsonSerialize(using = JsonDataSerializer.class)

处理空字段 @JsonInclude(Include.NON_NULL)

  • 使用map传参
@RequestMapping(value = "/findByName")
public ViewData> findByName(@RequestBody Map params) {
    List list = userService.findByName(params.get("name"));
    return ViewData.ok(list);
}
http://localhost:8080/user/findByName
{
    "name":"ZHANG"
}
  • 异常处理
public User addUser(User user) throws DataDuplicateException {
    List list = dao.findByName(user.getName());
    if(list!=null && list.size()>0){
        throw new DataDuplicateException("用户已经存在");
    }
    return this.save(user);
}
package com.caad.springboot.test.common.exception;

public class DataDuplicateException extends RuntimeException {

    public DataDuplicateException() {
        super();
    }

    public DataDuplicateException(String message) {
        super(message);
    }

}
@RequestMapping(value = "/add")
public ViewData add(@RequestBody UserRequ userRequ) {
    if(userRequ.getName()==null) return ViewData.error("用户名未填写");
    if(userRequ.getGender()==null) return ViewData.error("性别未填写");
    User user = new User();
    user.setCreateTime(new Date());
    user.setGender(GenderType.valueOf(userRequ.getGender()));
    user.setName(userRequ.getName());
    try {
        userService.addUser(user);
    }catch (DataDuplicateException e){
        return ViewData.error(-1, e.getMessage());
    }
    return ViewData.ok(user);
}
http://localhost:8080/user/add
{
    "name":"testsa2",
    "gender":"MALE"
}
  • 删除User接口
@RequestMapping(value = "/remove/{id}")
public ViewData remove(@PathVariable("id") Long id) {
    User user = new User();
    user.setId(id);
    user.setCreateTime(new Date());
    user.setGender(GenderType.UNKNOW);
    user.setName("test");
    userService.remove(user.getId());
    return ViewData.ok();
}

测试 http://localhost:8080/user/remove/1


mock模拟接口黑盒测试


  • 创建测试类UserControllerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserControllerTest{

    protected static final String SUCESS_CODE = "\"code\":1";

    @Autowired
    protected ObjectMapper objectMapper;

    protected MockMvc mvc;

    @Autowired
    UserController userController ;

    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.standaloneSetup(userController).build();
    }

    @Test
    public void testHelloController() throws Exception {

        //增加
        UserRequ requ = new UserRequ();
        requ.setName("01234567890123456789");
        requ.setGender("MALE");

        byte[] content = objectMapper.writeValueAsBytes(requ);

        RequestBuilder request = post("/user/add").accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON).content(content);
        MvcResult result = mvc.perform(request).andExpect(status().isOk()).andExpect(content().string(containsString(SUCESS_CODE))).andReturn();
        Integer status = result.getResponse().getStatus();
        Assert.assertTrue("正确", status == 200);

        String json = result.getResponse().getContentAsString();
        JavaType javaType = getCollectionType(ViewData.class, User.class);

        ViewData vd = objectMapper.readValue(json, javaType);
        Assert.assertTrue("出现业务异常", vd.getCode()==1);

    }

    private JavaType getCollectionType(Class collectionClass, Class... elementClasses) {
        return objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }

}
  • 将测试代码进行封装,减少重复劳动
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

public class ControllerSupperTest {

    protected static final String SUCESS_CODE = "\"code\":1";

    @Autowired
    protected ObjectMapper objectMapper;

    protected MockMvc mvc;

    protected ViewData doPost(String uri, Object requ, Class... elementClasses) throws Exception {

        byte[] content = "{}".getBytes();
        if(requ!=null) content = objectMapper.writeValueAsBytes(requ);

        RequestBuilder request = post(uri).accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON).content(content);
        //.andDo(MockMvcResultHandlers.print())
        MvcResult result = mvc.perform(request).andExpect(status().isOk()).andExpect(content().string(containsString(SUCESS_CODE))).andReturn();
        Integer status = result.getResponse().getStatus();
        Assert.assertTrue("正确", status == 200);

        String json = result.getResponse().getContentAsString();
        JavaType javaType = getCollectionType(ViewData.class, elementClasses);

        ViewData vd = objectMapper.readValue(json, javaType);
        Assert.assertTrue("出现业务异常", vd.getCode()==1);

        return vd;
    }

    private JavaType getCollectionType(Class collectionClass, Class... elementClasses) {
        return objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }

}
  • 调整测试类 UserControllerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserControllerTest extends ControllerSupperTest{

    @Autowired
    UserController userController ;

    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.standaloneSetup(userController).build();
    }

    @Test
    public void testHelloController() throws Exception {

        User user = null;

        //增加
        {
            UserRequ requ = new UserRequ();
            requ.setName("01234567890123456789");
            requ.setGender("MALE");

            ViewData data = (ViewData) this.doPost("/user/add", requ, User.class);
            Assert.assertTrue(data.getCode()==1);
            user = data.getData();
        }

    }

}
  • 完善测试方法
//修改
{
    UserRequ requ = new UserRequ();
    requ.setId(user.getId());
    requ.setName("testHelloController");
    requ.setGender("FEMALE");

    ViewData data = this.doPost("/user/update", requ, User.class);
    Assert.assertTrue(data.getCode()==1);
}

//查询,主键
{
    ViewData data = this.doPost("/user/find/" + user.getId(), null, UserResp.class);
    Assert.assertTrue(data.getCode()==1);
}

//查询,name
{
    Map map = new HashMap<>();
    map.put("name", "testHelloController");

    ViewData data = this.doPost("/user/findByName", map, List.class);
    Assert.assertTrue(data.getCode()==1);
}

//查找,所有
{
    ViewData data = this.doPost("/user/getAll", null, List.class);
    Assert.assertTrue(data.getCode()==1);
}

//查找,分页
{
    ViewData data = this.doPost("/user/pageAll",null, PageInfo.class);
    Assert.assertTrue(data.getCode()==1);
}

//删除
{
    ViewData data = this.doPost("/user/remove/" + user.getId(), null, PageInfo.class);
    Assert.assertTrue(data.getCode()==1);
}


抽取抽象逻辑,封装通用代码,控制框架行为


  • 抽取父类
public abstract class GenericService {

    @Autowired
    LongIdGenerator idGenerator;

    protected abstract GenericDao getDao();

    public T findById(Long id) {
        return getDao().findById(id);
    }

    public T save(T entity){
        if(entity==null) return null;
        if(entity.getId()==null){
            entity.setId(idGenerator.generate());
            getDao().insert(entity);
        }else {
            getDao().update(entity);
        }
        return entity;
    }

    public void remove(Long id){
        getDao().delete(id);
    }

    public List findAll(){
        return getDao().findAll();
    }

}

public interface GenericDao {

    public T findById(Long id);

    void insert(T entity);

    void delete(Long id);

    void update(T entity);

    List findAll();

}
public abstract class BaseEntity {

    public abstract Long getId();

    public abstract void setId(Long id);

}
  • 使用泛型主键
public abstract class BaseEntity {

    public abstract PK getId();

    public abstract void setId(PK id);

}

public interface GenericDao {

    public T findById(PK id);

    void insert(T entity);

    void delete(PK id);

    void update(T entity);

    List findAll();

}
public interface GenericDao {

    public T findById(PK id);

    void insert(T entity);

    void delete(PK id);

    void update(T entity);

    List findAll();

}

持续集成


  • 添加多环境配置
spring.profiles.active: '@profile.active@'
server.port: 8888

mybatis:
  config-locations: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
  type-aliases-package: com.caad.springboot.test.domain

---
spring:
  profiles: native
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://172.16.2.154:3307/aqs_test?useUnicode=true&characterEncoding=utf-8
    username: aqs
    password: r5rD6a8NBnWP9NGs

---
spring:
  profiles: dev
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://sql10.freemysqlhosting.net:3306/sql10210303?useUnicode=true&characterEncoding=utf-8
    username: sql10210303
    password: mWsVRVGwXD
---
  • 修改pom.xm

    
        dev
        
            dev
        
    
    
        native
        
            native
        
        
            true
        
    


    org.apache.maven.plugins
    maven-resources-plugin
    2.6
    
        
            
                src/main/resources/
                true
            
        
    

jenkins脚本

cd /root/.jenkins/workspace/test-1
mvn clean install -P dev
count=`ps -ef | grep Mybatis4Springboot | grep -v grep | wc -l`
if [ $count -gt 0 ];then
ps -ef | grep Mybatis4Springboot | grep -v grep | grep -v PID | awk '{print $2}' | xargs kill -9
fi
rm -rf /home/microservice/Mybatis4Springboot.jar
cp -rf ./target/Mybatis4Springboot-1.0.0-SNAPSHOT.jar /home/microservice/Mybatis4Springboot.jar

BUILD_ID=dontKillMe

nohup java -jar /home/microservice/Mybatis4Springboot.jar -Xmx512m -Xss256k >/dev/null &

其他资源


本文源码地址 https://github.com/bestaone/Mybatis4Springboot

阿里代码规范插件 https://github.com/alibaba/p3c

你可能感兴趣的:(Springboot2快速集成)