Spring使用过程中需要大量繁杂的XML文件配置,Spring3之后开始引入“约定大于配置”的理念,Spring Boot就是在这样的理念下抽象出来的框架。它本身并不替代、扩展Spring的特征,而是用于快速、敏捷开发Spring应用,以帮助开发者用少量的配置代码就可以快速上手Spring应用。此外还集成了一些第三方库用于零配置、开箱即用功能,以及大型项目常用的安全、配置等非功能性应用。
在Intellij IDEA中点击New Project,在其中选择Spring Initializr,可以设置项目的SDK,然后Next,
接着设置项目的Group、Artifact、Version等信息,点击Next
接着设置项目所需依赖以及spring boot的版本号,这里选择Spring Web和Mybatis,点击Next
最后选择项目存放位置并点击Finish,生成Spring Bo项目ot项目结构如左下,项目最后结果如右下所示:
由于项目使用Maven进行依赖管理,如果本地自己装有maven,则需要设置IDEA使用本地的maven而不是自身集成的,在idea的设置中搜索maven并设置其安装目录和以来仓库如下
接下来通过pom文件配置项目的依赖,如下所示为自动生成的pom.xml文件
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.1.RELEASEversion>
<relativePath/>
parent>
<groupId>com.torygroupId>
<artifactId>springbootdemoartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>springbootdemoname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.3version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.mchangegroupId>
<artifactId>c3p0artifactId>
<version>0.9.5.5version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
在标签中通过spring-boot-starter-parent
来对下面的spring相关的依赖版本进行管理,在parent中设置了版本version为2.3.1.RELEASE后,下面的相关依赖就不必再设置version了。
接下来是项目的groupId、artifactId、version、name、description、java版本等信息。
接着在标签中引入项目所需依赖,除了spring-boot-starter-web、mybatis-spring-boot-starter、spring-boot-starter-test之外,这里还使用了用于数据库连接的mysql-connector-java、连接池管理的c3p0
最后标签内是项目用到的构建工具
在application.properties文件中对数据库连接信息、mybatis属性进行设置如下
server.port代表项目的端口号,context-path代表项目根路径
# 项目端口、路径设置
server.port=8080
server.servlet.context-path=/SpringBootDemo
# 设置数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shop_demo
spring.datasource.username=root
spring.datasource.password=123456
# 设置mybatis
# 自动使用自增主键填充bean
mybatis.configuration.use-generated-keys=true
# 开启驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
# 允许使用别名替换列名
mybatis.configuration.use-column-label=true
# mapper文件地址
mybatis.mapper-locations=classpath:mapper/*.xml
# 给实体类设置别名
mybatis.type-aliases-package=com.tory.springbootdemo.entity
接下来使用Spring Boot实现一个简单的MVC的例子,实现对数据库区中域类型Area的增删改查请求操作
在数据库shop_demo下,存储区域信息的表格tb_area数据表结构如下:
在entity包下创建对应的实体类Area
package com.tory.springbootdemo.entity;
import java.util.Date;
public class Area {
private Integer areaId;
private String areaName;
private Integer priority;
private Date createTime;
private Date lastEditTime;
//getter and setter ......
}
在Dao层实现对数据库的增删改查操作,首先定义方法接口AreaDao
:
package com.tory.springbootdemo.dao;
import com.tory.springbootdemo.entity.Area;
import java.util.List;
public interface AreaDao {
//新增区域
int insertArea(Area area);
//删除区域
int deleteArea(int areaId);
//修改区域
int updateArea(Area area);
//查询所有区域
List<Area> queryArea();
//根据Id查询区域
Area queryAreaById(int areaId);
}
接着在resources下新建mapper文件夹用于存放mapper文件,在其中创建AreaDao.xml文件实现上面的接口操作
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tory.springbootdemo.dao.AreaDao">
<insert id="insertArea" useGeneratedKeys="true" keyProperty="areaId"
keyColumn="area_id" parameterType="com.tory.springbootdemo.entity.Area">
INSERT INTO tb_area(area_name, priority, create_time, last_edit_time)
VALUES (#{areaName}, #{priority}, #{createTime}, #{lastEditTime})
</insert>
<delete id="deleteArea">
DELETE FROM tb_area
WHERE area_id = #{areaId}
</delete>
<update id="updateArea" parameterType="com.tory.springbootdemo.entity.Area">
update tb_area
<set>
<if test="areaName != null">area_name=#{areaName},</if>
<if test="priority != null">priority=#{priority},</if>
<if test="lastEditTime != null">last_edit_time=#{lastEditTime}</if>
</set>
where area_id=#{areaId}
</update>
<select id="queryArea" resultType="com.tory.springbootdemo.entity.Area">
SELECT area_id, area_name, priority, create_time, last_edit_time
FROM tb_area
ORDER BY priority DESC
</select>
<select id="queryAreaById" resultType="com.tory.springbootdemo.entity.Area">
SELECT area_id, area_name, priority, create_time, last_edit_time
FROM tb_area
WHERE area_id = #{areaId}
</select>
</mapper>
最后需要在Application的main()方法上添加注解@MapperScan
开启mapper的扫描配置,扫描com.tory.springbootdemo.dao目录下的dao类
package com.tory.springbootdemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.tory.springbootdemo.dao")
public class SpringbootdemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootdemoApplication.class, args);
}
}
可以通过Test类测试上面的方法是否操作成功,在AreaDao类中点击Alt+Insert,弹出Generate提示,选择Test自动在test文件夹生成对应测试类AreaDaoTest
,运行queryArea()输出结果正确
package com.tory.springbootdemo.dao;
import com.tory.springbootdemo.entity.Area;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class AreaDaoTest {
@Autowired
private AreaDao areaDao;
@Test
void queryArea() {
List<Area> areaList=areaDao.queryArea();
for (Area area :areaList) {
System.out.println(area.getAreaName());
}
}
}
在项目目录com.tory.springbootdemo下新建service包用于存放service文件,在其中创建AreaService
接口,定义增删改查方方法:
package com.tory.springbootdemo.service;
import com.tory.springbootdemo.entity.Area;
import java.util.List;
public interface AreaService {
//创建Area对象
boolean addArea(Area area);
//通过Id删除Area
boolean deleteAreaById(int areaId);
//更新Area
Area updateArea(Area area);
//通过Id查询Area
Area queryAreaById(int areaId);
//查询所有Area
List<Area> queryAll();
}
接着创建impl文件夹用于存放接口的实现类,在其中实现AreaServiceImpl
,通过调用Dao层的方法完成具体的增删改查操作。对相关异常不进行处理而是简单抛出RuntimeException,之后再封装相关异常类。
为该类添加@service
注解表示这是一个service类。
对于增加、修改、删除需要进行事务管理,在对应的方法上添加注解@Transactional
package com.tory.springbootdemo.service.impl;
import com.tory.springbootdemo.dao.AreaDao;
import com.tory.springbootdemo.entity.Area;
import com.tory.springbootdemo.service.AreaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Service
public class AreaServiceImpl implements AreaService {
@Autowired
private AreaDao areaDao;
@Override
@Transactional
public boolean addArea(Area area) {
// 判断areaName不为空
if (area.getAreaName() != null && !"".equals(area.getAreaName())) {
// 设置时间
area.setCreateTime(new Date());
area.setLastEditTime(new Date());
try {
int effectedNum = areaDao.insertArea(area);
if (effectedNum > 0) {
return true;
} else {
throw new RuntimeException("添加区域信息失败!");
}
} catch (Exception e) {
throw new RuntimeException("添加区域信息失败:" + e.toString());
}
} else {
throw new RuntimeException("区域信息不能为空!");
}
}
@Override
@Transactional
public boolean deleteArea(int areaId) {
if (areaId > 0) {
try {
// 删除区域信息
int effectedNum = areaDao.deleteArea(areaId);
if (effectedNum > 0) {
return true;
} else {
throw new RuntimeException("删除区域信息失败!");
}
} catch (Exception e) {
throw new RuntimeException("删除区域信息失败:" + e.toString());
}
} else {
throw new RuntimeException("区域Id不能为空!");
}
}
@Override
@Transactional
public Area updateArea(Area area) {
if (area.getAreaId() != null && area.getAreaId() > 0) {
//如果Id合法
if ((area.getAreaName() != null && !area.getAreaName().trim().equals("")) || area.getPriority() != null) {
//更新内容不为空
area.setLastEditTime(new Date());
int count = areaDao.updateArea(area);
if (count < 1) {
throw new RuntimeException("更新失败");
}
} else {
throw new RuntimeException("更新内容为空");
}
} else {
throw new RuntimeException("Id不合法");
}
return areaDao.queryAreaById(area.getAreaId());
}
@Override
public Area getAreaById(int areaId) {
return areaDao.queryAreaById(areaId);
}
@Override
public List<Area> getAreaList() {
return areaDao.queryArea();
}
}
在项目的web目录下创建AreaController
类来对前端的请求进行管理。
为该类添加注解@RestController
,它等于@ResponseBody+@Controller,代表这是一个Controller类,并且将返回的数据对象转换为json格式。@RequestMapping
代表对指定的请求路径进行响应,value = "area"代表响应“area/"的web请求,produces
属性指定响应返回的编码格式防止出现中文乱码。
对于Controller中的具体方法,也需要使用@RequestMapping来指定请求路径,特别地,如果是Get请求,则可以用@GetMapping
来代替,对应的Post请求有@PostMapping
。例如@GetMapping(“list”),则浏览器访问“项目路径/area/list”就可以得到区域列表。
在具体的实现中通过调用Service层的接口实现对area区域的增删改查请求的响应,并将返回的结果以放在modelMap中,由于之前使用了@RequestMapping,返回结果会自动转换为Json格式。
package com.tory.springbootdemo.web;
import com.tory.springbootdemo.entity.Area;
import com.tory.springbootdemo.service.AreaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping(value = "area", produces = "application/json;charset=utf-8")
public class AreaController {
@Autowired
private AreaService areaService;
@PostMapping("add")
private Map<String, Object> addArea(@RequestBody Area area) {
Map<String, Object> modelMap = new HashMap<>();
// 添加区域信息
modelMap.put("success", areaService.addArea(area));
return modelMap;
}
@GetMapping("remove")
private Map<String, Object> removeArea(Integer areaId) {
Map<String, Object> modelMap = new HashMap<String, Object>();
// 删除区域信息
modelMap.put("success", areaService.deleteArea(areaId));
return modelMap;
}
@PostMapping("update")
private Map<String, Object> modifyArea(@RequestBody Area area) {
Map<String, Object> modelMap = new HashMap<>();
// 修改区域信息
modelMap.put("success", areaService.updateArea(area));
return modelMap;
}
@GetMapping("getById")
private Map<String, Object> getAreaById(Integer areaId) {
Map<String, Object> modelMap = new HashMap<>();
// 获取区域信息
Area area = areaService.getAreaById(areaId);
modelMap.put("area", area);
return modelMap;
}
@GetMapping("list")
private Map<String, Object> listArea() {
Map<String, Object> modelMap = new HashMap<>();
// 获取区域列表
List<Area> list = areaService.getAreaList();
modelMap.put("areaList", list);
return modelMap;
}
}
接着在IDEA中启动Spring boot项目,或者在项目根目录下输入./mvnw spring-boot:run
在浏览器中访问http://localhost:8080/SpringBootDemo/area/list,以Json格式返回区域列表如下:
之前在Service层抛出的异常类型都是RuntimeException
类型,这种异常抛出后程序就会终止执行,使得程序健壮性不好。我们可以自定义异常类型并进行捕获,然后返回结果信息给用户,这样程序并不会崩溃。
例如我们在项目的exception包下自定义异常类型AreaException
,简单定义构造方法和getter方法
package com.tory.springbootdemo.exception;
public class AreaException extends Exception{
private String message;
public AreaException(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
}
重写Service层的updateArea()
方法,将其中的RuntimeException改为AreaException并且向上抛出
@Override
@Transactional
public Area updateArea(Area area) throws AreaException {
if (area.getAreaId() != null && area.getAreaId() > 0) {
if ((area.getAreaName() != null && !area.getAreaName().trim().equals("")) || area.getPriority() != null) {
area.setLastEditTime(new Date());
int count = areaDao.updateArea(area);
if (count < 1) {
throw new AreaException("数据库更新失败");
}
} else {
throw new AreaException("更新内容为空");
}
} else {
throw new AreaException("Id不合法"); //抛出AreaException类型的异常
}
return areaDao.queryAreaById(area.getAreaId());
}
接着在handler包下定义AreaExceptionHandler
类,对AreaException类型的异常进行捕获处理,设置success为false并以json的格式返回异常信息
需要添加@ControllerAdvice
表示对Controller中的异常进行捕获,@ExceptionHandler(AreaException.class)
表示某个方法只捕获特定类型的异常
package com.tory.springbootdemo.handler;
import com.tory.springbootdemo.exception.AreaException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class AreaExceptionHandler {
@ExceptionHandler(AreaException.class)
@ResponseBody
private Map<String, Object> handleException(HttpServletRequest request, HttpServletResponse response, Exception e) {
Map<String, Object> modelMap = new HashMap<>();
modelMap.put("success", false);
modelMap.put("errMsg", e.getMessage());
response.setCharacterEncoding("utf-8"); //防止中文乱码
return modelMap;
}
}
当我们查询的id不存在时,程序并不会因异常而终止,而是抛出AreaException并被捕获处理,然后返回错误信息如下:
{"success":false,"errMsg":"Id不合法"}
在没有前端页面的情况下如果希望对服务器发送post测试请求可以使用MockMvc
。
在AreaController中点击Alt+Insert,选择Test自动生成对应的测试类AreaControllerTest
,为其添加注解@SpringBootTest
,并通过classes
属性为其指定启动类
首先通过@Autowired获取上下文对象,然后将其作为参数,在测试方法执行之前的@BeforeEach
通过MockMvcBuilders创建mockMvc
对象
在测试方法testAdd()
中,我们创建一个area对象,通过ObjectMapper
将其转化为json字符串,并以post请求发送给/area/add以测试添加区域的功能。
mockMvc通过perform()
方法模拟请求,在其中通过MockMvcRequestBuilders
的post()
方法发送post请求,并通过content()
方法指定发送的内容,contentType()
指定发送的格式
mockMvc通过andExpect()
对返回的状态码进行校验,andDo()
执行请求回调操作,andReturn()
获取返回的内容。
package com.tory.springbootdemo.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tory.springbootdemo.SpringbootdemoApplication;
import com.tory.springbootdemo.entity.Area;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(classes = SpringbootdemoApplication.class)
class AreaControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext context; //获取上下文对象
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public void testAdd() throws Exception {
Area area = new Area();
area.setAreaId(6);
area.setAreaName("南苑");
area.setPriority(5);
ObjectMapper mapper = new ObjectMapper();
String jsonStr = mapper.writeValueAsString(area); //将area对象转换为json字符串
String response = mockMvc.perform( //发送请求
MockMvcRequestBuilders.post("/area/add")
.content(jsonStr).contentType(MediaType.APPLICATION_JSON_UTF8)
)
.andExpect(MockMvcResultMatchers.status().isOk()) //校验返回的状态码
.andDo(MockMvcResultHandlers.print()) //执行回调操作,打印HTTP请求与响应信息
.andReturn().getResponse().getContentAsString(); //获取返回字符串
System.out.println(response);
}
}
测试打印的HTTP请求与相应结果如下所示,并且相应的area信息也添加到了数据库中
所有代码文件参见:https://github.com/SuperTory/SpringBootDemo/tree/master/Server