avatar

SpringBootCRUD实验

默认访问首页

1.配置路由

在任意controller下添加路由,接收/*的请求然后返回index.html

@RequestMapping({"/","/index.html"})
public String index()
{
return "index";
}

2.继承WebMvcConfigurerAdapter

//使用WebMvcConfigurerAdapter可以扩展SpringMVC的功能
//@EnableWebMvc
@Configuration
public class MyMVCconfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//super.addViewControllers(registry);
//浏览器发送/请求,来到起始页面
registry.addViewController("/zenshin").setViewName("index");
}
//所有webMvcConfigurerAdapter组件都会一起起作用
@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter()
{
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
};
return adapter;
}
}

这两种方法都是将”/“路由转到index视图;

国际化

SpringMVC国际化需要这么几步

  • 编写国际化配置文件
  • 使用ResourceBundleMessageSource管理国际化资源文件
  • 在页面使用fmt:message取出国际化内容。

SpringBoot实现国际化

  • 编写国际化配置文件
  • SpringBoot自动配置好了管理资源文件组件
@ConfigurationProperties(prefix = "spring.messages")
public class MessageSourceAutoConfiguration {

/**
* Comma-separated list of basenames (essentially a fully-qualified classpath
* location), each following the ResourceBundle convention with relaxed support for
* slash based locations. If it doesn't contain a package qualifier (such as
* "org.mypackage"), it will be resolved from the classpath root.
*/
private String basename = "messages"; //我们的配置文件可以直接放在类路径下叫messages.properties

@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(this.basename)) {
//设置国际化资源文件的基础名(去掉语言国家代码的)
messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
StringUtils.trimAllWhitespace(this.basename)));
}
if (this.encoding != null) {
messageSource.setDefaultEncoding(this.encoding.name());
}
messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale);
messageSource.setCacheSeconds(this.cacheSeconds);
messageSource.setAlwaysUseMessageFormat(this.alwaysUseMessageFormat);
return messageSource;
}
}
//这样在配置文件中配置:spring.messages.basename=i18n.login就可以找到我们自己的配置项
  • 去页面获取国际化的值;
    利用模板引擎
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" src="asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}"></h1>
<label class="sr-only" th:text="#{login.username}"></label>
<input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}"></label>
<input type="password" class="form-control" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me" />[[#{login.Rememberme}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.Signin}"></button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>

</body>

效果:根据浏览器语言设置的信息切换国际化
这样可能会出现乱码的现象,是因为idea没有给文件配置编码信息,没有转为ascii。
我们需要在IDEA中进行设置,File->Other Setting -> Setting for new project 这样以后新创建的项目就会变为这个配置

SpringBoot实现国际化原理

国际化能有效是因为有个一个对象为Local(区域信息对象)->有一个组件叫做LocaleResolver(获取区域信息对象)
SpringBoot中有一配置类:WebMvcAutoConfiguration,下面有一个Bean叫做localeResolver区域信息解析器

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
if (this.mvcProperties
.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale()); // 如果设置的是固定的,就用固定的
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();//否则就会AcceptHeaderLocaleResolver去获取请求头里面的语言信息
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
//AcceptHeaderLocaleResolver有一个方法是resolveLocale
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
if (defaultLocale != null && request.getHeader("Accept-Language") == null) { //从请求头获取语言信息
return defaultLocale;
}
Locale requestLocale = request.getLocale();
if (isSupportedLocale(requestLocale)) {
return requestLocale;
}
Locale supportedLocale = findSupportedLocale(request);
if (supportedLocale != null) {
return supportedLocale;
}
return (defaultLocale != null ? defaultLocale : requestLocale);
}

SpringBoot默认的区域信息解析器是根据请求头的信息进行解析的
如果我们想要用自己的方式进行国际化,可以对区域解析器进行重写

实现自己的区域解析器

  • 写自己的解析器实现LocaleResolver
/**
* 可以在链接上写带区域信息解析器
*/
public class MylocaleResolver implements LocaleResolver {
//解析区域信息
@Override
public Locale resolveLocale(HttpServletRequest request) {
String l = request.getParameter("l");
Locale locale = Locale.getDefault();
if(!StringUtils.isEmpty(l))
{
String[] spl = l.split("_");
locale = new Locale(spl[0],spl[1]);
}
return locale;
}
//设置区域信息
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

}
}
  • 给容器添加解析器
@Bean
public LocaleResolver localeResolver() //这里名称与返回值必须是这样
{
return new MylocaleResolver();
}
  • 在页面上添加链接
<!--点击链接请求带着语言参数-->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

这样就实现的点击切换

登陆

开发期间模板引擎页面修改以后,要实时生效

  • 禁用模板引擎缓存
  • 页面修改完以后ctrl+f9重新编译一下
#禁用缓存
spring.thymeleaf.cache=false

登陆操作

  • 前端编写表单提交
<body class="text-center">
<form class="form-signin" th:action="@{/user/login}" method="post"> <!--这里进行表单提交,提交路由是/uers/login,提交方式是post-->
<img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}"></h1>
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p> <!--登陆用户名错误返回给前端的值-->
<label class="sr-only" th:text="#{login.username}"></label>
<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}"></label>
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me" />[[#{login.Rememberme}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.Signin}"></button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
</form>
</body>
  • 服务端接收前端信息
    创建LoginController类,然后添加路由方法
@Controller
public class LoginController {
//@RequestMapping(value = "/user/login",method = RequestMethod.POST)
@PostMapping(value = "/user/login") // = @RequestMapping(value = "/user/login",method = RequestMethod.POST),简化操作
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map<String,Object> map)
{
if(!StringUtils.isEmpty(username) && "123456".equals(password))
{
return "dashboard";//成功跳转dashboard
}
else
{
map.put("msg","用户名或密码错误");
return "index";//失败还是返回index
}
}
}
  • 跳转到页面后,浏览器还是显示提交链接,需要重定向
    重定向其实是又请求了一次页面,所有这里分两步实现重定向功能
    • 首先添加一个映射关系
@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter()
{
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
registry.addViewController("/main").setViewName("dashboard"); // 添加映射关系
}
};
return adapter;
}
  • 在controller中进行重定向
if(!StringUtils.isEmpty(username) && "123456".equals(password))
{
return "redirect:/main"; //这样会重新请求一次服务器,然后就会返回dashboard页面了
}

利用拦截器进行登陆检查

要使用拦截器需要定义一个类然后实现HandlerInterceptor接口

public class LoginHeadlerInterceptor implements HandlerInterceptor {
//目标方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser"); //获取到session的值
if(user==null)
{
request.setAttribute("msg","没有权限请先登陆");
request.getRequestDispatcher("/").forward(request,response); //跳转链接到开始页面
return false;
}
else
{
return true;
}
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}
}

注册HandlerInterceptor到WebMvcConfigurerAdapter中

//所有webMvcConfigurerAdapter组件都会一起起作用
@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter()
{
//注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//super.addInterceptors(registry);
registry.addInterceptor(new LoginHeadlerInterceptor())//添加自定义的拦截器
.addPathPatterns("/**")//添加拦截请求
.excludePathPatterns("/","index.html","/user/login");//过滤的拦截请求
}
};
return adapter;
}

这样拦截器就会生效,没有登陆就进不去main页面

  • 页面从session中取值
<!--使用行内标签取出session的用户名-->
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="#">[[${session.loginUser}]]</a>

员工列表功能-CRUD

Restful-CRUD

CRUD满足Restful风格
URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作

普通CRUD(uri来区分操作) RestfulCRUD
查询 getEmp emp—GET
添加 addEmp?xxx emp—POST
修改 updateEmp?id=xxx&xxx=xx emp/{id}—PUT
删除 deleteEmp?id=1 emp/{id}—DELETE

请求的架构

实验功能 请求URI 请求方式
查询所有员工 emps GET
查询某个员工(来到修改页面) emp/{id} GET
来到添加页面 emp GET
添加员工 emp POST
来到修改页面(查出员工进行信息回显) emp/{id} GET
修改员工 emp PUT
删除员工 emp/{id} DELETE

thymeleaf抽取公共元素

抽取公共片段

<div th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</div>

引入公共片段

<div th:insert="~{footer :: copy}"></div>
<!--~{templatename::selector}:模板名::选择器
~{templatename::fragmentname}:模板名::片段名-->

默认效果:

insert的公共片段在div标签中
如果使用th:insert等属性进行引入,可以不用写~{}:
行内写法可以加上:[[~{}]];[(~{})];

三种引入公共片段的th属性:

  • th:insert:将公共片段整个插入到声明引入的元素中
  • th:replace:将声明引入的元素替换为公共片段
  • th:include:将被引入的片段的内容包含进这个标签中
    效果
<footer th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
引入方式
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
效果
<div>
    <footer>
    &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
</div>

<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>

<div>
&copy; 2011 The Good Thymes Virtual Grocery
</div>

例子

公共部分

<!--1、th:fragment="topbar"-->
<!--2、可以直接写一个id,然后通过选择器去找-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar" id="topbar">
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="#">[[${session.loginUser}]]</a>
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
</li>
</ul>
</nav>

使用部分

<!--引入topbar公共标签-->
<!--模板名:会使用thymeleaf的前后缀配置规则进行解析-->
<div th:replace="dashboard::topbar"></div>
<div th:replace="dashboard::#topbar"> <!--通过id选择器去找-->

引入片段传递参数

  • 在片段中定义变量
<!--activeUri变量的定义-->
<a th:class="${activeUri == 'emps' ? 'nav-link active' : 'nav-link'}" th:href="@{/emps}">
  • 然后外部引用的时候需要传入变量
<!--括号然后变量名=值-->
<div th:replace="commons/bar::#sidebar(activeUri='emps')"></div>

员工列表

后续完整代码会在后面展示

员工添加功能

点击员工添加按钮跳转到添加页面

@GetMapping("/emp")
public String toAddPage(Model model)
{
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
//来到添加页面
return "emp/add";
}

前端页面代码

<!--href本身就是get请求-->
<a class="btn btn-success" th:href="@{/emp}">员工添加</a>
前端页面
<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text" class="form-control" placeholder="zhangsan">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" class="form-control" placeholder="zhangsan@atguigu.com">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select name="department.id" class="form-control">
<!-- 提交的是部门id -->
<option th:value="${dept.getId()}" th:each="dept:${depts}" th:text="${dept.getDepartmentName()}"></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
链接到后端代码
//员工添加
//SpringMVC自动将请求和入参对象属性进行一一绑定,要求请求参数与javabean入参对象里面的属性名一样就行了
@PostMapping("/emp")
public String addEmp(Employee employee)
{
employeeDao.save(employee);
//来到员工列表页面
//redirect:表示重回向到一个地址
//forward:表示转发到一个地址
return "redirect:/emps";
}

这里的日期格式需要注意一下,日期格式。SpringBoot默认的日期格式是dd/MM/yyyy,如果修改,那么可以修改配置文件进行修改。

spring.mvc.date-format=yyyy-MM-dd HH:mm

当然可以通过前端表单配合实现。

员工修改功能

点击员工编辑按钮跳转到修改页面(页面与添加页面相同),然后将要修改的人的信息显示出来
编辑按钮代码

<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.getId()}">编辑</a>

这样就会带着员工id去当问后端,那么后端代码如何接收整个请求呢,

//来到修改页面查出当前员工,在页面进行展示
@GetMapping("/emp/{id}")
public String ToEditPage(@PathVariable("id") Integer id,Model model)
{
Employee employee = employeeDao.get(id);
model.addAttribute("emp",employee);
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
//回到修改页面
return "emp/add";
}

将该人员得信息返回到前端以后,需要改造前端代码才能将信息显示出来,因为这里是与添加在一个表单中所以我们需要做一些判断去与添加页面分开,并且我们在添加的时候需要使用post方式但是修改的时候需要使用put方式去实现修改,因为put是幂等的,post并不是幂等操作。但是表单是不支持put请求的所以为了支持Spring做了一些处理

<!--需要区分是员工修改还是员工添加-->
<form th:action="@{/emp}" method="post">
<!--发送put请求修改员工数据-->
<!--
1、SpringMVC中配置HiddenGttpMethodFilter(SpringBoot自动配置实现),这个操作实现的用input中_method中的方式去实现put
2、页面创建一个post表单
3、创建一个input项,name="_method";值就是我们指定的请求方式
-->
<input type="hidden" name="_method" value="put" th:if="${emp!=null}">
<!--这里需要传递一下id,传递员工实体的时候需要使用到员工id传到后台-->
<input type="hidden" name="id" th:if="${emp!=null}" th:value="${emp.getId()}">
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${emp.getLastName()}">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" class="form-control" placeholder="zhangsan@atguigu.com" th:value="${emp!=null}?${emp.getEmail()}">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1" th:checked="${emp!=null}?${emp.getGender()==1}">
<label class="form-check-label"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0" th:checked="${emp!=null}?${emp.getGender()==0}">
<label class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select name="department.id" class="form-control">
<!--提交的是部门id-->
<option th:selected="${emp!=null}?${dept.getId() == emp.getDepartment().getId()}" th:value="${dept.getId()}" th:each="dept:${depts}" th:text="${dept.getDepartmentName()}"></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${#dates.format(emp.getBirth(),'yyyy-MM-dd')}">
</div>
<button type="submit" class="btn btn-primary" th:text="${emp!=null}?'修改':'添加'">添加</button>
</form>

这样点击修改后就会走put请求去请求/emp,后端根据前端返回去实现修改方法

//员工修改页面:需要提交员工id
@PutMapping("/emp")
public String updateEmployee(Employee employee)
{
employeeDao.save(employee);
return "redirect:/emps";
}

至此,我们的员工修改功能就实现了。

员工删除功能

我们需要在之前删除按钮上做一些操作,

<!--在button外面包一个表单提交转换成delete的方式-->
<form th:action="@{/emp/}+${emp.getId()}" method="post">
<input type="hidden" name="_method" value="delete">
<button type="submit" class="btn btn-sm btn-danger">删除</button>
</form>

后端需要一个方式进行接受请求,删除的方法

//员工删除功能
@DeleteMapping("/emp/{id}")
public String deleteEmployee(@PathVariable("id") Integer id)
{
employeeDao.delete(id);
return "redirect:/emps";
}

因为一个按钮会出现一个表单,所以不建议这种方式实现,我们可以在外部放一个表单,然后利用js实现表单提交
1、将form提出来,但是不要写action,将其去掉

<form id = "deleteEmpForm" method="post">
<input type="hidden" name="_method" value="delete">
</form>

2、改造button

<!--这里th:attr="del_uri=@{/emp/}+${emp.getId()}"是自定义标签,用于获取url-->
<button th:attr="del_uri=@{/emp/}+${emp.getId()}" class="btn btn-sm btn-danger deleteBtn">删除</button>

3、编写js

<script>
$(".deleteBtn").click(function () {
//删除当前员工
$("#deleteEmpForm").attr("action",$(this).attr("del_uri")).submit();
});
</script>

这样就可以实现一个表单多个删除了。

错误处理机制

SpringBoot默认的错误处理机制

1、SpringBoot出现的默认效果是这样的,返回一个默认的错误页面

2、如果单纯访问链接,返回的错误是这样的(利用postman代替)

SpringBoot错误处理实现原理

在SpringBoot的自动配置的web文件夹中有一个ErrorMvcAutoConfiguration类,错误处理机制自动配置。
给容器中添加了如下重要组件
1、DefaultErrorAttributes :帮我们在页面共享信息

@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}

2、BasicErrorController : 处理/error请求

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//为什么能够区分是不是html还是json主要是浏览器发送的时候会在请求头Accept写text/html,这是用于区分的标准
//浏览器发送的请求Accept是text/html
//客户端发送的请求Accept是*/*
//产生html类型的数据
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//取哪个页面作为错误页面ModelAndView包含页面地址和页面内容,是由resolveErrorView方法决定的
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
//产生json类型的数据
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
//AbstractErrorController这是父类的方法
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
//得到所有的ErrorViewResolver,异常视图解析器,得到ModelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
}

3、ErrorPageCustomizer : 系统出现错误以后来到error请求进行处理(web.xml注册的错误页面规则)

//注册错误页面
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
+ this.properties.getError().getPath());//获取错误页面路径
errorPageRegistry.addErrorPages(errorPage);
}
//错判页面的出现是
@Value("${error.path:/error}")
private String path = "/error";
public String getPath() {
return this.path;
}

4、DefaultErrorViewResolver

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//默认SpringBoot可以去找到一个页面 error/404
String errorViewName = "error/" + viewName;
//模板引擎可以解析这个页面地址就用魔版引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
//模板引擎可以用的情况下返回errorViewModel指定的视图地址
return new ModelAndView(errorViewName, model);
}
//引擎如果不可用,就在静态资源文件夹下找errorViewModel对应的页面 error/404.html
return resolveResource(errorViewName, model);
}

步骤:
一旦系统出现4XX或者5XX的错误的时候,ErrorPageCustomizer就会生效,就会来到/error请求,然后就会来到BasicErrorController的代码中,被BasicErrorController处理,这里面获取了错误代码和错误原因以后通过ErrorViewResolver得到一个ModelAndView,去哪个页面是由DefaultErrorViewResolver解析得到的。

定制错误处理

定制错误页面

  • 如果有模板引擎找的是视图下的状态码 :error/状态码

    • 将错误页面命名为错误状态码.html放在模板引擎文件加下的error文件夹下,发生此状态码的错误后来到对应的页面;
    • 我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html)
  • 没有模板引擎的时候,在静态资源文件下找error/404.html

  • 模板引擎与静态资源都没有的时候来到SpringBoot默认的错位提示页面

    //在ErrorMvcAutoConfiguration下
    @Configuration
    @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
    @Conditional(ErrorTemplateMissingCondition.class)
    protected static class WhitelabelErrorViewConfiguration {
    private final SpelView defaultErrorView = new SpelView(
    "<html><body><h1>Whitelabel Error Page</h1>"
    + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
    + "<div id='created'>${timestamp}</div>"
    + "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
    + "<div>${message}</div></body></html>");//这就是返回的页面信息
    @Bean(name = "error")
    @ConditionalOnMissingBean(name = "error")
    public View defaultErrorView() {
    return this.defaultErrorView;
    }
  • 页面能获取到的信息

    • timestamp:时间戳
    • status:错误状态码
    • error:错误提示
    • message:异常消息
    • errors:JSR303数据校验的错误
    • 这些信息的获取是在DefaultErrorAttributes中定义的
      @RequestMapping(produces = "text/html")
      public ModelAndView errorHtml(HttpServletRequest request,
      HttpServletResponse response) {
      HttpStatus status = getStatus(request);
      //这个model就是用过getErrorAttributes得到的
      Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
      request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
      response.setStatus(status.value());
      ModelAndView modelAndView = resolveErrorView(request, response, status, model);
      return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
      }
      protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
      boolean includeStackTrace) {
      RequestAttributes requestAttributes = new ServletRequestAttributes(request);
      //errorAttributes的getErrorAttributes方法获取到的
      return this.errorAttributes.getErrorAttributes(requestAttributes,
      includeStackTrace);
      }
      //errorAttributes是一个接口,其中实现它的就是DefaultErrorAttributes,就是DefaultErrorAttributes够成的model,返回到前端的数据
      DefaultErrorAttributes中塞入的信息就是我们能够找到的信息

定制错误json

  • 自定义异常处理&返回值定制json数据

    • 编写自定义异常
      public class UserNotExistException extends  RuntimeException {
      public UserNotExistException() {
      super("用户不存在");
      }
      }
    • 在特定位置抛出异常
    • 编写Hendler
      @ControllerAdvice
      public class MyExceptionHendler {
      //浏览器客户端都是json
      @ResponseBody
      @ExceptionHandler(UserNotExistException.class)//这里就获取到了具体得异常类型
      public Map<String,Object> handException(Exception e )
      {
      Map<String,Object> map = new HashMap<>();
      map.put("code","user.notexit");
      map.put("message",e.getMessage());
      return map;
      }
      //这样有个缺点没有自适应效果,因为整个错误都返回了map数据转成了json
      }
  • 变成自适应,就是把请求转发到error来进行自适应

    @ControllerAdvice
    public class MyExceptionHendler {
    @ExceptionHandler(UserNotExistException.class)
    public String handException(Exception e, HttpServletRequest request)
    {
    Map<String,Object> map = new HashMap<>();
    //传入我们自己得错误状态码 否则不会到我们得错误页面
    /**
    * Integer status = getAttribute(requestAttributes,
    * "javax.servlet.error.status_code");
    */
    request.setAttribute("javax.servlet.error.status_code",400);
    map.put("code","user.notexit");
    map.put("message",e.getMessage());
    return "forward:/error";
    }
    }
  • 将我们得定制数据携带出去
    出现错误以后,会来到/error请求,会被BasicErrorController处理,相应出去可以获取得数据是由getEooroAttributes()得到的(是AbstractErrorController规定的方法)
    1、完全来编写一个ErrorController得实现类(或者编写继承AbstractErrorController的子类)放到容器中,因为BasicErrorController的注册条件是如果有该类或者该类的子类注册了,就不会重复注册了。这里应该继承一下BasicErrorController然后重写方法errorHtml()error这样实现把自己的model写进去。但是这样太麻烦了。
    2、页面上可以使用的数据或者是json能用的数据都是通过ErrorAttributes.getErrorAttributes()得到的这里的ErrorAttributes是一个接口,同样的SpringBoot也是使用同样的方法注册了一个DefaultErrorAttributes我们也可以通过实现ErrorAttributes接口来自定义ErrorAttributes。

    @ControllerAdvice
    public class MyExceptionHendler {
    @ExceptionHandler(UserNotExistException.class)
    public String handException(Exception e, HttpServletRequest request)
    {
    Map<String,Object> map = new HashMap<>();
    //传入我们自己得错误状态码
    /**
    * Integer status = getAttribute(requestAttributes,
    * "javax.servlet.error.status_code");
    */
    request.setAttribute("javax.servlet.error.status_code",404);
    map.put("code","user.notexit");
    map.put("message",e.getMessage());
    request.setAttribute("ext",map);//把这些扩展数据塞进request中
    return "forward:/error";
    }
    }
    //给容器中加入自己定义的ErrorAttributes
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
    Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
    map.put("name","zenshin");//在map中加入更多的数据
    Map<String, Object> ext = (Map<String, Object>) requestAttributes.getAttribute("ext", SCOPE_REQUEST);//从request中取出数据
    map.put("ext",ext);
    return map;
    }
    }

    最终的效果响应式自适应的,可以通过定制ErrorAttributes改变需要返回的model值

文章作者: zenshin
文章链接: https://zlh.giserhub.com/2020/03/30/springboot/testcrud/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 zenshin's blog
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论