SSM入门详解--reggieDay1

138 阅读12分钟

javaWeb看的差不多就写个外卖项目,感觉视频课程看的不过瘾,找了网上的笔记和源码。在此浓缩一下。

适合任何能看懂Java语法的初学者入门SSM(SpringBoot,MVC,MyBatis-Plus)。快速了解后端开发流程。参考文献:

环境搭建

使用JDK8即可,注意电脑中可能配置了多个Java版本,要在IDEA中设置统一,否则编译无法通过。

数据库

MySQL建库

指定字符集与排序规则 utf8mb4 、general_ci

建表本项目共11张表

address_book category dish dish_flavor employee order_detail orders setmeal setmeal_dish shopping_cart user

目前我们只需实现最基础的对单表CRUD,和登录校验。只需一张user表即可。

注意使用命令行工具source语句时,sql文件路径不能有中文。因此建议使用图形化工具而不要在命令行中操作,现阶段可以直接在Idea中对MySQL进行简单操作。

创建maven项目

从零开始:创建空项目

application.yml

启动类

Application.java

在项目的主文件夹下,如 jay.reggie,各种接口和实现类同一层级

静态资源

在SpringBoot中静态资源一般放在Static或Templa,此处我们放在Resource下,要配置类设置静态资源映射

//项目主目录下/config/WebMvcConfig

@Slf4j
@Configuration
public class WebMvcConfig extend WebMvcConfigurationSupport{
	@Override
	protect void addResouceHandler(ResourceHandlerRegistry){
		log.info("add");
		registry.addResHandler("请求路径").addResLocation("映射路径");
		registry.addResHadnler().;
	}
}

完整的项目目录是这样

image.png 记好这个结构,这是就是SpringMVC架构。

快速搭建

对数据库元素的增删改查

明确目的,我们要接受前端发送的http请求,获取路径和相应参数并操作数据库后返回所需数据。

为了更好的完成这些目的,项目使用MVC三层架构,是常见的设计模式之一:

controller

service

mapper

整个基础流程需要以下对象:

实体类->mapper->service->serviceImpl->controller

我们按照业务逻辑逆序介绍:

controller:

对不同的请求指定响应方法。采用Restful风格。即同一路径的不同请求方式视为不同请求,调用不同处理方法。

比如携带员工登录信息的Post请求发送至 /employee/login ,同时这个一级路径也会处理和员工有关的其他请求,

我们就需要一个EmployeeController,这是一个最基础的Controller

//EmployeeController.java

@RestController
@ResquestMapping("/employee")
public class EmployeeController{

	@Autowired 
	EmployeeService employeeService;

	@PostMapping("/login")
	public R<Employee> login(HttpServletRequest request,@RequestBody Employee employee){
		//do something
		return ;
	}
}

@RestController 是 Spring Boot 中用于开发 RESTful Web 服务的注解,使用该注解即可让一个类成为响应请求的控制器。RequestMapping PostMapping 则用来指定路径和请求方式。

此时发送给 /employee/login 的请求便会被我们写的 login方法处理。

SpringBoot封装了Web开发的许多功能,开发者只需使用注解即免去许多Web开发中处理请求、封装数据的繁杂工作,可快速开发可用的接口。故而Java开发也称为面向注解开发。

前端发来的数据是这样的:

image.png

要实现登录,在响应方法中我们具体要做的事情是:拿出数据即账号密码这两个字符串,根据字段查询数据库。比对结果,没查到或密码不一致返回失败,密码一致返回成功。

用这两个参数即可拿到数据 (HttpServletRequest request,@RequestBody Employee employee)

  • HttpServletRequest 是 Java Servlet API 中的一个接口,用于封装 HTTP 请求的所有信息。在 Spring MVC 中,可以通过将
  • HttpServletRequest 作为控制器方法的参数,直接访问底层的 HTTP 请求信息。

@RequestBody Employee employee

@RequestBody 是 Spring Boot 的注解,用于将 HTTP 请求体中的 JSON 或 XML 数据自动解析为 Java 对象。它通常用于处理 POST 或 PUT 请求,这些请求通常包含请求体数据。

这样就可以访问所需的数据。在方法中处理

//login()
	String username = emplyee.getUername(); 
	String password = employee.getPassword();

Employee 是一个实体类,后面会提到。

用这些数据查表:

//login()
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);

查表的核心就在于这个 LambdaQueryWrapper<> ,这是Mybatis-Plus提供的查询条件构造器。用<>泛型中的实体类指定要查哪张表。这里的Employee指定了数据库中对应的同名Employee表。

.eq() 条件查询方法,指定某个字段=某个值的的条件查询。等同于

select * where uername = {username};

注意LambdaQueryWrapper里面装的是条件而不是记录。

使用这些条件查表需要我们调用Service层的方法 employeeService.getOne(querryWrapper);

然后根据查询结果返回:

if (emp == null){ 
		//没这个人
    return R.error("登录失败");
}
if (!emp.getPassword().equals(password)){
		//密码错误
    return R.error("登录失败");
}
//登录成功,将员工id存入Session  并返回登录成功结果
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);

接下来说说服务层,返回的事最后讲。

Service

包括接口Servie和实现类Impl(忘了就回去看一眼项目结构):

和Employee有关的业务逻辑自然要一个EmployeeService接口

//EmployeeServce.java
public interface EmployeeService extends IService<Employee> {
}

IService 是 MyBatis-Plus 的核心接口。直接继承它便可以快速实现对实体类的增删改查基础业务方法,而无需手动编写大量的 SQL 语句。所以这里我们什么都不用写,简单的增删改查都实现过了,我们需要的getOne()便在其中。

实现类

//EmployeeServceImpl.java
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper,Employee> implements EmployeeService {
}

同上。

@Service是SpringBoot中的标记MVC架构中Service层实现类的注解,其作用是告诉Spring容器自动实现化后注入到需要它的地方。比如

//EmployeeController.java

@RestController
@ResquestMapping("/employee")
public class EmployeeController{

	@Autowired 
	EmployeeService employeeService;
}

此处的 @Autowired 便是 这里需要它。

实现的Service接口已经说过了,再看这个被继承的Impl实现类

extends ServiceImpl<EmployeeMapper, Employee>

ServiceImpl是MyBatis-Plus提供的服务类,其本身以及实现了IService,继承Impl的类可以直接使用基本的业务逻辑放方法比如 getOnesaveupdateByIdremoveByIdgetByIdlistpage 。唯一的需要做的就是通过泛型<>关联Mapper接口和实体类。

当前案例的Service层没有复杂的逻辑,就是查一下。因此我们啥都没做,只是熟悉了这些流程。

Mapper

Service似乎已经把crud数据的事做了,怎么又来了一个Mapper?

注意在MVC三层架构中,Service是业务层,管理的是业务逻辑的实现。真正和数据库打交道,调用JDBC的是Mapper,SQL语句是人家写的。前面也说了ServiceImpl要关联对应的Mapper和实体类。

//EmployeeMapper.java
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}

@Mapper 和 @Service同理,MyBatis-Plus对架构层级的标记。

BaseMapper 则是 MyBatis-Plus 提供一系列crud操作数据库方法的接口(真的是他在操作)。

  • 提供了通用的 CRUD 操作方法(如 selectByIdinsertupdateByIddeleteById 等),这些方法可以直接使用,无需手动实现。
  • 通过继承 BaseMapperEmployeeMapper 接口可以自动获得这些通用方法。
  • 通过泛型<>指定实体类,即要操作的表。

实体类

根据数库的某张表创建一个实体类,类的每一个属性名称对应表的一个字段,标识符要完全相同或开启下划线-驼峰映射。

比如一张employee表有如下字段

image.png

则创建一个对应的实体类

//Employee.java
@Data
public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String username;
    private String name;
    private String password;
    private String phone;
    private String sex;
    private String idNumber;
    private Integer status;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

}

@data注解

@Data 是 Lombok 提供的一个注解,用于生成实体类的常用方法。

自动生成

  • gettersetter 方法。
  • toString 方法。
  • equalshashCode 方法。
  • no-args 构造方法和 all-args 构造方法

Serializable 是 Java 提供的一个标记接口,用于表示一个类的对象可以被序列化。

private static final long serialVersionUID = 1L; 是序列化版本标识符,用于确保序列化和反序列化时类的版本一致性。

@TableField是MyBatis-Plus提供用于指定表字段的映射和行为的注解。

fill 指定自动填充行为,两个宏分别代表:

  • FieldFill.INSERT:在插入记录时自动填充字段。
  • FieldFill.INSERT_UPDATE:在插入和更新记录时自动填充字段。

至于具体填充的内容需要额外写配置类。

返回

查到所需数据后将其封装好返回给前端,为避免每次都封装,所以定义一个统一响应结果:

//R.java
@Data
public class R<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

}

约定好数据格式:

code:响应状态。0失败1成功

msg:失败的提示信息

date:成功响应所返回的数据封装为一个js对象

map:保留,其他内容。

比如登录失败

image.png

响应成功时

image.png

前端按格式统一处理

登录校验

分为认证与拦截两个方面。了解认证之前简单过一下会话技术:

会话

浏览器访问服务器,会建立一次会话,直至一方断开连接。

而一次会话会有多次请求与响应,直接网页关闭,连接断开,会话结束。

服务器需要识别不同的请求是否来自统一浏览器,以便在不同请求中共享数据。识别同一会话的不同请求即是会话跟踪技术:

cookiee

session

令牌

cookie

响应头中一项

set-cookie: key=value

浏览器会自动记录本次的服务器和cookie,以后每次访问该服务器,都会带上cookie:name=value

特定:

http协议自带

移动端不支持

用户可以自己禁用

不能跨域

同源:协议、ip、端口 都要相同。

浏览器会阻止一个源的网页从另一个源请求和加载数据

本次使用服务端session验证。这是适合快速入门的案例

session

基于cookie,同样在响应头中加一项 set-cookie :JSESSION=value,

每次请求头都会带上cookie:JESSION=value。

特定和cookie,唯一的区别就是将cookie存在了服务端。

本项目使用session

登录校验

登录校验分两步:对于所有请求,已经登录的打上标识。为登录的拦下来跳转到登录页面

会话追踪实现

会话追踪实现,通过session给浏览器带上标识

@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
    //1.将页面提交的明文密码进行md5加密
    String password = employee.getPassword();
    password = DigestUtils.md5DigestAsHex(password.getBytes());
    //2.根据页面提交的用户名username查数据库
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Employee::getUsername,employee.getUsername());
    Employee emp = employeeService.getOne(queryWrapper);
    //3.如果没有查询到则返回登录失败结果
    if (emp == null){
        return R.error("登录失败");
    }
    //4.密码比对,如果不一致则返回登录失败结果
    if (!emp.getPassword().equals(password)){
        return R.error("登录失败");
    }
    //5.查看员工账号状态是否锁定,若是禁用状态返回禁用信息
    if (emp.getStatus() == 0){
        return R.error("账号异常,已锁定");
    }
    //6.登录成功,将员工id存入Session  并返回登录成功结果
    request.getSession().setAttribute("employee",emp.getId());
    return R.success(emp);
}

唯一要做的就是在登录成功后,响应消息中使用 request.getSession().setAttribute("employee",emp.getId());

这个方法会创建并返回一个会话对象HttpSession,同时设置employee=id在服务端,设置set-cookie:jsesson=id给客服端。以此追踪此次会话。

浏览器每次请求都会带上Jsession=id,服务端通过这个id查找自己记录,来判断用户是否登录。

响应拦截

后端拦截响应并返回错误信息,前端根据返回信息跳转页面。注意页面跳转是前端通过Js完成的,后端需要拦截未登录的请求并通过响应json告诉前端:快跳啊。

创建过滤器类

//LoginCheckFliter.java
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
    //路径匹配器,支持通配符
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
   }

@WebFilter 是Servlet提供用来拦截请求的注解

urlPatterns指定要拦截的请求路径

“/*” 拦截所有请求

Fliter接口规定了三个方法init、doFliter、destory.

(必须)重写doFilter来设置拦截逻辑。放或不放?什么时候放?

注意当路径匹配上拦截器被调用后,这次请求是阻塞的。直到我们调用放行方法:

filterChain.doFilter(request,reponse) 请求才会正常通过。

设定放行逻辑

//doFliter()
{
	if(request.getSession().getAttribute("employee"!=null){
		Long empId = request.getSession().getAttribute("employee");
		BaseContext.setCurrent(empId);
		filterChain(request,respones);
		return;
	}
	else{
		response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));//通过会话对象手动返回错误信息
		return;
		
	}
}

从结果看session认证:

request.getSession()方法实际是通过请求头中cookie的JSESSION值,定位一个会话对象,

并getAttribute()访问会话对象中的(employee)字段,若该字段不为空,说明用户登录过。

简而言之就是以前登录过的用户会拥有一个合法的Jseeion值,进而让服务器认出来。

而为了获取Session,需要先把请求转为http请求:

HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

只有HttpServlet 才能使用getUrl,getSession等特有方法。

BaseContext.setCurrent() 设置了一个全局变量,可以在其他地方用getCurrent获取该值。

最后要让过滤器生效,在启动类中加上注解@ServletComponentScan

@ServletComponentScan
public class Application{
	public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class, args);
        log.info("Project started");
    }
}