学习目标:
掌握springboot项目创建及日常开发流程,能够独立进行项目创建、接口开发、接口调试等工作。了解ideal的常用配置及快捷键
学习内容:
一、创建springboot项目
ideal创建项目时可以用spring initializr会自动添加springboot依赖,也可以创建普通的maven项目,自己手动引入依赖。
1、使用spring initializr创建springboot项目
依次点击菜单 File
->New->Project,选择spring initializr
当前start.spring.io只提供了springboot3以上版本。springboot依赖本机的jdk版本,对应关系如下:https://zhuanlan.zhihu.com/p/652895555。
选择所需的依赖,点击创建后会自动生成项目目录及入口application类和application.properties配置文件
2、创建普通maven项目,手动引入依赖
依次点击菜单 File
->New->Project,选择Maven Archetype,Archetype选择org.apache.maven.archetypes:maven-archetype-webapp
pom文件中添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.14</version>
</dependency>
maven依赖包版本管理有三种方式
1)、支持继承自parent
2)、使用dependencyManagement
<scope>import</scope>表示为当前项目依赖为多继承关系,常用于项目中自定义父工程,需要注意的是只能用在dependencyManagement里面,且仅用于type=pom的dependency。参考:springCloud中dependencyManagement、type、scope在父模块和子模块分别的作用 - 简书
3)、直接指定版本
在src/main目录下手动创建java目录、包目录及application入口类,在src/resource目录下创建application.yml配置文件
创建启动类BootDemoApplication
package sccba.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BootDemoApplication.class, args);
}
}
创建配置文件application.yaml,指定端口及访问跟路径
server:
port: 6371
context-path: /
springboot支持在不同的目录中存在多个配置文件,它的读取顺序是:
1、config/application.properties(项目根目录中config目录下)
2、config/application.yml
3、application.properties(项目根目录下)
4、application.yml
5、resources/config/application.properties(项目resources目录中config目录下)
6、resources/config/application.yml
7、resources/application.properties(项目的resources目录下)
8、resources/application.yml
顺序可以通过实验验证:
1~8 个位置 分别定义不同的 server 端口号 8001~8008即可验证结果顺序
注:
1、如果同一个目录下,有application.yml也有application.properties,默认先读取application.properties。
2、如果同一个配置属性,在多个配置文件都配置了,默认使用第1个读取到的,后面读取的不覆盖前面读取到的。
3、创建SpringBoot项目时,一般的配置文件放置在项目的resources目录下,因为配置文件的修改,通过热部署不用重新启动项目,而热部署的作用范围是classpath下。
二、开发helloworld接口
新建controller包,在controller包下创建HelloController类
@RestController
@RequestMapping("/hi")
public class HelloController {
@GetMapping("/hello")
public String hello(String name) {
return "hello," + name + ",welcome to springboot ";
}
}
启动BootDemoApplication
在浏览器访问hello接口http://localhost:8001/hi/hello?name=sccba
1、@Controller、@Service、@Repository、@Component 注解的类会纳入 Spring 容器中进行管理.
2、@Controller 用于标注控制层组件;@Service 用于标注业务层组件;@Repository 用于标注数据持久化层组件;@Component 泛指组件,用于标注不好归类的组件。
3、默认情况 bean 的名称为类名(首字母小写),可以通过 value 属性自定义 bean 的名称。默认情况下 bean 是单例的,可以使用 @Scope 注解指定。
4、如果不使用springMVC时,@Controller、@Service、@Component 三者使用其实是没有什么差别的,但如果使用了springMVC,@Controller就被赋予了特殊的含义。spring会遍历上面扫描出来的所有bean,过滤出那些添加了注解@Controller的bean,将Controller中所有添加了注解@RequestMapping的方法解析出来封装成RequestMappingInfo存储到RequestMappingHandlerMapping中的mappingRegistry。后续请求到达时,会从mappingRegistry中查找能够处理该请求的方法。
5、对于 Spring Boot 项目,启动类会默认标记 @SpringBootApplication 注解,它的内部依赖了 @ComponentScan 注解:默认启动类同目录下(./**/*)任意子孙包中类上的注解都会被自动扫描,启动类上级目录下类中的注解默认是无法自动扫描到的(此时可以在启动类上自定义扫描路径:@ComponentScan(value = "com.wmx"))。
三、配置swagger与knife4j
pom.xml引入依赖
<!--swagger,访问地址/swagger-ui/index.html-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
在applicaton.yml中添加配置
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher #Spring Boot 2.6及以后,swagger配置报错,需添加此配置
#============swagger3.0配置,访问地址/swagger-ui/index.html==========
swagger:
enabled: true
添加swagger配置类
package sccba.example.config;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
@Configuration
@EnableOpenApi
public class SwaggerConfig {
/**
* 用于读取配置文件 application.properties 中 swagger 属性是否开启,默认值false
*/
@Value(value = "${swagger.enabled:false}")
Boolean swaggerEnabled;
@Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
// 是否开启swagger
.enable(swaggerEnabled)
.select()
/**
* RequestHandlerSelectors,配置要扫描接口的方式
* basePackage指定要扫描的包
* any()扫描所有,项目中的所有接口都会被扫描到
* none()不扫描
* withClassAnnotation()扫描类上的注解
* withMethodAnnotation()扫描方法上的注解
*/
// 过滤条件,扫描指定路径下的文件
.apis(RequestHandlerSelectors.basePackage("sccba.example.admin.controller")
.or(RequestHandlerSelectors.basePackage("sccba.example.blog.controller")))
//加了Api注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
//加了ApiOperation注解的方法,才生成接口文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
// 指定路径处理,PathSelectors.any()代表不过滤任何路径
// .paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
/*作者信息*/
Contact contact = new Contact("sccba", "https://sccba.org", "7aaaaaaa");
return new ApiInfo(
"Spring Boot 集成 Swagger3 测试",
"Spring Boot 集成 Swagger3 测试接口文档",
"v1.0",
"https://sccba.org",
contact,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList()
);
}
}
重启BootDemoApplication,在浏览器输入swagger访问的地址:http://localhost:8001/swagger-ui/index.html
配置knife4j,在pom.xml添加依赖
<!--knife4j,访问地址/doc.html-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
在application.yml添加配置
#==============knife4j配置,访问地址http://localhost:6371/doc.html===========
knife4j:
# 开启增强配置
enable: true
# 开启生产环境屏蔽
production: false
重启 BootDemoApplication,在浏览器输入swagger访问的地址:http://localhost:8001/doc.html
四、mybatis配置
在pom.xml添加依赖
<!-- 数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
在application.yml添加配置
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://4.10.12.17:3306/boot_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
mybatis-plus:
## 这个可以不用配置,因其默认就是这个路径
mapper-locations: classpath:/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: sccba.example.*.dao
global-config:
# 数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: AUTO
#字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
field-strategy: not_empty
#驼峰下划线转换
column-underline: true
#数据库大写下划线转换
#capital-mode: true
#逻辑删除配置
logic-delete-value: 0
logic-not-delete-value: 1
db-type: mysql
#刷新mapper 调试神器
refresh: true
# 原生配置
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
#指定日志输出
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations指定了mapper.xml的配置路径,typeAliasesPackage指定了扫描的实体类包路径。在相应路径下创建***Mapper类及***Mapper.xml
package sccba.example.admin.dao;
import org.apache.ibatis.annotations.Mapper;
import sccba.example.admin.domain.entity.UserDO;
/**
* 根据习惯不同命名可能是***Dao ***PO ***Mapper
*/
@Mapper
public interface UserMapper {
UserDO get(Long id);
}
<?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="sccba.example.admin.dao.UserMapper">
<select id="get" resultType="sccba.example.admin.domain.entity.UserDO">
select * from user where id = #{value}
</select>
</mapper>
五、DO、DTO、BO、VO、POJO定义和转换
分层领域模型规约:
DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
BO(Business Object):业务对象,由 Service 层输出的封装业务逻辑的对象。
VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止
使用 Map 类来传输。
分层使用和转换(个人理解)
controller层:接受前端的VO,DTO;接受service层BO,DTO;向前端发送VO,DTO
service层:接受controller层VO,DTO,BO;接受dao层的DO,DTO;向controller层返回BO,DTO
dao层:接受service层的VO,DTO,BO,DO;从数据库查询DO;向service层返回DO
UserDO
package sccba.example.admin.domain.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName UserDO
* @Description 此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
* @Author chenqiang
* @Date 2024/1/9 10:19
* @Version 1.0
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDO {
private Long id;
private String name;
/**
* vo 中没有 password 字段
*/
private String password;
private Integer age;
private String tel;
private Long depId;
}
UserDTO
package sccba.example.admin.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName UserDTO
* @Description 后端内部传输用,例如多张表字段合并到一个对象
* @Author chenqiang
* @Date 2024/1/9 11:19
* @Version 1.0
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
private Long id;
private String name;
private Integer age;
private String tel;
private Long depId;
}
UserVO
package sccba.example.admin.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName UserVO
* @Description 显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
* @Author chenqiang
* @Date 2024/1/9 10:19
* @Version 1.0
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("用户实体类")
public class UserVO {
@ApiModelProperty(value = "姓名", required = true, example = "联盟")
private String name;
@ApiModelProperty(value = "年龄", required = true, example = "20")
private Integer age;
@ApiModelProperty(value = "电话", required = true, example = "12345665555")
private String tel;
@ApiModelProperty(value = "部门", required = true, example = "研发部")
private String depName;
}
UserQuery
package sccba.example.admin.domain.query;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName UserQuery 数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装
* @Description TODO
* @Author c_see
* @Date 2024/1/11 10:50
* @Version 1.0
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserQuery {
private Long userId;
private Long depId;
}
UserTransfer,定义数据转换类进行各个领域对象间的转换,使用ideal插件GenerateO2O快速生成代码。
package sccba.example.admin.domain.transfer;
import sccba.example.admin.domain.dto.UserDTO;
import sccba.example.admin.domain.entity.UserDO;
import sccba.example.admin.domain.vo.UserVO;
/**
* @ClassName UserTransfer
* @Description 定义转换工具类
* @Author c_see
* @Date 2024/1/12 16:02
* @Version 1.0
**/
public class UserTransfer {
public static UserDTO toDTO(UserDO userDO) {
if (userDO == null) {
return null;
}
UserDTO userDTO = new UserDTO();
userDTO.setId(userDO.getId());
userDTO.setName(userDO.getName());
userDTO.setAge(userDO.getAge());
userDTO.setTel(userDO.getTel());
userDTO.setDepId(userDO.getDepId());
return userDTO;
}
public static UserVO toVO(UserDTO userDTO) {
if (userDTO == null) {
return null;
}
UserVO userVO = new UserVO();
userVO.setName(userDTO.getName());
userVO.setAge(userDTO.getAge());
userVO.setTel(userDTO.getTel());
return userVO;
}
}
使用alt+insert快捷键生成代码
UserService
package sccba.example.admin.service;
import sccba.example.admin.domain.dto.UserDTO;
public interface UserService {
public UserDTO getById(Long userId);
}
UserServiceImpl
package sccba.example.admin.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import sccba.example.admin.dao.UserMapper;
import sccba.example.admin.domain.dto.UserDTO;
import sccba.example.admin.domain.entity.UserDO;
import sccba.example.admin.domain.transfer.UserTransfer;
import sccba.example.admin.service.UserService;
/**
* @ClassName UserServiceImpl
* @Description TODO
* @Author c_see
* @Date 2024/1/12 15:27
* @Version 1.0
**/
@Service(value = "userService")
public class UserServiceImpl implements UserService {
@Autowired //根据类型注入
private UserMapper userMapper;
@Override
public UserDTO getById(Long userId) {
UserDO userDO = userMapper.get(userId);
//TODO 内部使用UserBO封装中间所需的逻辑对象,可以包括一个或多个其它的对象。常见操作DO转DTO,DO转BO、BO转DTO
return UserTransfer.toDTO(userDO);
}
}
UserController
package sccba.example.admin.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.*;
import sccba.example.admin.domain.dto.UserDTO;
import sccba.example.admin.domain.query.UserQuery;
import sccba.example.admin.domain.transfer.UserTransfer;
import sccba.example.admin.domain.vo.UserVO;
import sccba.example.admin.service.UserService;
import sccba.example.util.R;
import javax.annotation.Resource;
@Api(tags = "测试")
@RestController
@RequestMapping("/user")
public class UserController {
@Resource(name = "userService")//根据名称注入
private UserService userService;
@ApiOperation("测试swagger3接口")
@PostMapping("/swagger3")
public String show2(@ApiParam(value = "用户对象", required = true) @RequestBody UserVO user) {
return "hello," + user.getName() + ",welcome to springboot swagger3!";
}
/**
*
* @param userQuery
* @return
*/
@ApiOperation("获取用户列表")
@GetMapping("/list")
public R getUsers(UserQuery userQuery){
UserDTO userDTO = userService.getById(userQuery.getUserId());
return R.ok().putData(UserTransfer.toVO(userDTO));
}
}
定义统一返回类R
package sccba.example.util;
import java.util.HashMap;
import java.util.Map;
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", 0);
put("msg", "操作成功");
}
public static R error() {
return error(1, "操作失败");
}
public static R error(String msg) {
return error(500, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ioterror() {
R r = new R();
r.put("errorMsg", "failed");
r.put("errorCode", "FFFFFFFF");
return r;
}
public static R iotok() {
R r = new R();
r.put("errorMsg", "success");
r.put("errorCode", "00000000 ");
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
@Override
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public R putData(Object value) {
super.put("data", value);
return this;
}
}
接口调用效果
六、单元测试
一般需要对service、controller层的方法编写单元测试案例
在pom.xml引入依赖,使用spring向导会自动添加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
创建测试类,在service或者controller类右键->Go To ->Test 创建测试类
选择需要测试的方法后自动生成测试类,在测试类中编写单元测试案例
UserServiceImplTest
package sccba.example.admin.service.impl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import sccba.example.admin.domain.dto.UserDTO;
import sccba.example.admin.service.UserService;
@SpringBootTest
class UserServiceImplTest {
@Autowired
private UserService userService;
@Test
void getById() {
UserDTO userDTO = userService.getById(1L);
System.out.println(userDTO);
}
@Test
void getUsersDep() {
UserDTO userDTO = userService.getUsersDep(1L);
System.out.println(userDTO);
}
}
UserControllerTest
package sccba.example.admin.controller;
import com.alibaba.fastjson2.JSON;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
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 sccba.example.admin.domain.vo.UserVO;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void insert() throws Exception {
UserVO userVO = new UserVO();
userVO.setName("老吴2");
userVO.setAge(52);
userVO.setTel("1236548989");
mockMvc.perform(MockMvcRequestBuilders.post("/user/insert")
.contentType(MediaType.APPLICATION_JSON).content(JSON.toJSONString(userVO)))
.andExpect(status().isOk())
.andDo(print());
}
@Test
void getUsers() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user/get")
.contentType(MediaType.APPLICATION_JSON).param("userId","1")
.param("depId",""))
.andExpect(status().isOk())
.andDo(print());
}
@Test
void getUsersDep() {
}
}
七、ideal常用配置及快捷键
1、设置注释格式
2、配置maven
3、安装插件
4、Ctrl相关快捷键
Ctrl + F 在当前文件进行文本查找 (必备)
Ctrl + R 在当前文件进行文本替换 (必备)
Ctrl + Z 撤销 (必备)
Ctrl + Y 删除光标所在行 或 删除选中的行 (必备)
Ctrl + X 剪切光标所在行 或 剪切选择内容
Ctrl + C 复制光标所在行 或 复制选择内容
Ctrl + D 复制光标所在行 或 复制选择内容,并把复制内容插入光标位置下面 (必备)
Ctrl + W 递进式选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展选中范围 (必备)
Ctrl + N 根据输入的 类名 查找类文件
Ctrl + / 注释光标所在行代码,会根据当前不同文件类型使用不同的注释符号 (必备)
Ctrl + End 跳到文件尾
Ctrl + Home 跳到文件头
Ctrl + Space 基础代码补全,默认在 Windows 系统上被输入法占用,需要进行修改,建议修改为 ALT+/(必备)
Ctrl + Tab 编辑窗口切换,如果在切换的过程又加按上delete,则是关闭对应选中的窗口
5、alt相关快捷键
Alt + Enter IntelliJ IDEA 根据光标所在问题,提供快速修复选择,光标放在的位置不同提示的结果也不同 (必备)
Alt + Insert 代码自动生成,如生成对象的 set / get 方法,构造函数,toString() 等(必备)
6、Shift相关快捷键
Shift + F6 对文件 / 文件夹 重命名
Shift + F7 在 Debug 模式下,智能步入。断点所在行上有多个方法调用,会弹出进入哪个方法
7、Ctrl + Alt相关快捷键
Ctrl + Alt + L 格式化代码,可以对当前文件和整个包目录使用 (必备)
Ctrl + Alt + O 优化导入的类,可以对当前文件和整个包目录使用 (必备)
Ctrl + Alt + 左方向键 退回到上一个操作的地方 (必备)
Ctrl + Alt + 右方向键 前进到上一个操作的地方 (必备)
8、Ctrl + Shift相关快捷键
Ctrl + Shift + F 根据输入内容查找整个项目 或 指定目录内文件 (必备)
Ctrl + Shift + R 根据输入内容替换对应内容,范围为整个项目 或 指定目录内文件 (必备)
Ctrl + Shift + J 自动将下一行合并到当前行末尾 (必备)
Ctrl + Shift + Z 取消撤销 (必备)
Ctrl + Shift + W 递进式取消选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展取消选中范围 (必备)
Ctrl + Shift + N 通过文件名定位 / 打开文件 / 目录,打开目录需要在输入的内容后面多加一个正斜杠 (必备)
Ctrl + Shift + U 对选中的代码进行大 / 小写轮流转换 (必备)
Ctrl + Shift + / 代码块注释 (必备)
Ctrl + Shift + 左键单击 把光标放在某个类变量上,按此快捷键可以直接定位到该类中 (必备)
9、 其他快捷键
F2 跳转到下一个高亮错误 或 警告位置 (必备)
F3 在查找模式下,定位到下一个匹配处
F7 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则进入当前方法体内,如果该方法体还有方法,则不会进入该内嵌的方法中
F8 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则不进入当前方法体内
F9 在 Debug 模式下,恢复程序运行,但是如果该断点下面代码还有断点则停在下一个断点上