【Spring Boot】构建后台管理系统

本文详述了使用SpringBoot构建后台管理系统的步骤,包括项目创建、静态资源处理、登录布局实现、模板引擎Thymeleaf的使用、页面跳转、数据渲染、拦截器、文件上传和异常处理。通过实例代码展示了如何处理用户登录、页面布局、数据展示、文件操作及错误处理等核心功能。
摘要由CSDN通过智能技术生成


Spring Boot 构建后台管理系统

一、项目创建

使用Spring Initializr创建新项目,选中所需模块 thymeleaf、web-starter、devtools、lombok。

image-20210511140200431

image-20210511140246158
返回顶部


二、静态资源处理

自动配置好,我们只需要把所有静态资源放到 static 文件夹下
在这里插入图片描述

返回顶部


三、简单布局

1.项目结构

在这里插入图片描述


2.任务目标

  • 主要目标就是实现简单的登录界面、用户登陆跳转到主界面

3.具体实现

3.1 登录界面定制

登陆界面:用户名、密码

image-20210824205734115


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 表单修改

image-20210511153302844

返回顶部


四、模板抽取

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">
   &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>
           &copy; 2011 The Good Thymes Virtual Grocery           insert将整个带标签内容插入当前标签内部
        </footer>
    </div>
    
    <footer>
    	&copy; 2011 The Good Thymes Virtual Grocery              replace使用引用的标签替换当前的外层标签
    </footer>
    
    <div>
    	&copy; 2011 The Good Thymes Virtual Grocery              include使用引用标签中的文本内容填充当前标签
    </div>
</body>

4.2 网页基本布局

如下图所示,基本的管理系统界面主要包含三部分左边导航栏、头部导航栏、中间主题内容部分,并且在不同的导航跳转界面中,左侧导航栏、头部导航栏部分基本保持不变。

所以我们可以将这两部分看做是公共部分,进行模板的抽取,当需要利用的时候我们就进行提取,主体部分可以根据具体的几面具体设置。

image-20210512143201649

新建common.html存储公共界面信息。注意抽取的同时提取对应的 js、css资源文件。

image-20210511152800486

抽取完后,接下来就需要引用了。

image-20210512144951253

返回顶部


五、页面跳转

image-20210512145028032

假设导航栏中的上面一部分,我们需要实现页面的跳转;如果按照正常写法,每个页面的跳转都需要重新编写导航栏部分,代码量就会显得很大,实现了模板的抽取后,只需要引用需要即可。并且需要修改只需要修改公共抽取页面一个地方的信息即可,接下来我们实现页面的跳转。

针对于表格页面,我们单独定义一个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 实现抽取填充:

image-20210512150142906

页面的跳转只需要在公共抽取部分修改一次即可(以basic_table为例):

image-20210512145935344

效果展示:

image-20210512150611570

返回顶部


六、数据渲染

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";
}
#用户名密码
TridentInternet[[${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>

image-20210512154127078

返回顶部


七、拦截器

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/**"); // 放行的路径
    }
}

当配置了拦截器后我们可以将之前的用戶登陆检验注释,并添加日志測試:

image-20210512160758160
在这里插入图片描述
可以看出,在注释了==/main.html==中的测试后,再次运行直接登陆主页面,拦截器起到了作用,对登陆用户进行了判断,实施拦截!

然后我们进行登陆测试:

在这里插入图片描述

image-20210512164908203


7.3 拦截器原理

1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】

在这里插入图片描述

2、先来顺序执行 所有拦截器的 preHandle方法

  • 如果当前拦截器prehandler返回为true,则执行下一个拦截器的preHandle
  • 如果当前拦截器返回为false,直接倒序执行所有已经执行了的拦截器的 afterCompletion;
    在这里插入图片描述

3、如果任何一个拦截器返回false,直接跳出不执行目标方法(如上图)

4、所有拦截器都返回True,执行目标方法
在这里插入图片描述

5、倒序执行所有拦截器的postHandle方法。

image-20210512170712914

6、前面的步骤有任何异常都会直接倒序触发 afterCompletion

7、页面成功渲染完成以后,也会倒序触发 afterCompletion

在这里插入图片描述

8.示意图

image-20210512171338257
返回顶部


八、文件上传

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";
    }

}

image-20210512190352944


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";
    }
}

在这里插入图片描述

image-20210512193951656


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格式呈现相同的数据

  • image.pngimage.png

  • **要对其进行自定义,添加 ** View 解析为error

  • 要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。

  • error/下的4xx,5xx页面会被自动解析;

    • image.png
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());
    • image.png
  • 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则

    • image.png
  • ErrorViewResolver 实现自定义处理异常;

    • response.sendError 。error请求就会转给controller
    • 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
    • basicErrorController 要去的页面地址是 ErrorViewResolver

3、异常处理自动配置原理
  • ErrorMvcAutoConfiguration 自动配置异常处理规则

    • 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
      • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
      • DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
      • image.png
      • image.png
    • **容器中的组件:类型:**BasicErrorController --> id:basicErrorController(json+白页 适配响应)
      • 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);
      • 容器中有组件 View->id是error;(响应默认错误页)
      • 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
    • **容器中的组件:**类型:**DefaultErrorViewResolver -> id:**conventionErrorViewResolver
      • 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
      • error/404、5xx.html

如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)

image.png写出去json

image.png


4、异常处理步骤流程

1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException

2、进入视图解析流程(页面渲染)

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;

  • 1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器

  • image.png

  • 2、系统默认的 异常解析器;

  • image.png

    • 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
    • 2、默认没有任何人能处理异常,所以异常会被抛出
      • 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
      • 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
      • image.png
      • 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
      • 4、模板引擎最终响应这个页面 error/500.html

返回顶部


參考:https://www.bilibili.com/video/BV19K4y1L7MT?p=22

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑着蜗牛ひ追导弹'

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值