回顾Servlet!手把手自定义MVC框架!!!

前言

​ 不得不说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中,监听器主要分为如下三大类:

img

  • 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 :会话监听
     * @version: 1.0
     */
    @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帮我们做好的注入、映射、事务、动态代理等等,不得不说轮子好用。

但是同样的用久了轮子,也会忘记怎么走路了的。现在的重复造轮子更多的是对自己技术的负责,技术的最终目的还是为了能方案落地才行。当然最主要现在公司一般都追求敏捷,大多数的时间也献给了公司,剩下的时间玩玩泥巴可能都不够了,毕竟还有生活。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值