首页
文章分类
逆向网安
中英演讲
杂类教程
学习笔记
前端开发
汇编
数据库
.NET
服务器
Python
Java
PHP
Git
算法
安卓开发
生活记录
读书笔记
作品发布
人体健康
网上邻居
留言板
欣赏小姐姐
关于我
Search
登录
1
利用AList搭建家庭个人影音库
4,657 阅读
2
浅尝Restful Fast Request插件,一句话完成 逆向过程
3,958 阅读
3
完美破解The Economist付费墙
2,725 阅读
4
i茅台app接口自动化csharp wpf实现,挂机windows服务器每日自动预约
2,610 阅读
5
青龙面板基本使用并添加修改微信/支付宝步数脚本
2,033 阅读
Search
标签搜索
PHP
Laravel
前端
csharp
安卓逆向
JavaScript
Python
Java
爬虫
抓包
Git
winform
android
Fiddler
Vue
selenium
LeetCode
每日一题
简单题
docker
Hygge
累计撰写
95
篇文章
累计收到
445
条评论
首页
栏目
逆向网安
中英演讲
杂类教程
学习笔记
前端开发
汇编
数据库
.NET
服务器
Python
Java
PHP
Git
算法
安卓开发
生活记录
读书笔记
作品发布
人体健康
页面
网上邻居
留言板
欣赏小姐姐
关于我
用户登录
搜索到
2
篇与
的结果
2022-08-21
Java-微信公众号实现网站登录功能
序言微信登录常见方式平常大家见到过最多的扫码登录应该是 开放平台网页登录 大概形式就是:点击微信登录后会出现一个黑页面,页面中有一个二维码,扫码后可以自动获取用户信息然后登录,但是这种方式需要申请开放平台比较麻烦。如图利于推广方式另外一种扫码登录方式只需要一个微信服务号就行,大概流程是:点击微信登录,网站自己弹出一个二维码、扫描二维码后弹出公众号的关注界面、只要一关注公众号网站自动登录、第二次扫描登录的时候网站直接登录,这种扫码登录的方式个人觉得非常利于推广公众号。流程如下:一、获取二维码二、前端轮询接口,查看扫码情况未扫描:扫描成功:三、扫描二维码方式一:利于推广方式基本流程使用微信扫码登录 我们肯定要先来了解一下扫码登录的基本流程啦前端首先向服务端发送一个请求,用来获取二维码的url和唯一随机数(用UUID即可,这个随机数可以理解为这个二维码的key值,一一对应,所以尽量要用唯一的)同时服务端要记录这条随机数(存redis和其他数据库均可,找个地方记录下来)前端自从收到二维码和随机数后,展示二维码,并轮询一个检查二维码状态的接口(用来判断用户是否扫码并确认)很关键的一步客户扫码并确认后,会回调给服务端一个请求,服务端就能拿到对应的二维码的key(之前产生的随机数)前端轮询中 发现二维码状态值变为用户扫码已确认的值后,向后端发送业务请求(登录。。。。)前提准备导入依赖 <!-- 对接微信登录 开始 --> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-mp</artifactId> <version>4.3.8.B</version> </dependency> <!-- 对接微信登录 结束 --> <!-- 读取配置文件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <!-- 网络请求框架 --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <!-- fast json --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.10</version> </dependency> <!-- END fast json -->网络请求工具类util/HttpClientUtil.javapublic class HttpClientUtil { public static String doGet(String url, Map<String, String> param) { // 创建Httpclient对象 CloseableHttpClient httpclient = HttpClients.createDefault(); String resultString = ""; CloseableHttpResponse response = null; try { // 创建uri URIBuilder builder = new URIBuilder(url); if (param != null) { for (String key : param.keySet()) { builder.addParameter(key, param.get(key)); } } URI uri = builder.build(); // 创建http GET请求 HttpGet httpGet = new HttpGet(uri); // 执行请求 response = httpclient.execute(httpGet); // 判断返回状态是否为200 if (response.getStatusLine().getStatusCode() == 200) { resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (response != null) { response.close(); } httpclient.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } public static String doGet(String url) { return doGet(url, null); } public static String doPost(String url, Map<String, String> param) { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建参数列表 if (param != null) { List<NameValuePair> paramList = new ArrayList<>(); for (String key : param.keySet()) { paramList.add(new BasicNameValuePair(key, param.get(key))); } // 模拟表单 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList, "utf-8"); httpPost.setEntity(entity); } // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } public static String doPost(String url) { return doPost(url, null); } /** * 请求的参数类型为json * * @param url * @param json * @return {username:"",pass:""} */ public static String doPostJson(String url, String json) { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建请求内容 StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON); httpPost.setEntity(entity); // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } }扫码登录工具类util/JsonUtil.javapublic class MyStringUtil { /** * 从字节数组到十六进制字符串转换 */ public static String bytes2HexString(byte[] b) { byte[] buff = new byte[2 * b.length]; for (int i = 0; i < b.length; i++) { buff[2 * i] = hex[(b[i] >> 4) & 0x0f]; buff[2 * i + 1] = hex[b[i] & 0x0f]; } return new String(buff); } private final static byte[] hex = "0123456789ABCDEF".getBytes(); //length用户要求产生字符串的长度 public static String getRandomString(int length) { String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < length; i++) { int number = random.nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } }1.公网映射开发的接口需要暴露在公网,微信服务器会进行回调调用。博主开发中配合使用的是: Sunny-Ngrok内网转发内网穿透 - 国内内网映射服务器:https://www.ngrok.cc/ (需要实名认证、支付宝人脸识别、人脸识别费用一两块钱)此时访问http://lisok.free.idcfengye.com/就是访问本地的localhost:80822.微信公众平台测试号地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login需要记录appID、appsecret项目中配置application.yml:# 扫描公众号登录 wechat: appId: ... appSecret: ... qrCodeUrl: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN tokenUrl: https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=SECRET # 验签使用的token token: ... # token 随机填配置类:properties/WxConfig/** * @author Hygge * @date 2022/08/13 * @description 微信登录的一些配置信息 通过读取yml获得 */ @Component @Data @ConfigurationProperties(prefix = "wechat") public class WxConfig { private String appId; private String appSecret; private String qrCodeUrl; private String tokenUrl; /** * 验签使用的token */ private String token; }3.配置微信消息路由器MP\_微信消息路由器官方文档:https://github.com/Wechat-Group/WxJava/wiki/MP_%E5%BE%AE%E4%BF%A1%E6%B6%88%E6%81%AF%E8%B7%AF%E7%94%B1%E5%99%A8微信推送给公众号的消息类型很多,而公众号也需要针对用户不同的输入做出不同的反应。如果使用if ... else ...来实现的话非常难以维护,这时可以使用WxMpMessageRouter来对消息进行路由。WxMpMessageRouter支持从4个角度对消息进行匹配,然后交给事先指定的WxMpMessageRouter作用举个栗子,如果没有消息路由,那么所有的消息各种类型都会集中在一块处理,如文本消息、图片消息、订阅通知、扫码通知、取消订阅通知等。有了消息路由就可以将属于一类的消息转发给专门用来处理这类消息的路由(Java类)config/WeChatConfig.java/** * @author Hygge * @date 2022/08/20 * @description */ @Configuration public class WeChatConfig { @Resource private WxConfig wxConfig; @Resource private WxMpService wxMpService; @Resource private TextHandler textHandler; @Resource private ImageHandler imageHandler; @Resource private SubscribeHandler subscribeHandler; @Resource private UnSubscribeHandler unSubscribeHandler; @Resource private ScanHandler scanHandler; // 单例 @Bean @Scope("singleton") public WxMpService wxMpService() { WxMpDefaultConfigImpl wxMpDefaultConfig = new WxMpDefaultConfigImpl(); wxMpDefaultConfig.setAppId(wxConfig.getAppId()); wxMpDefaultConfig.setSecret(wxConfig.getAppSecret()); wxMpDefaultConfig.setToken(wxConfig.getToken()); WxMpService wxService = new WxMpServiceImpl(); wxService.setWxMpConfigStorage(wxMpDefaultConfig); return wxService; } @Bean public WxMpMessageRouter messageRouter() { // 创建消息路由 final WxMpMessageRouter router = new WxMpMessageRouter(wxMpService); // 添加文本消息路由 router.rule().async(false).msgType(WxConsts.XmlMsgType.TEXT).handler(textHandler).end(); // 添加关注事件路由 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.SUBSCRIBE).handler(subscribeHandler).end(); // 添加扫码事件路由 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.SCAN).handler(scanHandler).end(); return router; } }handler/TextHandler.java/** * @author Hygge * @date 2022/08/20 * @description 用來處理文本消息的路由 */ @Component public class TextHandler implements WxMpMessageHandler { @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException { // 接收的消息内容 String inContent = wxMpXmlMessage.getContent(); // 响应的消息内容 String outContent; // 根据不同的关键字回复消息 if (inContent.contains("游戏")) { outContent = "仙剑奇侠传"; } else if (inContent.contains("动漫")) { outContent = "进击的巨人"; } else { outContent = inContent; } // 构造响应消息对象 return WxMpXmlOutMessage.TEXT().content(outContent).fromUser(wxMpXmlMessage.getToUser()) .toUser(wxMpXmlMessage.getFromUser()).build(); } } handler/ScanHandler.java/** * @author Hygge * @date 2022/08/20 * @description 用來處理用戶掃碼登錄的事件 */ @Component @Slf4j public class ScanHandler implements WxMpMessageHandler { @Resource private UserService userService; @Resource private RedisTemplate<String, Object> redisTemplate; @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException { log.info("ScanHandler调用"); // 获取YYYY-MM-DD HH:MM:SS格式的时间 String content = "您在" + (new DateTime().toString("yyyy-MM-dd HH:mm:ss")) + "通过微信扫码登录PaperFF网站,感谢您的使用。"; // 获取场景值 String openId = wxMpXmlMessage.getFromUser(); // 判断用户是否存在 User user = userService.getUserByOpenId(openId); if (user == null) { // 如果用户不存在,则创建用户 user = userService.createUserByWeChat(openId); } // 将场景值和用户信息存入redis redisTemplate.opsForValue().set(wxMpXmlMessage.getEventKey(), user, 2, TimeUnit.MINUTES); return WxMpXmlOutMessage.TEXT().fromUser(wxMpXmlMessage.getToUser()).toUser(wxMpXmlMessage.getFromUser()) .content(content).build(); } }订阅(关注)的路由逻辑跟扫码的一样。4.配置消息接口地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/loginController/WechatLoginController.java/** * @author Hygge * @date 2022/08/13 * @description 微信登录的控制器 */ @Slf4j @RestController @RequestMapping("/login/wechat") public class WechatLoginController { @Resource private WxConfig wxConfig; @Resource private RedisTemplate<String, Object> redisTemplate; @Resource private WxMpService wxMpService; @Resource private WxMpMessageRouter wxMpMessageRouter; @Resource private WeChatLoginService weChatLoginService; /** * 获取登录二维码 * * @return 登录二维码 */ @GetMapping("/getQrCode") public R getQrCode() { try { // 获取AccessToken String accessToken = weChatLoginService.getAccessToken(); String getQrCodeUrl = wxConfig.getQrCodeUrl().replace("TOKEN", accessToken); // 这里生成一个带参数的二维码,参数是scene_str String sceneStr = MyStringUtil.getRandomString(8); String json = "{\"expire_seconds\": 120000, \"action_name\": \"QR_STR_SCENE\"" + ", \"action_info\": {\"scene\": {\"scene_str\": \"" + sceneStr + "\"}}}"; String result = HttpClientUtil.doPostJson(getQrCodeUrl, json); JSONObject jsonObject = JSONObject.parseObject(result); jsonObject.put("sceneStr", sceneStr); return R.ok().put("data", jsonObject); } catch (Exception e) { e.printStackTrace(); return R.error(e.getMessage()); } } /** * 根据二维码场景值获取用户的openId => 获取用户信息 * * @param eventKey * @return */ @RequestMapping("/getOpenId") public R getOpenId(@RequestParam String eventKey) { if (Boolean.FALSE.equals(redisTemplate.hasKey(eventKey))) { return R.error("等待用户扫码"); } User user = (User) redisTemplate.opsForValue().get(eventKey); redisTemplate.delete(eventKey); return R.ok("登录成功").put("data", user); } /** * 微信官方的回调处理 * 官方文档:<a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html">...</a> * * @param request 微信发送的请求参数 */ @RequestMapping(value = "/message", produces = "application/xml; charset=UTF-8") public String handleMessage(HttpServletRequest request, HttpServletResponse response) throws Exception { //获取微信请求参数 String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String echoStr = request.getParameter("echostr"); if (!wxMpService.checkSignature(timestamp, nonce, signature)) { throw new IllegalArgumentException("非法请求,可能属于伪造的请求!"); } else { // 验签通过,直接返回echostr // response.getWriter().write(echoStr); // return null; // 解析消息体,封装为对象 WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(request.getInputStream()); WxMpXmlOutMessage outMessage; try { // 将消息路由给对应的处理器,获取响应 outMessage = wxMpMessageRouter.route(inMessage); } catch (Exception e) { log.error("微信消息路由异常", e); outMessage = null; } // 将响应消息转换为xml格式返回 response.getWriter().write(outMessage == null ? "" : outMessage.toXml()); return null; } } }WeChatLoginServiceImpl.java/** * @author Hygge * @date 2022/08/21 * @description */ @Service public class WeChatLoginServiceImpl implements WeChatLoginService { @Resource private WxConfig wxConfig; @Resource private WxMpService wxMpService; @Override public String getAccessToken() { String getTokenUrl = wxConfig.getTokenUrl() .replace("APPID", wxConfig.getAppId()).replace("SECRET", wxConfig.getAppSecret()); String result = HttpClientUtil.doGet(getTokenUrl); JSONObject jsonObject = JSONObject.parseObject(result); return jsonObject.getString("access_token"); } @Override public WxMpUser getUserInfoByOpenid(String openId) { return wxMpService.getUserService().userInfo(openId); } }5.前端部分代码import {request, METHOD} from '@/api/request.js' import {SCAN_WECHAT_CODE, POLL_WECHAT_CODE} from "@/services/api.js"; import loadQrCode from "@/assets/images/qrcode_loading.gif" import loseQrCode from "@/assets/images/lose.png" export default { name: 'Login', data() { return { loginType: 'scanCode', qrcode: loadQrCode, sceneStr: '', t: 0, } }, methods: { toggleLoginType() { this.loginType = this.loginType === 'scanCode' ? 'input' : 'scanCode'; }, async getQrCode() { window.clearInterval(this.t); let response = await request(SCAN_WECHAT_CODE, METHOD.GET) this.qrcode = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' + response.data.data.ticket; this.sceneStr = response.data.data.sceneStr; // 开始监听轮询 this.t = window.setInterval(this.getOpenId, 6000); window.setTimeout(() => { this.qrcode = loseQrCode; window.clearInterval(this.t); }, response.data.data.expire_seconds) }, async getOpenId() { let response = await request(POLL_WECHAT_CODE, METHOD.GET, { eventKey: this.sceneStr }) if (response.data.code === 200) { window.clearInterval(this.t); console.log(response.data.data); } } }, mounted() { this.getQrCode() } }方式二:登录常见方式..用到在更新引用1.WxJava - 微信开发 Java SDK:https://github.com/Wechat-Group/WxJava2.微信公众平台 - 测试号管理 : https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login3.Sunny-Ngrok内网转发内网穿透 - 国内内网映射服务器 : https://www.ngrok.cc/ 4.WxJava微信公众号开发实战:https://baobao555.tech/archives/535.springboot前后端分离-使用微信扫码登录(后端):https://blog.csdn.net/xiaoping__/article/details/1242588816.微信公众平台开发概述 : https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html7.内网穿透工具--Sunny-Ngrok讲解:https://blog.csdn.net/weixin_44563573/article/details/1209075278.程序员大阳 - 微信公众号开发 :[https://blog.csdn.net/woshisangsang/category_11369636.html](
2022年08月21日
980 阅读
0 评论
1 点赞
2022-06-29
如何优雅的写 Controller 层代码?
前言本篇主要要介绍的就是 controller 层的处理,一个完整的后端请求由 4 部分组成: ``接口地址(也就是 URL 地址)请求方式(一般就是 get、set,当然还有 put、delete)请求数据(request,有 head 跟 body)响应数据(response)本篇将解决以下 3 个问题:当接收到请求时,如何优雅的校验参数返回响应数据该如何统一的进行处理接收到请求,处理业务逻辑时抛出了异常又该如何处理Controller 层参数接收(太基础了,可以跳过)常见的请求就分为 get 跟 post 两种:@RestController @RequestMapping("/product/product-info") public class ProductInfoController { @Autowired ProductInfoService productInfoService; @GetMapping("/findById") public ProductInfoQueryVo findById(Integer id) { ... } @PostMapping("/page") public IPage findPage(Page page, ProductInfoQueryVo vo) { ... } }RestController:之前解释过,@RestController=@Controller+ResponseBody。加上这个注解,springboot 就会吧这个类当成 controller 进行处理,然后把所有返回的参数放到 ResponseBody 中。@RequestMapping:请求的前缀,也就是所有该 Controller 下的请求都需要加上 /product/product-info 的前缀。@GetMapping("/findById"):标志这是一个 get 请求,并且需要通过 /findById 地址才可以访问到。@PostMapping("/page"):同理,表示是个 post 请求。参数:至于参数部分,只需要写上 ProductInfoQueryVo,前端过来的 json 请求便会通过映射赋值到对应的对象中,例如请求这么写,productId 就会自动被映射到 vo 对应的属性当中。size : 1 current : 1 productId : 1 productName : 泡脚统一状态码返回格式为了跟前端妹妹打好关系,我们通常需要对后端返回的数据进行包装一下,增加一下状态码,状态信息,这样前端妹妹接收到数据就可以根据不同的状态码,判断响应数据状态,是否成功是否异常进行不同的显示。当然这让你拥有了更多跟前端妹妹的交流机会,假设我们约定了 1000 就是成功的意思。如果你不封装,那么返回的数据是这样子的:{ "productId": 1, "productName": "泡脚", "productPrice": 100.00, "productDescription": "中药泡脚加按摩", "productStatus": 0, }经过封装以后是这样子的{ "code": 1000, "msg": "请求成功", "data": { "productId": 1, "productName": "泡脚", "productPrice": 100.00, "productDescription": "中药泡脚加按摩", "productStatus": 0, } }封装 ResultVo这些状态码肯定都是要预先编好的,怎么编呢?写个常量 1000?还是直接写死 1000?要这么写就真的书白读的了,写状态码当然是用枚举拉:①首先先定义一个状态码的接口,所有状态码都需要实现它,有了标准才好做事:public interface StatusCode { public int getCode(); public String getMsg(); }②然后去找前端妹妹,跟他约定好状态码(这可能是你们唯一的约定了)枚举类嘛,当然不能有 setter 方法了,因此我们不能在用 @Data 注解了,我们要用 @Getter。@Getter public enum ResultCode implements StatusCode{ SUCCESS(1000, "请求成功"), FAILED(1001, "请求失败"), VALIDATE_ERROR(1002, "参数校验失败"), RESPONSE_PACK_ERROR(1003, "response返回包装失败"); private int code; private String msg; ResultCode(int code, String msg) { this.code = code; this.msg = msg; } }③写好枚举类,就开始写 ResultVo 包装类了,我们预设了几种默认的方法,比如成功的话就默认传入 object 就可以了,我们自动包装成 success。@Data public class ResultVo { // 状态码 private int code; // 状态信息 private String msg; // 返回对象 private Object data; // 手动设置返回vo public ResultVo(int code, String msg, Object data) { this.code = code; this.msg = msg; this.data = data; } // 默认返回成功状态码,数据对象 public ResultVo(Object data) { this.code = ResultCode.SUCCESS.getCode(); this.msg = ResultCode.SUCCESS.getMsg(); this.data = data; } // 返回指定状态码,数据对象 public ResultVo(StatusCode statusCode, Object data) { this.code = statusCode.getCode(); this.msg = statusCode.getMsg(); this.data = data; } // 只返回状态码 public ResultVo(StatusCode statusCode) { this.code = statusCode.getCode(); this.msg = statusCode.getMsg(); this.data = null; } }使用,现在的返回肯定就不是 return data;这么简单了,而是需要 new ResultVo(data); @PostMapping("/findByVo") public ResultVo findByVo(@Validated ProductInfoVo vo) { ProductInfo productInfo = new ProductInfo(); BeanUtils.copyProperties(vo, productInfo); return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo))); }最后返回就会是上面带了状态码的数据了。统一校验注:springboot 2.3之前的集成在spring-boot-starter-web里了,所以不需要额外引入包springboot 2.3之后需要引入 spring-boot-starter-validation原始做法假设有一个添加 ProductInfo 的接口,在没有统一校验时,我们需要这么做。@Data public class ProductInfoVo { // 商品名称 private String productName; // 商品价格 private BigDecimal productPrice; // 上架状态 private Integer productStatus; } @PostMapping("/findByVo") public ProductInfo findByVo(ProductInfoVo vo) { if (StringUtils.isNotBlank(vo.getProductName())) { throw new APIException("商品名称不能为空"); } if (null != vo.getProductPrice() && vo.getProductPrice().compareTo(new BigDecimal(0)) < 0) { throw new APIException("商品价格不能为负数"); } ... ProductInfo productInfo = new ProductInfo(); BeanUtils.copyProperties(vo, productInfo); return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo))); }这 if 写的人都傻了,能忍吗?肯定不能忍啊。@Validated 参数校验好在有 @Validated,又是一个校验参数必备良药了。有了 @Validated 我们只需要在 vo 上面加一点小小的注解,便可以完成校验功能。@Data public class ProductInfoVo { @NotNull(message = "商品名称不允许为空") private String productName; @Min(value = 0, message = "商品价格不允许为负数") private BigDecimal productPrice; private Integer productStatus; } @PostMapping("/findByVo") public ProductInfo findByVo(@Validated ProductInfoVo vo) { ProductInfo productInfo = new ProductInfo(); BeanUtils.copyProperties(vo, productInfo); return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo))); } 运行看看,如果参数不对会发生什么?我们故意传一个价格为 -1 的参数过去:productName : 泡脚 productPrice : -1 productStatus : 1{ "timestamp": "2020-04-19T03:06:37.268+0000", "status": 400, "error": "Bad Request", "errors": [ { "codes": [ "Min.productInfoVo.productPrice", "Min.productPrice", "Min.java.math.BigDecimal", "Min" ], "arguments": [ { "codes": [ "productInfoVo.productPrice", "productPrice" ], "defaultMessage": "productPrice", "code": "productPrice" }, 0 ], "defaultMessage": "商品价格不允许为负数", "objectName": "productInfoVo", "field": "productPrice", "rejectedValue": -1, "bindingFailure": false, "code": "Min" } ], "message": "Validation failed for object\u003d\u0027productInfoVo\u0027. Error count: 1", "trace": "org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object \u0027productInfoVo\u0027 on field \u0027productPrice\u0027: rejected value [-1]; codes [Min.productInfoVo.productPrice,Min.productPrice,Min.java.math.BigDecimal,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [productInfoVo.productPrice,productPrice]; arguments []; default message [productPrice],0]; default message [商品价格不允许为负数]\n\tat org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:164)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:660)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:124)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1594)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:830)\n", "path": "/leilema/product/product-info/findByVo" }大功告成了吗?虽然成功校验了参数,也返回了异常,并且带上"商品价格不允许为负数"的信息。但是你要是这样返回给前端,前端妹妹就提刀过来了,当年约定好的状态码,你个负心人说忘就忘?用户体验小于等于 0 啊!所以我们要进行优化一下,每次出现异常的时候,自动把状态码写好,不负妹妹之约!优化异常处理首先我们先看看校验参数抛出了什么异常:Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors我们看到代码抛出了 org.springframework.validation.BindException 的绑定异常,因此我们的思路就是 AOP 拦截所有 controller,然后异常的时候统一拦截起来,进行封装!完美!玩你个头啊完美,这么呆瓜的操作 springboot 不知道吗?spring mvc 当然知道拉,所以给我们提供了一个 @RestControllerAdvice 来增强所有 @RestController,然后使用 @ExceptionHandler 注解,就可以拦截到对应的异常。这里我们就拦截 BindException.class 就好了。最后在返回之前,我们对异常信息进行包装一下,包装成 ResultVo,当然要跟上 ResultCode.VALIDATE_ERROR 的异常状态码。这样前端妹妹看到 VALIDATE_ERROR 的状态码,就会调用数据校验异常的弹窗提示用户哪里没填好。@RestControllerAdvice public class ControllerExceptionAdvice { @ExceptionHandler({BindException.class}) public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) { // 从异常对象中拿到ObjectError对象 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage()); } }来看看效果,完美。1002 与前端妹妹约定好的状态码:{ "code": 1002, "msg": "参数校验失败", "data": "商品价格不允许为负数" }统一响应统一包装响应再回头看一下 controller 层的返回:return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));开发小哥肯定不乐意了,谁有空天天写 new ResultVo(data) 啊,我就想返回一个实体!怎么实现我不管!好把,那就是 AOP 拦截所有 Controller,再 @After 的时候统一帮你封装一下咯。怕是上一次脸打的不够疼,springboot 能不知道这么个操作吗?@RestControllerAdvice(basePackages = {"com.bugpool.leilema"}) public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { // response是ResultVo类型,或者注释了NotControllerResponseAdvice都不进行包装 return !methodParameter.getParameterType().isAssignableFrom(ResultVo.class); } @Override public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) { // String类型不能直接包装 if (returnType.getGenericParameterType().equals(String.class)) { ObjectMapper objectMapper = new ObjectMapper(); try { // 将数据包装在ResultVo里后转换为json串进行返回 return objectMapper.writeValueAsString(new ResultVo(data)); } catch (JsonProcessingException e) { throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage()); } } // 否则直接包装成ResultVo返回 return new ResultVo(data); } } ①@RestControllerAdvice(basePackages = {"com.bugpool.leilema"}) 自动扫描了所有指定包下的 controller,在 Response 时进行统一处理。②重写 supports 方法,也就是说,当返回类型已经是 ResultVo 了,那就不需要封装了,当不等与 ResultVo 时才进行调用 beforeBodyWrite 方法,跟过滤器的效果是一样的。③最后重写我们的封装方法 beforeBodyWrite,注意除了 String 的返回值有点特殊,无法直接封装成 json,我们需要进行特殊处理,其他的直接 new ResultVo(data); 就 ok 了。打完收工,看看效果: @PostMapping("/findByVo") public ProductInfo findByVo(@Validated ProductInfoVo vo) { ProductInfo productInfo = new ProductInfo(); BeanUtils.copyProperties(vo, productInfo); return productInfoService.getOne(new QueryWrapper(productInfo)); }此时就算我们返回的是 po,接收到的返回就是标准格式了,开发小哥露出了欣慰的笑容。{ "code": 1000, "msg": "请求成功", "data": { "productId": 1, "productName": "泡脚", "productPrice": 100.00, "productDescription": "中药泡脚加按摩", "productStatus": 0, ... } }NOT 统一响应不开启统一响应原因:开发小哥是开心了,可是其他系统就不开心了。举个例子:我们项目中集成了一个健康检测的功能,也就是这货。@RestController public class HealthController { @GetMapping("/health") public String health() { return "success"; } }公司部署了一套校验所有系统存活状态的工具,这工具就定时发送 get 请求给我们系统:“兄弟,你死了吗?”“我没死,滚”“兄弟,你死了吗?”“我没死,滚”是的,web 项目的本质就是复读机。一旦发送的请求没响应,就会给负责人发信息(企业微信或者短信之类的),你的系统死啦!赶紧回来排查 bug 吧!好吧,没办法,人家是老大,人家要的返回不是:{ "code": 1000, "msg": "请求成功", "data": "success" }人家要的返回只要一个 success,人家定的标准不可能因为你一个系统改。俗话说的好,如果你改变不了环境,那你就只能我**新增不进行封装注解:因为百分之 99 的请求还是需要包装的,只有个别不需要,写在包装的过滤器吧?又不是很好维护,那就加个注解好了。所有不需要包装的就加上这个注解。@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface NotControllerResponseAdvice { }然后在我们的增强过滤方法上过滤包含这个注解的方法:@RestControllerAdvice(basePackages = {"com.bugpool.leilema"}) public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { // response是ResultVo类型,或者注释了NotControllerResponseAdvice都不进行包装 return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class) || methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class)); } ...最后就在不需要包装的方法上加上注解:@RestController public class HealthController { @GetMapping("/health") @NotControllerResponseAdvice public String health() { return "success"; } }这时候就不会自动封装了,而其他没加注解的则依旧自动包装:统一异常每个系统都会有自己的业务异常,比如库存不能小于 0 子类的,这种异常并非程序异常,而是业务操作引发的异常,我们也需要进行规范的编排业务异常状态码,并且写一个专门处理的异常类,最后通过刚刚学习过的异常拦截统一进行处理,以及打日志。①异常状态码枚举,既然是状态码,那就肯定要实现我们的标准接口 StatusCode。@Getter public enum AppCode implements StatusCode { APP_ERROR(2000, "业务异常"), PRICE_ERROR(2001, "价格异常"); private int code; private String msg; AppCode(int code, String msg) { this.code = code; this.msg = msg; } }②异常类,这里需要强调一下,code 代表 AppCode 的异常状态码,也就是 2000;msg 代表业务异常,这只是一个大类,一般前端会放到弹窗 title 上;最后 super(message); 这才是抛出的详细信息,在前端显示在弹窗体中,在 ResultVo 则保存在 data 中。@Getter public class APIException extends RuntimeException { private int code; private String msg; // 手动设置异常 public APIException(StatusCode statusCode, String message) { // message用于用户设置抛出错误详情,例如:当前价格-5,小于0 super(message); // 状态码 this.code = statusCode.getCode(); // 状态码配套的msg this.msg = statusCode.getMsg(); } // 默认异常使用APP_ERROR状态码 public APIException(String message) { super(message); this.code = AppCode.APP_ERROR.getCode(); this.msg = AppCode.APP_ERROR.getMsg(); } }③最后进行统一异常的拦截,这样无论在 service 层还是 controller 层,开发人员只管抛出 API 异常,不需要关系怎么返回给前端,更不需要关心日志的打印。@RestControllerAdvice public class ControllerExceptionAdvice { @ExceptionHandler({BindException.class}) public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) { // 从异常对象中拿到ObjectError对象 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage()); } @ExceptionHandler(APIException.class) public ResultVo APIExceptionHandler(APIException e) { // log.error(e.getMessage(), e); 由于还没集成日志框架,暂且放着,写上TODO return new ResultVo(e.getCode(), e.getMsg(), e.getMessage()); } }④最后使用,我们的代码只需要这么写。 if (null == orderMaster) { throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId); }{ "code": 2003, "msg": "订单不存在", "data": "订单号不存在:1998" }就会自动抛出 AppCode.ORDER_NOT_EXIST 状态码的响应,并且带上异常详细信息订单号不存在:xxxx。后端小哥开发有效率,前端妹妹获取到 2003 状态码,调用对应警告弹窗,title 写上订单不存在,body 详细信息记载"订单号不存在:1998"。同时日志还自动打上去了。
2022年06月29日
251 阅读
0 评论
1 点赞