慕课网 Spring Boot 入门和进阶
课程链接地址:
目录
- 入门
- 1.第一个SpringBoot程序
- 2.自定义属性配置
- 3.Controller的使用
- 4.Spring-data-jpa
- 进阶
- 5.表单验证
- 6.AOP处理请求
- 7.统一异常处理
- 8.单元测试
1.第一个SpringBoot程序
1.1创建新的工程步骤:
1.2修改maven的默认选项,不修改的话,会报错误找不到spring*的包。
1.3目录结构
1.4pom.xml文件
- 默认生成,无需修改
1.5程序启动入口
- GirlApplication类上有标注@SpringBootApplication,可以右键单击此类进行启动项目。
1.6添加新的访问
- 给新的Controller类加上注解@RestController
- 给say()方法加上注解@RequestMapping(),value是路径,method是请求方式。
- 启动项目后,访问http://127.0.0.1:8080/hello
2.自定义属性配置
2.1用properties文件
2.2用yml文件
- 格式相对于properties文件更简便。
- 关键词:空格+值
2.3用注入方式配置变量
- 在注入时定义变量类型,配置时不用定义。例如图中的cupSize,并不是在配置文件中定义的类型,而是在Controller类引入时定义的,private String cupSize
- 也可以在配置文件中,再使用配置。
2.4配置文件的分组配置使用
- 配置文件的属性分组
- 创建属性类,加入注解@Component,@ConfigurationProperties(prefix = "girl")
- Controller类,@Autowired注解引用属性类对象,注意给引用的类加上@Component注解,这里是GirlProperties类
2.5不同环境下不同配置的用法。
- 不同的启动方式
- 命令行启动prod环境
- 项目目录下输入mvn install,等待maven编译完成
- 输入java -jar target/girl-0.0.1-SHAPSHOT.jar --spring.profiles.active=prod,项目启动
- IDE中启动dev环境
- 分别访问 http://127.0.0.1:8080/hello,http://127.0.0.1:8081/hello,得到2个环境下的结果。
- 命令行启动prod环境
3.Controller的使用
- @Controller:处理http请求
- @RestController:Spring4之后新加的注解,原来返回json需要@ResponseBody配合@Controller
- @RequestMapping:配置url映射
3.1Controller
- 使用模板,类似于jsp页面,pom文件中加入模板引擎thymeleaf依赖。
<!--spring官方的模板,因为用模板会影响性能,所以不建议使用,改用前后端分离Restful-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- resources目录下,加入文件夹templates,加入index.html页面。
<h1>hello Spring Boot!</h1>
- Controller类中。方法返回值为return "index";
@Controller
public class HelloController {
//使用模板 返回index.html
@RequestMapping(value = "{"/hi","/hello"}",method = RequestMethod.GET)
public String say() {
return "index";
}
}
3.2RestController=Controller+ResponseBody
3.3RequestMapping
- @PathVariable获取url中的数据,请求地址:/http/say/10
- @RequestParam获取请求参数的值,请求地址:/http/say?id=10
- @GetMapping组合注解,@RequestMapping(value = "{"/say"}",method = RequestMethod.GET)简写为@GetMapping(value = "/say")
@GetMapping(value = "/say/{id}")
//@GetMapping(value = "/{id}/say")
//请求地址:/http/say/10
public String say(@PathVariable("id") Integer myId) {
return "id: " + myId;
}
//@RequestMapping(value = "/say2",method = RequestMethod.GET)
//请求地址:/http/say2?id=10,required是否必传,defaultValue默认值,不能是int,需要是字符"0"
public String say2(@RequestParam(value="id",required=false,defaultValue="0") Integer myId) {
return "id: " + myId;
}
4.数据库操作Spring-data-jpa
- jpa定义了一系列对象持久化的标准,可以看做是spring对hibernate的整合。
4.1 RESTful API设计
4.2 添加依赖和配置文件
- pom.xml文件,加入jpa依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- application.yml文件,加入datasource和jpa配置
- ddl-auto的参数,常用的有create,update
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/dbgirl
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
4.3 创建实体类
- 新建Girl类,并加上@Entity,类中的属性值对应数据库表中的字段。
- @GeneratedValue注释为自增长,
- @Id表示id字段,
- 必须要有空参数的构造方法。
@Entity
public class Girl {
@Id
@GeneratedValue//自增长注解
private Integer id;
//@NotBlank(message = "这个字段必须传")
private String cupSize;
@Min(value = 18, message = "未成年少女禁止入门")
private Integer age;
public Girl() {}
public Integer getId() {return id;}
public void setId(Integer id) {this.id = id;}
public String getCupSize() {return cupSize;}
public void setCupSize(String cupSize) {this.cupSize = cupSize;}
public Integer getAge() {return age;}
public void setAge(Integer age) {this.age = age;}
}
4.4 Controller类中写处理方法
- 创建Controller类,根据RESTful API,创建增删改查方法。
- 创建Repository接口,继承
JpaRepository<Girl, Integer>
。括号里是我们的类类型和id类型。 - 如果JpaRepository中的方法不够用,就在自己的Repository接口中扩展新的方法。
public List<Girl> findByAge(Integer age)
@RestController
public class GirlController {
@Autowired
private GirlRepository girlRepository;
//查询所有女生,get方式,/girls
@GetMapping(value="/girls")
public List<Girl> girlList(){
return girlRepository.findAll();
}
//添加一个女生,post方式,/girls
@PostMapping(value = "/girls")
public Girl girlAdd(@RequestParam("cupSize") String cupSize,
@RequestParam("age") Integer age){
Girl girl = new Girl();
girl.setCupSize(cupSize);
girl.setAge(age);
return girlRepository.save(girl);
}
//查询一个
@GetMapping(value="/girls/{id}")
public Girl girlFindOne(@PathVariable("id") Integer id){
return girlRepository.findOne(id);
}
//更新
@PutMapping(value="/girls/{id}")
public Girl girlUpdate(@PathVariable("id") Integer id,
@RequestParam("cupSize") String cupSize,
@RequestParam("age") Integer age){
Girl girl = new Girl();
girl.setid(id);
girl.setCupSize(cupSize);
girl.setAge(age);
return girlRepository.save(girl);
}
//删除
@DeleteMapping(value="/girls/{id}")
public String girlDelete(@PathVariable("id") Integer id){
girlRepository.delete();
}
//通过年龄查询出列表
@GetMapping(value="/girls/age/{age}")
public List<Girl> girlListByAge(@PathVariable("age") Integer age){
return girlRepository.findByAge(age);
}
}
//自己写子类来扩展方法。
public interface GirlRepository extends JpaRepository<Girl, Integer> {
//扩展JpaRepository方法,通过年龄来查询
public List<Girl> findByAge(Integer age);
}
- put方式,需要选择x-www-form-urlencoded,不能选择form-data,multipart/form-data与x-www-form-urlencoded区别:
- multipart/form-data:既可以上传文件等二进制数据,也可以上传表单键值对,只是最后会转化为一条信息;
- x-www-form-urlencoded:只能上传键值对,并且键值对都是间隔分开的。
4.5 事务管理
- 给自己的业务方法加上@Transactional,一般只有查询的时候不用加事务。
- 数据库中cupSize字段,改成1个字符长度。插入girlB数据就不成功了。
@Service
public class GirlService {
@Autowired
private GirlRepository girlRepository;
@Transactional
public void insertTwo() {
Girl girlA = new Girl();
girlA.setCupSize("A");
girlA.setAge(18);
girlRepository.save(girlA);
Girl girlB = new Girl();
girlB.setCupSize("BBBB");
girlB.setAge(19);
girlRepository.save(girlB);
}
}
@RestController
public class GirlController {
……
@Autowired
private GirlService girlService;
……
//事务管理业务方法
@PostMapping(value = "/girls/two")
public void girlTwo() {
girlService.insertTwo();
}
}
PS:项目进行分层整理,然后进入下个阶段
5.表单验证
5.1 修改添加方法,参数改为实体对象
/*修改添加的方法
@PostMapping(value = "/girls")
public Girl girlAdd2(@RequestParam("cupSize") String cupSize,@RequestParam("age") Integer age){
Girl girl= new Girl();
girl.setCupSize(cupSize);
girl.setAge(age);
return girlRepository.save(girl);
}*/
//修改后的方法,参数是一个实体
@PostMapping(value = "/girls")
public Girl girlAdd(Girl gril) {
girl.setCupSize(girl.getCupSize());
girl.setAge(girl.getAge());
return girlRepository.save(girl);
}
5.2 给添加的对象加验证
* Girl类中给age变量加18岁限制条件。 ` @Min(value = 18,message = "未成年少女禁止入门")`
* GirlController类中添加BindingResult参数及方法,验证的结果会放到这个对象中。
//Girl类中给age变量加18岁限制条件。
private String cupSize;
@Min(value = 18,message = "未成年少女禁止入门")
private Integer age;
//GirlController类中添加BindingResult参数及方法。
@PostMapping(value = "/girls")
public Girl girlAdd(@Valid Girl girl, BindingResult bindingResult) {
if (bindingResult.hasErrors()){
System.out.println(bindingResult.getFieldError().getDefaultMessage());
return null;
}
girl.setCupSize(girl.getCupSize());
girl.setAge(girl.getAge());
return girlRepository.save(girl);
}
6.AOP处理请求
- 用来进行统一的操作处理
6.1 一、pom.xml文件添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
6.2 二、启动类GirlApplication.class上加注解,但是aop不用加
6.3 三、建立处理文件HttpAspect.class
- @Before和@After注解,可以增加对Controller类中方法访问时需要进行的操作。
- 增加@Pointcut后,可以在切面上操作。
- logger.info("这个方法可以打印日志");,这个方法可以打印日志
package com.imooc.aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect//1.加入此注解
@Component//2.将HttpAspect 加入到spring容器
public class HttpAspect {
private final static Logger logger= LoggerFactory.getLogger(HttpAspect.class);
@Pointcut("execution(public * com.imooc.controller.GirlController.*(..))")
public void log(){
}
//@Before("execution(public * com.imooc.controller.GirlController.girlList(..))")//拦截getList方法的任何参数都拦截
//@Before("execution(public * com.imooc.controller.GirlController.*(..))")//拦截所有方法
@Before("log()")
public void doBefore(){
//System.out.println("Before1111");
logger.info("doBefore1111");
}
//@After("execution(public * com.imooc.controller.GirlController.*(..))")
@After("log()")
public void doAfter(){
//System.out.println("After22222");
logger.info("doAfter2222");
}
}
6.4 四、测试
- 给Controller.class中的getList()方法,添加查看顺序的语句
private final static Logger logger = LoggerFactory.getLogger(GirlController.class);
/**
* 查询所有女生列表
* 和girlAdd()访问地址相同,注意用get方式提交是查询。post是添加
* @return
*/
@GetMapping(value = "/girls")
public List<Girl> girlList() {
//System.out.println("getList 查看执行顺序");
logger.info("getList 执行");
return girlRepository.findAll();
}
6.5 扩展
- 在HttpAspect文件中,处理http请求头中的路径url,method,ip,类方法,参数,获取返回的对象等。
//@Before("execution(public * com.imooc.controller.GirlController.girlList(..))")//拦截getList方法的任何参数都拦截
//@Before("execution(public * com.imooc.controller.GirlController.*(..))")//拦截所有方法
@Before("log()")
public void doBefore(JoinPoint joinPoint){
//System.out.println("Before1111");
//logger.info("doBefore1111");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//url
logger.info("url={}",request.getRequestURL());
//method
logger.info("method={}",request.getMethod());
//ip
logger.info("ip={}",request.getRemoteAddr());
//类方法
logger.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
//参数
logger.info("args={}",joinPoint.getArgs());
}
//@After("execution(public * com.imooc.controller.GirlController.*(..))")
@After("log()")
public void doAfter(){
//System.out.println("After22222");
logger.info("doAfter2222");
}
//获取返回的内容,比如此实例中返回的json对象
@AfterReturning(pointcut = "log()",returning="object")
public void doAfterReturning(Object object){
logger.info("response={}",object);//Girl实体类中没有toString()方法时,打印对象内存地址
logger.info("response={}",object.toString());//Girl实体类中添加toString方法,
}
7.统一异常处理
7.1 异常情况模拟
- 给Girl类添加金额属性,加上必传验证,生成get/set方法。
@Id
@GeneratedValue
private Integer id;
@NotBlank(message = "这个字段必须传")
private String cupSize;
@Min(value = 18, message = "未成年少女禁止入门")
private Integer age;
@NotNull(message = "金额必传")
private Double money;
- 直接测试新增girlAdd()方法,不传金额,前台返回错误页面500。
- 控制台是空指针异常,因为不能通过表单验证,girlAdd()方法返回的是null对象,HttpAspect类中的doAfterReturning()方法接收的是null。(注释logger.info()语句可以解决,此为忽略的方法。)
7.2定义发生错误或异常时返回的数据封装,格式
- 控制台打印getDefaultMessage()结果,改为返回给前台。
- 方法返回值改Girl为Object,getDefaultMessage()方法返回的是String类型,save()方法返回Girl类型,所有就定义为Object。
/**
* 添加一个女生
* @return
*/
@PostMapping(value = "/girls")//注意用get方式提交是查询。post是添加
public Object girlAdd(@Valid Girl girl, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return bindingResult.getFieldError().getDefaultMessage();
}
return girlRepository.save(girl);
}
- 返回的格式整理如下,返回3个字段的数据。
//错误时 //正确时
{ {
"code": 1, "code": 0,
"msg": "金额必传", "msg": "成功",
"data": null "data": {
} "id": 11,
"cupSize": "A",
"age": 29,
"money": 300
}
}
- domain包下,新建Result类,用来定义结果对象。
public class Result<T> {
private Integer code;//错误码
private String msg;//提示信息
private T data;//具体内容
public Integer getCode() {return code;}
public void setCode(Integer code) {this.code = code;}
public String getMsg() {return msg;}
public void setMsg(String msg) {this.msg = msg;}
public T getData() {return data;}
public void setData(T data) {this.data = data;}
}
- 再次改造Controller
@PostMapping(value = "/girls")//注意用get方式提交是查询。post是添加
public Result<Girl> girlAdd(@Valid Girl girl, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Result result = new Result();
result.setCode(1);
result.setMsg(bindingResult.getFieldError().getDefaultMessage());
return result;
}
Result result = new Result();
result.setCode(0);
result.setMsg("成功");
result.setData(girlRepository.save(girl));
return result;
}
- 因为重复代码,写一个ResultUtil工具类
public class ResultUtil {
public static Result succss(Object object){
Result result = new Result();
result.setCode(0);
result.setMsg("成功");
result.setData(object);
return result;
}
//成功时也可能不含Object
public static Result success(){
return succss(null);
}
public static Result error(Integer code,String msg){
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
- 用ResultUtil工具类,简化Controller代码
public Result<Girl> girlAdd(@Valid Girl girl, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultUtil.error(1,bindingResult.getFieldError().getDefaultMessage());
}
return ResultUtil.succss(girlRepository.save(girl));
}
7.3在Service类中写自己的业务逻辑
- 业务需求,获取某女生的年龄并判断:
- 小于10,返回“应该在上小学”。
- 大于10且小于16,返回“可能在上初中”。
@GetMapping(value = "/girls/getAge/{id}")
public void getAge(@PathVariable("id") Integer id) throws Exception {
girlService.getAge(id);
}
- 在Service中写业务逻辑
public void getAge(Integer id) throws Exception {
Girl girl = girlRepository.findOne(id);
Integer age = girl.getAge();
if (age < 10) {
//返回,你还在上小学吧
throw new Exception("你还在上小学吧");
} else if (age > 10 && age < 16) {
//返回,你可能在上初中
throw new Exception("你可能上初中");
}
}
- 请求id号为12号的girl信息,前台控制台返回的结果:
7.4异常捕获
- 通过上面的异常,里面的格式并不是我们想要的,所以需要自己写一个类捕获异常类:ExceptionHandle。
- 因为返回浏览器的数据是json,而这个类又没有RestController注释,需要给ExceptionHandle.handle方法加入ResponseBody注解。
@ControllerAdvice
public class ExceptionHandle {
@ExceptionHandler(value = Exception.class)//声明捕获哪个异常类
@ResponseBody//返回浏览器是json,而这个类又没有RestController注释,就得加入ResponseBody注释
public Result handle(Exception e) {
return ResultUtil.error(100,e.getMessage());
}
}
- 执行以后,前台和控制台的返回结果就是我们想要的格式了。前台是3个字段数据的json,控制台不报异常信息。
- 业务处理逻辑保留在一个环节:Service.getAge(),验证<10,就直接往外抛异常,Controller.getAge()方法调用Service.getAge()方法,不用处理,也是抛出异常,最终由handle捕获处理。
7.5自定义异常
- 创建自定义异常类,定义一个code变量,记录错误代码。
- Spring框架中,自定义异常只有继承RuntimeException才能支持事务回滚。
public class GirlException extends RuntimeException {
//spring框架中,自定义异常只有继承RuntimeException才能支持事务回滚
private Integer code;//定义一个code变量,错误代码
public GirlException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {return code;}
public void setCode(Integer code) {this.code = code;}
}
- 使用自定义异常后的Service
public void getAge(Integer id) throws Exception {
Girl girl = girlRepository.findOne(id);
Integer age = girl.getAge();
if (age < 10) {
//返回,你还在上小学吧
throw new GirlException(100, "你还在上小学吧");
} else if (age > 10 && age < 16) {
//返回,你可能在上初中
throw new GirlException(101, "你可能上初中");
}
}
- 捕获异常类,加入代码
if (e instanceof GirlException){}
,作用是判断异常是不是自定义的异常。 - 加入日志记录,可以让控制台打印出异常信息,不加控制台是看不到的。
@ControllerAdvice
public class ExceptionHandle {
private final static Logger logger = LoggerFactory.getLogger(ResponseBody.class);
@ExceptionHandler(value = Exception.class)//声明捕获哪个异常类
@ResponseBody//返回浏览器是json,而这个类又没有RestController注释,就得加入ResponseBody注释
public Result handle(Exception e) {
if (e instanceof GirlException) {
GirlException girlException = (GirlException) e;
return ResultUtil.error(girlException.getCode(), girlException.getMessage());
} else {
logger.error("【系统异常】{}", e);
return ResultUtil.error(-1, "未知错误");
}
}
}
7.6异常错误代码代码管理
- 创建枚举类型的类来管理错误代码。
public enum ResultEnum {
UNKONW_ERROR(-1, "未知错误"),
SUCCESS(0, "成功"),
PRIMARY_SCHOOL(100, "你可能还在上小学"),
MIDDLE_SCHOOL(101, "你可能在上初中"),;
private Integer code;
private String msg;
ResultEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {return code;}
public String getMsg() {return msg;}
}
- 修改GirlException的构造方法。
public GirlException(ResultEnum resultEnum) {
super(resultEnum.getMsg());
this.code = resultEnum.getCode();
}
- 修改Service代码,参数改为枚举类型的常量
if (age < 10) {
//throw new GirlException(100, "你还在上小学吧");
throw new GirlException(ResultEnum.PRIMARY_SCHOOL);
} else if (age > 10 && age < 16) {
//throw new GirlException(101, "你可能上初中");
throw new GirlException(ResultEnum.MIDDLE_SCHOOL);
}
8.单元测试
- 使用场景,测试Service中的方法,通过id查询一个女生信息并返回。
- 使用场景,测试Controller方法,浏览器返回的状态码,返回的内容。
8.1 测试方法1,通过test目录下的测试类service:
- 创建Test类,加上注解@RunWith(SpringRunner.class),表示使用测试类(底层使用junit),注解@SpringBootTest,能够启动整个工程。其他测试如junit测试,@Test,Assert断言。
@RunWith(SpringRunner.class)//使用测试
@SpringBootTest//启动整个spring工程
public class GirlServiceTest {
@Autowired
private GirlService girlService;
@Test
public void findOneTest(){
Girl girl = girlService.findOne(12);
Assert.assertEquals(new Integer(9),girl.getAge());
}
}
8.2 测试方法2,AutoConfigureMockMvc测试controller:
- 通过IDEA自带测试方法,右键要测试的方法,选择Go To--Test--选择要测试的方法,然后会生成相应的测试类。
- 给类加上@AutoConfigureMockMvc注解,结合mvc.perform()
- MockMvcResultMatchers.status().isOk()测试返回状态码,
- MockMvcResultMatchers.content().string("abc")测试返回内容。
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class GirlControllerTest {
/*
//传统测试方法
@Autowired
private GirlController girlController;
@Test
public void girlList() throws Exception {
girlController.girlList();
}*/
@Autowired
private MockMvc mvc;
@Test
public void girlList() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/girls"))
.andExpect(MockMvcResultMatchers.status().isOk())//测试返回状态码
.andExpect(MockMvcResultMatchers.content().string("abc"));//测试返回内容
}
}
8.2 测试方法3,通过maven命令:
maven clean package
,打包命令。- 中途如果出现测试用例失败,会增加打包时间,查看日志会看到失败的原因,注释掉测试中的错误语句,比如此例中的
andExpect(MockMvcResultMatchers.content().string("abc"))
,可以缩短这个时间。 - 还可以跳过单元测试,使用命令
maven clean package -Dmaven.test.skip=true