Spring Boot 开发实战


🍳 Web开发实战案例:员工信息 CRUD


本案例采用 SpringBoot + Thymeleaf,没有使用 Mybatis,全程模拟数据。

只是一个小 Demo,很多功能都没有完善,只实现了最基本的 CRUD 和登录,登录的密码写死了是123456,用户名随意。

由于实际开发中一般不使用 Thymeleaf,所以本篇对于 Thymeleaf 不做过多详细介绍,会用即可。

📂 源码在此:https://gitee.com/veal98/springboot_demo

Thymeleaf 取值语法:

👇 案例的最终效果如下:

📄 目录结构如下:

1. 准备工作

准备工作包括静态资源导入,实体类编写,以及模拟数据库操作

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

实体类

  • Department 部门类

    /**
     * 部门表
     */
    
    public class Department {
        private Integer id;
        private String departmentName;
    
        public Department() {
        }
    
        public Department(Integer id, String departmentName) {
            this.id = id;
            this.departmentName = departmentName;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getDepartmentName() {
            return departmentName;
        }
    
        public void setDepartmentName(String departmentName) {
            this.departmentName = departmentName;
        }
    
        @Override
        public String toString() {
            return "Department{" +
                    "id=" + id +
                    ", departmentName='" + departmentName + '\'' +
                    '}';
        }
    }
    
  • Employee 员工类

    /**
     * 员工表
     */
    
    public class Employee {
        private Integer id;
        private String lastName;
        private String email;
        private Integer gender; // 0 1
        private Department department;
        private Date birth;
    
        public Employee() {
        }
    
        public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
            this.id = id;
            this.lastName = lastName;
            this.email = email;
            this.gender = gender;
            this.department = department;
            this.birth = new Date(); // 偷个懒,以默认的创建日期构造
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public Integer getGender() {
            return gender;
        }
    
        public void setGender(Integer gender) {
            this.gender = gender;
        }
    
        public Department getDepartment() {
            return department;
        }
    
        public void setDepartment(Department department) {
            this.department = department;
        }
    
        public Date getBirth() {
            return birth;
        }
    
        public void setBirth(Date birth) {
            this.birth = birth;
        }
    
        @Override
        public String toString() {
            return "Employee{" +
                    "id=" + id +
                    ", lastName='" + lastName + '\'' +
                    ", email='" + email + '\'' +
                    ", gender=" + gender +
                    ", department=" + department +
                    ", birth=" + birth +
                    '}';
        }
    }
    

数据库操作类

  • DepartmentDao

    /**
     * 部门 dao
     */
    @Repository
    public class DepartmentDao {
    
        // 模拟数据库中的数据
        private static Map<Integer, Department> departments = null;
        static{
            departments = new HashMap<>(); // 创建一个部门表
            departments.put(101, new Department(101, "教学部"));
            departments.put(102, new Department(102, "市场部"));
            departments.put(103, new Department(103, "教研部"));
            departments.put(104, new Department(104, "运营部"));
            departments.put(105, new Department(105, "后勤部"));
    
        }
    
        // 获得所有的部门信息
        public Collection<Department> getDepartment(){
            return departments.values();
        }
    
        // 根据 id 获取部门信息
        public Department getDepartmentById(Integer id){
            return departments.get(id);
        }
    }
    
  • EmployeeDao

    /**
     * 员工 Dao
     */
    @Repository
    public class EmployeeDao {
    
        // 模拟数据库中的数据
        private static Map<Integer, Employee> employees = null;
        // 员工有所属的部门
        @Autowired
        private DepartmentDao departmentDao;
        static{
            employees = new HashMap<>(); // 创建一个员工表
            employees.put(101, new Employee(1001, "AA", "123456@qq.com", 1, new Department(101,"教学部")));
            employees.put(102, new Employee(1002, "BB", "1546556@qq.com", 0, new Department(102, "市场部")));
            employees.put(103, new Employee(1003, "CC", "765543@qq.com", 1, new Department(103, "教研部")));
            employees.put(104, new Employee(1004, "DD", "34654234@qq.com", 0, new Department(104, "运营部")));
            employees.put(105, new Employee(1005, "EE", "72423423456@qq.com", 1, new Department(105, "后勤部")));
        }
    
        // 增加一个员工(主键自增)
        private static Integer initId = 1006;
        public void save(Employee employee){
            if(employee.getId() == null) // 设置Id
                employee.setId(initId ++);
            employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId())); // 设置部门
            employees.put(employee.getId(),employee); // 放入 Map (数据库)
        }
    
        // 查询全部员工信息
        public Collection<Employee> getAll(){
            return employees.values();
        }
    
        // 根据 id 查询员工信息
        public Employee getEmployeeById(Integer id){
            return employees.get(id);
        }
    
        // 根据 id 删除员工
        public void delete(Integer id){
            employees.remove(id);
        }
    
    }
    

Thymeleaf 中链接使用标签 @{...},使用 th:href = "@{...}" 导入资源路径,例如:

<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

2. 默认访问首页

当访问 http://localhost:8080/ 的时候,默认去找静态资源文件夹下的 index.html,此处我们想要默认加载 templates 文件夹(在此文件夹中的资源才能被 Thymeleaf 解析)下的 index.html 作为首页。

扩展 SpringMVC 配置:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    // 添加视图映射
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }
}

3. 国际化

① 创建国际化配置文件

在 resources 文件夹下创建 i18n 文件夹,以及login.propertieslogin_zh_CN.properties

创建完毕后,两个文件会自动合并成一个文件夹:

右键可直接添加配置文件

② 编写国际化配置文件

可点击 Resource Bundle 进行可视化配置:

添加如下配置:

同时在全局配置文件中添加如下配置:

# 国际化配置文件的真实路径
spring.messages.basename = i18n.login

Thymeleaf 中,使用标签#{...} 来获取配置文件中的信息,例如:

<label class="sr-only" th:text="#{login.username}">Username</label>

③ 实现点击链接切换语言

给 url 地址添加参数 language,国际化解析器对参数进行解析:

<a class="btn btn-sm" th:href="@{/index.html(language='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(language='en_US')}">English</a>

自定义的一个简单的国际化解析器: config.MyLocaleResolver.java

package com.smallbeef.springboot_demo.config;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

/**
 * 国际化解析器
 */
public class MyLocaleResolver implements LocaleResolver {

    // 解析请求
    @Override
    public Locale resolveLocale(HttpServletRequest httpServletRequest) {
        // 获取请求url中的参数
        String language = httpServletRequest.getParameter("language");
        Locale locale =  Locale.getDefault(); // 如果没有就使用默认的
        // 如果请求携带了国际化的参数
        if(!StringUtils.isEmpty(language)){
            // zh_CN
            String[] split = language.split("_");
            // 国家,地区
            locale =  new Locale(split[0], split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

    }
}

参考LocaleResolver 类(获取区域信息对象)的源码如下:

将我们自己写的国际化组件配置到 Spring 的容器中(@Bean),即注册国际化组件:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    // 添加视图映射
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }

    // 自定义的国际化组件生效
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
}

效果如下:

4. 登陆

① 禁用模板引擎的缓存

在全局配置文件中禁用模板引擎的缓存,使得开发期间模板引擎页面修改以后实时生效

# 禁用缓存
spring.thymeleaf.cache=false 

② 信息校验

登录界面的部分前端代码:

<body class="text-center">
    <form class="form-signin" th:action="@{/user/login}" method="post">
        .....
        <!--错误提示信息-->
        <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
        
        .....
        
        <input type="text"  name="username" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus="">
        
        ......
        
        <input type="password" name="password" class="form-control" placeholder="Password" th:placeholder="#{login.password}" required="">
        ......
        
    </form>
</body>

控制器 LoginController

@Controller
public class LoginController {

    @RequestMapping("/user/login")
    public String login(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            Model model, HttpSession session){

        // 简单的登录验证
        if(!StringUtils.isEmpty(username) && "12345".equals(password)){
            session.setAttribute("loginUser", username);  // 用户信息存入 session
            return "redirect:/main.html"; 
        }
        else{
            // 显示登录失败信息
            model.addAttribute("msg","用户名或者密码错误!");
            return "index";
        }
    }
}

dashboard.html 是我们的后台界面,将其映射到 main.html 路径

registry.addViewController("/main.html").setViewName("dashboard");

效果如下:

5. 拦截器进行登陆检查

现在如果我们直接输入 http://localhost:8080/main.html 可以跳过登录直接访问,显然这是不合理的。此时需要拦截器进行登录检查

① 拦截器

public class LoginHandlerIntercepter implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 登录成功后 ,应该有用户的 session
        Object loginUser = request.getSession().getAttribute("loginUser");

        if(loginUser == null){ // 未登录
            request.setAttribute("msg", "没有权限,请先登录");
            request.getRequestDispatcher("/index.html").forward(request,response);
            return false; // 表示拦截
        }
        else
            return true; // 表示放行
    }
}

② 注册拦截器

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    // 添加视图映射
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
        registry.addViewController("/main.html").setViewName("dashboard");
    }

    // 自定义的国际化组件生效
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }

    // 注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginHandlerIntercepter()).addPathPatterns("/**")
                .excludePathPatterns("/index.html","/","/user/login", "/css/**","/js/**","/img/**");
    }
}

效果如下,如果我们未经登录直接访问 main.html 的话:

③ 回显用户名

在后台管理界面实现用户名回显:

在前端界面从 session 中取出数据就行了:

<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="#">[[${session.loginUser}]]</a>

6. RESTFul CRUD

CRUD 满足 Rest 风格

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/1 GET
来到添加页面 emp GET
添加员工 emp POST
来到修改页面(查出员工进行信息回显) emp/1 GET
修改员工 emp PUT
删除员工 emp/1 DELETE

7. CRUD - 员工列表

展示员工信息

点击员工管理后调用 list 函数显示所有员工信息:

<a class="nav-link active" th:href="@{/emps}" >
    ...
    员工管理
</a>

EmployeeController

@Controller
public class EmployeeController {

    @Autowired
    EmployeeDao employeeDao; // 因为没有数据库,所有数据库都定义在 Dao 层里面了

    @RequestMapping("/emps")
    public String list(Model model){
        Collection<Employee> employees = employeeDao.getAll();
        model.addAttribute("emps",employees);
        return "emp/list"; // 跳转员工信息显示界面
    }
}

员工信息显示界面 list.html

从 model 中循环获取员工数据

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <div class="table-responsive">
        <table class="table table-striped table-sm">
            <thead>
                <tr>
                    <th>id</th>
                    <th>lastName</th>
                    <th>email</th>
                    <th>gender</th>
                    <th>department</th>
                    <th>birth</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="emp:${emps}">
                    <td th:text="${emp.id}"></td>
                    <td>[[${emp.lastName}]]</td>
                    <td th:text="${emp.getEmail()}"></td>
                    <td th:text="${emp.gender}==0?'':''"></td>
                    <td th:text="${emp.department.departmentName}"></td>
                    <td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}"></td>
                    <td>
                        <button class="btn btn-sm btn-primary" >编辑</button>
                        <button class="btn btn-sm btn-danger deleteBtn">删除</button>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</main>

8. CRUD - 员工添加

步骤

  • 按钮提交
  • 跳转到添加界面
  • 添加员工成功
  • 返回首页

按钮

<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">员工添加</a></h2>

跳转到添加界面:(采用 RESTFul 风格)

// 跳转到员工添加界面
@GetMapping("/emp")
public String toAddPage(Model model){
    // 查出所有部门信息
    Collection<Department> departments = departmentDao.getDepartment();
    model.addAttribute("departments",departments); // 存入model 使得前端能够获取
    return "emp/add";
}

添加界面 add.html

<form th:action="@{/emp}" method="post"> <!--虽然和跳转到员工界面的controller是同一个url,但是请求方式不一样-->
    <div class="form-group">
        <label>LastName</label>
        <!--正确的 name 才能够将信息顺利地提交给后端-->
        <input name="lastName" type="text" class="form-control" placeholder="飞天小牛肉" >
    </div>
    <div class="form-group">
        <label>Email</label>
        <input name="email" type="email" class="form-control" placeholder="xiaoniurou@qq.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>
        <!--text 是下拉框显示的文字,提交的是 value 即部门id-->
        <select class="form-control" name="department.id">
            <option th:each="dept:${departments}"
                    th:text="${dept.departmentName}"
                    th:value="${dept.id}">
            </option>
        </select>
    </div>
    <div class="form-group">
        <label>Birth</label>
        <input name="birth" type="text" class="form-control" placeholder="2020/12/29">
    </div>
    <button type="submit" class="btn btn-primary">添加</button>
</form>

form 表单中的 action 路径虽然和跳转到员工界面的 controller(toAddPage)是同一个url,但是请求方式不一样(RESTFul 风格)

添加员工

// 添加员工
@PostMapping("/emp")
public String addEmp(Employee employee){
    employeeDao.save(employee); // 添加员工信息
    return "redirect:/emps"; // 跳转到首页
}

还要注意日期格式问题,SpingBoot 默认日期格式是 yyyy/MM/dd,我们可在全局配置文件中将其修改成 yyyy-MM-dd

# 日期格式化(默认是yyyy/MM/dd)
spring.mvc.format.date=yyyy-MM-dd

最终效果如下

9. CRUD - 员工修改

OK,接下来进行员工的修改:(编辑按钮在 list 界面)

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

url 上传入员工 id,后台获取 id 对该员工进行操作

和员工添加一样,写两个方法一个跳转到员工修改界面,一个用来实现修改员工信息

// 跳转到修改员工界面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id") Integer id,Model model){
    //查出原来的数据
    System.out.println(id);
    Employee employee = employeeDao.getEmployeeById(id);
    System.out.println(employee);
    model.addAttribute("emp",employee);
    //查询所有部门信息
    Collection<Department> departments=departmentDao.getDepartment();
    model.addAttribute("departments",departments);
    return "emp/update";
}

// 修改员工
@PutMapping("/updateEmp")
public String updateEmp(Employee employee){
    employeeDao.save(employee); // 保存员工信息
    return "redirect:/emps"; // 跳转到首页
}

修改界面 update.html

<form th:action="@{/updateEmp}" method="post">
    <!--隐藏域,标名我们提交的id-->
    <input type="hidden" th:value="${emp.getId()}" name="id">
    <div class="form-group">
        <label>LastName</label>
        <input th:value="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="yanhang">
    </div>
    <div class="form-group">
        <label>Email</label>
        <input th:value = "${emp.getEmail()}" name="email" type="email" class="form-control" >
    </div>
    <div class="form-group">
        <label>Gender</label><br/>
        <div class="form-check form-check-inline">
            <input th:checked="${emp.getGender()==1}" 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 th:checked="${emp.getGender()==0}" 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>
        <!--text 是下拉框显示的文字,提交的是 value 即部门id-->
        <select class="form-control" name="department.id">
            <option th:selected="${dept.getId() == emp.getDepartment().getId()}"
                    th:each="dept:${departments}"
                    th:text="${dept.departmentName}"
                    th:value="${dept.id}">
            </option>
        </select>
    </div>
    <div class="form-group">
        <label>Birth</label>
        <input th:value = "${#dates.format(emp.getBirth(),'yyyy-MM-dd')}" name="birth" type="text" class="form-control" >
    </div>
    <button type="submit" class="btn btn-primary">添加</button>
</form>

10. CRUD - 员工删除

<a class="btn btn-sm btn-danger" th:href="@{'/delemp/'+${emp.getId()}}">删除</a>
// 删除员工
@GetMapping("/delemp/{id}")
public String toDeleteEmp(@PathVariable("id")Integer id){
    employeeDao.delete(id);
    return "redirect:/emps";
}

11. BUG 提醒

兄弟萌,在创建员工信息的时候,一定要注意Map 的 key 要和后面的 Employee 对象的 id 一致,否则没办法通过 getEmployeeById 查询到对应的员工信息。如下是错误的示范:

✅ 下面是正确的:

12. 登出

<a class="nav-link" th:href="@{/user/logout}">退出</a>
// 登出
@RequestMapping("/user/logout")
public String UserLogout(HttpSession session){
    session.invalidate();
    return "redirect:/index.html"; // 重定向到首页
}

完结撒花~ 🎉

📚 References


文章作者: Gtwff
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Gtwff !
  目录