zoukankan      html  css  js  c++  java
  • 如果你想开发一个应用(1-14)

    之前在前端引用了axios,那么紧接着,后台要做如何的修改呢?直接返回html肯定是不对的,这时候,一个名为webapi的技术就出现了

    webapi

    webapi区别于普通的api,是指“使用http协议通过网络调用的API”即软件组织的外部接口。有时候也叫RESTful api,虽然他们实际上还是有一些区别的,但是基本上可以近似的理解他们是相同的,关于他们的定义,阮博写的还是非常的清晰。

    SpringMVC中的webapi

    在之前的程序中,我们返回的都是一个jsp模板的名字,然后框架自动去渲染这个jsp模板。但显然这个是不符合webapi的,那么我们想让他仅仅返回数据怎么办呢?这里介绍两个注解:ResponseBodyRestController,我们首先创建一个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的插件,所以暂时来说,安装它需要科学上网。

    安装方式:

    1. 点击chrome最右边的三个点
    2. 在弹出菜单中选择更多工具
    3. 在弹出菜单中选择扩展程序(图1)
    4. 然后在搜索店内应用中搜索postman(图2)
    5. 接着一直下一步即可

    图一

    图二

    如果安装完成后,多出一个类似应用程序的图标,因为经常使用我把他弄到了桌面的快捷方式,图标是这样的:

    3.PNG

    当看见这个火箭人的时候,就证明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内输入相应信息:

    4.PNG

    点击发送按钮,在body中可以看到已经自动格式化好的返回信息:

    5.PNG

    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里比较重要的两个部分:

    1. data节点:此页面所使用的数据模型,vue与普通的jq之类的框架最大的区别就是数据驱动,这一点一定要牢牢记住
    2. create节点:页面布局创建时执行,这里让它在页面创建时执行ajax

    运行,并在浏览器重输入http://localhost:8080/test/然后按f12,可以看到返回,哦 还有报错信息(警告不用理他,好多都是空格 tab 这类的问题):

    6.PNG

    这是一个跨域问题,在前后端分离开发的时候很常见的错误,在SpringMVC中解决这种问题主要有三种方法

    • 在Action上添加CrossOrigin注解
    • 在Controller上添加CrossOrigin注解

    这里我选择了第三种方法,因为只有一个人开发,所以很犯懒,一股脑的把跨域权限全部打开,在WebConfig类内覆盖addCorsMappings方法:

    public void addCorsMappings(CorsRegistry registry){
       registry.addMapping("/**");
    }
    

    重新运行一下tomcat服务器,并重新客户端测试:

    7.PNG

    可以完美访问。

    参数

    我们看到,他这时候还是在接收着两个参数,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>	
    

    代码复杂了写,但依然很清晰,但是输入值并提交之后,最终的界面如下:

    8.PNG

    很明显,客户端传送的username和password服务端并没有接受到,这是为什么呢?我们f12看一下浏览器的http协议头的传值部分:

    9.PNG

    可以看到,提交方式为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格式,接下来就可以针对具体业务进行双方的开发了。

    谢谢观看

  • 相关阅读:
    初识RabbitMQ
    ThreadPoolExecutor中execute和submit的区别
    MYSQL bin_log 开启及数据恢复
    MYSQL 悲观锁和乐观锁简单介绍及实现
    linux php多版本
    easyui汉化啊!
    虚化技术的额外开销
    拍脑袋空想不可能有创新
    大规模WEB服务技术
    xunsearch bsd 10.1安装心酸路。。。
  • 原文地址:https://www.cnblogs.com/jiangchao226/p/8205162.html
Copyright © 2011-2022 走看看