javaWeb看的差不多就写个外卖项目,感觉视频课程看的不过瘾,找了网上的笔记和源码。在此浓缩一下。
适合任何能看懂Java语法的初学者入门SSM(SpringBoot,MVC,MyBatis-Plus)。快速了解后端开发流程。参考文献:
- 瑞吉外卖项目详细分析笔记及所有功能补充代码_瑞吉外卖完整补充-CSDN博客
- gitee.com/dkgk8/reggi…
- 黑马程序员Java项目实战《瑞吉外卖》,轻松掌握springboot + mybatis plus开发核心技术的真java实战项目_哔哩哔哩_bilibili
环境搭建
使用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().;
}
}
完整的项目目录是这样
记好这个结构,这是就是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开发也称为面向注解开发。
前端发来的数据是这样的:
要实现登录,在响应方法中我们具体要做的事情是:拿出数据即账号密码这两个字符串,根据字段查询数据库。比对结果,没查到或密码不一致返回失败,密码一致返回成功。
用这两个参数即可拿到数据 (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的类可以直接使用基本的业务逻辑放方法比如 getOne 、save、updateById、removeById、getById、list、page 。唯一的需要做的就是通过泛型<>关联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 操作方法(如
selectById、insert、updateById、deleteById等),这些方法可以直接使用,无需手动实现。 - 通过继承
BaseMapper,EmployeeMapper接口可以自动获得这些通用方法。 - 通过泛型<>指定实体类,即要操作的表。
实体类
根据数库的某张表创建一个实体类,类的每一个属性名称对应表的一个字段,标识符要完全相同或开启下划线-驼峰映射。
比如一张employee表有如下字段
则创建一个对应的实体类
//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 提供的一个注解,用于生成实体类的常用方法。
自动生成
getter和setter方法。toString方法。equals和hashCode方法。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:保留,其他内容。
比如登录失败
响应成功时
前端按格式统一处理
登录校验
分为认证与拦截两个方面。了解认证之前简单过一下会话技术:
会话
浏览器访问服务器,会建立一次会话,直至一方断开连接。
而一次会话会有多次请求与响应,直接网页关闭,连接断开,会话结束。
服务器需要识别不同的请求是否来自统一浏览器,以便在不同请求中共享数据。识别同一会话的不同请求即是会话跟踪技术:
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");
}
}