文章目录
- 01. 环境搭建
- 02. 页面查询接口定义
- 2.1 模型类(实体类)介绍
- 2.2 定义请求类型
- 2.3 定义响应类型
- 2.3 分页查询接口定义
- 03. 创建CMS服务工程
- 04. 分页查询CmsPage接口测试
- 05. Swagger常用注解
- 06. 新增页面接口开发与测试
- 07. 修改页面接口开发与测试
- 08. 删除页面接口开发与测试
- 09. 异常处理
- 9.1 异常处理的问题分析
- 9.2 异常处理流程
- 9.3 可预知异常处理
- 9.4 不可预知异常处理
01. 环境搭建
创建xuecheng文件夹,并使用idea打开
① 配置maven
② 下载并安装mongodb并启动服务:
在bin的同级目录下新建data\db文件夹
D:\install\mongodb-win32-x86_64-2008plus-ssl-4.0.12\bin>mongod --dbpath=..\data\db
③ 下载并安装navicat,连接mongodb服务
④ 导入工程xc-framework-common、model、parent、utils、api
⑤ 使用Studio3T导入数据库
02. 页面查询接口定义
本次定义页面查询接口,本接口供前端请求查询页面列表,支持分页及自定义条件查询方式。具体需求如下:
- 分页查询CmsPage 集合下的数据
- 根据站点Id、模板Id、页面别名查询页面信息
- 接口基于Http Get请求,响应Json数据
2.1 模型类(实体类)介绍
xc-framework-model项目工程,接口的定义离不开数据模型,根据前边对需求的分析,整个页面管理模块的数据模型如下:
CmsSite:站点模型、CmsTemplate:页面模板、CmsPage:页面信息 ,其中页面信息如下:
@Data
@ToString
@Document(collection = "cms_page")
public class CmsPage {
/**
* 页面名称、别名、访问地址、类型(静态/动态)、页面模版、状态
*/
//站点ID
private String siteId;
//页面ID
@Id
private String pageId;
//页面名称
private String pageName;
//别名
private String pageAliase;
//访问地址
private String pageWebPath;
//参数
private String pageParameter;
//物理路径
private String pagePhysicalPath;
//类型(静态/动态)
private String pageType;
//页面模版
private String pageTemplate;
//页面静态化内容
private String pageHtml;
//状态
private String pageStatus;
//创建时间
private Date pageCreateTime;
//模版id
private String templateId;
//参数列表
private List<CmsPageParam> pageParams;
//模版文件Id
// private String templateFileId;
//静态文件Id
private String htmlFileId;
//数据Url
private String dataUrl;
}
提示:
① 定义一个页面需要指定页面所属站点
② 定义一个页面需要指定页面使用的模板,多个页面可以使用相同的模板,比如:商品信息模板,每个商品就是一个页面,所有商品使用同一个商品信息模板
2.2 定义请求类型
① 定义请求模型QueryPageRequest,此模型作为查询条件类型,为后期扩展需求,请求类型统一继承RequestData类型。
package com.xuecheng.framework.domain.cms.request;
import com.xuecheng.framework.model.request.RequestData;
import lombok.Data;
@Data
public class QueryPageRequest extends RequestData {
//站点id
private String siteId;
//页面ID
private String pageId;
//页面名称
private String pageName;
//别名
private String pageAliase;
//模版id
private String templateId;
}
2.3 定义响应类型
响应结果类型,分页查询统一使用QueryResponseResult
public interface Response {
public static final boolean SUCCESS = true;
public static final int SUCCESS_CODE = 10000;
}
@Data
@ToString
@NoArgsConstructor
public class ResponseResult implements Response {
//操作是否成功
boolean success = SUCCESS;
//操作代码
int code = SUCCESS_CODE;
//提示信息
String message;
public ResponseResult(ResultCode resultCode){
this.success = resultCode.success();
this.code = resultCode.code();
this.message = resultCode.message();
}
public static ResponseResult SUCCESS(){
return new ResponseResult(CommonCode.SUCCESS);
}
public static ResponseResult FAIL(){
return new ResponseResult(CommonCode.FAIL);
}
}
package com.xuecheng.framework.model.response;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class QueryResponseResult extends ResponseResult {
QueryResult queryResult;
public QueryResponseResult(ResultCode resultCode,QueryResult queryResult){
super(resultCode);
this.queryResult = queryResult;
}
}
@Data
@ToString
public class QueryResult<T> {
//数据列表
private List<T> list;
//数据总数
private long total;
}
package com.xuecheng.framework.model.response;
public interface ResultCode {
//操作是否成功,true为成功,false操作失败
boolean success();
//操作代码
int code();
//提示信息
String message();
}
package com.xuecheng.framework.model.response;
import lombok.ToString;
@ToString
public enum CommonCode implements ResultCode{
SUCCESS(true,10000,"操作成功!"),
FAIL(false,11111,"操作失败!"),
UNAUTHENTICATED(false,10001,"此操作需要登陆系统!"),
UNAUTHORISE(false,10002,"权限不足,无权操作!"),
SERVER_ERROR(false,99999,"抱歉,系统繁忙,请稍后重试!");
//操作是否成功
boolean success;
//操作代码
int code;
//提示信息
String message;
private CommonCode(boolean success,int code, String message){
this.success = success;
this.code = code;
this.message = message;
}
@Override
public boolean success() {
return success;
}
@Override
public int code() {
return code;
}
@Override
public String message() {
return message;
}
}
2.3 分页查询接口定义
xc-framework-api项目工程
页面查询接口,此接口编写后会在CMS服务工程编写Controller类实现此接口,Api工程的接口将作为各微服务远程调用使用 。
package com.xuecheng.api.cms;
import com.xuecheng.framework.domain.cms.request.QueryPageRequest;
import com.xuecheng.framework.model.response.QueryResponseResult;
public interface CmsPageControllerApi {
public QueryResponseResult findList(int page, int size, QueryPageRequest queryPageRequest);
}
03. 创建CMS服务工程
① 创建maven工程, CMS工程的名称为 xc-service-manage-cms,父工程为xc-framework-parent 。
② 在classpath下配置application.properties
server.port=31001
spring.data.mongodb.host=127.0.0.1
spring.data.mongodb.database=xc_cms
spring.data.mongodb.port=27017
spring.application.name=xc-service-manage-cms
③ Spring Boot应用需要创建一个应用启动类,启动过程中会扫描Bean并注入spring 容器
此类创建在本工程com.xuecheng.manage_cms包下 :
@SpringBootApplication
//扫描实体类,位于xc-framework-model服务下
@EntityScan("com.xuecheng.framework.domain.cms")
//扫描接口,位于xc-framework-api服务下
@ComponentScan(basePackages={"com.xuecheng.api"})
//扫描本项目下的所有类
@ComponentScan(basePackages={"com.xuecheng.manage_cms"})
public class ManageCmsApplication {
public static void main(String[] args) {
SpringApplication.run(ManageCmsApplication.class,args);
}
}
04. 分页查询CmsPage接口测试
在 xc-service-manage-cms项目工程下:
① Controller层:
package com.xuecheng.manage_cms.controller;
@RestController
@RequestMapping("/cms/page")
public class CmsPageController implements CmsPageControllerApi {
@Autowired
PageService pageService;
@Override
@GetMapping("/list/{page}/{size}")
public QueryResponseResult findList(@PathVariable("page") int page, @PathVariable("size")int size, QueryPageRequest queryPageRequest) {
//调用service
return pageService.findList(page,size,queryPageRequest);
}
}
② Service层:service 层方法如果没有特殊情况就和controller层保持一致即可
package com.xuecheng.manage_cms.service;
@Service
public class PageService {
@Autowired
CmsPageRepository cmsPageRepository;
/**
* 页面查询方法
* @param page 页码,从1开始记数
* @param size 每页记录数
* @param queryPageRequest 查询条件
* @return
*/
public QueryResponseResult findList(int page, int size, QueryPageRequest queryPageRequest){
//Controller层调用Service层
if(page<=0){
page = 1;
}
if(size<=0){
size = 10;
}
//两个属性,一个是数据总数total,另一个是数据列表List<T> list
QueryResult queryResult = new QueryResult();
//调用dao层接口查询数据,需要传入分页参数
Pageable pageable = PageRequest.of(page-1,size);
Page<CmsPage> all = cmsPageRepository.findAll(pageable);
queryResult.setTotal(all.getTotalElements());
queryResult.setList(all.getContent());
//最终相应的类型为QueryResponseResult,构造方法接受两个参数,一个是resultCode,另一个是QueryResult
QueryResponseResult queryResponseResult = new QueryResponseResult(CommonCode.SUCCESS,queryResult);
return queryResponseResult;
}
}
③ dao层:
package com.xuecheng.manage_cms.dao;
public interface CmsPageRepository extends MongoRepository<CmsPage,String> {
}
测试:启动30001服务,访问:http://localhost:31001/cms/page/list/1/2
05. Swagger常用注解
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
① 在Java类中添加Swagger的注解即可生成Swagger接口,常用Swagger注解如下:
- @Api:修饰整个类,描述Controller的作用
- @ApiOperation:描述一个类的一个方法,或者说一个接口
- @ApiParam:单个参数描述
- @ApiModel:用对象来接收参数
- @ApiModelProperty:用对象接收参数时,描述对象的一个字段
- @ApiResponse:HTTP响应其中1个描述
- @ApiResponses:HTTP响应整体描述
- @ApiIgnore:使用该注解忽略这个API
- @ApiError :发生错误返回的信息
- @ApiImplicitParam:一个请求参数
- @ApiImplicitParams:多个请求参数
② 编写Swagger配置类:
package com.xuecheng.api.config;
@Configuration
@EnableSwagger2
public class Swagger2Configuration {
//com.xuecheng包下面的所有加了@Controller注解类中的的接口方法都可以扫描到
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.xuecheng"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("学成网api文档")
.description("学成网api文档")
.version("1.0")
.build();
}
}
③ 修改接口工程中页面查询接口,添加Swagger注解:
package com.xuecheng.api.cms;
//接口注解
@Api(value="cms页面管理接口",description = "cms页面管理接口,提供页面的增、删、改、查")
public interface CmsPageControllerApi {
//方法注解
@ApiOperation("分页查询页面列表")
//参数注解
@ApiImplicitParams({
@ApiImplicitParam(name="page",value = "页码",required=true,paramType="path",dataType="int"),
@ApiImplicitParam(name="size",value = "每页记录数",required=true,paramType="path",dataType="int")
})
public QueryResponseResult findList(int page, int size, QueryPageRequest queryPageRequest);
}
在QueryPageRequest类中使用注解 ApiModelProperty 对属性注释:
package com.xuecheng.framework.domain.cms.request;
@Data
public class QueryPageRequest {
//接收页面查询的查询条件
//属性注解
@ApiModelProperty("站点id")
private String siteId;
//页面ID
private String pageId;
//页面名称
private String pageName;
//别名
private String pageAliase;
//模版id
private String templateId;
//....
}
访问:http://localhost:31001/swagger-ui.html
输入page=1,size=5即可:
06. 新增页面接口开发与测试
① 定义响应模型:
package com.xuecheng.framework.domain.cms.response;
@Data
public class CmsPageResult extends ResponseResult {
//新增的页面数据响应给前端以便使用
CmsPage cmsPage;
public CmsPageResult(ResultCode resultCode,CmsPage cmsPage) {
//调用父类的构造方法
super(resultCode);
this.cmsPage = cmsPage;
}
}
其中 super(resultCode)调用的就是构造方法ResponseResult(ResultCode resultCode):
package com.xuecheng.framework.model.response;
@Data
@ToString
@NoArgsConstructor
public class ResponseResult implements Response {
//操作是否成功
boolean success = SUCCESS;
//操作代码
int code = SUCCESS_CODE;
//提示信息
String message;
public ResponseResult(ResultCode resultCode){
this.success = resultCode.success();
this.code = resultCode.code();
this.message = resultCode.message();
}
public static ResponseResult SUCCESS(){
return new ResponseResult(CommonCode.SUCCESS);
}
public static ResponseResult FAIL(){
return new ResponseResult(CommonCode.FAIL);
}
}
② 定义新增页面的api接口:
package com.xuecheng.api.cms;
@Api(value="cms页面管理接口",description = "cms页面管理接口,提供页面的增、删、改、查")
public interface CmsPageControllerApi {
//新增页面
@ApiOperation("新增页面")
public CmsPage add(CmsPage cmsPage);
}
③ 在cms_page集合上创建页面名称、站点Id、页面webpath为唯一索引 ,用于判断添加的页面是否重复:
右键–》add Index --》Add Fields–》create Index
④ dao层:
添加根据页面名称、站点Id、页面webpath查询页面方法,此方法用于校验页面是否存在 ,查询数据库
package com.xuecheng.manage_cms.dao;
public interface CmsPageRepository extends MongoRepository<CmsPage,String> {
// 根据页面名称、站点id、页面访问路径查询
CmsPage findByPageNameAndSiteIdAndPageWebPath(String pageName,String siteId,String pageWebPath);
}
⑤ Service层:
package com.xuecheng.manage_cms.service;
@Service
public class PageService {
@Autowired
CmsPageRepository cmsPageRepository;
/**
* 新增页面
* @param cmsPage
* @return
*/
public CmsPageResult add(CmsPage cmsPage){
//根据数据库查询该页面是否存在,由pageName,siteId,pageWebPath可以唯一确定一个页面
CmsPage newCmsPage = cmsPageRepository.findByPageNameAndSiteIdAndPageWebPath(
cmsPage.getPageName(), cmsPage.getSiteId(), cmsPage.getPageWebPath());
//对于对象一定要判空,采用短路法
if(newCmsPage==null){
//说明数据库中没有此页面,可以插入
//主键由SpringData自动生成
cmsPage.setPageId(null);
cmsPageRepository.save(cmsPage);
//响应结果
CmsPageResult cmsPageResult = new CmsPageResult(CommonCode.SUCCESS,cmsPage);
return cmsPageResult;
}
CmsPageResult cmsPageResult = new CmsPageResult(CommonCode.FAIL,null);
return cmsPageResult;
}
}
⑥ Controller层:
package com.xuecheng.manage_cms.controller;
@RestController
@RequestMapping("/cms/page")
public class CmsPageController implements CmsPageControllerApi {
@Autowired
PageService pageService;
/**
* 新增页面
* 页面接收的数据为json数据,需要转为CmsPage对象,因此使用@RequestBody注解
* @param cmsPage
* @return
*/
@Override
@PostMapping("/add")
public CmsPageResult add(@RequestBody CmsPage cmsPage){
return pageService.add(cmsPage);
}
}
⑦ 接口测试:
使用Swagger测试:http://localhost:31001/swagger-ui.html
进入add()方法:点击右边的测试数据,默认值都为String,可随便修改一个数据,比如pageName改为测试页面
点击Try it out后:
再次新增此页面:
使用postman测试,post请求:http://localhost:31001/cms/page/add
请求数据:
{
"dataUrl": "string",
"htmlFileId": "string",
"pageAliase": "string",
"pageCreateTime": "2021-08-19T16:29:39.251Z",
"pageHtml": "string",
"pageId": "string",
"pageName": "测试页面1",
"pageParameter": "string",
"pageParams": [
{
"pageParamName": "string",
"pageParamValue": "string"
}
],
"pagePhysicalPath": "string",
"pageStatus": "string",
"pageTemplate": "string",
"pageType": "string",
"pageWebPath": "string",
"siteId": "string",
"templateId": "string"
}
再次添加此页面:
07. 修改页面接口开发与测试
修改页面用户操作流程:
- 用户进入修改页面,在页面上显示了修改页面的信息
- 用户修改页面的内容,点击“提交”,提示“修改成功”或“修改失败”
①添加Controller层的api接口:
package com.xuecheng.api.cms;
@Api(value="cms页面管理接口",description = "cms页面管理接口,提供页面的增、删、改、查")
public interface CmsPageControllerApi {
//根据id查询界面
@ApiOperation("根据id查询页面")
public CmsPage findById(String id);
//修改页面
@ApiOperation("修改页面")
public CmsPageResult exit(String id,CmsPage cmsPage);
}
② dao层:SpringData Mongodb自己提供了这两个接口方法
使用 Spring Data提供的findById方法完成根据主键查询 。
使用 Spring Data提供的save方法完成数据保存
③ service层:
package com.xuecheng.manage_cms.service;
@Service
public class PageService {
@Autowired
CmsPageRepository cmsPageRepository;
/**
* 根据id查询页面
* @param id
* @return
*/
public CmsPage getById(String id){
Optional<CmsPage> optional = cmsPageRepository.findById(id);
if(optional.isPresent()){
CmsPage cmsPage = optional.get();
return cmsPage;
}
return null;
}
/**
* 修改页面
* @param id
* @param cmsPage
* @return
*/
public CmsPageResult update(String id,CmsPage cmsPage){
//根据id查询页面
CmsPage newCmsPage = getById(id);
if(newCmsPage!=null){
//更新模板id
newCmsPage.setTemplateId(cmsPage.getTemplateId());
//更新所属站点
newCmsPage.setSiteId(cmsPage.getSiteId());
//更新页面别名
newCmsPage.setPageAliase(cmsPage.getPageAliase());
//更新页面名称
newCmsPage.setPageName(cmsPage.getPageName());
//更新访问路径
newCmsPage.setPageWebPath(cmsPage.getPageWebPath());
//更新物理路径
newCmsPage.setPagePhysicalPath(cmsPage.getPagePhysicalPath());
//执行更新
cmsPageRepository.save(newCmsPage);
return new CmsPageResult(CommonCode.SUCCESS,newCmsPage);
}
return new CmsPageResult(CommonCode.FAIL,null);
}
}
⑤ Controller层:
package com.xuecheng.manage_cms.controller;
@RestController
@RequestMapping("/cms/page")
public class CmsPageController implements CmsPageControllerApi {
@Autowired
PageService pageService;
/**
* 修改页面
* 提交数据使用post、put都可以,只是根据http方法的规范,put方法是对服务器指定资源进行修改,所以这里使用put方法对页面修改进行修改。
* 页面提交的是json数据,需要转为对象
* @param id
* @param cmsPage
* @return
*/
@PutMapping("/edit/{id}")
@Override
public CmsPageResult exit(@PathVariable("id") String id,@RequestBody CmsPage cmsPage){
return pageService.update(id,cmsPage);
}
}
⑥ 使用Swagger测试:
08. 删除页面接口开发与测试
删除和更新操作,都需要一个主键,这里根据id删除
① 响应类型判断:
新增和修改页面都返回了CmsPage,单数删除页面就不需要了,无论返回什么类型至少要包含success,code,message
package com.xuecheng.framework.model.response;
@Data
@ToString
@NoArgsConstructor
public class ResponseResult implements Response {
//操作是否成功
boolean success = SUCCESS;
//操作代码
int code = SUCCESS_CODE;
//提示信息
String message;
public ResponseResult(ResultCode resultCode){
this.success = resultCode.success();
this.code = resultCode.code();
this.message = resultCode.message();
}
public static ResponseResult SUCCESS(){
return new ResponseResult(CommonCode.SUCCESS);
}
public static ResponseResult FAIL(){
return new ResponseResult(CommonCode.FAIL);
}
}
其中Response用来定义ResponseResult类中用到的魔法值:
public interface Response {
public static final boolean SUCCESS = true;
public static final int SUCCESS_CODE = 10000;
}
package com.xuecheng.framework.model.response;
@ToString
public enum CommonCode implements ResultCode{
SUCCESS(true,10000,"操作成功!"),
FAIL(false,11111,"操作失败!"),
UNAUTHENTICATED(false,10001,"此操作需要登陆系统!"),
UNAUTHORISE(false,10002,"权限不足,无权操作!"),
SERVER_ERROR(false,99999,"抱歉,系统繁忙,请稍后重试!");
//操作是否成功
boolean success;
//操作代码
int code;
//提示信息
String message;
private CommonCode(boolean success,int code, String message){
this.success = success;
this.code = code;
this.message = message;
}
@Override
public boolean success() {
return success;
}
@Override
public int code() {
return code;
}
@Override
public String message() {
return message;
}
}
② 删除页面的api接口:
package com.xuecheng.api.cms;
@Api(value="cms页面管理接口",description = "cms页面管理接口,提供页面的增、删、改、查")
public interface CmsPageControllerApi {
@ApiOperation("删除页面")
public ResponseResult delete(String id);
}
③ dao层:使用 Spring Data提供的deleteById方法完成删除操作 。
④ Service层:
package com.xuecheng.manage_cms.service;
/**
* @author Administrator
* @version 1.0
* @create 2018-09-12 18:32
**/
@Service
public class PageService {
@Autowired
CmsPageRepository cmsPageRepository;
/**
* 删除页面
* @param id
* @return
*/
public ResponseResult delete(String id){
//查询页面是否存在
CmsPage cmsPage = getById(id);
if(cmsPage!=null){
cmsPageRepository.deleteById(id);
return new ResponseResult(CommonCode.SUCCESS);
}
return new ResponseResult(CommonCode.FAIL);
}
}
⑤ Controller层:
package com.xuecheng.manage_cms.controller;
@RestController
@RequestMapping("/cms/page")
public class CmsPageController implements CmsPageControllerApi {
@Autowired
PageService pageService;
/**
* 删除页面
* @param id
* @return
*/
@DeleteMapping("/del/{id}")
@Override
public ResponseResult delete(@PathVariable("id") String id) {
return pageService.delete(id);
}
}
⑥ 启动项目,使用swagger测试:
09. 异常处理
9.1 异常处理的问题分析
从新增页面的Service方法来看,原始代码:
/**
* 新增页面
* @param cmsPage
* @return
*/
public CmsPageResult add(CmsPage cmsPage){
//根据数据库查询该页面是否存在,由pageName,siteId,pageWebPath可以唯一确定一个页面
CmsPage newCmsPage = cmsPageRepository.findByPageNameAndSiteIdAndPageWebPath(
cmsPage.getPageName(), cmsPage.getSiteId(), cmsPage.getPageWebPath());
//对于对象一定要判空,采用短路法
if(newCmsPage==null){
//说明数据库中没有此页面,可以插入
//主键由SpringData自动生成
cmsPage.setPageId(null);
cmsPageRepository.save(cmsPage);
//响应结果
CmsPageResult cmsPageResult = new CmsPageResult(CommonCode.SUCCESS,cmsPage);
return cmsPageResult;
}
CmsPageResult cmsPageResult = new CmsPageResult(CommonCode.FAIL,null);
return cmsPageResult;
}
存在的问题:
- 上边的代码只要操作不成功仅向用户返回“错误代码:11111,失败信息:操作失败”,无法区别具体的错误信息。
- service方法在执行过程出现异常在哪捕获?在service中需要都加try/catch,如果在controller也需要添加try/catch,代码冗余严重且不易维护。
解决方案:
- 在Service方法中的编码顺序是先校验判断,有问题则抛出具体的异常信息,最后执行具体的业务操作,返回成功信息。
- 在统一异常处理类中去捕获异常,无需controller捕获异常,向用户返回统一规范的响应信息。
修改后的新增页面代码模板:
public CmsPageResult add(CmsPage cmsPage){
//如果这里不做参数校验,cmsPage.getPageName()方法就有可能报空指针异常,先处理异常再处理正常逻辑
if(cmsPage==null){
//抛出非法参数异常
}
//根据数据库查询该页面是否存在,由pageName,siteId,pageWebPath可以唯一确定一个页面
CmsPage newCmsPage = cmsPageRepository.findByPageNameAndSiteIdAndPageWebPath(
cmsPage.getPageName(), cmsPage.getSiteId(), cmsPage.getPageWebPath());
if(newCmsPage!=null){
//抛出页面已经存在的异常,然后处理正常逻辑
}
//主键由SpringData自动生成
cmsPage.setPageId(null);
cmsPageRepository.save(cmsPage);
//响应结果
CmsPageResult cmsPageResult = new CmsPageResult(CommonCode.SUCCESS,cmsPage);
return cmsPageResult;
}
9.2 异常处理流程
系统对异常的处理使用统一的异常处理流程:
- 自定义异常类型。
- 自定义错误代码及错误信息。
- 对于可预知的异常由程序员在代码中主动抛出,由SpringMVC统一捕获。
可预知异常是程序员在代码中手动抛出本系统定义的特定异常类型,由于是程序员抛出的异常,通常异常信息比较齐全,程序员在抛出时会指定错误代码及错误信息,获取异常信息也比较方便。 - 对于不可预知的异常(运行时异常)由SpringMVC统一捕获Exception类型的异常。
不可预知异常通常是由于系统出现bug、或一些不要抗拒的错误(比如网络中断、服务器宕机等),异常类型为RuntimeException类型(运行时异常)。 - 可预知的异常及不可预知的运行时异常最终会采用统一的信息格式(错误代码+错误信息)来表示,最终也会随请求响应给客户端 。
异常捕获及处理流程:
- 在controller、service、dao中程序员抛出自定义异常;springMVC框架抛出框架异常类型。
- 统一由异常捕获类捕获异常,并进行处理。
- 捕获到自定义异常则直接取出错误代码及错误信息,响应给用户。
- 捕获到非自定义异常类型首先从Map中找该异常类型是否对应具体的错误代码,如果有则取出错误代码和错误信息并响应给用户,如果从Map中找不到异常类型所对应的错误代码则统一为99999错误代码并响应给用户。
- 将错误代码及错误信息以Json格式响应给用户。
对于dao层、service层、controller层以及SpringMvc框架抛出的异常,我们不在他们的代码中进行异常捕获,而是进行统一异常处理,因为如果我们在每个service层的方法或者每个controller层中的方法中都进行异常捕获,会使得代码很冗余,不易维护。
9.3 可预知异常处理
① 自定义异常类:
package com.xuecheng.framework.exception;
/**
* 自定义异常类,在该异常类中抛出异常信息:错误代码+异常信息
*/
public class CustomException extends RuntimeException {
ResultCode resultCode;
public CustomException(ResultCode resultCode){
this.resultCode = resultCode;
}
public ResultCode getResultCode() {
return resultCode;
}
}
在代码中抛出自定义异常:
if(newCmsPage!=null){
//抛出页面已经存在的异常,然后处理正常逻辑
throw new CustomException(CmsCode.CMS_ADDPAGE_EXISTSNAME);
}
② 还可以进一步封装一个异常抛出类:
package com.xuecheng.framework.exception;
/**
* 异常抛出类,封装抛出异常那行代码
*/
public class ExceptionCast {
public static void cast(ResultCode resultCode){
throw new CustomException(resultCode);
}
}
在代码中抛出自定义异常:
if(newCmsPage!=null){
//抛出页面已经存在的异常,然后处理正常逻辑
ExceptionCast.cast(CmsCode.CMS_ADDPAGE_EXISTSNAME);
}
③ 异常捕获类:
package com.xuecheng.framework.exception;
@ControllerAdvice
public class ExceptionCatch {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionCatch.class);
//所有CustomException.class类型的异常都会在这捕获到
@ExceptionHandler(CustomException.class)
//将返回的对象转为json数据
@ResponseBody
public ResponseResult customException(CustomException customException){
//捕获异常后进行日志打印
LOGGER.error("catch exception : {}",customException.getMessage());
//取出错误信息和错误代码
ResultCode resultCode = customException.getResultCode();
return new ResponseResult(resultCode);
}
}
④ 在xc-service-manage-cms项目的主配置类中添加扫描common项目下的包:
package com.xuecheng.manage_cms;
@SpringBootApplication
@EntityScan("com.xuecheng.framework.domain.cms")//扫描实体类
@ComponentScan(basePackages={"com.xuecheng.api"})//扫描接口
@ComponentScan(basePackages={"com.xuecheng.framework"})
@ComponentScan(basePackages={"com.xuecheng.manage_cms"})//扫描本项目下的所有类
public class ManageCmsApplication {
public static void main(String[] args) {
SpringApplication.run(ManageCmsApplication.class,args);
}
}
⑤ 修改service下的新增页面方法:
与CmsPage有关的响应码:
package com.xuecheng.framework.domain.cms.response;
@ToString
public enum CmsCode implements ResultCode {
CMS_ADDPAGE_EXISTSNAME(false,24001,"页面名称已存在!"),
CMS_GENERATEHTML_DATAURLISNULL(false,24002,"从页面信息中找不到获取数据的url!"),
CMS_GENERATEHTML_DATAISNULL(false,24003,"根据页面的数据url获取不到数据!"),
CMS_GENERATEHTML_TEMPLATEISNULL(false,24004,"页面模板为空!"),
CMS_GENERATEHTML_HTMLISNULL(false,24005,"生成的静态html为空!"),
CMS_GENERATEHTML_SAVEHTMLERROR(false,24005,"保存静态html出错!"),
CMS_COURSE_PERVIEWISNULL(false,24007,"预览页面为空!");
//操作代码
boolean success;
//操作代码
int code;
//提示信息
String message;
private CmsCode(boolean success, int code, String message){
this.success = success;
this.code = code;
this.message = message;
}
@Override
public boolean success() {
return success;
}
@Override
public int code() {
return code;
}
@Override
public String message() {
return message;
}
}
修改后的新增页面方法:
public CmsPageResult add(CmsPage cmsPage){
//根据数据库查询该页面是否存在,由pageName,siteId,pageWebPath可以唯一确定一个页面
CmsPage newCmsPage = cmsPageRepository.findByPageNameAndSiteIdAndPageWebPath(
cmsPage.getPageName(), cmsPage.getSiteId(), cmsPage.getPageWebPath());
if(newCmsPage!=null){
//抛出页面已经存在的异常,然后处理正常逻辑
ExceptionCast.cast(CmsCode.CMS_ADDPAGE_EXISTSNAME);
}
//主键由SpringData自动生成
cmsPage.setPageId(null);
cmsPageRepository.save(cmsPage);
//响应结果
CmsPageResult cmsPageResult = new CmsPageResult(CommonCode.SUCCESS,cmsPage);
return cmsPageResult;
}
⑥ 测试:
9.4 不可预知异常处理
使用postman测试添加页面,不输入cmsPost信息,提交,报错信息如下:
org.springframework.http.converter.HttpMessageNotReadableException 此异常是springMVC在进行参数转换时报的错误。
上边的响应信息在客户端是无法解析的,在异常捕获类中添加对Exception异常的捕获:
@ControllerAdvice
public class ExceptionCatch {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionCatch.class);
//捕获Exception类的异常
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseResult exception(Exception exception){
//捕获异常后进行日志打印
LOGGER.error("catch exception : {}",exception.getMessage());
return null;
}
}
针对上边的问题其解决方案是:
- 我们在map中配置HttpMessageNotReadableException和错误代码。
- 在异常捕获类中对Exception异常进行捕获,并从map中获取异常类型对应的错误代码,如果存在错误代码则返回此错误,否则统一返回99999错误。
① 在通用错误代码类CommCode中配置非法参数异常 :
ILLEGAL_ARGUMENT(false,10003,"非法参数")
② 在异常捕获类中配置 HttpMessageNotReadableException 为非法参数异常:
package com.xuecheng.framework.exception;
@ControllerAdvice
public class ExceptionCatch {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionCatch.class);
//使用EXCEPTIONS存放异常类型和错误代码的映射,ImmutableMap的特点的一旦创建不可改变,并且线程安全
private static ImmutableMap<Class<? extends Throwable>,ResultCode> EXCEPTIONS;
//使用builder来构建一个异常类型和错误代码的异常
protected static ImmutableMap.Builder<Class<? extends Throwable>,ResultCode> builder = ImmutableMap.builder();
static{
//在这里加入一些基础的异常类型判断
builder.put(HttpMessageNotReadableException.class,CommonCode.ILLEGAL_ARGUMENT);
}
//捕获Exception类的异常
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseResult exception(Exception exception){
//捕获异常后进行日志打印
LOGGER.error("catch exception : {}",exception.getMessage());
if(EXCEPTIONS == null){
EXCEPTIONS = builder.build();
}
final ResultCode resultCode = EXCEPTIONS.get(exception.getClass());
final ResponseResult responseResult;
if (resultCode != null) {
responseResult = new ResponseResult(resultCode);
} else {
responseResult = new ResponseResult(CommonCode.SERVER_ERROR);
}
return responseResult;
}
}