乐趣区

关于java:JavaWebServlet经典回顾

前言

​ 不得不说 SpringMVC 的设计奇妙,如果应用 Servlet 原生自带的 API,光是办法转发就有够头疼麻烦的,间接看代码如下:

public class BaseServlet extends HttpServlet {
    private static final long serialVersionUID = -68576590380714085L;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            // 获取申请参数 action 的值
            String action = request.getParameter("action");
            System.out.println("action:" + action);
            // 通过 action 值,反射以后对象调用以该值命名的办法
            if (action == null || action.length() == 0) {Result result = new Result(false, "短少 action 参数");
                printResult(response, result);
                return;
            }
            Class<?> clazz = this.getClass();
            Method method = clazz.getMethod(action, HttpServletRequest.class, HttpServletResponse.class);
            method.invoke(this, request, response);
        } catch (Exception ex) {ex.printStackTrace();
            Result result = new Result(false, "action 参数不正确,未匹配到 web 办法");
            printResult(response, result);
        }
    }

    /**
     * 公共办法
     * 输入 JSON 到客户端
     * @param resp
     * @param result
     * @throws ServletException
     * @throws IOException
     */
    protected void printResult(HttpServletResponse resp, Result result) throws ServletException, IOException {resp.setContentType("application/json;charset=utf-8");
        JSON.writeJSONString(resp.getWriter(), result);
    }
}

1.0 版本:定义父类 Servlet,所有子 Servlet 集成父类,依据办法参数进行具体子 Servlet 的办法调用,弊病的话,1:每个 servlet 都要继承,2:办法名反复问题等等,在此角度上咱们进行本人的一个简化版本的 MVC 框架的实现,基于注解和反射进行;

Web.xml 配置

这里配置次要是配置一个总控制器,须要留神 url-pattern 匹配规定;

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>com.itheima.study.mvc.controller.CenterServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

注解自定义

注解次要是两个注解:

Controller:表明须要组件类

RequestMapping:办法映射门路

Controller:

package com.itheima.study.mvc.anno;

import java.lang.annotation.*;

/**
 * 模拟 springmvc RequestMapping
 * @author lijie
 * @date 2020-08-01
 * @version v1.0.0
 */
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {

    /** DESC:映射门路 */
    String value() default "";}

RequestMapping:

package com.itheima.study.mvc.anno;

import java.lang.annotation.*;

/**
 * 模拟 springmvc RequestMapping
 * @author lijie
 * @date 2020-08-01
 * @version v1.0.0
 */
@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {

    /** DESC:映射门路 */
    String value();}

扫描包工具

public final class ClassScanner {private ClassScanner() { }

    /**
     * 取得包上面的所有的 class
     * @param
     * @return List 蕴含所有 class 的实例
     */
    public static List<Class<?>> getClasssFromPackage(String packageName) {List<Class<?>> clazzs = new ArrayList<>();
        // 是否循环搜寻子包
        boolean recursive = true;
        // 包名对应的门路名称
        String packageDirName = packageName.replace('.', '/');
        Enumeration<URL> dirs;

        try {dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
            while (dirs.hasMoreElements()) {URL url = dirs.nextElement();
                String protocol = url.getProtocol();
                if ("file".equals(protocol)) {String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    findClassInPackageByFile(packageName, filePath, recursive, clazzs);
                }
            }

        } catch (Exception e) {e.printStackTrace();
        }
        return clazzs;
    }

    /**
     * 在 package 对应的门路下找到所有的 class
     */
    private static void findClassInPackageByFile(String packageName, String filePath, final boolean recursive, List<Class<?>> clazzs) {File dir = new File(filePath);
        if (!dir.exists() || !dir.isDirectory()) {return;}
        // 在给定的目录下找到所有的文件,并且进行条件过滤
        File[] dirFiles = dir.listFiles(new FileFilter() {

            @Override
            public boolean accept(File file) {boolean acceptDir = recursive && file.isDirectory();// 承受 dir 目录
                boolean acceptClass = file.getName().endsWith("class");// 承受 class 文件
                return acceptDir || acceptClass;
            }
        });

        for (File file : dirFiles) {if (file.isDirectory()) {findClassInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, clazzs);
            } else {String className = file.getName().substring(0, file.getName().length() - 6);
                try {clazzs.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + "." + className));
                } catch (Exception e) {e.printStackTrace();
                }
            }
        }
    }
}

自定义实现

思路:

  1. 扫描具体包下的所有类
  2. 判断是否为 Controller 标识的类
  3. 判断办法是否蕴含 RequestMapping 注解类
  4. 匹配对应的 映射门路,执行对应办法

拓展:

  1. 对匹配门路的欠缺
  2. Tomcat 启动的时候将类扫描加载进一个全局容器,缩小每次申请时匹配的性能耗费

拓展临时就想到这些,前期有工夫能够进行深层次的批改;

/**
 * TODO
 * Created with IntelliJ IDEA.
 * @author lijie
 * @date 2020-08-01
 * @version v1.0.0
 */
public class CenterServlet extends HttpServlet {

    private static final long serialVersionUID = -7316297883149893301L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String contextPath = req.getContextPath();
        String requestPath = req.getServletPath();
        System.out.println("全文门路:contextPath -->" + contextPath);
        System.out.println("申请门路:requestPath -->" + requestPath);

        List<Class<?>> classList = ClassScanner.getClasssFromPackage("com.study.mvc.controller");
        for (Class<?> item : classList) {
            // 不蕴含 Controller 注解则不持续进行
            boolean isController = item.isAnnotationPresent(Controller.class);
            if (!isController) {continue;}
            // 类办法为空不继续执行
            Method[] methods = item.getMethods();
            if (methods == null || methods.length <= 0) {continue;}

            // 遍历类办法,获取映射注解,判断办法是否存在注解;Object instance = null;
            for (Method method : methods) {boolean annotationPresent = method.isAnnotationPresent(RequestMapping.class);
                if (annotationPresent) {String value = method.getAnnotation(RequestMapping.class).value();
                    if (Objects.equals(value, requestPath)) {
                        // 提早加载避免结构无意义对象
                        if (instance == null) {
                            try {instance = item.newInstance();
                            } catch (InstantiationException | IllegalAccessException e) {System.out.println("实例化对象产生异样:");
                                e.printStackTrace();}
                        }
                        // 执行办法
                        try {method.invoke(instance, req, resp);
                        } catch (IllegalAccessException | InvocationTargetException e) {System.out.println("办法调用产生了异样:");
                            e.printStackTrace();}
                    }
                }

            }
        }
        ResponseUtils.printResult(resp, new Result(false, "申请门路有误!!"));
    }
}

增强实现 – 容器治理

为了更近一步模拟 SpringMVC,这次增强了容器治理,在 Tomcat 启动的时候将门路、办法、以及 Controller 加载进去容器中;以便于后续的复用

1】增加配置文件

mvc.base.package=com.study.mvc.controller

2】定义全局的容器

/**
 * Created with IntelliJ IDEA.
 * @author lijie
 * @date 2020-08-01
 * @version v1.0.0
 */
public class ContextUtils {
    /**
     * Servlet 启动时存储对应的 @Contoller 的 ClassBean
     */
    public static final Map<String, Object> BEAN_MAP = new HashMap<>();

    /**
     * Servlet 启动时存储对应的 @RequestMapping 的门路和对应的办法
     */
    public static final Map<String, Method> URL_MAP = new HashMap<>();}

3】增加监听器

/**
 * Servlet 启动监听类
 * Created with IntelliJ IDEA.
 * @author lijie
 * @date 2020-08-01
 * @version v1.0.0
 */
@WebListener("全局监听器")
public class ContextListener implements ServletContextListener {
    /** DESC:读取配置文件,加载配置文件 */
    private static final Properties PROPERTIES = new Properties();
    static {InputStream is = ContextListener.class.getClassLoader().getResourceAsStream("app.properties");
        try {PROPERTIES.load(is);
        } catch (IOException e) {System.out.println("类初始化资源谬误!");
        }
    }

    @Override
    public void contextInitialized(ServletContextEvent sce) {System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@ 容器初始化开始 @@@@@@@@@@@@@@@@@@@@@@@@@@@");
        String basePakcage = PROPERTIES.getProperty("mvc.base.package");
        List<Class<?>> classList = HmClassScanner.getClasssFromPackage(basePakcage);
        for (Class<?> classItem : classList) {
            // 不蕴含 Controller 注解则不持续进行
            boolean isController = classItem.isAnnotationPresent(Controller.class);
            if (!isController) {continue;}
            // 类办法为空不继续执行
            Method[] methods = classItem.getMethods();
            if (methods == null || methods.length <= 0) {continue;}

            // 存储 Controller Bean
            String finalControllerValue = null;
            String controllerValue = classItem.getAnnotation(Controller.class).value();
            if (controllerValue.isEmpty() || Objects.equals(controllerValue, "")) {finalControllerValue = classItem.getSimpleName();
            } else {finalControllerValue = controllerValue;}

            // 判断是否存在反复的 Bean;否则存入 BEAN 容器中
            Object o = ContextUtils.BEAN_MAP.get(finalControllerValue);
            if (o != null) {throw new RuntimeException(finalControllerValue + "---> 存在反复的 Bean");
            }
            try {ContextUtils.BEAN_MAP.put(finalControllerValue, classItem.newInstance());
            } catch (InstantiationException | IllegalAccessException e) {System.out.println("实例化对象谬误 ---- >" + e.getMessage());
            }

            // 将所有门路注册在门路容器中
            for (Method method : methods) {boolean annotationPresent = method.isAnnotationPresent(RequestMapping.class);
                if (!annotationPresent) {continue;}
                String value = method.getAnnotation(RequestMapping.class).value();
                if (value == null || value.isEmpty() || Objects.equals(value, "")) {continue;}
                ContextUtils.URL_MAP.put(value, method);
            }
        }

        System.out.println("==================== Bean ====================");
        for (Map.Entry<String, Object> bean : ContextUtils.BEAN_MAP.entrySet()) {String key = bean.getKey();
            Object value = bean.getValue();
            System.out.println(key + "---->" + value);
        }
        System.out.println("==================== Bean ====================\n");

        System.out.println("==================== URL ====================");
        for (Map.Entry<String, Method> url : ContextUtils.URL_MAP.entrySet()) {String key = url.getKey();
            Object value = url.getValue();
            System.out.println(key + "---->" + value);
        }
        System.out.println("==================== URL ====================\n");

        System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@ 容器初始化胜利 @@@@@@@@@@@@@@@@@@@@@@@@@@@");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@ 容器销毁中 @@@@@@@@@@@@@@@@@@@@@@@@@@@");
        ContextUtils.URL_MAP.clear();
        ContextUtils.BEAN_MAP.clear();
        System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@ 容器销毁胜利 @@@@@@@@@@@@@@@@@@@@@@@@@@@");
    }
}

4】批改总控制器

/**
 * TODO
 * Created with IntelliJ IDEA.
 * @author lijie
 * @date 2020-08-01
 * @version v1.0.0
 */
public class CenterServlet extends HttpServlet {

    private static final long serialVersionUID = -7316297883149893301L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String contextPath = req.getContextPath();
        String requestPath = req.getServletPath();
        System.out.println("全文门路:contextPath -->" + contextPath);
        System.out.println("申请门路:requestPath -->" + requestPath);

        // 获取办法名称
        Method method = ContextUtils.URL_MAP.get(requestPath);
        if (method == null) {ResponseUtils.printResult(resp, new Result(false, "申请门路有误!!"));
            return;
        }

        // 获取改办法所在类的注解名称,获取类实例
        String beanFinalKey = null;
        Class<?> declaringClass = method.getDeclaringClass();
        String controllerValue = declaringClass.getAnnotation(Controller.class).value();
        if (controllerValue.isEmpty() || Objects.equals(controllerValue, "")) {beanFinalKey = declaringClass.getSimpleName();
        } else {beanFinalKey = controllerValue;}
        Object o = ContextUtils.BEAN_MAP.get(beanFinalKey);

        // 通过实例调用办法
        try {method.invoke(o, req, resp);
        } catch (IllegalAccessException | InvocationTargetException e) {e.printStackTrace();
        }
    }
}

5】增加测试方法:

@Controller
public class TestCourseController {@RequestMapping("/test/add")
    public void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("获取客户端申请...");
        System.out.println("调用 Service... 实现增加");
        System.out.println("响应客户端...");
        Result result = new Result(true, "增加学科胜利");
        ResponseUtils.printResult(resp, result);
    }

    // 删除学科
    public void delete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取前端申请数据
        System.out.println("获取客户端申请...");
        System.out.println("调用 Service... 实现删除");
        System.out.println("响应客户端...");
        Result result = new Result(true, "删除学科胜利");
        // super.printResult(resp, result);
    }

    // 更新学科
    public void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取前端申请数据
        System.out.println("获取客户端申请...");
        System.out.println("调用 Service... 实现更新");
        System.out.println("响应客户端...");
        Result result = new Result(true, "更新学科胜利");
        // super.printResult(resp, result);
    }

    // 查问学科
    public void query(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取前端申请数据
        System.out.println("获取客户端申请...");
        System.out.println("调用 Service... 实现查问");
        System.out.println("响应客户端...");
        CourseService courseService = new CourseServiceImpl();
        Result result = new Result(true, "查问学科胜利", courseService.findAll());
        // super.printResult(resp, result);
    }
}


完整版自定义 MVC 思路

思路:

筹备工作:

  1. 提供一个工具类,扫描某个包下所有的字节码文件类;
  2. 编写注解、@RequestMapping、@Compenent,用来形容类和办法门路;
  3. 编写注解、@AutoSetter,用来进行主动注入;

正式工作:

  1. 编写配置文件,动静配置包门路;
  2. 监听器:Servlet 启动的时候创立,依据配置文件扫描包下的字节码文件

    • 调用工具类办法扫描字节码文件,获取容器实例,注册到 ServetContext 域中,实例过程:

      1】将所有 @Compenent 的类信息,类实例,注册到 Map 的 Bean 容器中,同时单例对立治理

      2】将所有 @Compenent 类下,带有 @RequestMapping 的办法进行解析 value 的地址值,注册办法到 Method 容器中

      3】将所有 @Compenent 类下,带有 @AutoSetter 下的字段值获取 Bean 容器的实例化对象反射注入赋值

  3. 过滤器:在申请达到具体的办法前对申请体、响应体进行对立的 UTF- 8 编码操作
  4. 顶级 Servlet(dispartherServlet):

    • 1】Servlet 初始化操作,获取容器对象;
    • 2】客户端申请发送解决流程:

      获取申请门路,req.getServletPath();{残缺门路:req.getRequestURL(), 带 contextPath 门路:req.getRequestURI()}

      通过申请门路获取容器中的办法,并应用反射技术执行改办法

实现:

[这边应用原生 Sevlet3.0 应用全注解的形式代替,Web.xml]()

①:注解定义

这里三个注解定义模拟 SpringMVC

  • @AutoSetter :简化版本的注入注解
  • @Component:申明组件
  • @RequestMapping:映射办法注册
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoSetter {String value();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {String value() default "";
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {String value();
}

②:定义 Bean

见到名识意,别离保留形容组件对象以及映射门路所在的办法;

@Data
@AllArgsConstructor
public class ComponentBean {
    /** Desced -- Bean 在容器中的 id */
    private String id;
    /** Desced -- Bean 的全限定名称 */
    private String className;//
    /** Desced -- Bean 的类的字节码对象信息 */
    private Class<?> clazz;
    /** Desced -- Bean 的实例对象 */
    private Object instance;
}
@Data
@AllArgsConstructor
public class ComponentMethod {
    /** DESC:以后对象某一办法实例 */
    private Method method;
    /** DESC:以后对象类的字节码实例 */
    private Class<?>  clazz;
    /** DESC:对象 */
    private Object instance;
}

③:常量类定义

public class AppConst {
    /**
     * 存储 ApplicationContext 的 Key
     **/
    public static final String APPLICATION_CONTEXT             = "ApplicationContext";
    /**
     * 资源文件名称
     */
    public static final String APPLICATION_CONFIG_NAME         = "application.properties";
    /**
     * 属性名
     **/
    public static final String APPLICATION_CONFIG_PACKAGE_PROP = "mvc.base.package";
}

④:ApplicationContext 容器外围类

  1. 传入配置文件名称,在结构的时候初始化 methodMaps、beanMaps 保留 Bean 和映射办法;
  2. initBean 办法次要是将所有的 @component 注解标注进行实例化,并且保留到 BeanMap 容器中,底层应用 ConcurrentHashMap;
  3. initBeanField 次要对所有 须要注入的成员变量进行注入,并且将映射门路所在的办法注入到 MethodMap 中
/**
 * @author:seanyang
 * @date:Created in 2019/8/9
 * @description:利用上下文
 *  存储注解类的实例
 *  存储注解办法的实例
 * @version: 1.0
 */
@Slf4j
@Getter
public class ApplicationContext {
    private static Properties properties;

    /** DESC:定义容器,解析办法注解,存储映射地址与办法实例 */
    private final Map<String, ComponentMethod> methodMaps;

    /** DESC:定义容器,解析类注解,存储 bean 实例 */
    private final Map<String, ComponentBean> beanMaps;


    /**
     * Desced: 初始化容器  <br>
     * @param path 配置文件门路
     * @author lijie
     * @date 2020/8/6 22:27
     */
    public ApplicationContext(String path) throws AppException {log.debug("path:{}", path);
        // 创立容器对象、加载配置文件(约定配置文件寄存在类加载门路下)
        beanMaps   = new ConcurrentHashMap<>();
        methodMaps = new ConcurrentHashMap<>();
        InputStream is = ApplicationContext.class.getClassLoader().getResourceAsStream(path);
        // 初始化 Bean、进行主动注入
        try {this.initBean(is);
            this.initBeanField();} catch (Exception e) {e.printStackTrace();
            throw new AppException("初始化上下文失败," + e.getMessage());
        }

    }


    /**
     * Desced: 依据资源加载字节码文件并进行注册  <br>
     * @param resource  资源文件
     * @author lijie
     * @date 2020/8/6 22:29
     */
    private void initBean(InputStream resource) throws Exception {
        // 取得 component-scan 标签的根本包名称
        if (resource == null) {throw new AppException("本地配置文件资源为空!");
        }
        // 资源加载
        Properties properties = new Properties();
        properties.load(resource);
        String pakcageName = properties.getProperty(AppConst.APPLICATION_CONFIG_PACKAGE_PROP);
        if (pakcageName == null) {return;}

        // 获取字节码文件,注册应用了 @Component 的 Bean
        List<Class<?>> classsFromPackage = ClassScannerUtils.getClasssFromPackage(pakcageName);
        if (null != classsFromPackage && classsFromPackage.size() > 0) {for (Class<?> aClass : classsFromPackage) {
                // 判断是否应用的 @HmComponent 注解
                if (aClass.isAnnotationPresent(Component.class)) {
                    // 取得该类上的注解对象
                    Component component = aClass.getAnnotation(Component.class);
                    // 判断属性是否赋值 如果 Component 没有值 就赋值为以后类名
                    String beanId = "".equals(component.value()) ? aClass.getSimpleName() : component.value();
                    // 创立 BeanProperty 存储到 beanMaps 中
                    ComponentBean myBean = new ComponentBean(beanId, aClass.getName(), aClass, aClass.newInstance());
                    this.beanMaps.put(beanId, myBean);
                }
            }
        }
    }

    /**
     * 读取类成员属性注解,并初始化注解
     * @throws Exception
     */
    private void initBeanField() throws Exception {if (this.beanMaps == null || this.beanMaps.size() == 0) {return;}
        for (Map.Entry<String, ComponentBean> entry : this.beanMaps.entrySet()) {ComponentBean bean = entry.getValue();
            Object instance = bean.getInstance();
            Class<?> clazz = bean.getClazz();
            Field[] declaredFields = clazz.getDeclaredFields();
            // 成员实例进行主动注入
            if (declaredFields != null && declaredFields.length > 0) {for (Field declaredField : declaredFields) {if (declaredField.isAnnotationPresent(AutoSetter.class)) {String injectionBeanId = declaredField.getAnnotation(AutoSetter.class).value();
                        Object injectionBean = this.beanMaps.get(injectionBeanId).getInstance();
                        declaredField.setAccessible(true);
                        declaredField.set(instance, injectionBean);
                    }
                }
            }
            // 注册映射门路
            Method[] methods = clazz.getMethods();
            if (methods != null && methods.length > 0) {for (Method method : methods) {if (method.isAnnotationPresent(RequestMapping.class)) {String requestPath = method.getAnnotation(RequestMapping.class).value();
                        ComponentMethod cMethod = new ComponentMethod(method, clazz, instance);
                        methodMaps.put(requestPath, cMethod);
                    }
                }
            }
        }
    }
}

⑤:上下文监听器:容器什么时候结构注册?Tomcat 启动的时候,就进行结构注入;

/**
 * @author:seanyang
 * @date:Created in 2019/8/9
 * @description:容器上下文监听器
 * 负责加载配置文件
 * 依据配置文件加载资源
 * @version: 1.0
 */
@Slf4j
@WebListener("ContextLoaderListener")
public class ContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {log.debug("ContextLoaderListener contextInitialized......");
        // 取得以后 ServletContext,获取配置初始化配置文件门路参数
        ServletContext servletContext = servletContextEvent.getServletContext();
        try {
            // 创立容器对象,将容器对象存储在 servletContext 域中
            ApplicationContext applicationContext = new ApplicationContext(AppConst.APPLICATION_CONFIG_NAME);
            servletContext.setAttribute(AppConst.APPLICATION_CONTEXT, applicationContext);
        } catch (Exception e) {e.printStackTrace();
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {}}

⑥:外围转发器 DispatherServlet:

次要做了两件事:

  1. 拦挡所有 .do 结尾的申请
  2. 依据申请的门路,从 MethodMap 容器中获取办法以及对象实例进行执行转发,并将后果用 application/json;charset=utf-8 返回;
/**
 * @author:lijie
 * @description:申请转发控制器,负责把所有客户端的申请门路,转发调用对应的控制器类实例的具体方法
 * @version: 1.0
 */
@WebServlet(
        name = "contextServlet",
        urlPatterns = "*.do",
        loadOnStartup = 1,
        description = "外围控制器",
        displayName = "My MVC ContextServlet"
)
public class DispatherServlet extends HttpServlet {
    private static final long serialVersionUID = 6091161103788682549L;

    // 读取上下文信息
    private ApplicationContext applicationContext;

    @Override
    public void init(ServletConfig config) throws ServletException {super.init(config);
        ServletContext servletContext = config.getServletContext();
        applicationContext = (ApplicationContext) servletContext.getAttribute(AppConst.APPLICATION_CONTEXT);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {super.doGet(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 解析申请地址 例如:/mm/xxx.do
        String servletPath = req.getServletPath();
        if (servletPath.endsWith("/")) {servletPath = servletPath.substring(0, servletPath.lastIndexOf("/"));
        }
        int lastIndex = servletPath.indexOf(".do");
        if (lastIndex != -1) {servletPath = servletPath.substring(0, lastIndex);
        }
        String mappingPath = servletPath;

        // 依据门路,找到 HmMethod 对象,并调用控制器的办法
        JSONObject object = new JSONObject();
        object.put("flag", false);
        ComponentMethod cMethod = applicationContext.getMethodMaps().get(mappingPath);
        if (Objects.nonNull(cMethod)) {
            // 取出办法资源进行执行
            Method method = cMethod.getMethod();
            try {Object invoke = method.invoke(cMethod.getInstance(), req, resp);
                this.printResult(resp, invoke);
            } catch (Exception e) {object.put("message", e.getMessage());
                this.printResult(resp, object);
            }
        } else {object.put("message", "申请门路有误,mappingPath =" + mappingPath);
            this.printResult(resp, object);
        }
    }

    private void printResult(HttpServletResponse response, Object obj) throws IOException {response.reset();
        response.setContentType("application/json;charset=utf-8");
        JSON.writeJSONString(response.getWriter(), obj);
    }
}

⑦:附加 EncodingFilter,字符串编码过滤器:

/**
 * 字符集过滤器
 * 对立解决申请与响应字符集
 * 默认 utf-8
 */
@WebFilter(
        description = "",
        filterName = "characterEncodingFilter",
        urlPatterns = "/*",
        initParams = {@WebInitParam(name = "encoding", value = "UTF-8")}
)
public class EncodingFilter implements Filter {

    /** DESC:定义变量存储编码 */
    private String encoding;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {encoding = filterConfig.getInitParameter("encoding") == null ? encoding : filterConfig.getInitParameter("encoding");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {servletRequest.setCharacterEncoding(encoding);
        servletResponse.setContentType("text/html;charset=" + encoding);
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {}
}

问题:

问题 1:控制器门路问题

解决方案①:拦挡*.do、*.htm,例如:/user/add.do,这是最传统的形式,最简略也最实用。不会导致动态文件(jpg,js,css)被拦挡。

解决方案②:拦挡 /,例如:/user/add,能够实现当初很风行的 REST 格调。很多互联网类型的利用很喜爱这种格调的 URL;弊病:会导致动态文件(jpg,js,css)被拦挡后不能失常显示。想实现 REST 格调,事件就是麻烦一些。

解决方案③:拦挡 /*,这是一个谬误的形式,匹配任意前缀,申请能够走到 Action 中,但转到 jsp 时再次被拦挡,不能拜访到 jsp。

问题 2:Transient 关键字和 @Transient 注解

1、Java 语言的关键字,变量修饰符,如果用 transient 申明一个实例变量,当对象存储时,它的值不须要维持。换句话来说就是,用 transient 关键字标记的成员变量不参加序列化过程。

2、Hibernate 注解,实体类中应用了 @Table 注解后,想要增加表中不存在的字段,就要应用 @Transient 这个注解了。应用 @Transient 示意该属性并非是一个要映射到数据库表中的字段, 只是起辅助作用.ORM 框架将会疏忽该属性

问题 3:Lombok 注解 @Builder 踩坑

   // 如果对象为空, 并且有进行成员变量的初始化赋值,会产生 NPE;// 如果没有进行成员变量的初始化赋值则不会
   Dict build = null;
   if (build == null) {build = Dict.builder().build();}
   System.out.println(build == null);
   System.out.println(build);

增加自定义权限校验器

如果通过了下面的自定义 MVC 思路的实现的话,其实对这种权限校验的自定义实现也并不是特地的难。首先咱们须要一个根底的 RBAC 权限模型,如下:

用户表 <-> 角色表 === 用户角色表

角色表 <-> 权限表 === 角色权限表

①:用户的革新,在用户登录后查问出用户所领有的所有权限

    @Override
    public Result<User> login(User param) {String username = param.getUsername();
        Assert.notBlank(username, "用户名为空");
        String password = param.getPassword();
        Assert.notBlank(password, "明码为空");
        User user = this.findByUserName(param.getUsername());
        if (user == null) {return Result.faild("登录失败,用户不存在!");
        }
        if (!Objects.equals(param.getPassword(), user.getPassword())) {return Result.faild("登录失败,账号或者明码不正确!");
        }
        // 查问用户的权限
        SqlSession   session    = super.getSession();
        UserMapper   dao        = super.getDao(session, UserMapper.class);
        List<String> permission = dao.selectUserPermission(user.getId());
        if (CollUtil.isNotEmpty(permission)) {List<String> collect = permission.stream().distinct().collect(Collectors.toList());
            user.setAuthorityList(collect);
        }
        super.closeSession(session);
        return Result.success(user);
    }

这里为了简略起见,就一条 SQL 关联查问出了用户权限,因为用户和角色是多对多的关系,一个用户多个角色,角色和权限也是多对多的关系,一个角色多个权限,那么间接的一个用户也是领有多个权限的,x = y 那么 y = x;反着站在用户独自立场想,一个用户有多个角色(一对多),一个角色有多个权限;

-- 语句 1(程序保障用户的存在)SELECT tp.keyword 
FROM t_permission tp
LEFT JOIN tr_role_permission trp ON trp.permission_id = tp.id
LEFT JOIN t_role tr ON tr.id = trp.role_id
LEFT JOIN tr_user_role tur ON tur.role_id = tr.id
WHERE tur.user_id = #{userId}

-- 语句 2 
SELECT tp.* FROM t_role tr 
LEFT JOIN tr_user_role tur ON tur.role_id = tr.id 
LEFT JOIN tr_role_permission trp ON trp.role_id = tr.id
LEFT JOIN t_permission tp ON tp.id = trp.permission_id
WHERE tur.user_id = 1

-- 语句 3(通过语句保障用户的确关联存在)SELECT t.*,tr.*,tp.* FROM t_user t
LEFT JOIN tr_user_role tur ON tur.user_id = t.id         -- 关联以后用户的所有角色
LEFT JOIN t_role tr ON tr.id = tur.role_id                 -- 关联以后角色的所有一一对应的信息
LEFT JOIN tr_role_permission trp ON trp.role_id = tr.id -- 关联角色对应的所有权限信息
LEFT JOIN t_permission tp ON tp.id = trp.permission_id  -- 关联每个角色一一对应的权限信息
WHERE t.id = 1

②:定义本人的权限注解

/**
 * Desc:受权注解
 * @author lijie
 * @date 2020-08-07
 * @version v1.0.0
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HasPerm {String value() default "";
}

③:封装权限 Bean

@Data
@AllArgsConstructor
public class ComponentPerm {
    /**
     * Desc: 权限
     */
    private String          permission;
    /**
     * Desc: 权限领有的办法
     */
    private ComponentMethod componentMethod;
}

④:定义 SecurityContext 容器组件

@Slf4j
@Getter
public class SecurityContext {

    /**
     * Desc: 权限 Map
     */
    private final Map<String, ComponentPerm> permMaps;


    /**
     * Desc: 初始化平安容器  <br>
     * @param context
     * @return null
     * @author lijie
     * @date 2020/8/7 13:42
     */
    public SecurityContext(ApplicationContext context) throws AppException {log.info("context --> {}", context);
        permMaps = new ConcurrentHashMap<>();
        if (context == null) {throw new AppException("初始化权限容器失败,ApplicationContext is null!");
        }

        try {this.initSecurityBean(context);
        } catch (Exception e) {e.printStackTrace();
            throw new AppException("初始化权限容器失败" + e.getMessage());
        }
    }

    /**
     * Desc: 初始化 SecurityBean  <br>
     * @param context
     * @return void
     * @author lijie
     * @date 2020/8/7 13:47
     */
    private void initSecurityBean(ApplicationContext context) {
        // 判断 MethodMaps 中是否蕴含数据,权限注解肯定是应用再映射注解之上的
        Map<String, ComponentMethod> methodMaps = context.getMethodMaps();
        if (methodMaps == null && methodMaps.size() < 0) {return;}
        for (Map.Entry<String, ComponentMethod> entry : methodMaps.entrySet()) {ComponentMethod componentMethod = entry.getValue();
            Method          originMethod    = componentMethod.getMethod();
            // 注册 Bean
            if (originMethod.isAnnotationPresent(HasPerm.class) && originMethod.isAnnotationPresent(RequestMapping.class)) {String reqUrl = originMethod.getAnnotation(RequestMapping.class).value();
                if (reqUrl == null || reqUrl.trim().isEmpty()) {return;}
                String value  = originMethod.getAnnotation(HasPerm.class).value();
                if (value == null || value.trim().isEmpty()) {return;}
                ComponentPerm componentPerm = new ComponentPerm(value, componentMethod);
                permMaps.put(reqUrl, componentPerm);
            }
        }
    }
}

⑤:外围过滤器的定义

  1. Filter 初始化的时候先把 SecurityContext 容器注册到 ServletContext 中
  2. 每次申请通过 Security 容器获取门路,如果不存在门路的办法阐明没注解,间接放行。
  3. 存在的话须要去判断会话、用户、权限
/**
 * Desc:权限控制器
 * @author lijie
 * @date 2020-08-07
 * @version v1.0.0
 */
@Slf4j
@WebFilter(filterName = "PermissionFilter", urlPatterns = "/*")
public class PermissionFilter extends BaseSecurityFilter{

    @Getter
    private SecurityContext securityContext;

    @Getter
    private ApplicationContext appContext;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {ServletContext servletContext = filterConfig.getServletContext();
        String         contextPath    = servletContext.getContextPath();
        log.info("contextPath --> {}", contextPath);
        // 初始化后的办法存入全局域中
        appContext = (ApplicationContext) servletContext.getAttribute(AppConst.APPLICATION_CONTEXT);
        try {securityContext = new SecurityContext(appContext);
            servletContext.setAttribute(AppSecurityConst.APPLICATION_CONTEXT_SECURITYCONST, securityContext);
        } catch (AppException e) {throw new ServletException(e.getMessage());
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest  req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        // 获取申请地址
        String servletPath = req.getServletPath();
        if (servletPath.endsWith("/")) {servletPath = servletPath.substring(0, servletPath.lastIndexOf("/"));
        }
        if (servletPath.endsWith(".do")) {servletPath = servletPath.substring(0, servletPath.indexOf(".do"));
        }

        // 依据拜访门路判断是否在拜访权限中,不在则继续执行;ComponentPerm componentPerm = securityContext.getPermMaps().get(servletPath);
        if (componentPerm == null) {chain.doFilter(request, response);
            return;
        }
        // 会话校验
        log.info("进入权限校验,以后申请门路 --> {}", req.getServletPath());
        HttpSession session = req.getSession(false);
        if (session == null) {log.error("会话曾经过期,请从新登录!");
            this.printeResult(res, null, false);
            return;
        }
        log.info("用户 session --> {}", session);

        // 用户校验
        User user = (User) session.getAttribute(GlobalConst.SESSION_KEY_USER);
        if (user == null) {log.error("十分申请,用户不存在!");
            this.printeResult(res, null, false);
            return;
        }
        log.info("存在 user --> {}", user);

        // 权限校验
        List<String> authorityList = user.getAuthorityList();
        if (authorityList == null || authorityList.isEmpty()) {this.printeResult(res, "权限有余,申请失败!", true);
            return;
        }
        log.info("权限 authorityList --> {}", authorityList);

        // 是否蕴含权限
        String permission = componentPerm.getPermission();
        if (!authorityList.contains(permission)) {this.printeResult(res, "权限有余,申请失败!", true);
            return;
        }
        log.info("蕴含权限 --> {}", permission);
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {}

    /**
     * Desc: 返回后果  <br>
     * @param response
     * @author lijie
     * @date 2020/8/7 14:17
     */
    private void printeResult(HttpServletResponse response, String message, boolean isJson) throws IOException {if (isJson) {response.setContentType("application/json;charset=utf-8");
            JSON.writeJSONString(response.getWriter(), Result.faild(message));
        }
        else {response.sendRedirect("http://localhost:8081/mm/login.html");
        }
    }
}

温习 Servlet 三大组件

①:过滤器 Filter

Filter 过滤器与 Servlet 十分相似,但它具备拦挡客户端(浏览器)申请的性能,通常申请会在达到 Servlet 之前先通过 Filter 过滤,Filter 过滤器能够扭转申请中的内容,来满足理论开发中的须要。每个过滤器都要间接或者间接实现 Filter;

理论开发场景:字符编码过滤,避免 XSS 攻打,避免 SQL 注入,权限过滤 等等作用非常弱小;

实现办法如下:

== 注解配置时:多个过滤器时 Servlet 会依照名称以此执行。Web.xml 配置:依照配置程序先后执行 ==

/**
 * 字符集过滤器
 * 对立解决申请与响应字符集
 * 默认 utf-8
 */
@WebFilter(filterName = "characterEncodingFilter",
           urlPatterns = "/*",
           initParams = {@WebInitParam(name = "encoding", value = "UTF-8")})
public class EncodingFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {// 我的项目启动时过滤器的初始化操作}
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        // 对每次申请进行过滤
        req = (HttpServletRequest) req;
        res = (HttpServletResponse) res;
        
        xxxx
        // 如果通过校验或者其余则调用
        chain.doFilter(res,req);
    }
    @Override
    public void destroy() {// 我的项目进行过滤器的一些操作,通常用来开释资源}
}

②、监听器 Listener

监听器次要是监听某个对象的的状态变动,比如说申请,会话,全局;在 Servlet 中,监听器次要分为如下三大类:

  • ServletContext:服务器启动创立、服务器敞开销毁

    个别次要用于我的项目启动的时候加载一些配置,或者启动一些中间件如 MQ 客户端监听等等

    @Slf4j
    @WebListener("ContextLoaderListener")
    public class ContextListener implements ServletContextListener {
    
        @Override
        public void contextInitialized(ServletContextEvent servletContextEvent) {ServletContext servletContext = servletContextEvent.getServletContext();
            try {
                // 创立容器对象,将容器对象存储在 servletContext 域中
                ApplicationContext applicationContext = new ApplicationContext(AppConst.APPLICATION_CONFIG_NAME);
                servletContext.setAttribute(AppConst.APPLICATION_CONTEXT, applicationContext);
            } catch (Exception e) {e.printStackTrace();
            }
        }
        @Override
        public void contextDestroyed(ServletContextEvent servletContextEvent) {}}
  • HttpSession:第一次调用 request.getSession 时创立,服务器敞开销毁,session 过期,手动销毁;

    该监听器能够用来统计网站的在线人数

    /**
     * @author:seanyang
     * @date:Created in 2019/8/22
     * @description:会话监听

*/
@Slf4j
@WebListener
public class MmSessionListener implements HttpSessionListener {

  @Override
  public void sessionCreated(HttpSessionEvent httpSessionEvent) {// Session 创立}

  @Override
  public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {// Session 销毁}

}


- `ServletRequestListener`:每一次申请都会创立 request,申请完结销毁;统计页面的拜访人数等等,对申请进行一些非凡的操作

@Slf4j
@WebListener
public class MmRequestListener implements ServletRequestListener {

  @Override
  public void requestInitialized(ServletRequestEvent sre) {ServletRequest servletRequest = sre.getServletRequest();
      // xxxxxxxxxxxxxxxxx 创立
  }

  @Override
  public void requestDestroyed(ServletRequestEvent sre) {ServletRequest servletRequest = sre.getServletRequest();
      // xxxxxxxxxxxxxxxxx 销毁
  }

}








## 温习树节点数据组装(不递归)
// 判断查问的后果是否为空
Result<PageResult> listMethod = this.findListMethod(req, res);
List<Dict> rows = (List<Dict>) listMethod.getResult().getRows();
if (listMethod == null || rows.isEmpty()) {return Result.faild();
}

// 封装整体后果
Map<Integer, Dict> collect = rows.stream().collect(Collectors.toMap(Dict::getId, dict -> dict));
for (Dict row : rows) {
    // 父 ID 为空必定是顶级元素,持续走上面的
    Integer pid = row.getPid();
    if (pid == null) {continue;}
    // 父 ID 不为空子元素,那么通过 Map 获取父元素,设置到子元素中
    List<Dict> subList = collect.get(pid).getSubList();
    if (subList == null) {subList = new ArrayList<>();
    }
    subList.add(row);
    collect.get(pid).setSubList(subList);
}
// 最初拿到所有的顶级元素即可
List<Dict> results = collect.values()
    .stream()
    .filter(dict -> dict.getPid() == null)
    .collect(Collectors.toList());
results.forEach(System.out::println);







## Servlet 踩坑汇合

> Servlet 服务器始终无奈返回 Session 给客户端,排查了许久发现 response.reset()是罪魁祸首
>
> - reset()用于重置,然而在重置的时候也会清空相干数据,例如 session 存的信息

private void printResult(HttpServletResponse response, Object obj) throws IOException {

// response.reset()
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("UTF-8");
JSON.writeJSONString(response.getWriter(), obj);

}






## 结言

> 不得不说思考、看代码是一个枯燥乏味的过程,工作这么久,天天应用这个框架那个框架,忽然一回首发现最重要的基础知识曾经遗记的差不多。>
> 遇到问题只会谷歌、百度、必应,然而实际上遇到问题了,根底如果不够扎实,理解的不够透彻,基本难以解决问题,技术上也并不能进精。最开始温习的时候是最难熬的把,很多货色这里用着不不便,那里不不便,比方 Spring 帮咱们做好的注入、映射、事务、动静代理等等,不得不说轮子好用。>
> 然而同样的用久了轮子,也会遗记怎么走路了的。当初的反复造轮子更多的是对本人技术的负责,技术的最终目标还是为了能计划落地才行。当然最次要当初公司个别都谋求麻利,大多数的工夫也献给了公司,剩下的工夫玩玩泥巴可能都不够了,毕竟还有生存。
退出移动版