文章目录
Spring Boot 构建后台管理系统
一、项目创建
使用Spring Initializr创建新项目,选中所需模块 thymeleaf、web-starter、devtools、lombok。
返回顶部
二、静态资源处理
自动配置好,我们只需要把所有静态资源放到 static 文件夹下
返回顶部
三、简单布局
1.项目结构
2.任务目标
- 主要目标就是实现简单的登录界面、用户登陆跳转到主界面
3.具体实现
3.1 登录界面定制
登陆界面:用户名、密码
3.2 构建用户对象类
package com.zyx.core.web.demo.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String username;
private String password;
}
3.3 Controller层
LoginPage() 方法给出登陆页面请求,最终转到login.html
Form() 方法实现页面表单提交请求处理:
- User user封装登陆的用户对象信息
- HttpSession session将登陆的用户信息暂存在session中,用于页面跳转信息传递
- Model model将信息存贮在请求域中
主要的逻辑思路:
- 发送登陆请求实现登陆界面的显示
- 前端代码实现登陆表单的信息提交,判断用户输入信息(这里暂时不和数据库连接,就默认username不为空,密码为123456即可!)
- 登陆到主页面后,如果Form()直接给出 /main 请求,那么每一次刷新主页面,都是一次表单的重提交。为了避免表单的重复提交,我们可以在转到main.html之前进行用户的信息判断,利用session中存储的用户信息,是否为登录状态;若为登录状态则直接转到主页面;否则回退到登录界面重新登陆。
- 编写 MainPage() 进行用户的登陆判断(相当于拦截器), 在 Form() 中用户登陆后,就将信息存在session中,并且重定向至 MainPage() 进行用户登陆的判断。
package com.zyx.core.web.demo.controler;
import com.zyx.core.web.demo.bean.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpSession;
@Controller
public class WebController {
/**
* 登陆
* @return
*/
@GetMapping(value = {"/","/login"})
public String LoginPage(){
return "login";
}
/**
* 表单提交
* @param user
* @param session
* @param model
* @return
*/
@PostMapping(value = "/login")
public String Form(User user, HttpSession session, Model model){
// 判断登陆信息的完整性
if ((StringUtils.hasLength(user.getUsername()))
&& (StringUtils.hasLength(user.getPassword()))
&& ("123456".equals(user.getPassword()))) {
// 把登陆成功的用户保存起来
session.setAttribute("loginUser", user);
// 登陆成功重定向到主页面
return "redirect:/main.html";
} else {
// 提示信息
model.addAttribute("msg", "账号或密码有误~");
// 回到登录页
return "login";
}
}
/**
* 主页面
* @param session
* @param model
* @return
*/
@GetMapping(value = "/main.html")
public String MainPage(HttpSession session,Model model){
// 判断是否登陆
Object loginUser = session.getAttribute("loginUser");
// 通过session存储的用户对象信息判断
if (loginUser != null){
return "main";
} else {
// 为空表示没有登陆,跳回登陆界面
model.addAttribute("msg","未登录!请先登录~");
return "login";
}
}
}
3.4 表单修改
返回顶部
四、模板抽取
4.1 Thymeleaf 的使用
- th:insert/replace/include
- th:insert is the simplest: it will simply insert the specified fragment as the body of its host tag.
- th:replace actually replaces its host tag with the specified fragment.
- th:include is similar to th:insert , but instead of inserting the fragment it only inserts the contents of this fragment.
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
<body>
...
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
</body>
The Result is:
<body>
...
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery insert将整个带标签内容插入当前标签内部
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery replace使用引用的标签替换当前的外层标签
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery include使用引用标签中的文本内容填充当前标签
</div>
</body>
4.2 网页基本布局
如下图所示,基本的管理系统界面主要包含三部分左边导航栏、头部导航栏、中间主题内容部分,并且在不同的导航跳转界面中,左侧导航栏、头部导航栏部分基本保持不变。
所以我们可以将这两部分看做是公共部分,进行模板的抽取,当需要利用的时候我们就进行提取,主体部分可以根据具体的几面具体设置。
新建common.html存储公共界面信息。注意抽取的同时提取对应的 js、css资源文件。
抽取完后,接下来就需要引用了。
返回顶部
五、页面跳转
假设导航栏中的上面一部分,我们需要实现页面的跳转;如果按照正常写法,每个页面的跳转都需要重新编写导航栏部分,代码量就会显得很大,实现了模板的抽取后,只需要引用需要即可。并且需要修改只需要修改公共抽取页面一个地方的信息即可,接下来我们实现页面的跳转。
针对于表格页面,我们单独定义一个Controller,给出请求路径,实现跳转:
package com.zyx.core.web.demo.controler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class TableController {
/**
* basic_table
* @return
*/
@GetMapping(value = "/basic_table")
public String basic_table(){
return "table/basic_table";
}
/**
* dynamic_table
* @return
*/
@GetMapping(value = "/dynamic_table")
public String dynamic_table(){
return "table/dynamic_table";
}
/**
* editable_table
* @return
*/
@GetMapping(value = "/editable_table")
public String editable_table(){
return "table/editable_table";
}
/**
* pricing_table
* @return
*/
@GetMapping(value = "/pricing_table")
public String pricing_table(){
return "table/pricing_table";
}
/**
* responsive_table
* @return
*/
@GetMapping(value = "/responsive_table")
public String responsive_table(){
return "table/responsive_table";
}
}
给出请求后,我们只需要在页面中的对应部分指定连接跳转请求即可,首先我们将 basic_table.html 实现抽取填充:
页面的跳转只需要在公共抽取部分修改一次即可(以basic_table为例):
效果展示:
返回顶部
六、数据渲染
6.1 表格内容的遍历
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
// 表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
// 将user信息存储在请求域中
model.addAttribute("users",users);
return "table/dynamic_table";
}
# | 用户名 | 密码 |
Trident | Internet | [[${user.password}]] |
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>#</th>
<th>用户名</th>
<th>密码</th>
</tr>
</thead>
<tbody>
<tr class="gradeX" th:each="user,stats:${users}">
<td th:text="${stats.count}">Trident</td>
<td th:text="${user.userName}">Internet</td>
<td >[[${user.password}]]</td>
</tr>
</tbody>
</table>
返回顶部
七、拦截器
7.1 HandlerInterceptor 接口
拦截器是在面向切面编程中应用的,就是在你的service或者一个方法前调用一个方法,或者在方法后调用一个方法比如动态代理就是拦截器的简单实现,在你调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作。
定义拦截器要实现HandlerInterceptor接口,并实现该接口中提供的三个方法:
- preHandle方法:进入Handler方法之前执行。可以用于身份认证、身份授权。比如如果认证没有通过表示用户没有登陆,需要此方法拦截不再往下执行(return false),否则就放行(return true)。
- postHandle方法:进入Handler方法之后,返回ModelAndView之前执行。可以看到该方法中有个modelAndView的形参。应用场景:从modelAndView出发:将公用的模型数据(比如菜单导航之类的)在这里传到视图,也可以在这里同一指定视图。
- afterCompletion方法:执行Handler完成之后执行。应用场景:统一异常处理,统一日志处理等。
package com.zyx.core.web.demo.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("拦截的请求路劲是:"+requestURI);
// 登陆检查
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser!=null){
// 放行
return true;
}
// 拦截
request.setAttribute("msg","请先登陆!!!");
// 重定向至登陆界面
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle方法执行了"+modelAndView);
}
/**
* 页面渲染完成后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion方法执行了,异常是:"+ex);
}
}
7.2 配置容器注册拦截器
package com.zyx.core.web.demo.config;
import com.zyx.core.web.demo.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 1.编写一个拦截器实现HandlerInterceptor接口
* 2.将拦截器注册到容器中 (实现WebMvcConfigurer接口的addInterceptors方法)
* 3.指定拦截规则 ---- /** 会拦截所有包括静态资源文件
*/
@Configuration // 配置容器
public class AdminWebConfig implements WebMvcConfigurer {
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器
registry.addInterceptor(new LoginInterceptor()) // 添加拦截器
.addPathPatterns("/**") // 拦截所有路径 --- 静态资源也会被拦截;不加下面一行页面显示没有样式!
.excludePathPatterns("/", "/login","/css/**","/fonts/**","/images/**","/js/**"); // 放行的路径
}
}
当配置了拦截器后我们可以将之前的用戶登陆检验注释,并添加日志測試:
可以看出,在注释了==/main.html==中的测试后,再次运行直接登陆主页面,拦截器起到了作用,对登陆用户进行了判断,实施拦截!
然后我们进行登陆测试:
7.3 拦截器原理
1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
- 如果当前拦截器prehandler返回为true,则执行下一个拦截器的preHandle
- 如果当前拦截器返回为false,直接倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false,直接跳出不执行目标方法(如上图)
4、所有拦截器都返回True,执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
8.示意图
返回顶部
八、文件上传
8.1 文件提交表单
<div class="panel-body">
<!-- 文件上传表单的固定写法 -->
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1"
placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">姓名</label>
<input type="text" name="username" class="form-control" id="exampleInputPassword1"
placeholder="Password">
</div>
<!-- 单文件上传 -->
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImage" id="exampleInputFile">
</div>
<!-- 多文件上传 -->
<div class="form-group">
<label for="exampleInputFile">自拍</label>
<input type="file" name="photos" multiple>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
8.2 提交信息测试
package com.zyx.core.admin.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Controller
public class FileUploadController {
/**
* MultipartFile 会自动封装
*
* @param email
* @param username
* @param headerImage
* @param photos
* @return
*/
@PostMapping(value = "/upload")
public String FileUpload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImage") MultipartFile headerImage,
@RequestPart("photos") MultipartFile[] photos) {
// 控制台输出 测试上传的信息
log.info("上传的信息:email={},username={},headerImage_size={},photos={}",
email, username, headerImage.getSize(), photos);
return "index";
}
}
8.3 表单文件存储
package com.zyx.core.admin.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
@Slf4j
@Controller
public class FileUploadController {
/**
* MultipartFile 会自动封装
*
* @param email
* @param username
* @param headerImage
* @param photos
* @return
*/
@PostMapping(value = "/upload")
public String FileUpload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImage") MultipartFile headerImage,
@RequestPart("photos") MultipartFile[] photos) {
// 测试上传的信息
log.info("上传的信息:email={},username={},headerImage_size={},photos={}",
email, username, headerImage.getSize(), photos);
// 单文件存储
if (headerImage != null) {
try {
// 获取文件名
String originalFilename = headerImage.getOriginalFilename();
// 存储文件
headerImage.transferTo(new File("G:\\Projects\\IdeaProjects\\MyWeb\\src\\main\\resources\\upload\\"
+ originalFilename));
} catch (Exception e) {
e.printStackTrace();
}
}
// 多文件存储
if (photos.length>0){
for (MultipartFile photo:photos){
if (photo!=null){
try {
// 获取文件名
String originalFilename = photo.getOriginalFilename();
// 存储文件
photo.transferTo(new File("G:\\Projects\\IdeaProjects\\MyWeb\\src\\main\\resources\\upload\\"
+ originalFilename));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return "index";
}
}
8.4 文件上传自动配置原理
文件上传自动配置类-MultipartAutoConfiguration - MultipartProperties
- 自动配置好了StandardServletMultipartResolver 【文件上传解析器】
- 原理步骤
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成MultipartFile
- 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
FileCopyUtils => 实现文件流的拷贝
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos)
返回顶部
九.异常处理
9.1 错误处理
1、默认规则
- 默认情况下,Spring Boot提供
/error
处理所有错误的映射 - 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
- **要对其进行自定义,添加 **
View
解析为error
- 要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制但替换其内容。 - error/下的4xx,5xx页面会被自动解析;
2、定制错误处理逻辑
- error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
- @ControllerAdvice+@ExceptionHandler处理全局异常;底层是ExceptionHandlerExceptionResolver 支持的
- @ResponseStatus+自定义异常 ;底层是ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用response.sendError(statusCode, resolvedReason);tomcat发送的/error
- Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
- 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
- ErrorViewResolver实现自定义处理异常;
- response.sendError 。error请求就会转给controller
- 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
- basicErrorController 要去的页面地址是ErrorViewResolver;
3、异常处理自动配置原理
- ErrorMvcAutoConfiguration 自动配置异常处理规则
- 容器中的组件:类型:DefaultErrorAttributes ->id:errorAttributes
- public classDefaultErrorAttributesimplementsErrorAttributes,HandlerExceptionResolver
- DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
- **容器中的组件:类型:**BasicErrorController --> id:basicErrorController(json+白页 适配响应)
- 处理默认/error 路径的请求;页面响应newModelAndView(“error”, model);
- 容器中有组件 View->id是error;(响应默认错误页)
- 容器中放组件BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
- **容器中的组件:**类型:**DefaultErrorViewResolver -> id:**conventionErrorViewResolver
- 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
- error/404、5xx.html
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
写出去json
4、异常处理步骤流程
1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException
2、进入视图解析流程(页面渲染)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;
- 1、遍历所有的handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
- 2、系统默认的 异常解析器;
- 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
- 2、默认没有任何人能处理异常,所以异常会被抛出
- 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
- 2、解析错误视图;遍历所有的ErrorViewResolver 看谁能解析。
- 3、默认的DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
- 4、模板引擎最终响应这个页面error/500.html
返回顶部