1. 程式人生 > >axios傳送post請求,springMVC接收不到資料問題

axios傳送post請求,springMVC接收不到資料問題

最近做專案的時候,前端非同步請求用到了尤大推薦的axios,發現一個問題,就是POST請求的時候,後臺人員說他們的接口裡面取不到我傳過去的資料。

案例重現

axios.js
let axios = import('axios');
instance = axios.create({
  baseURL: '/ghcws',
  timeout: 10000,
});
export default instance;
userService.js
import axios = import('./axios');
export async function () {
  axios.post('/api/doLogin'
, { usesrname: 'admin', password: 'admin' }) }
後端的springMVC的關鍵程式碼(簡化版)
@RequestMappting("/api/doLogin")
public Object doLogin(@RequestParam String username, @RequestParam String password) throws Exception {
  System.out.println("username: "+username);
  System.out.println("password: "+password);
  JSONObject json = new
JSONObject(); json.put("success", true); return json; }

這個時候前端發的請求後端就接收不到引數了。

我們可以開啟chrome開發者工具,看看axios的請求的請求頭詳情,發現Request-Headers的Content-Typeapplication/json;charset=UTF-8,Request Payload為

{username: "admin", password: "admin"}

我們同樣的用jquery的ajax把我們這個請求同樣的傳送一遍
發現Request-Headers的Content-Typeapplication/x-www-form-urlencoded;charset=UTF-8

,URL encode為

username=admin&password=admin

到這裡,由於是前端換了一個傳送ajax請求的工具,導致以前的介面不能用了,後端朋友們首先想到的就是我們前端人員寫錯了,然後我們就要開始苦逼的研究了。

可以看出,兩個請求唯一的不同就是Content-Type的問題,朋友們,是Request Headers中的Content-Type哈,不是Response中的哈,不要搞錯了。

那不同點找到了,那我們就可以開始搞了,我們大膽的猜想,如果把axios的post請求的Content-Type也變成application/x-www-form-urlencoded,那麼問題想必就迎刃而解了。
我們看看axios的原始碼

axios.create = function create(instanceConfig) {
  return createInstance(utils.merge(defaults, instanceConfig));
};

create方法就是把我們傳入的引數和預設引數合併,然後建立一個axios例項,我們再看看defaults這個配置物件

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');
/* 這個表明預設的Content-Type就是我們想要的 */
var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};
/* 看方法名就知道,這個是設定ContentType用的(Content-Type沒有設定的時候) */
function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}
/* 這個是用來區別對待瀏覽器和nodejs請求發起工具的區別的 */
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}
/* 這裡終於看到了萬眾期待的預設配置 */
var defaults = {
  adapter: getDefaultAdapter(),
  /* 這個transformRequest配置就厲害了
   * 官方描述`transformRequest` allows changes to the request data before it is sent to the server 
   * 這個函式是接受我們傳遞的引數,並且在傳送到伺服器前,可以對其進行更改
   * */
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    /* 關鍵點1、如果用URLSearchParams物件傳遞引數,就可以用我們想要的Content-Type傳遞 */
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    /* 關鍵點2、這裡我們看到,如果引數Object的話,就是通過json傳遞 */
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],
  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],
  timeout: 0,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  maxContentLength: -1,
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};
defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
module.exports = defaults;

通過上面的原始碼註解,我們找到了著手點:

  1. 用URLSearchParams傳遞引數
  2. 改寫transformRequest
    很顯然,如果我們不是通過axios.create方法建立例項,再拿來呼叫,我們就只能採用第一種解決辦法
第一種方法解決方案

改寫userService

import axios = import('axios');
let param = new URLSearchParams();
param.append("username", "admin");
param.append("password", "admin");
export async function () {
  axios.post('/api/doLogin', param)
}

果不其然,這就成功了。
如果不想用URLSearchParams,還是覺得Json方便,那麼我們可以重新配置transformRequest

第二種方法解決方案

改寫axios的create的配置

import axios from 'axios';
// 這裡我自己重寫了一下型別判斷的所有方法,當然也可以用util庫
import { isFormData,
  isArrayBuffer,
  isStream,
  isFile,
  isBlob,
  isURLSearchParams,
  isObject,
  isUndefined } from './Type';
function setContentTypeIfUnset(headers, value) {
  if (!isUndefined(headers) && isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}
const instance = axios.create({
  baseURL: '/ghcws',
  timeout: 10000,
  transformRequest: [function transformRequest(data, headers) {
    /* 把類似content-type這種改成Content-Type */
    let keys = Object.keys(headers);
    let normalizedName = 'Content-Type';
    keys.forEach(name => {
      if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
        headers[normalizedName] = headers[name];
        delete headers[name];
      }
    });
    if (isFormData(data) ||
      isArrayBuffer(data) ||
      isStream(data) ||
      isFile(data) ||
      isBlob(data)
    ) {
      return data;
    }
    if (isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    /* 這裡是重點,其他的其實可以照著axios的原始碼抄 */
    /* 這裡就是用來解決POST提交json資料的時候是直接把整個json放在request payload中提交過去的情況
     * 這裡簡單處理下,把 {name: 'admin', pwd: 123}這種轉換成name=admin&pwd=123就可以通過
     * x-www-form-urlencoded這種方式提交
     * */
    if (isObject(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      let keys2 = Object.keys(data);
      /* 這裡就是把json變成url形式,並進行encode */
      return encodeURI(keys2.map(name => `${name}=${data[name]}`).join('&'));
    }
    return data;
  }]
});
export default instance;

當然不用create方法也是可以通過修改axios.defaults.transformRequest實現相同效果。

那麼現在問題雖然解決了,但是為什麼之前後端就是接收不到json型別的引數呢????

其實原因很簡單,因為axios post一個物件到後端的時候,是直接把json放到請求體中的,提交到後端的,而後端是怎麼取引數的,是用的

@RequestParam

這個是什麼意思,這個是隻能從請求的地址中取出引數,也就是隻能從username=admin&password=admin這種字串中解析出引數,這樣是不能提取出請求體中的引數的。
那麼現在我們又可以大膽的猜想了,如果我們不這麼去取引數,而是直接去請求體中取引數不就行了麼。
我們可以不改前端,只需要改改後端程式碼就可以了。

解決方案
@RequestMappting("/api/doLogin")
@ResponseBody
public Object doLogin(@RequestBody Map map) throws Exception {
  System.out.println("username: "+map.get("username"));
  System.out.println("password: "+map.get("password"));
  JSONObject json = new JSONObject();
  json.put("success", true);
  return json;
}

通過@RequestBody 註解,springmvc可以把json中的資料繫結到Map中, 我們就可以取出了.
或者也可以

@RequestBody Pojo pojo

這樣也可以把json繫結到實體類中,也能取到引數。

總結

總共三種解決方案:

  • 前端不用json傳參,改用URLSearchParams
  • 前端改寫axios的transformRequest
  • 後端使用@RequestBody註解繫結json到實體類