论看源码的重要性,写给自己程序员的第二年

记:笔者于2018年6月毕业至今,即将进入程序员的转折点,在第二年到第三年。

之前写代码总是自己闷头造车,crud写的越来越多,但是真正的技术上的进步却很少看到,现在已经进入到半吊子时期,基本的业务需求都能写下来,但是真正遇到一些稀奇古怪的问题就感觉到半吊子深深的无力感。

出现的场景如下:

1、公司方面业务需要,整合用户,想实现单点登录,登出的方式,选用cas框架。

2、奇怪的bug ,原项目整合cas客户端后偶尔出现异常不被全局异常拦截,直接将堆栈信息打印在页面上的问题。

针对于场景一:

  公司原有的老项目接入cas,在项目中整合进cas客户端,却频繁出现无法单点登出的情况。采用的方式是重复去复现bug的场景,定位看是否有规律产生,就像我们知道的那样,机器是不会骗人的,经过多次的重复测试复现之后,将问题稳定复现出来。如下:系统A 和系统B  如果用户在系统A调用登出接口,则系统B必定登出,如果用户在系统B调用单点登出接口,则系统A有一定的概率下会登出失败。

  之前cas项目是公司的一个同事写的,现在该同事已经离职,新接手项目,只能闷头去看了。鉴于复现的场景的问题,询问了部署的场景,猜测是因为集群的问题导致的。项目A是采用集群的方式部署的,java的有两台机器,项目B是单机。初步猜测原因可能是在项目A登出的时候通知项目B,单机必然通知到。项目B通知项目A的时候偶发通知不到的情况。(为了能简单理解如此描述,实际的通知过程是:项目A -> cas ->项目B  或者 项目B -> cas ->项目A)。

  在网上搜寻解决方案的时候看到三种解决方案:(1)将集群项目A的配置改成ip hash,实际现在线上的方式是ip轮询的方式  (2)通知的时候采用ip广播的形式,将机器部署的ip广播出去,如果本台机器没有执行成功就转发给下一台机器。(3)重写session存储。

  以上是解决方案,但是关于为什么cas会出现这个问题,网上并没有很详细的解释。个人猜测是因为用来作为登录的标识是存放在内存中,并不是redis中,但是在接手项目时,同事明确告知标识是存储在redis中的,到redis库中看的情况确实是存储了,而且失效的时候,redis中这个key也没有值,但是在登出失败的情况下,redis中的这个值还是存在。在网上看博客的解决方案是,看到有一篇说到-cas本身就是不支持集群的,突然恍然大悟,去翻看源码中存储session标识的代码,如下:

public final class HashMapBackedSessionMappingStorage implements SessionMappingStorage {
    private final Map<String, HttpSession> MANAGED_SESSIONS = new HashMap();
    private final Map<String, String> ID_TO_SESSION_KEY_MAPPING = new HashMap();
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public HashMapBackedSessionMappingStorage() {
    }

    public synchronized void addSessionById(String mappingId, HttpSession session) {
        this.ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId);
        this.MANAGED_SESSIONS.put(mappingId, session);
    }
}

  可以看到 SessionMappingStorage 的实现类中 实际上存储session 和session 和mapping关系映射的内容是存储在map当中的,也就是写在内存当中。那么解决这个问题就不难了,重写这个方法的实现并添加到filter中,这两个关系都采用redis来存储。完结,撒花。网上也有人采用广播的形式的方法,也是很好的,但是对比来说实现成本较大,而且也不是从根本上来解决问题。对于采用ip hash的方式在测试服务器上搭建集群来看也是可行的,但是领导不允许线上采用ip hash   enenen   所以此路不通。

  总结看来源码也没那么可怕,找到思路和重点关注的问题点去看问题,解决方案就出来了。

针对于场景二:

  在写java项目的时候,我们通常都会在项目创建之初写一个全局异常处理类,将系统中由于代码错误或者其他问题产生的异常情况封装成code message的固定形式返回给前台页面。之前项目一直平稳的运行着,在引入cas客户端后,解决单点登出问题的时候出现情况:接口访问500,堆栈信息直接打印在前台。

  还是先将问题定位,之前怀疑是与异常类型有关,在登录接口是 ,票据验证不通过,登录失败的情况下打印出来是 TicketValidationException  还出现过 由于redis服务器挂掉出现: RedisConnectionFailureException。在本地模拟

@GetMapping("/test")
    public void test() throws Exception {
        throw new TicketValidationException("票据验证异常");
        // throw new RedisConnectionFailureException("redis连接异常");
    }

  两种异常均能被拦截,正常返回。至此,此路不通,跟异常类型一点原因都没有,去查找异常发生的位置,发现了如下代码:

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        if(!this.handlerInitialized.getAndSet(true)) {
            HANDLER.init();
        }

        if(HANDLER.process(request, response)) {
            filterChain.doFilter(servletRequest, servletResponse);// 发生异常的代码行
        }

    }

  好嘛,这还不简单,我来try catch你,  先catch下TicketValidationException  模拟线上发生的票据不通过的情况,然后一脸震惊,居然还是把堆栈信息打印在前台页面上,,,接着周五快下班了,大家都懂的,这个问题就被搁置了。回家后还是在想这个问题,打开电脑试了下  RedisConnectionFailureException  可以被捕获到,但是页面返回还是不正确的。在这里重写response后正常返回。重写response的方法如下:

public static void outResponse(HttpServletResponse response, Object object) {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter out = null;
        try {
            out = response.getWriter();
            out.append(JSONObject.toJSONString(object));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }

  redis连接异常解决了,那么票据验证失败异常呢?在catch了redis连接异常之后catch到 Exception大异常类,输出response 发现能够可以被拦截到。那为什么票据验证失败这个异常拦截不到呢,去看源码里的实现:

catch (TicketValidationException var8) {
    this.logger.debug(var8.getMessage(), var8);
    this.onFailedValidation(request, response);
    if (this.exceptionOnValidationFailure) {
      throw new ServletException(var8);
    }
    response.sendError(403, var8.getMessage());
    return;

  好家伙,他在这里抛出的居然是  ServletException 修改代码,发生票据异常的时候捕获的内容应该捕获  ServletException 至此完结。

  听到源码的时候就感觉它像个大老虎站在那里,但是认真的去研究他的时候,真的像个小猫咪一样温柔可亲。不排除确实有大佬写的代码晦涩难懂,但是毕竟都是人写出来的~,只要肯用心去读,肯定会有所收获。希望疫情赶快过去,2020大家都能被善待。

posted @ 2020-04-13 16:49  柒木木家  阅读(557)  评论(0编辑  收藏  举报