纯原生手写一个集转发,异常处理,参数转换,校验和鉴权为一体的简单请求转发器

78 阅读11分钟

1 基本转发器
为什么要有中央转发器
原先前端请求后端都是通过多个不同的servlet进行拦截操作,而随着业务的复杂,导致servlet数量过多,而且每个servlet都要继承HttpServlet进行重复性较高的工作,所以我们需要将HttpServlet抽离出来,让每个servlet变成普通的controller,专注与service交互不用过多关注req和resp,因此dispatchservlet就诞生了。
dispatchservlet的作用是拦截符合要求的前端请求,通过对路径的解析和对get发送的oper选择到对应的controller方法,这里用到Aop思想,通过对应的路径拼接成对应的controller,这里使用到了之前写ioc容器,先注册controller成bean对象,再根据名获取ioc容器中对应的bean实例,就可以通过反射操作controller,通过遍历controller的方法找到与请求的相同方法,获取该方法的参数列表,从请求中获取,如果是json对象,先对json进行操作。这里controller的方法类型均为String通过调用方法对返回的String处理,实现转发或者传入json

基本转发器的构建
首先让我们的DispatchServlet继承HttpServlet类实现service方法,通过获取RequestURI对字符串截取操作获取相应的beanName,再从ioc中获取对应的controllerBean

String requestURI = req.getRequestURI();
//        System.out.println(requestURI);
// 从路径中提取 controllerName 和 operName
// 从路径中提取 controllerName
String[] pathSegments = requestURI.split("/");
String controllerName = pathSegments[pathSegments.length - 1].split("\\.")[0] + "Controller";
//以后要getparam获取oper 为空默认为index
//        String operName = "test";
String operName = req.getParameter("oper");

try {

    MySpringAppAnnotationContext mySpringAppAnnotationContext = new MySpringAppAnnotationContext(BeanScan.class);
    Object beanController = mySpringAppAnnotationContext.getBean(controllerName);

获取到controller后,通过反射获取类中的所有方法并进行遍历,同时从request中获取oper对应的方法名,若方法名等于oper,就找到了对应方法的接口

req.setCharacterEncoding("UTF-8");
Method[] methods = beanController.getClass().getDeclaredMethods();
for (Method method : methods) {
    //获取方法名
    String methodName = method.getName();
    resp.setContentType("text/json;charset=utf-8");
    resp.setContentType("application/json");
    if (operName.equals(methodName) && authInterceptor.preHandle(req, resp, method)) {

接着invoke调用接口方法,获取string类型的返回值,根据返回值,进行相应的转发和重定向以及JSON的处理

//调用controller的方法
method.setAccessible(true);
//invoke返回的为obj
Object returnObj = method.invoke(beanController, parameterValues);
String methodReturnStr = (String) returnObj;
//                    System.out.println(methodReturnStr);
//对返回的Sting做判断

if (methodReturnStr.startsWith("redirect:")) {
    String redirectStr = methodReturnStr.substring("redirect:".length());
    resp.sendRedirect(redirectStr);
} else if (methodReturnStr.startsWith("forward:")) {
    String forwardStr = methodReturnStr.substring("forward:".length());
    //                        System.out.println(forwardStr);
    req.getRequestDispatcher(forwardStr).forward(req, resp);
} else if (JSON.isValid(methodReturnStr)) {
    //对Json数据处理  响应数据

    writer.write(methodReturnStr);


} else resp.getWriter().write(methodReturnStr);

注意此时的controller方法类型均为String

这样一个最简单的转发器就成了,仅仅起到转发功能

2 实现全局异常处理
想要实现一个基本的全局异常处理器很简单,先讲讲

为什么要全局异常处理器:
我觉得最重要一点就是装逼,哦不换一个词语...优雅!实现全局异常处理就是让别人第一眼看到你的代码就惊呼:这个异常处理的实在是优雅,太优雅了!!!
开个玩笑,我还是总结了几个挺有用优点:

  1. 简化代码对可能发生的异常进行操作:

通过实现全局异常处理,可以避免在每个方法中使用 try-catch 块来处理异常,简化代码编写,提高代码可读性和可维护性。

  1. 提高代码的健壮性,可维护性

全局异常处理能够捕获未被处理的异常,避免应用程序崩溃

  1. 统一异常逻辑处理,进行相应的操作:

全局异常处理能够将所有未被处理的异常集中到一个地方进行处理,避免代码中出现重复的异常处理逻辑, 提高代码的可重用性和可维护性,同时可以进行相应操作,例如输出日志信息等

  1. 避免出现大量堆栈报红,提高用户体验:

通过全局异常处理,可以在应用程序出现异常时给用户提供友好的错误提示,避免用户面对一堆堆的堆栈信 息,提高用户体验。

怎样实现呢:
我们首先肯定要知道实现原理是什么:
这里有一个上层代码和下层代码的概念,通常处理异常有两种方法,一个是直接trycatch,另一个就是向上抛出,这里的向上抛出是一个相对的概念,谁调用我谁就是上层代码,被谁调用,谁就是下层代码,如果将下层代码的异常都向上抛出给上层代码,那么下层代码中就不需要具体实现对异常处理逻辑,而是由上层代码统一处理,这就是实现简易全局异常处理器的基本原理,与spring中实现原理有所出入,但也无伤大雅,能实现相应的功能,就不一定要生搬硬套spring的做法,有一个较清晰的理解就行。
注意也不能什么异常都向上抛出,要根据具体代码逻辑选择合适的异常处理方式。

具体的实现原理:
这里并没有使用到动态代理,而是采用普通自定义异常类,可能后续会进行修改,但现在也能实现异常处理

首先定义一个异常处理类的接口

public interface ExceptionHandler {

    void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException;
}

接着 定义一个MyExceptionHandler实现这个接口

public class MyExceptionHandler implements ExceptionHandler {
    @Override
    public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (e instanceof MyException){
            response.getWriter().write(JSON.toJSONString(RespBean.error("发生异常:"+e.getMessage())));
            System.out.println("发生异常:"+e.getMessage());
            LoggerUtil.error("发生异常:"+e.getMessage());
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }else if (e instanceof NullPointerException) {
            response.getWriter().write(JSON.toJSONString(RespBean.error("发生空指针异常:" + e.getMessage())));
            System.out.println("发生空指针异常:" + e.getMessage());
            LoggerUtil.error("发生空指针异常:"+e.getMessage());
        } else if (e instanceof IllegalArgumentException) {
            response.getWriter().write(JSON.toJSONString(RespBean.error("发生非法参数异常:" + e.getMessage())));
            System.out.println("发生非法参数异常:" + e.getMessage());
            LoggerUtil.error("发生非法参数异常:"+e.getMessage());
        } else if (e instanceof ArrayIndexOutOfBoundsException) {
            response.getWriter().write(JSON.toJSONString(RespBean.error("发生数组下标越界异常:" + e.getMessage())));
            System.out.println("发生数组下标越界异常:" + e.getMessage());
            LoggerUtil.error("发生数组下标越界异常:"+e.getMessage());
        } else if (e instanceof ClassCastException) {
            response.getWriter().write(JSON.toJSONString(RespBean.error("发生类型转换异常:" + e.getMessage())));
            System.out.println("发生类型转换异常:" + e.getMessage());
            LoggerUtil.error("发生类型转换异常:"+e.getMessage());
        } else if (e instanceof IOException) {
            response.getWriter().write(JSON.toJSONString(RespBean.error("发生 IO 异常:" + e.getMessage())));
            System.out.println("发生 IO 异常:" + e.getMessage());
            LoggerUtil.error("发生 IO 异常:"+e.getMessage());
        } else {
            response.getWriter().write(JSON.toJSONString(RespBean.error("发生未知异常:" + e.getMessage())));
            System.out.println("发生未知异常:" + e.getMessage());
            LoggerUtil.error("发生未知异常:"+e.getMessage());
        }
    }
}

是的没错就是这么简单根据传入的exception类型判断,做出对应的异常处理操作,同时使用到之前写的日志类,并将异常信息写入前端页面

然后再顶层代码中进行try catch就OK了

} catch (Exception e) {
    new MyExceptionHandler().handleException(e, req, resp);
}

3 实现参数类型自动转换
引言:
基于原先写的中央转发器漏洞百出,现在一步一步进行优化,首先是最重要的参数问题,因为从前端传入的数据无论是get还是JSON都是字符串类型,如果controller中的方法参数为Integer之类,就需要进行参数转换,其实在controller中进行转换也行,但这样大大增加了代码的复杂性并使其更加臃肿,为了将参数转发方法抽离出,也是为了让controller更好的完成自己分内的事,因此需要在DispatchServlet中进行参数类型自动转换。

原理与步骤:
这里我使用的方法较为丑陋,因为我还没找到比较优雅的方法处理全部类型的参数转换,只能通过对应的参数名,找到对应参数转换的方法再分别进行转换,基本数据类型和引用数据类型比较简单,这里只给出较少的类型转换,仅作示例,

switch (typeName) {
    case "java.lang.Integer":
        parameterObj = Integer.parseInt(parameterValue);
        break;
    case "java.lang.String":
        if (parameterValue.length() == 1) {
            parameterObj = String.valueOf(parameterValue);
        } else {
            parameterObj = parameterValue;
        }
        break;
    case "int":
        parameterObj = Integer.parseInt(parameterValue);
        break;

重点是json类型的格式转换,这里我使用了RequestParam注解,用于加在controller方法参数上,以便中央转发器对注解进行检测,发现有该注解,就进行json对象处理,先readline再将json字符串转成对应的json对象,再通过aclass获取相应的对象。如果没有就说明整个都为该参数对象,就直接进行转换对象

Class<?> aClass = Class.forName(parameter.getType().getName());
//  Object newInstance = aClass.newInstance();
BufferedReader reader = req.getReader();
String readLine = reader.readLine();
//                                        System.out.println(readLine);
JSONObject jsonObject = JSON.parseObject(readLine);
if (jsonObject.getObject(parameterName, aClass) != null) {
    parameterObj = jsonObject.getObject(parameterName, aClass);
} else {
    parameterObj = jsonObject.toJavaObject(aClass);
}

再将这些转换后的参数统一放入参数数组,进行invoke调用

4 自动校验
引言:
参考JSR-303规范,完成相关注解包括但不限于@Not Null@NotEmpty@Range。注意这些校验均为后端校验,与前端无关。
以下是参数类型自动转换的几个优点:
1. 简化代码。使用自动类型转换,我们可以避免手动编写大量的类型转换代码,减少代码量。
2. 提高代码的可读性。手动进行类型转换时,代码可能会变得很冗长,难以理解。而使用自动类型转换,代码更加简洁清晰,易于阅读。
3. 减少错误。手动进行类型转换时,可能会出现类型不匹配、空指针异常等错误。而自动类型转换可以 减少这些错误,提高程序的健壮性。
4. 增强程序的灵活性。使用自动类型转换,程序的输入可以更加灵活,不需要预先知道参数的类型,可 以接受多种类型的输入,使程序更加通用。
总之,使用参数类型自动转换可以提高程序的可读性、可维护性和灵活性,同时减少代码中的错误,提高开 发效率。

代码解析:
首先我们需要自定义注解用来标记要进行校验的参数,这里注解均用在controller中

@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface NotEmpty {
        String message() default "字符串不能为空!";
    }

@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface NotNull {
        String message() default "值不能为空!";
    }


@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface Range {
        int min() default Integer.MIN_VALUE;
        int max() default Integer.MAX_VALUE;
        String message() default "值不在参数范围!";
    }

这里仅列举三个,可以满足大多数的校验,要想增加也行。

接下来我们要定义一个校验器的接口,以便所有的校验器都可以实现他

public interface Validator {

    boolean validate(Object value);
}

这里我们对应三个校验注解实现3个对应的校验器,原理很简单,布尔类型对传入的参数,进行相应的规则校验,符合就返回true符合就false

public class NotEmptyValidator implements Validator {
    @Override
    public boolean validate(Object value) {
        return !value.toString().isEmpty();
    }
}


@MyComponent
    public class NotNullValidator implements Validator {
        @Override
        public boolean validate(Object value) {
            return value!=null;
        }
    }



public class RangeValidator implements Validator {
    private final int min;
    private final int max;

    public RangeValidator(int min, int max) {
        this.min = min;
        this.max = max;
    }

    @Override
    public boolean validate(Object value) {
        if (value == null) {
            return false;
        }
        int intValue = (int) value;
        //属于这个范围就返回真
        return intValue >= min && intValue <= max;
    }

}

接着就是要在DispathServlet中配置校验器

else if (annotation instanceof NotNull) {
    //com.ysm.spring.dsVerification.annotation.NotNull+"Validator";
    NotNull notNull = parameter.getDeclaredAnnotation(NotNull.class);
    String validatorName = Utility.toLowerFirstChar(annotation.annotationType().getSimpleName()) + "Validator";
    Validator validator = (Validator) mySpringAppAnnotationContext.getBean(validatorName);
    //                                    boolean validateNotNull = validator.validate(parameterValue);
    //开始校验
    if (!validator.validate(parameterValue)) {
        RespBean error = RespBean.error(notNull.message());
        writer.write(JSON.toJSONString(error));
        writer.close();
    }


} else if (annotation instanceof NotEmpty) {
    String validatorName = Utility.toLowerFirstChar(annotation.annotationType().getSimpleName()) + "Validator";
    Validator validator = (Validator) mySpringAppAnnotationContext.getBean(validatorName);

    NotEmpty notEmpty = parameter.getDeclaredAnnotation(NotEmpty.class);
    //开始校验
    if (!validator.validate(parameterValue)) {
        RespBean error = RespBean.error(notEmpty.message());
        writer.write(JSON.toJSONString(error));
        writer.close();

    }

} else if (annotation instanceof Range) {
    //                                    String validatorName = Utility.toLowerFirstChar(annotation.annotationType().getSimpleName())+"Validator";
    //                                    RangeValidator validator =(RangeValidator) mySpringAppAnnotationContext.getBean(validatorName);
    Range range = parameter.getDeclaredAnnotation(Range.class);
    RangeValidator rangeValidator = new RangeValidator(range.min(), range.max());
    //                                    boolean validateRange = rangeValidator.validate(parameterValue);
    //开始校验
    if (!rangeValidator.validate(parameterValue)) {
        RespBean error = RespBean.error(range.message());
        writer.write(JSON.toJSONString(error));
        writer.close();
    }

先判断是否存在注解,然后获取注解的值,接着从ioc中拿到对应的校验器实例,并调用,如果不符合就直接相应前端错误信息,如果符合就继续。

实例
image.png

引言:
什么是权限鉴定,具体项目中,用户通常有不同的身份,不同身份对应的权限不同,例如管理员和普通用户,这就要对某些接口进行相应的权限鉴定,来防止权限不足的用户请求接口。
我总结了一下几个优点:
1.安全性
通过鉴定权限,可以确保项目中的敏感数据或高权限操作只能被授权的用户访问和调用。这可以有效地防止未经授权的用户获取并篡改,保障系统的安全性。例如只读读写等
2.规范性
接口权限鉴定可以保障数据的完整性。只有经过鉴定的用户才能访问系统中的数据
3.灵活性
通过接口权限鉴定,管理员可以根据需要对用户权限进行管理,根据实际情况灵活地控制用户的访问权限。

代码实现步骤:
老样子先定义自定义注解,这里定义的注解灵活性较高,要根据不同的业务需求进行调整

@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface AuthCheck {
        /**
* 是否需要登录验证
* @return null
*/
        String type() default "yes";
        String value() default "power=普通员工";
    }

在定义一个鉴定接口

public interface HandlerInterceptor {
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Method method) throws Exception;
    //    void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;
    //    void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}

接着定义一个AuthInterceptor实现这个接口

//权限拦截器
@MyComponent
    public class AuthInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Method method) throws Exception {
            HttpSession session = request.getSession();
            User user = (User) session.getAttribute("user");
            String sessionId = session.getId();
            Cookie cookie = new Cookie("JSESSIONID", sessionId);
            cookie.setSecure(true);
            response.addCookie(cookie);

            //        进行接口权限鉴定
            AuthCheck authCheck = method.getDeclaredAnnotation(AuthCheck.class);


            if (authCheck != null) {
                String type = authCheck.type();
                if ("no".equals(type)) {
                    return true;
                } else if ("yes".equals(type) && user == null) {
                    response.getWriter().write(JSON.toJSONString(RespBean.error("你尚未登录!")));
                    return false;
                }
                String checkStr = authCheck.value();
                String[] split = checkStr.split("=");
                String key = split[0];
                String value = split[1];
                if (value.contains(user.getPower())) {
                    return true;
                } else {
                    response.getWriter().write(JSON.toJSONString(RespBean.error("你的权限不足!")));
                    return false;
                }

            }
            return true;
        }
    }

首先从session中获取对应的user对象,如果没有并且注解标记不需要登录,就返回真,否则假,并且响应页面你尚未登录。如果不为空,而需要鉴定,就开始鉴定user的角色是否符合要求,符合就返回真,不符合就是假。

在dispatchServlet中运用很简单,在if条件判断中进行鉴定权限即可,返回真通过才能,进入到if内部,否则直接响应网页权限不足或者没有登录。
if (operName.equals(methodName) && authInterceptor.preHandle(req, resp, method))