瑞吉外卖 功能架构图
技术架构图
数据库结构图
技术选型
资源配置 前端资源配置 . 创建配置类WebMvcConfig,设置静态资源映射
用于在Springboot项目中, 默认静态资源的存放目录为 : “classpath:/resources/“, “classpath:/static/“, “classpath:/public/“ ; 而在我们的项目中静态资源存放在 backend, front 目录中, 那么这个时候要想访问到静态资源, 就需要设置静态资源映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;@Slf4j @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers (ResourceHandlerRegistry registry) { log.info("开始进行静态资源映射..." ); registry.addResourceHandler("/backend/**" ).addResourceLocations("classpath:/backend/" ); registry.addResourceHandler("/front/**" ).addResourceLocations("classpath:/front/" ); } }
完善登录功能 问题分析:用户如果不登录,直接访问系统首页面,照样可以正常访问。
上述这种设计并不合理,我们希望看到的效果应该 是,只有登录成功后才可以访问系统中的页面,如果没有登录, 访问系统中的任何界面都直接跳转到登录页面。
问题解决:
可以使用我们之前讲解过的 过滤器、拦截器来实现,在过滤器、拦截器中拦截前端发起的请求,判断用户是否已经完成登录,如果没有登录则返回提示信息,跳转到登录页面。
自定义一个过滤器 LoginCheckFilter 并实现 Filter 接口, 在doFilter方法中完成校验的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import com.alibaba.fastjson.JSON;import com.itheima.reggie.common.R;import lombok.extern.slf4j.Slf4j;import org.springframework.util.AntPathMatcher;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@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; String requestURI = request.getRequestURI(); log.info("拦截到请求:{}" ,requestURI); String[] urls = new String []{ "/employee/login" , "/employee/logout" , "/backend/**" , "/front/**" }; boolean check = check(urls, requestURI); if (check){ log.info("本次请求{}不需要处理" ,requestURI); filterChain.doFilter(request,response); return ; } if (request.getSession().getAttribute("employee" ) != null ){ log.info("用户已登录,用户id为:{}" ,request.getSession().getAttribute("employee" )); filterChain.doFilter(request,response); return ; } log.info("用户未登录" ); response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN" ))); 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 ; } }
全局异常处理 问题 :在添加员工时,当添加同一个姓名员工时,会报错,主要就是因为在 employee 表结构中,我们针对于username字段,建立了唯一索引,添加重复的username数据时,违背该约束,就会报错。但是此时前端提示的信息并不具体,用户并不知道是因为什么原因造成的该异常,我们需要给用户提示详细的错误信息 。
处理 :
在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。
异常处理方法逻辑:
指定捕获的异常类型为 SQLIntegrityConstraintViolationException
解析异常的提示信息, 获取出是那个值违背了唯一约束
组装错误信息并返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;import java.sql.SQLIntegrityConstraintViolationException;@ControllerAdvice(annotations = {RestController.class, Controller.class}) @ResponseBody @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(SQLIntegrityConstraintViolationException.class) public R<String> exceptionHandler (SQLIntegrityConstraintViolationException ex) { log.error(ex.getMessage()); if (ex.getMessage().contains("Duplicate entry" )){ String[] split = ex.getMessage().split(" " ); String msg = split[2 ] + "已存在" ; return R.error(msg); } return R.error("未知错误" ); } }
员工分页查询 前端请求:
最终发送给服务端的请求为 :** GET请求 , 请求链接 /employee/page?page=1&pageSize=10&name=xxx
请求
说明
请求方式
GET
请求路径
/employee/page
请求参数
page , pageSize , name
分页插件配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor (); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); return mybatisPlusInterceptor; } }
具体逻辑:
A. 构造分页条件
B. 构建搜索条件 - name进行模糊匹配
C. 构建排序条件 - 更新时间倒序排序
D. 执行查询
E. 组装结果并返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @GetMapping("/page") public R<Page> page (int page,int pageSize,String name) { log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name); Page pageInfo = new Page (page,pageSize); LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper (); queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name); queryWrapper.orderByDesc(Employee::getUpdateTime); employeeService.page(pageInfo,queryWrapper); return R.success(pageInfo); }
前端JS精度处理问题 在分页查询时,服务端会将返回的R对象进行json序列化,转换为json格式的数据,而员工的ID是一个Long类型的数据,而且是一个长度为 19 位的长整型数据, 该数据返回给前端是没有问题的。
从而造成页面传递过来的员工id的值和数据库中的id值不一致
简单代码测试:
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <script > alert(1420038345634918401); </script > </head > <body > </body > </html >
解决方案:
我们只需要让js处理的ID数据类型为字符串类型即可, 这样就不会损失精度了。
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <script > alert ("1420038345634918401" ); </script > </head > <body > </body > </html >
具体实现步骤:
1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
1). 引入JacksonObjectMapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import com.fasterxml.jackson.databind.DeserializationFeature;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.module .SimpleModule;import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;import java.math.BigInteger;import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime;import java.time.format.DateTimeFormatter;import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;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); } }
该自定义的对象转换器, 主要指定了, 在进行json数据序列化及反序列化时, LocalDateTime、LocalDate、LocalTime的处理方式, 以及BigInteger及Long类型数据,直接转换为字符串。
2). 在WebMvcConfig中重写方法extendMessageConverters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override protected void extendMessageConverters (List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..." ); MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter (); messageConverter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 ,messageConverter); }
公共字段自动填充 使用Mybatis Plus提供的公共字段自动填充功能
字段名
赋值时机
说明
createTime
插入(INSERT)
当前时间
updateTime
插入(INSERT) , 更新(UPDATE)
当前时间
createUser
插入(INSERT)
当前登录用户ID
updateUser
插入(INSERT) , 更新(UPDATE)
当前登录用户ID
实现步骤:
1、在实体类的属性上加入@TableField注解,指定自动填充的策略。
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。
编写元对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;import lombok.extern.slf4j.Slf4j;import org.apache.ibatis.reflection.MetaObject;import org.springframework.stereotype.Component;import java.time.LocalDateTime;@Component @Slf4j public class MyMetaObjecthandler implements MetaObjectHandler { @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 )); } @Override public void updateFill (MetaObject metaObject) { log.info("公共字段自动填充[update]..." ); log.info(metaObject.toString()); metaObject.setValue("updateTime" ,LocalDateTime.now()); metaObject.setValue("updateUser" ,new Long (1 )); } }
自动填充createUser和updateUser时设置的用户id是固定值,现在我们需要完善,改造成动态获取当前登录用户的id。 解决 :我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。 如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取。
实现步骤:
1). 编写BaseContext工具类,基于ThreadLocal封装的工具类
2). 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
3). 在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id
DTO实体类
实体模型
描述
DTO
Data Transfer Object(数据传输对象),一般用于展示层与服务层之间的数据传输。
Entity
最常用实体类,基本和数据表一一对应,一个实体类对应一张表。
VO
Value Object(值对象), 主要用于封装前端页面展示的数据对象,用一个VO对象来封装整个页面展示所需要的对象数据
PO
Persistant Object(持久层对象), 是ORM(Objevt Relational Mapping)框架中Entity,PO属性和数据库中表的字段形成一一对应关系
问题: 新增菜品页面,只能展示菜品基本信息,无法展示菜品口味信息
这个时候,我们需要自定义一个实体类,然后继承自 Dish,并对Dish的属性进行拓展,增加 flavors 集合属性(内部封装DishFlavor)。清楚了这一点之后,接下来就进行功能开发。
手机短信发送 使用阿里云短信服务,
场景
案例
验证码
APP、网站注册账号,向手机下发验证码; 登录账户、异地登录时的安全提醒; 找回密码时的安全验证; 支付认证、身份校验、手机绑定等。
短信通知
向注册用户下发系统相关信息,包括: 升级或维护、服务开通、价格调整、 订单确认、物流动态、消费确认、 支付通知等普通通知短信。
推广短信
向注册用户和潜在客户发送通知和推广信息,包括促销活动通知、业务推广等商品与活动的推广信息。增加企业产品曝光率、提高产品的知名度。
导入依赖,以及官方测试需要的sdk,模板代码
将官方提供的main方法封装为一个工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import com.aliyuncs.DefaultAcsClient;import com.aliyuncs.IAcsClient;import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;import com.aliyuncs.exceptions.ClientException;import com.aliyuncs.profile.DefaultProfile;public class SMSUtils { public static void sendMessage (String signName, String templateCode,String phoneNumbers,String param) { DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou" , "xxxxxxxxxxxxxxxx" , "xxxxxxxxxxxxxx" ); IAcsClient client = new DefaultAcsClient (profile); SendSmsRequest request = new SendSmsRequest (); request.setSysRegionId("cn-hangzhou" ); request.setPhoneNumbers(phoneNumbers); request.setSignName(signName); request.setTemplateCode(templateCode); request.setTemplateParam("{\"code\":\"" +param+"\"}" ); try { SendSmsResponse response = client.getAcsResponse(request); System.out.println("短信发送成功" ); }catch (ClientException e) { e.printStackTrace(); } } }
购物车 添加购物车:
在ShoppingCartController中创建add方法,来完成添加购物车的逻辑实现,具体的逻辑如下:
A. 获取当前登录用户,为购物车对象赋值
B. 根据当前登录用户ID 及 本次添加的菜品ID/套餐ID,查询购物车数据是否存在
C. 如果已经存在,就在原来数量基础上加1
D. 如果不存在,则添加到购物车,数量默认就是1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @PostMapping("/add") public R<ShoppingCart> add (@RequestBody ShoppingCart shoppingCart) { log.info("购物车数据:{}" ,shoppingCart); Long currentId = BaseContext.getCurrentId(); shoppingCart.setUserId(currentId); Long dishId = shoppingCart.getDishId(); LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(ShoppingCart::getUserId,currentId); if (dishId != null ){ queryWrapper.eq(ShoppingCart::getDishId,dishId); }else { queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId()); } ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper); if (cartServiceOne != null ){ Integer number = cartServiceOne.getNumber(); cartServiceOne.setNumber(number + 1 ); shoppingCartService.updateById(cartServiceOne); }else { shoppingCart.setNumber(1 ); shoppingCart.setCreateTime(LocalDateTime.now()); shoppingCartService.save(shoppingCart); cartServiceOne = shoppingCart; } return R.success(cartServiceOne); }
下单,结算 下单具体逻辑:
A. 获得当前用户id, 查询当前用户的购物车数据
B. 根据当前登录用户id, 查询用户数据
C. 根据地址ID, 查询地址数据
D. 组装订单明细数据, 批量保存订单明细
E. 组装订单数据, 批量保存订单数据
F. 删除当前用户的购物车列表数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 @Autowired private ShoppingCartService shoppingCartService;@Autowired private UserService userService;@Autowired private AddressBookService addressBookService;@Autowired private OrderDetailService orderDetailService;@Transactional public void submit (Orders orders) { Long userId = BaseContext.getCurrentId(); LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper <>(); wrapper.eq(ShoppingCart::getUserId,userId); List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper); if (shoppingCarts == null || shoppingCarts.size() == 0 ){ throw new CustomException ("购物车为空,不能下单" ); } User user = userService.getById(userId); Long addressBookId = orders.getAddressBookId(); AddressBook addressBook = addressBookService.getById(addressBookId); if (addressBook == null ){ throw new CustomException ("用户地址信息有误,不能下单" ); } long orderId = IdWorker.getId(); AtomicInteger amount = new AtomicInteger (0 ); List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> { OrderDetail orderDetail = new OrderDetail (); orderDetail.setOrderId(orderId); orderDetail.setNumber(item.getNumber()); orderDetail.setDishFlavor(item.getDishFlavor()); orderDetail.setDishId(item.getDishId()); orderDetail.setSetmealId(item.getSetmealId()); orderDetail.setName(item.getName()); orderDetail.setImage(item.getImage()); orderDetail.setAmount(item.getAmount()); amount.addAndGet(item.getAmount().multiply(new BigDecimal (item.getNumber())).intValue()); return orderDetail; }).collect(Collectors.toList()); orders.setId(orderId); orders.setOrderTime(LocalDateTime.now()); orders.setCheckoutTime(LocalDateTime.now()); orders.setStatus(2 ); orders.setAmount(new BigDecimal (amount.get())); orders.setUserId(userId); orders.setNumber(String.valueOf(orderId)); orders.setUserName(user.getName()); orders.setConsignee(addressBook.getConsignee()); orders.setPhone(addressBook.getPhone()); orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName()) + (addressBook.getCityName() == null ? "" : addressBook.getCityName()) + (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName()) + (addressBook.getDetail() == null ? "" : addressBook.getDetail())); this .save(orders); orderDetailService.saveBatch(orderDetails); shoppingCartService.remove(wrapper); }
备注:
上述逻辑处理中,计算购物车商品的总金额时,为保证我们每一次执行的累加计算是一个原子操作,我们这里用到了JDK中提供的一个原子类 AtomicInteger
项目优化 Redis缓存 1.使用Redis缓存短信验证码
2.缓存菜品信息
3.使用SpringCache注解开发
4.缓存套餐数据
代码改造 :1). 在UserController中注入RedisTemplate对象,用于操作Redis
1 2 @Autowired private RedisTemplate redisTemplate;
2). 在UserController的sendMsg方法中,将生成的验证码保存到Redis
1 2 redisTemplate.opsForValue().set(phone, code, 5 , TimeUnit.MINUTES);
3). 在UserController的login方法中,从Redis中获取生成的验证码,如果登录成功则删除Redis中缓存的验证码
1 2 3 4 Object codeInSession = redisTemplate.opsForValue().get(phone);redisTemplate.delete(phone);
数据库主从复制 MySQL复制过程分成三步:
1). MySQL master 将数据变更写入二进制日志( binary log)
2). slave将master的binary log拷贝到它的中继日志(relay log)
3). slave重做中继日志中的事件,将数据变更反映它自己的数据
读写分离 ShardingJDBC
Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。
导入依赖,增加配置
在application.yml中增加配置
1 2 3 spring: main: allow-bean-definition-overriding: true
该配置项的目的,就是如果当前项目中存在同名的bean,后定义的bean会覆盖先定义的。
Swagger Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。功能主要包含以下几点:
A. 使得前后端分离开发更加方便,有利于团队协作
B. 接口文档在线自动生成,降低后端开发人员编写接口文档的负担
C. 接口功能测试
knife4j。knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。而我们要使用kinfe4j,需要在pom.xml中引入如下依赖即可:
1 2 3 4 5 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-spring-boot-starter</artifactId > <version > 3.0.2</version > </dependency >
2). 导入knife4j相关配置类
A. 在该配置类中加上两个注解 @EnableSwagger2 @EnableKnife4j ,开启Swagger和Knife4j的功能。
B. 在配置类中声明一个Docket类型的bean, 通过该bean来指定生成文档的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Slf4j @Configuration @EnableSwagger2 @EnableKnife4j public class WebMvcConfig extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers (ResourceHandlerRegistry registry) { log.info("开始进行静态资源映射..." ); registry.addResourceHandler("/backend/**" ).addResourceLocations("classpath:/backend/" ); registry.addResourceHandler("/front/**" ).addResourceLocations("classpath:/front/" ); } @Override protected void extendMessageConverters (List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..." ); MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter (); messageConverter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 ,messageConverter); } @Bean public Docket createRestApi () { return new Docket (DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller" )) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo () { return new ApiInfoBuilder () .title("瑞吉外卖" ) .version("1.0" ) .description("瑞吉外卖接口文档" ) .build(); } }
注意: Docket声明时,指定的有一个包扫描的路径,该路径指定的是Controller所在包的路径。因为Swagger在生成接口文档时,就是根据这里指定的包路径,自动的扫描该包下的@Controller, @RestController, @RequestMapping等SpringMVC的注解,依据这些注解来生成对应的接口文档。
3). 设置静态资源映射
由于Swagger生成的在线文档中,涉及到很多静态资源,这些静态资源需要添加静态资源映射,否则接口文档页面无法访问。因此需要在 WebMvcConfig类中的addResourceHandlers方法中增加如下配置。
1 2 registry.addResourceHandler("doc.html" ).addResourceLocations("classpath:/META-INF/resources/" ); registry.addResourceHandler("/webjars/**" ).addResourceLocations("classpath:/META-INF/resources/webjars/" );
4). 在LoginCheckFilter中设置不需要处理的请求路径
1 2 3 4 "/doc.html" ,"/webjars/**" ,"/swagger-resources" ,"/v2/api-docs"
注解测试
可以通过 @ApiModel , @ApiModelProperty 来描述实体类及属性
描述Controller、方法及其方法参数,可以通过注解: @Api, @APIOperation, @ApiImplicitParams, @ApiImplicitParam
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Data @ApiModel("返回结果") public class R <T> implements Serializable { @ApiModelProperty("编码") private Integer code; @ApiModelProperty("错误信息") private String msg; @ApiModelProperty("数据") private T data; @ApiModelProperty("动态数据") private Map map = new HashMap (); }