1:返回前端数据的通用格式
/**
* 通用返回结果,服务端响应的数据最终都会封装成此对象
* @param <T>
*/
@Data
public class R<T> {
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;
}
}
day01:登录(密码加密)/退出/验证页面
1:登录
1:md5(密码加密)
//1、将页面提交的密码password进行md5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
2:登录成功将输入存入Session中
//6、登录成功,将员工id存入Session并返回登录成功结果
request.getSession().setAttribute("employee",emp.getId());
2:退出(清除Session中的信息)
/**
* 员工退出
* @param request
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理Session中保存的当前登录员工的id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
3:验证页面(过滤器)
1:技术点:Spring中提供的路径匹配器 ;
AntPathMatcher PATH_MATCHER = new AntPathMatcher();
boolean match = PATH_MATCHER.match(url, requestURI);
2:加上@ServletComponentScan扫描过滤器
创建过滤器类之后,在引导类加上@ServletComponentScan注解.来扫描过滤器配置的@WebFilter注解, 扫描上之后, 过滤器在运行时就生效了。
3:完整代码:
如果响应是中文的话需要进行处理中文编码格式
/**
* 检查用户是否已经完成登录
*/
@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 {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次请求的URI
String requestURI = request.getRequestURI();// /backend/index.html
log.info("拦截到请求:{}",requestURI);
//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3、如果不需要处理,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;
}
}
return false;
}
}
day02:新增员工/分页查询/启用/禁用/编辑
1:新增员工
1:增添员工代码
/**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
//设置初始密码123456,需要进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
employeeService.save(employee);
return R.success("新增员工成功");
}
2:全局异常处理器
在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。
/*
异常处理器:解决用户名重复的问题
账号重复出现的报错信息:
Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'wht2' for key 'idx_username'
*/
//1:先加入@ControllerAdvice注解指定拦截哪一类的Controller方法。
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
//2:@ExceptionHandler 来指定拦截的是那一类型的异常。
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());//获取异常信息
//3:判断异常信息是否为账号重复出现的信息
if (ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
//4:获取账户名字,返回错误信息
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("位置错误");
}
}
上述的全局异常处理器上使用了的两个注解 @ControllerAdvice , @ResponseBody , 他们的作用分别为:
@ControllerAdvice : 指定拦截那些类型的控制器;
@ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
上述使用的两个注解, 也可以合并成为一个注解 @RestControllerAdvice
2:分页查询
1:先创建个拦截器
/*
配置MP的分页插件
是一个拦截器
*/
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mpMethod(){
//1:创建拦截器对象
MybatisPlusInterceptor mp = new MybatisPlusInterceptor();
//2:添加拦截器所拦截的分页对象
mp.addInnerInterceptor(new PaginationInnerInterceptor());
return mp;
}
}
2:查询
/**
* 员工信息分页查询
* @param page 当前查询页码
* @param pageSize 每页展示记录数
* @param name 员工姓名 - 可选参数
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
//1:构造分页构造器
Page pageInfo = new Page(page, pageSize);
//2;构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
//3:添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//4:添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//5:执行调查
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
3:启用/禁用
1:修改的代码实现
/**
* 根据id修改员工信息
* @param request
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee) {
log.info(employee.toString());
//1:先获取登录用户的信息id
Long empId = (Long) request.getSession().getAttribute("employee");
//2:因为要修改信息,表中的更新时间和修改人也要进行设置
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
//3:调用修改方法操作数据库
employeeService.updateById(employee);
//4:返回修改成功的消息
return R.success("员工{}信息修改成功" + employee.toString());
}
2:前端JS中Long类型精度丢失
后端’响应数据:在分页查询时,服务端会将返回的R对象进行json序列化,转换为json格式 的数据,而员工的ID是一个Long类型的数据,而且是一个长度为 19 位的长 整型数据, 该数据返回给前端是没有问题的。
前端JS中问题原因: js在对长度较长的长整型数据进行处理时, 会损失精度, 从而导致提 交的id和数据库中的id不一致
解决方案:只需要让js处理的ID数据类型为字符串类型即可, 这样就不会损失精度了。
实现方法:由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来 实现的, 涉及到SpringMVC中的一个消息转换器 MappingJackson2HttpMessageConverter, 所以我们要解决这个问题, 就需要对 该消息转换器的功能进行拓展。
实现步骤:
1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换
2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
该自定义的对象转换器, 主要指定了, 在进行json数据序列化及反序列化时, LocalDateTime、LocalDate、LocalTime的处理方式, 以及BigInteger及Long类型数据,直接转换为字符串。
**1). 引入JacksonObjectMapper**
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
**2). 在WebMvcConfig中重写方法extendMessageConverters**
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
4:编辑
1:在EmployeeController中增加方法, 根据ID查询员工信息。
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
day03:公共字段自动填充/CRUD分类
1:公共字段自动填充
1:实体类的属性上加入@TableField注解,指定自动填充的策略。
2:按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现 MetaObjectHandler接口。
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("createUser",new Long(1));
metaObject.setValue("updateUser",new Long(1));
}
/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",new Long(1));
}
}
3:编写完了元数据对象处理器之后,我们就可以将之前在新增和修改方法中手动赋值的代码删除或注释掉
4:功能完善 ThreadLocal
1):出现的问题
经过上述的分析之后,发现我们可以使用JDK提供的一个类, 来解决此问题,它是JDK中提供的 ThreadLocal。
2:解决方案:ThreadLocal
如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取。
实现步骤:
1). 编写BaseContext工具类,基于ThreadLocal封装的工具类
2). 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
3). 在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id
1:编写BaseContext工具类,
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}
2:在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
在doFilter方法中, 判定用户是否登录, 如果用户登录, 在放行之前, 获取HttpSession中的登录用户信息, 调用BaseContext的setCurrentId方法将当前登录用户ID存入ThreadLocal。
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
3:MyMetaObjectHandler中从ThreadLocal中获取
将之前在代码中固定的当前登录用户1, 修改为动态调用BaseContext中的getCurrentId方法获取当前登录用户ID
2:新增分类
/**
* 分类管理
*/
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 新增分类
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);
categoryService.save(category);
return R.success("新增分类成功");
}
}
3:分类信息分页查询
在CategoryController中增加分页查询的方法,在方法中传递分页条件进行查询,并且需要对查询到的结果,安排设置的套餐顺序字段sort进行排序。
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
//分页构造器
Page<Category> pageInfo = new Page<>(page,pageSize);
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);
//分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
4:删除分类
1:删除操作
/**
* 根据id删除分类
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id){
log.info("删除分类,id为:{}",id);
categoryService.removeById(id);
return R.success("分类信息删除成功");
}
2:功能完善
在上述的测试中,我们看到分类数据是可以正常删除的。但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。完善后的逻辑为:
根据当前分类的ID,查询该分类下是否存在菜品,如果存在,则提示错误信息
根据当前分类的ID,查询该分类下是否存在套餐,如果存在,则提示错误信息
执行正常的删除分类操作
2.1:准备菜品(Dish)及套餐(Setmeal)实体类
2.2:Mapper接口DishMapper和SetmealMapper
2.23:Service接口DishService和SetmealService
2.4:Service实现类DishServiceImpl和SetmealServiceImpl
2.5: 创建自定义异常
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}
2.6:在CategoryService中扩展remove方法
public interface CategoryService extends IService<Category> {
//根据ID删除分类
public void remove(Long id);
}
2.7:在CategoryServiceImpl中实现remove方法
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id删除分类,删除之前需要进行判断
* @param id
*/
@Override
public void remove(Long id) {
//添加查询条件,根据分类id进行查询菜品数据
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);
//如果已经关联,抛出一个业务异常
if(count1 > 0){
throw new CustomException("当前分类下关联了菜品,不能删除");//已经关联菜品,抛出一个业务异常
}
//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if(count2 > 0){
throw new CustomException("当前分类下关联了套餐,不能删除");//已经关联套餐,抛出一个业务异常
}
//正常删除分类
super.removeById(id);
}
2.8:在GlobalExceptionHandler中处理自定义异常
在全局异常处理器中增加方法,用于捕获我们自定义的异常 CustomException
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
2.9:改造CategoryController的delete方法
注释掉原有的代码,在delete方法中直接调用categoryService中我们自定义的remove方法。
/**
* 根据id删除分类
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id){
log.info("删除分类,id为:{}",id);
//categoryService.removeById(id);
categoryService.remove(id);
return R.success("分类信息删除成功");
}
5:修改分类
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
day04:文件上传/下载/菜品CRUD
1:文件上传介绍
1:前端必须条件(三个)![](https://img-blog.csdnimg.cn/57381986daff4f5f8b109bd2c85cdcf9.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pWyamF2YeeahOWwj-WkqQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
<form method="post" action="/common/upload" enctype="multipart/form-data">
<input name="myFile" type="file" />
<input type="submit" value="提交" />
</form>
2:服务端接收文件(MultipartFile)
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
System.out.println(file);
return R.success(fileName);
}
2:文件下载介绍
3:文件上传下载代码实现
1:接收上传的文件并保存在固定的位置
/*
负责文件的上传与下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
//1:设置图片上传的位置
@Value("${reggie.path}")
private String basePath;
//2:文件上传
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
//1:获取原始的文件名...例如:a.jbg
String originalFilename = file.getOriginalFilename();
//2:获取文件的后缀名(.jpg)
String lastName = originalFilename.substring(originalFilename.lastIndexOf("."));
//3:使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + lastName;
//4:创建一个目录对象'
File file1 = new File(basePath);
//判断当前目录是否存在
if (!file1.exists()){
//不存在就创建
file1.mkdirs();
}
//5:将临时文件转存到指定的位置
try {
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
}
2:实现下载的功能,显示到浏览器中
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//网络输出流,通过输出流将文件写回浏览器
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}