- 通过一个实例快速搭建SSM。
- 通过实例来看看各个环节的最佳实践。
- 如何使用业务异常。
- 如何设计一个对前端友好的接口。
- 通过单元测试使你的工作更加轻松和安全。
- 简单聊聊代码规范。
- 利用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
要装换的字段使用 @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