写在前面:第一次接触springboot的案例,的确比ssm少了太多的配置,参考尚硅谷+狂神说。员工管理系统导入资源后,前面的准备工作基本上是照着视频敲的,目的为了入门springboot项目的搭建。员工的CRUD操作是自己基于学过的SSM框架写的,虽然bug百出,但最后还是把功能都实现了,留下两个待解决的问题,再接再励~。

1. 准备工作

1.1 新建springboot项目





1.2 编写pom.xml

<!--thymeleaf模板引擎-->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<!--lombok插件-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

1.3 导入静态资源文件

1.4 准备pojo实体

Department 类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
   
    private Integer id;
    private String departmentName;
}

Employee 类:

@Data
@NoArgsConstructor
public class Employee {
   

    private Integer id;
    private String lastName;
    private String email;
    private Integer gender;
    private Department department;
    private Date birthday;

    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.birthday = new Date();
    }
}

添加注解@Data @AllArgsConstructor @NoArgsConstructor
按住ALT+7 可以查看到类的结构,以Department 为例

**注意:**如果遇到没有上述结构,需要按住IDEA的Lombok插件,setting中安装并重启IDEA

1.5 编写dao层

当我们没有数据库文件时,如何模拟一个数据库
DepartmentDao :

//使用类来模拟数据库
@Repository
public class DepartmentDao {
   

    //模拟数据库中的数据
    private static Map<Integer, Department> departments = null;
    //赋值数据
    static {
   
        departments = new HashMap<Integer, Department>();
        departments.put(101,new Department(101,"教学部"));
        departments.put(102,new Department(102,"文体部"));
        departments.put(103,new Department(103,"财务部"));
        departments.put(104,new Department(104,"后勤部"));
    }
    //获取所有部门的信息
    public Collection<Department> getDepartments(){
   
        return departments.values();
    }
    //通过id获取部门信息
    public Department getDepartmentById(Integer id){
   
        return departments.get(id);
    }
}

EmployeeDao :

//使用类来模拟数据库
public class EmployeeDao {
   

    //模拟数据库中的数据
    private static Map<Integer, Employee> employees = null;
    //员工所属的部门
    @Autowired
    private static DepartmentDao departmentDao;

    static {
   
       employees = new HashMap<Integer, Employee>();
        employees.put(101,
                new Employee(101,"AA","A515801535@qq.com",0,
                        new Department(103,"财务部")));
        employees.put(102,
                new Employee(102,"BB","B515801535@qq.com",1,
                        new Department(104,"后勤部")));
        employees.put(103,
                new Employee(103,"CC","C515801535@qq.com",0,
                        new Department(101,"教学部")));
    }

    //设置主键自增
    private static Integer initId = 1004;

    //增加一个员工
    public void save(Employee employee){
   
        if (employee.getId() == null){
   
            employee.setId(initId++);
        }
        //分配部门
        employee.setDepartment(departmentDao.getDepartmentById(employee.getId()));
        employees.put(employee.getId(),employee);
    }
    //查询所有的员工信息
    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);
    }
}

2. 首页实现

2.1 IndexController.java编写

@Controller
public class IndexController {
   
    @RequestMapping({
   "/","index"})
    public String index(){
   
        return "index";
    }
}

首页这种跳转我们更倾向于使用springMVC扩展方法

2.2 springMVC扩展

@Configuration
//@EnableWebMvc
public class ExtendsMvc implements WebMvcConfigurer {
   
    //方法的作用是添加视图的控制跳转
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
   
        registry.addViewController("/index").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
        registry.addViewController("/").setViewName("index");
    }
}

使用地址:

127.0.0.1:8080
127.0.0.1:8080/
127.0.0.1:8080/index
127.0.0.1:8080/index.html

均可以访问

当此时需要注意的是:@EnableWebMvc不能使用,因为使用后springboot对springmvc的默认配置失效,我们没有去配置springmvc的静态资源路径,静态资源的请求立即失效。

3. 国际化

所谓国际化,指的是网页可以支持多语言的切换,例如:

所以我们要在项目的首页也实现此功能,点击进行中英切换

3.1 分析国际化原理

  1. 进入WebMvcAutoConfiguration搜索AcceptHeaderLocaleResolver
  2. 进入AcceptHeaderLocaleResolver类

    可见请求头的地区解析器实现了一个地区解析器
  3. 继续往下看,发现实现地区解析器的方法

3.2 国际化的实现

  1. 国际化配置文件

    注意命名:

    en_US:表示英文
    zh_CN:表示中文
    login.properties:

    login.btn=登录
    login.password=密码
    login.remember=请记住我
    login.tip=请登录
    login.username=用户名
    

    login_en_US.properties

    login.btn=Sign
    login.password=password
    login.remember=Remember me
    login.tip=Please sign in
    login.username=username
    

    login_zh_CN.properties:

    login.btn=登录
    login.password=密码
    login.remember=请记住我
    login.tip=请登录
    login.username=用户名
    
  2. 前端展示

  3. 依葫芦画瓢,我们也根据请求头地区解析器的实现原理,自定义一个地区解析器,在config包下

    public class MyLocaleResolver implements LocaleResolver {
         
        //解析请求
        @Override
        public Locale resolveLocale(HttpServletRequest httpServletRequest) {
         
            //获取请求中的语言参数
            String language = httpServletRequest.getParameter("language");
    
            Locale defaLocale = Locale.getDefault();//如果没有就使用默认的
            //如果请求参数携带了国际化的参数
            if(StringUtils.isEmpty(language)){
         
                //国家,地区
                String[] split = language.split("_");
                defaLocale = new Locale(split[0], split[1]);
            }
            return defaLocale;
        }
    
        @Override
        public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
         
        }
    }
    
    
  4. 将地区的解析装配到容器中

    @Configuration
    //@EnableWebMvc
    public class ExtendsMvc implements WebMvcConfigurer {
         
        //方法的作用是添加视图的控制跳转
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
         
            registry.addViewController("/index").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
            registry.addViewController("/").setViewName("index");
        }
        //自定义的国际化组件就生效了
        @Bean
        public LocaleResolver setMyLocaleResolver(){
         
            return new MyLocaleResolver();
        }
    }
    

3.3 国际化遇到的问题

按照如上的配置完之后发现一个问题:国际化不生效,切换并不会生效。**

3.4 解决方案

方法其实很很搞笑,就是我们直接把配置文件下的setMyLocaleResolver方法名改成localeResolver 即可。
原因:为什么会这样呢?
因为我们这个方法

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

的作用是在组件中添加一个bean,它的id为localeResolver
在springboot启动时,如果没有自定义配置文件时,会去加载这个默认的方法添加地区解析器的bean对象到容器中,@ConditionalOnMissingBean的作用就是容器中没有id = localeResolver这个bean时会执行这个默认的方法添加bean到容器中。

经过上述的分析,得出结论:
没有生效的原因就是我们没有把自定义的地区解析器加到容器中,我们使用的还是默认地区解析器。
那为什么改个方法名就可以了?bean的默认id不是类名的小写字母开头吗?
<mark>其实不然,</mark>
在基于JavaConfiguration下的,也就是@Cofiguration下的类中使用@Bean,当然这个通常作用于方法上,默认id是方法名,而不是类名的小写字母开头。
在基于组件扫描的情况下,即@Component,@Sevice,@Controller,@Respositry下创建的bean的对象默认id为类名的小写字母开头。
故我们刚刚只是类型为LocaleResolver,id = setMyLocaleResolver的一个对象添加容器中了,容器中还有默LocaleResolver,id = localeResolver的对象存在,当然只会去调用默认的自动配置(因为@ConditionalOnMissingBean),故国际化是不会生效的!!

从bean在不同情况下的默认id入手排查,参考:https://www.cnblogs.com/1540340840qls/p/6962777.html

4. 登录功能实现

4.1 表单

<form class="form-signin" th:action="@{/user/login}">
			<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
			<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
			<p th:text="${msg}" style="color: red" th:if="${not #strings.isEmpty(msg)}"></p>
			<label class="sr-only">Username</label>
			<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
			<label class="sr-only">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.remember}]]
        </label>
			</div>
			<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</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>

4.2 前端控制器

@Controller
@RequestMapping("user")
public class LoginController {
   
    @RequestMapping("login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password")String password,
                        Model model){
   
        //具体业务
        if(!StringUtils.isEmpty(username) && "123".equals(password)){
   
            return "redirect:/main.html";
        }else{
   
            model.addAttribute("msg","用户名或密码错误");
            return "index";
        }
    }
}

main.html主要用来作为中间视图,过滤掉请求参数显示在地址栏。

5. 登录拦截器

设置拦截器的作用:可以拦截掉非法登录后台的操作,必须是用户登录了才能访问dashboard.html。

5.1自定义拦截器

public class MyInterceptor implements HandlerInterceptor {
   
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
        Object loginUser = request.getSession().getAttribute("loginUser");
        //session中没有数据,说明还没有登录,拦截
        if (null == loginUser){
   
            request.setAttribute("msg","你们有权限登录");
            request.getRequestDispatcher("/index.html").forward(request,response);
            return false; 	//拦截
        }else{
   
            return true;	//放行
        }

    }
}

5.2 mvc扩展

    //自定义拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**"). //拦截所有
                excludePathPatterns("/index.html","/","/css/**","/js/**","/img/**","/user/login");  //放行
    }

🐖:需要在登录控制器中添加登录成功LoginController

session.setAttribute("loginUser",username);

两个作用:

  1. loginUser不为空时代表已登录
  2. 将username携带至dashboard.html

6. 展示员工列表

6.1 抽取公共部分代码

进入main.html

可见左边导航栏和上方导航栏均属属于固定的不变的,变得只是点击左边导航栏不同选项时的高亮显示。
<mark>删除一些项目中暂不需要的代码后重新展示。</mark>

  1. 新建重复代码块

common.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<!--头部导航栏-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="upbar">
    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${
   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>

<!--左侧导航栏-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
    <div class="sidebar-sticky">
        <ul class="nav flex-column">
            <li class="nav-item">
                <a class="nav-link active th:href="@{
   /main.html}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                        <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                        <polyline points="9 22 9 12 15 12 15 22"></polyline>
                    </svg>
                    首页 <span class="sr-only">(current)</span>
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link active th:href="@{
   /emps/list}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
                        <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                        <circle cx="9" cy="7" r="4"></circle>
                        <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                        <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                    </svg>
                    员工管理
                </a>
            </li>
        </ul>
    </div>
</nav>
</html>
  1. dashboard.html替换的部分使用嵌入的方法替换
<div th:replace="~{/commons/common::upbar}"></div>
<div th:replace="~{/commons/common::sidebar}"></div>

6.2 员工列表Controller

新建EmployeeController类:

@Controller
@RequestMapping("emps")
public class EmployeeController {
   
    //注入员工Dao
    @Autowired
    private EmployeeDao employeeDao;

    @RequestMapping("list")
    public String list(Model model){
   
        Collection<Employee> emps = employeeDao.getAll();
        model.addAttribute("emps",emps);
        return "emp/list";
    }
}

6.3前端展示员工列表

  1. 将list.html存放在新建的emp文件夹中
  2. 同样类似于dashboard.html嵌入公共代码部分
<div th:replace="~{/commons/common::upbar}"></div>
<div th:replace="~{/commons/common::sidebar}"></div>
  1. table中接收服务器的数据
<table class="table table-striped table-sm">
	<thead>
		<tr>
			<th>id</th>
			<th>lastName</th>
			<th>email</th>
			<th>birthday</th>
			<th>department</th>
		</tr>
	</thead>
	<tbody>
		<tr th:each="emp:${emps}">
			<td>[[${
   emp.id}]]</td>
			<td >[[${
   emp.lastName}]]</td>
			<td>[[${
   emp.email}]]</td>
			<td th:text="${emp.gender}==0?'女':'男'"></td>
			<td th:text="${#dates.format(emp.birthday,'yyyy-mm-dd')}"></td>
			<td>[[${
   emp.department.departmentName}]]</td>
			<td>
			<button class="btn-sm btn-primary">编辑</button>
			<button class="btn-sm btn-danger">删除</button>
			</td>
		</tr>
	</tbody>
</table>

6.4 点击高亮显示

需求是当我们点击左侧导航栏可以高亮显示当前界面:

  1. 首页高亮

dashboard.html首页中发送参数?active=main.html

<div th:replace="~{/commons/common::sidebar(active='main.html')}"></div>
  1. common.html中用三目运算符接收
<a th:class="${active =='main.html'}? 'nav-link active':'nav-link'" th:href="@{/main.html}">
  1. 同理,员工管理选项
list.html:
	<div th:replace="commons/common::sidebar(active='list.html')"></div>
common.html:
	<a th:class="${active =='list.html'}? 'nav-link active':'nav-link'" th:href="@{/emps/list}">

7. 增加员工实现

7.1 增加员工按钮

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

页面展示:

7.2 增加员工页面

  1. 在resources下的emp新建add.html
  2. 增加表单:
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
	<form th:action="@{/emps/addEmploy}" method="post">
		<div class="form-group">
			<label for="lastname">姓名:</label>
			<input type="text" class="form-control" id="lastname" name="lastName" placeholder="lastName">
		</div>
		<div class="form-group">
			<label for="email">邮箱:</label>
			<input type="email" class="form-control" id="email" name="email" placeholder="email">
		</div>
		<label class="radio-inline">
			<input type="radio" name="gender"  value="1"><input type="radio" name="gender"  value="0"></label>
		<div class="form-group">
			<label>部门:</label>
			<select class="form-control" name="department.id">
				<option th:each="department:${departments}" th:value="${department.id}"
						th:text="${department.departmentName}"></option>
			</select>
		</div>
		<div class="form-group">
			<label for="birthday">生日:</label>
			<input type="date" class="form-control" id="birthday" name="birthday" placeholder="birthday">
		</div>
		<input type="submit" class="btn-sm btn-success" >新增</input>
	</form>
</main>

7.3 跳转增加员工页面controller

    @RequestMapping("add")
    public String add(Model model){
   
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments",departments);  //为了展示下拉框
        return "emp/add";
    }

7.4 增加员工controller

保存员工应当修改一处:

分配部门应当在controller处进行,这样是为了避免随着自增的员工id,导致没有部门可选(一个员工对于一个部门的)

    @RequestMapping("addEmploy")
    public String addEmploy(Employee employee, HttpServletRequest request){
   
    //分配部门
        employee.setDepartment(departmentDao.
                getDepartmentById(Integer.parseInt(request.getParameter("department.id"))));
        employeeDao.save(employee);
        return "redirect:/emps/list";
    }

8. 修改员工实现

8.1 编辑员工页面跳转

<td>
	<a th:href="@{/emps/showUpdate/{id}(id=${emp.id})}" class="btn-sm btn-primary">编辑</a>
	<a class="btn-sm btn-danger">删除</a>
</td>

需要注意的是thymeleaf传值id的时候,写法千万不能写错,如果写错则进不去Controller,直接报出404

8.2 update.html

<mark>与add.html界面一样</mark>

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
	<form th:action="@{/emps/addEmploy}" method="post">
		<div class="form-group">
			<label for="lastname">姓名:</label>
			<input type="text" class="form-control" id="lastname" name="lastName" placeholder="lastName">
		</div>
		<div class="form-group">
			<label for="email">邮箱:</label>
			<input type="email" class="form-control" id="email" name="email" placeholder="email">
		</div>
		<label class="radio-inline">
			<input type="radio" name="gender"  value="1"><input type="radio" name="gender"  value="0"></label>
		<div class="form-group">
			<label>部门:</label>
			<select class="form-control" name="department.id">
				<option th:each="department:${departments}" th:value="${department.id}"
						th:text="${department.departmentName}"></option>
			</select>
		</div>
		<div class="form-group">
			<label for="birthday">生日:</label>
			<input type="date" class="form-control" id="birthday" name="birthday" placeholder="birthday">
		</div>
		<input type="submit" class="btn-sm btn-success" >新增</input>
	</form>
</main>

8.3 员工信息的回显

  1. 控制器的跳转
    @RequestMapping("showUpdate/{id}")
    public String showUpdate(Model model,@PathVariable("id") Integer id){
   
        Employee employee = employeeDao.getEmployeeById(id);
        Department department = employee.getDepartment();

        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments",departments);  //为了展示下拉框
        model.addAttribute("employee",employee);
        return "emp/update";
    }
  1. 前端的展示
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
	<form th:action="@{/emps/update/{id}(id=${employee.id})}" method="post">
		<div class="form-group">
			<label>姓名:</label>
			<input type="text" class="form-control" th:name="lastName" th:value="${employee.lastName}">
		</div>
		<div class="form-group">
			<label for="email">邮箱:</label>
			<input type="email" class="form-control" id="email" th:name="email" th:value="${employee.email}">
		</div>
		<label class="radio-inline">
			<input type="radio" name="gender"  value="1" th:checked="${employee.gender}==1"><input type="radio" name="gender"  value="0" th:checked="${employee.gender}==0"></label>
		<div class="form-group">
			<label>部门:</label>
			<select class="form-control" th:name="department.id">
				<option th:selected="${employee.department.id == dept.id}"
						th:text="${dept.departmentName}"
						th:value="${dept.id}"  th:each="dept:${departments}"
						></option>
			</select>
		</div>
		<div class="form-group">
			<label>生日:</label>
			<input type="date"  th:value="${employee!=null}?${#dates.format(employee.birthday,'yyyy-MM-dd')}" class="form-control" th:name="birthday">
		</div>
		<input type="submit" class="btn-sm btn-success" value="修改"></input>
	</form>
</main>

8.4 修改员工实现

  1. EmployeeDao新增方法
    //修改员工
    public void updateEmployee(Employee employee){
   
        employees.put(employee.getId(),employee);
    }

Map结构会覆盖key值相同的value

  1. 控制器的编写:
    @PostMapping("update/{id}")
    public String update(Employee employee,@PathVariable("id") String id,Model model){
   
        employee.setId(Integer.parseInt(id));  //为employee设置id
        Department department = departmentDao.getDepartmentById(employee.getDepartment().getId());
        employee.setDepartment(department); //为employee设置部门
        employeeDao.updateEmployee(employee);  //修改
        Collection<Employee> emps = employeeDao.getAll();
        model.addAttribute("emps",emps);
        return "forward:/emps/list";    //转发至/emps/list
    }

9. 删除员工实现

  1. 删除的按钮跳转
<td>
	<a th:href="@{/emps/showUpdate/{id}(id=${emp.id})}" class="btn-sm btn-primary">编辑</a>
	<a class="btn-sm btn-danger" th:href="@{/emps/delete/{id}(id=${emp.id})}" >删除</a>
</td>
  1. 删除控制器的编写
 @GetMapping("delete/{id}")
 public String delete(@PathVariable("id") Integer id,Model model){
   
     employeeDao.delete(id);
     Collection<Employee> emps = employeeDao.getAll();
     model.addAttribute("emps",emps);
     return "forward:/emps/list";    //转发至/emps/list
 }

10. 404界面处理与

基于约定 > 配置的springboot我们在resources中新建error,将404页面移动进去

访问错误路径:

11.注销用户

  1. 注销按钮(common.html)
 <a class="nav-link" th:href="@{user/out}">注销</a>

  1. 用户的控制器LoginController
    @GetMapping("out")
    public String out(HttpSession session){
   
        session.invalidate();  //销毁session
        return "redirect:index";
    }

12.遇到未能解决的问题

  1. 公共代码为什么不能放public中?<mark>6.1处</mark>
  2. <mark>8.3处员工信息的回显</mark>