@Data
public class Employee implements Serializable {...}
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
public interface EmployeeService extends IService<Employee> {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
}
注意:在backend,request.js中更改前端接收响应的时间,方便debug
// 超时,运行为10000,debug时为1000000
timeout: 1000000
防止用户在未登录的情况下访问到其他界面
(1)框架
//设置过滤器名字和拦截pattern,在main类中设置@ServletComponentScan进行组件扫描
@WebFilter(filterName = "LoginFilter",urlPatterns = "/*")
@Slf4j
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
log.info("拦截到请求{}",request.getRequestURL());
filterChain.doFilter(request,servletResponse);
}
}
(2)逻辑
//路径匹配器,支持通配符
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;
// log.info("拦截到请求{}",request.getRequestURL());
//1.获取本次请求的URI,这里URI(/employee/login)和URL(http://localhost:8080/backend/page/login/login.html)完全不同
String requestURI = request.getRequestURI();
// log.info("Url:{}",request.getRequestURL());
log.info("URI{}",requestURI);
//定义不需要处理的请求路径,登录登出不需要,静态页面也不需要(数据都是动态ajax向服务端的数据库请求的)
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2.判断是否需要登录验证,不需要登录则返回
boolean check = checkURLs(urls, requestURI);
if(check){
filterChain.doFilter(request, response);
log.info("无需登录");
return;//注意不要忘记返回
}
//3.若需要登录,就判断登录状态,如果登录了就放行
if(request.getSession().getAttribute("employee") != null){
log.info("已经登录");
filterChain.doFilter(request, response);
return;//注意不要忘记返回
}
//4.未登录,则根据前端的要求(request.js),设置响应
//以输出流响应前端
log.info("未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 判断requestURL是否是不需要验证登录的url
* @param urls
* @param requestURI
* @return
*/
private boolean checkURLs(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url ,requestURI);
if(match)
return true;
}
return false;
}
关于doFilter后要返回的问题:
调用doFilter后,Filter链的下一个filter执行,如此知道全部filter执行完成,按照逆序(这一点跟springboot中的拦截器一样)返回到每个filter,执行他们后面的代码,所以是需要return的
区分URI和URL:
URL:http://localhost:8080/backend/page/login/login.html (是浏览器上url栏的内容)
URI:/employee/login 向服务器发送的请求
/**
* 根据提交的表单,生成数据库记录,并初始化
* @param request
* @param employee
* @return
*/
@PostMapping
public R<String> addEmployee(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 creatorId = (long) request.getSession().getAttribute("employee");
employee.setCreateUser(creatorId);
employee.setUpdateUser(creatorId);
employeeService.save(employee);
return R.success("新员工添加成功");
}
注意:这里在类上有注解@PostMapping("/employee"),不要在方法上重复这个路径
如果插入重复的username,则会从mysql抛出一个异常,我们用全局异常类来处理
/**
* 全局异常处理类,方法返回响应体
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@Slf4j
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
// error message:Duplicate entry '123456' for key 'idx_username'
String message = ex.getMessage();
if (message.contains("Duplicate entry")){
//按照" "将字符创分为小串
String[] split = message.split(" ");
String msg = "用户名" + split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
tips,使用ctrl+F5,清除缓存刷新
首先配置mybatis-plus的分页插件,为此需要一个mybatis-plus的配置类
/**
* 配置mybatisPlus中的分页插件
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
在EmployeeController类中,捕获分页的请求
/**
* 处理请求http://localhost:8080/employee/page?page=1&pageSize=10
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
// log.info("page:{},pageSize:{}",page,pageSize);
//1.构造分页对象
Page<Employee> employeePage = new Page<>(page,pageSize);
//2.构造查询条件
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//3.增加可能的过滤条件
//如果有指定name的查询,加上过滤条件
//参数0表示在name不为空时才进行该查询
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//4.查询结果排序
queryWrapper.orderByDesc(Employee::getUpdateTime);
//5.执行查询,employeeService会将数据放入我们传入的employeePage中
employeeService.page(employeePage,queryWrapper);
return R.success(employeePage);
}
/**
* 对employee的修改请求(put,http://localhost:8080/employee)
* @param request
* @param employee 获取前端已经修改好的employee对象
* @return
*/
@PutMapping()
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
long empId = (long) request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);//设置是编号为empId的用户修改了本用户
employeeService.updateById(employee);
return R.success("修改成功");
}
这里会出现一个问题,我们在数据库中并没有找到要更新的记录,这是因为id过长,而在前端js对其进行了舍入,在Long长度大于17位时会出现精度丢失的问题.解决方法是从后端传入string给前端而不是long,为此需要消息转换器
如下消息转换器将转化java对象为json,这里将long转换为json
/**
* 对象映射器:基于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);
}
}
在mvc配置类的消息转换器扩展方法中,加入这个消息转换器
/**
* 扩展mvc中的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
//消息转换器会将我们return给前端的R对象转换为对应的json
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter =
new MappingJackson2HttpMessageConverter();
//设置对象转换器,使用我们自己定义的消息转换器对象
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将我们设置的消息转换器放入mvc框架的转化器集合中(放在首位才会优先使用)
converters.add(0,messageConverter);
}
/**
* 处理按照id查询员工信息的请求(url为 /employee/id的值)
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable long id){
// log.info("id:{}",id);
Employee employee = employeeService.getById(id);
return R.success(employee);
}
问题:比如更新时间,创建时间等等字段,在employee表中有,在别的表也有,这会导致很多重复代码
mybatis-plus框架提供了解决方法,统一对他们进行处理:
1.在要处理的Bean类中加入注解
@TableField(fill = FieldFill.INSERT)//插入(即创建)时填充字段
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)//插入,更新都填充字段
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
2.创建一个组件实现MetaObjectHandler接口
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("insert时,进行公共字段填充");
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("update时,进行公共字段填充");
}
}
重写方法
@Override
public void insertFill(MetaObject metaObject) {
log.info("insert时,进行公共字段填充");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
//这里暂时写死
metaObject.setValue("updateUser", new Long(1));
metaObject.setValue("createUser", new Long(1));
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("update时,进行公共字段填充");
metaObject.setValue("updateTime", LocalDateTime.now());
//这里暂时写死
metaObject.setValue("updateUser", new Long(1));
}
引入新问题,如何获得当前登录的empId?
1.创建使用线程域变量的工具类
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户的id
* 作用域在一个线程内
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id){
threadLocal.set(id);
}
public static Long getCurrentId(){
return threadLocal.get();
}
}
2.过滤器中,登录成功情况下获取empId
//在线程作用域中存入登录用户id
long empId = (long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
3.在统一处理界面将empId获取
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// log.info("insert时,进行公共字段填充");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
//通过线程域获取数据
Long currentId = BaseContext.getCurrentId();
metaObject.setValue("updateUser", currentId);
metaObject.setValue("createUser", currentId);
}
@Override
public void updateFill(MetaObject metaObject) {
// log.info("update时,进行公共字段填充");
metaObject.setValue("updateTime", LocalDateTime.now());
Long currentId = BaseContext.getCurrentId();
metaObject.setValue("updateUser", currentId);
}
}
自定义异常类:
/**
* 自定义业务异常
*/
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
在全局异常处理类中:
/**
* 捕获自定义异常,返回给客户端
* @param ex
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> customExceptionHandler(CustomException ex){
return R.error(ex.getMessage());
}
tips:项目引入新界面时,在maven中使用clear package,以更新目录
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
/**
* 捕获上传文件的请求
* @param file 跟请求中form-data中的name字段一致
* @return
*/
@RequestMapping("/upload")
public R<String> upload(MultipartFile file){
log.info(file.toString());
return null;
}
}
在dish
表中加入菜品,并为菜品设置口味选项等消息,这些信息在dish_favor
表中;
1.提供Dish,DishFavor类到Service类,其中Dish需要提供Controller
2.需要一个类来接收请求,因为post请求中有口味信息,不能使用Dish简单地接收,这里需要提供一个新的类,继承了Dish,还包含了DishFavor
/**
* 数据传输对象,包含了Dish对象中没有的字段
*/
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
3.在DishController类中接收请求,用DishDto接收json数据
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto); //稍后定义
return R.success("新增菜品成功");
}
4.自定义方法saveWithFlavor
因为涉及了两张表的操作,这里在方法上引入事务,ReggieApplication类上标注@EnableTransactionManagement开启事务功能
public interface DishService extends IService<Dish> {
//在dish表存储dish,在dishFlavor表中存储flavor
public void saveWithFlavor(DishDto dishDto);
}
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired
DishFlavorService dishFlavorService;
/**
* 新增菜品,并将该菜品的口味信息(可能有多个)存储到对应表中
* @param dishDto 封装了菜品和口味的对象
*/
@Override
@Transactional //涉及了多张表的操作,需要开启事务
public void saveWithFlavor(DishDto dishDto) {
this.save(dishDto);//当我们将新菜品存入数据库,根据雪花算法生成了id
//获取这个新生成的id
Long dishId = dishDto.getId();
//获取每个flavor,为其设置dishId
List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishId);
}
//在数据库中存储所有flavor
dishFlavorService.saveBatch(flavors);
}
}
tips:在查数据库可视化工具时如果发现没有数据,找下一页或者禁用限制行
分页查询出菜品的各种信息,其中包含一个菜品种类的信息,在category表中
思路:先查出Dish,再根据Dish的分页查询结果,再查询Category,将最终结果封装在DishDto的分页对象中
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
Page<Dish> pageInfo = new Page<>(page,pageSize);
//我们需要分类名称,而Dish没有,在DisDto中封装这个字段
Page<DishDto> dishDtoPage = new Page<>();
//1.Dish的分页查询
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(!StringUtils.isEmpty(name), Dish::getName,name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
dishService.page(pageInfo, queryWrapper);
//2.根据已有的分页查询结果,查询出分类名称
//将分页数据中除了records的部分全部复制到dishDtoPage中
BeanUtils.copyProperties(pageInfo, dishDtoPage,"records");
//单独处理records
List<Dish> records = pageInfo.getRecords();
//通过流,将records每个item(Dish对象)和对categoryName的查询结果一起存入DishDto对象,并整理成list返回
List<DishDto> dishDtos = records.stream().map((item)->{
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);
Category category = categoryService.getById(dishDto.getCategoryId());
String categoryName = category.getName();
if (categoryName!=null) dishDto.setCategoryName(categoryName);
return dishDto;
}).collect(Collectors.toList());
//将新records放入新分页对象
dishDtoPage.setRecords(dishDtos);
return R.success(dishDtoPage);
}