之前在前端引用了axios,那么紧接着,后台要做如何的修改呢?直接返回html肯定是不对的,这时候,一个名为webapi的技术就出现了
webapi
webapi区别于普通的api,是指“使用http协议通过网络调用的API”即软件组织的外部接口。有时候也叫RESTful api,虽然他们实际上还是有一些区别的,但是基本上可以近似的理解他们是相同的,关于他们的定义,阮博写的还是非常的清晰。
SpringMVC中的webapi
在之前的程序中,我们返回的都是一个jsp模板的名字,然后框架自动去渲染这个jsp模板。但显然这个是不符合webapi的,那么我们想让他仅仅返回数据怎么办呢?这里介绍两个注解:ResponseBody
和RestController
,我们首先创建一个TestController控制器进行说明,他的代码很简单,首先:
@Controller
public class TestController {
@ResponseBody
@RequestMapping(value = "/test", method = {RequestMethod.GET})
public Object test(){
return "Hello world";
}
}
然后在浏览器中直接访问http://localhost:8082/test
,在返回页面查看源代码,只有 Hello world
仿佛我们又回到了直接使用Servlet输出String的时代。
你可能注意到了,返回值是一个Object,那么我们返回一个对象试一下:
@ResponseBody
@RequestMapping(value = "/test", method = {RequestMethod.GET})
public Object test(){
User user=new User();
user.setId(1);
user.setName("zhangsan");
user.setPassword("123456");
user.setCreateTime(new Date());
return user;
}
查看一下返回信息:
{"name":"zhangsan","password":"123456","passwordSalt":null,"createTime":1514992830293,"id":1}
ok,比较完美,但是,如果一个控制器中所有的action均为webapi接口,这显然是一个很常见的事情,毕竟谁都不喜欢页面和json混合使用,那么这样写就是有些啰嗦了,这是就可以使用RestController
使用它的效果就相当于所有的action都戴上了ResponseBody
注解。我们使用这个注解对这个测试控制器进行一下修改:
@RestController
public class TestController {
@RequestMapping(value = "/test", method = {RequestMethod.GET})
public Object test(){
User user=new User();
user.setId(1);
user.setName("zhangsan");
user.setPassword("123456");
user.setCreateTime(new Date());
return user;
}
}
运行,同样用刚刚土土的浏览器测试法测试一下,查看一下返回信息,依然是:
{"name":"zhangsan","password":"123456","passwordSalt":null,"createTime":1514992830293,"id":1}
PostMan
使用浏览器的测试方式虽然很方便,但是局限性也非常大,比如它只能测试Get方式,只能使用?传参的方式,无法对header赋值等等,这时候一个工具是非常必要的,有一款常用的工具是PostMan就非常的好用它是一个chrome的插件,所以暂时来说,安装它需要科学上网。
安装方式:
- 点击chrome最右边的三个点
- 在弹出菜单中选择更多工具
- 在弹出菜单中选择扩展程序(图1)
- 然后在搜索店内应用中搜索postman(图2)
- 接着一直下一步即可
如果安装完成后,多出一个类似应用程序的图标,因为经常使用我把他弄到了桌面的快捷方式,图标是这样的:
当看见这个火箭人的时候,就证明postman已经安装完成。
接下来双击我们测测试一下,在测试之前对代码进行一下修改:
@RestController
public class TestController {
@RequestMapping(value = "/test", method = {RequestMethod.POST})
public Object test(String username,String password){
User user=new User();
user.setId(1);
user.setName(username);
user.setPassword(password);
user.setCreateTime(new Date());
return user;
}
}
然后运行,并如图在Postman内输入相应信息:
点击发送按钮,在body中可以看到已经自动格式化好的返回信息:
SpringMVC跨域
服务端的的配置完成之后,我们想到的就是客户端如何来调用它,回到vue的项目中,在views文件夹内,创建一个Test.vue文件,里边只写一个测试代码,访问服务端test服务,代码如下:
<style></style>
<template>
<div>
{{ txt }}
</div>
</template>
<script>
export default {
data() {
return {
txt:'',
}
},
created(){
this.$http.post("/test",).then(res=>{
this.txt=res.data
},res=>{
this.txt=res
}
)
}
}
</script>
代码虽然简单,但是已经可以看出一个vue组件的基本结构:
style节点###
存放本组件所需的css,可以通过scoped来控制css类的作用域
template节点###
一个组件的布局,即html模板,主要就用来开发的dom结构
script###
vue组件最重要的部分,猜也能猜到用来存储整个页面的js逻辑部分。
这里可以看到js里比较重要的两个部分:
- data节点:此页面所使用的数据模型,vue与普通的jq之类的框架最大的区别就是数据驱动,这一点一定要牢牢记住
- create节点:页面布局创建时执行,这里让它在页面创建时执行ajax
运行,并在浏览器重输入http://localhost:8080/test/
然后按f12,可以看到返回,哦 还有报错信息(警告不用理他,好多都是空格 tab 这类的问题):
这是一个跨域问题,在前后端分离开发的时候很常见的错误,在SpringMVC中解决这种问题主要有三种方法
- 在Action上添加CrossOrigin注解
- 在Controller上添加CrossOrigin注解
这里我选择了第三种方法,因为只有一个人开发,所以很犯懒,一股脑的把跨域权限全部打开,在WebConfig类内覆盖addCorsMappings方法:
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**");
}
重新运行一下tomcat服务器,并重新客户端测试:
可以完美访问。
参数
我们看到,他这时候还是在接收着两个参数,username和password,这里是null,这里添加两个输入框,用户输入用户名和密码,然后发送到服务端,服务端返回在页面底部显示,此功能修改后vue代码如下:
<template>
<div>
<table>
<tr>
<td>用户名</td><td><input type="text" v-model="username"></td>
</tr>
<tr><td>密码</td><td><input type="text" v-model="password"></td></tr>
<tr><td colspan="2"><input type="button" @click="click" value="提交"></td></tr>
</table>
<br>
<div>{{ message }}</div>
</div>
</template>
<script>
export default {
data() {
return {
username:'',
password:'',
message:''
}
},
methods:{
click:function (event) {
var data={
username:this.username,
password:this.password
}
this.$http.post("/test",data).then(res=>{
this.message=res.data.name+'__'+res.data.password
},res=>{
this.txt=res
}
)
}
}
}
</script>
代码复杂了写,但依然很清晰,但是输入值并提交之后,最终的界面如下:
很明显,客户端传送的username和password服务端并没有接受到,这是为什么呢?我们f12看一下浏览器的http协议头的传值部分:
可以看到,提交方式为Payload方式,不同于一般formData。Payload是一种更加支持json数据的方式,这里的解决方式也有几种,比如修改配置强制为formData方式,用query方式等,这里我选择了一个不修改客户端,只修改服务端的方式,即读取requestBody,通过map方式接受参数:
顺便说一下,一般这种清醒下,我都选择修改服务端。
@RestController
public class TestController {
@RequestMapping(value = "/test", method = {RequestMethod.POST})
public Object test(@RequestBody Map map ){
String username=map.get("username").toString();
String password=map.get("password").toString();
User user=new User();
user.setId(1);
user.setName(username);
user.setPassword(password);
user.setCreateTime(new Date());
return user;
}
}
这里的代码并不好,实际中这样的话客户端如果传少参数,传错参数都会报异常,正确的方式应该在服务端进行一下验证。
在此客户端服务器均重启测试一下:
token
webapi是基于http协议的,在之前我们了解到基于http协议就意味着它是无状态,短链接的,但是作为一个应用,必须知道当前使用的用户是哪一个。 也就是必须保持一个会话。
还记得之前jsp页面的会话是如何保持的么?通过一个jsessionid来进行自动处理的,这里我们也这样操作,服务端根据登录状态,保存一个令牌,有令牌进行处理,其中令牌保存方式现在采用最简单的房,仅仅保存在一个静态列表中,然后需要根据时间来决定令牌的生效级生效,所以,我们需要一个简单的令牌管理类:
public class TokenUtil {
private static final int INTERVAL = 7;// token过期时间间隔 天
private static final String SALT = "jtodos";// 加盐
private static final int HOUR = 3;// 检查token过期线程执行时间 时
private static Map<String, Token> tokenMap = new HashMap<String, Token>();
private static TokenUtil tokenUtil = null;
static ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
static {
//开启监听
listenTask();
}
public static TokenUtil getTokenUtil() {
if (tokenUtil == null) {
synInit();
}
return tokenUtil;
}
private static synchronized void synInit() {
if (tokenUtil == null) {
tokenUtil = new TokenUtil();
}
}
private TokenUtil() {//禁止实例化
}
public static Map<String, Token> getTokenMap() {
return tokenMap;
}
public static Token generateToken(String uniq, int id) {//创建token id为业务id
String signature=MD5(System.currentTimeMillis() + SALT + uniq + id);
Token token = new Token(signature, System.currentTimeMillis(),id);
synchronized (tokenMap) {
tokenMap.put(signature, token);
}
return token;
}
public static boolean removeToken(String signature) {//删除
synchronized (tokenMap) {
tokenMap.remove(signature);
}
return true;
}
public static long volidateToken(String signature) { //检查token
Token token = (Token) tokenMap.get(signature);
if (token != null && token.getSignature().equals(signature)) {
return token.getId();
}
return -1;
}
public final static String MD5(String s) {
try {
byte[] btInput = s.getBytes();
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
return byte2hex(mdInst.digest());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static String byte2hex(byte[] b) {
StringBuilder sbDes = new StringBuilder();
String tmp = null;
for (int i = 0; i < b.length; i++) {
tmp = (Integer.toHexString(b[i] & 0xFF));
if (tmp.length() == 1) {
sbDes.append("0");
}
sbDes.append(tmp);
}
return sbDes.toString();
}
public static void listenTask() {
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
//定制每天的HOUR点,从明天开始
calendar.set(year, month, day + 1, HOUR, 0, 0);
// calendar.set(year, month, day, 17, 11, 40);
Date date = calendar.getTime();
scheduler.scheduleAtFixedRate(new ListenToken(), (date.getTime() - System.currentTimeMillis()) / 1000, 60 * 60 * 24, TimeUnit.SECONDS);
}
static class ListenToken implements Runnable {
public ListenToken() {
super();
}
public void run() {//监听Token列表
try {
synchronized (tokenMap) {
for (int i = 0; i < 5; i++) {
if (tokenMap != null && !tokenMap.isEmpty()) {
for (Map.Entry<String, Token> entry : tokenMap.entrySet()) {
Token token = (Token) entry.getValue();
int interval = (int) ((System.currentTimeMillis() - token.getTimestamp()) / 1000 / 60 / 60 / 24);
if (interval > INTERVAL) {
tokenMap.remove(entry.getKey());
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
当然,还需要一个token对象:
public class Token {
private String signature;
private long timestamp;
private long id;//userId
public Token(String signature, long timestamp,long id) {
if (signature == null)
throw new IllegalArgumentException("signature can not be null");
this.timestamp = timestamp;
this.signature = signature;
this.id=id;
}
public long getId(){
return id;
}
public String getSignature() {
return signature;
}
public long getTimestamp() {
return timestamp;
}
// 重写哈希code timestamp 不予考虑, 因为就算 timestamp 不同也认为是相同的 token.
public int hashCode() {
return signature.hashCode();
}
public boolean equals(Object object) {
if (object instanceof Token)
return ((Token) object).signature.equals(this.signature);
return false;
}
//调试用
@Override
public String toString() {
return "Token [signature=" + signature + ", timestamp=" + timestamp + "]";
}
}
这样,我们就可以再客户端保存一个token的值,来模拟jsessionid的角色,获取我们实际所需的对象,具体验证方式如下:
Long userId=TokenUtil.volidateToken("token");
if(userId==-1){
throw new RuntimeException("当前token已失效");
}else{
}
拦截器
但是,所有的东西就怕但是两个字,我们计划做的是一个日记的应用,既然是日记,我们就会希望只看到自己的日记(彩蛋除外),那么,几乎每个接口都需要验证token的,这样的工作即枯燥又繁杂,该如何解决呢?
还记得servlet中的filter么,在SpringMVC中提供了一个类似的,或者说加强版的东东,叫做拦截器,他比过滤器强大之处在于他可以访问ioc里的各个bean,这就提供了可以直接访问服务的能力,他的实现方式也很简单,需要继承一个HandlerInterceptor接口,然后在WebConfig中注册一下即可,我们设置一个用于权限控制的拦截器,具体代码如下:
public class SysPermissionInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI();
//无权限页面直接过去 不用拦截
if(url.contains("/denied")){
return true;
}
String token= request.getHeader("token");
//判断失败 直接跳到无权限页
if (checkToken(token)&&checkUrl(url)) {
request.getRequestDispatcher("/denied").forward(request,response);
return false;
}
if(checkUrl(url)) {
long id = TokenUtil.volidateToken(token);
if (id == -1) {
request.getRequestDispatcher("/denied").forward(request, response);
return false;
}
//防止id重复 将id注入到请求里
request.setAttribute("tokenId", id);
}
return true;
}
//在执行handler返回modelAndView之前来执行
//如果需要向页面提供一些公用 的数据或配置一些视图信息,使用此方法实现 从modelAndView入手
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
//System.out.println("HandlerInterceptor1...postHandle");
}
//执行handler之后执行此方法
//作系统 统一异常处理,进行方法执行性能监控,在preHandle中设置一个时间点,在afterCompletion设置一个时间,两个时间点的差就是执行时长
//实现 系统 统一日志记录
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
//System.out.println("HandlerInterceptor1...afterCompletion");
}
//帮助方法
private boolean checkToken(String token){
return null==token||"".equals(token);
}
//帮助方法
private boolean checkUrl(String url){
if(url.contains("/不许拦截的url,如login")) return false;
return true;
}
}
还需要一个denied的action,这个就很简单了,直接返回没有权限即可。
最后,还需要在WebConfig进行一下注册:
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SysPermissionInterceptor());
}
这样,就对任何action的请求都会进行token的验证
格式约定
回到denied,既然是双方独立开发,那么就要约定一个固定的json格式,否则任何一方的修改都可能会导致客户端的数据解析失败,这里以denied返回的权限失败为例,定义一个基准的json格式:
{
"msg": "没有权限",
"code": 200,
"data": ""
}
这里暂时约定,code统一为200,以配合http的状态码,如果以后修改为直接使用http状态码也方便,然后,约定msg返回错误信息,数据放到data中,即判断如果msg=="",从data节点内读取返回的数据,否则输出异常信息。
同样的,如果每个action都进行json的维护,那工作量同样是即枯燥又易错的,最简单的方法当时在拦截器的afterCompletion方法中进行配置,但为了提高灵活度,我决定做一个父类,在父类的方法内包装json对象,然后子类调用,父类的代码如下:
public abstract class BaseController {
public Map<String,Object> result(){
return result(200,"","");
}
public Map<String,Object> result(Object data){
return result(200,"",data);
}
public Map<String,Object> result(int code,Object data){
return result(code,"",data);
}
public Map<String,Object> result(int code,String msg,Object data){
Map<String,Object> resutl=new HashMap<String,Object>();
resutl.put("code",code);
resutl.put("msg",msg);
resutl.put("data",data);
return resutl;
}
}
这是一个抽象类,里边有若干个result方法的重载。
所有的contrller都继承这个类,然后返回result方法的返回值:
@ResponseBody
@RequestMapping(value = "/denied",method = {RequestMethod.POST,RequestMethod.GET})
public Object denied(){
return result(200,"没有权限","");
}
这样,返回的信息就是一个基准的json信息了,客户端就可以根据这个格式进行解析。
现在,客户端与服务端链接的部分框架已经基本完成,并定义了双方共同约定的json格式,接下来就可以针对具体业务进行双方的开发了。
谢谢观看