Java利用反射机制实现山寨SpringMVC

还是接着上一篇博客的话题,dao层基本上是搞定了,service层业务逻辑不太好抽象,所以现在就是action层的问题了,之前用Servlet写Action层的时候,还是用了一点视图模型分离的思想,用这样的方式来向jsp传递数据:

@WebServlet("/dish/list")
public class DishListServlet extends javax.servlet.http.HttpServlet {
    private UserService userService;
    private DishService dishService;
    public DishListServlet(){
        userService = new UserService();
        dishService = new DishService();
    }
    protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {

    }

    protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
        // 获取菜品列表
        Envelope envelope = dishService.getDishes(lastId);
        // 设置信息
        request.setAttribute("envelope",envelope);
        // 转发给视图页面
        request.getRequestDispatcher("/dishList.jsp").forward(request,response);
    }
}

或者是直接response.getWriter直接输出字符串,这样有一个问题在于,每添加一个功能,我就需要增加一个Servlet,去取参数,转换参数类型等,最后再输出结果,非常的繁琐,于是我决定使用SpringMVC的方式,通过简单的注解,就能将一个模块的所有方法放在同一个Action类里,方便进行管理。

实现思路其实也不难,一启动项目的时候拿到action层的所有类的所有处理方法,存储一个URL-方法映射,便于一会快速找到请求的处理方法,然后配置一个Servlet拦截所有请求,再根据URI找到方法执行即可。

1.实现标记注解

首先是给类上标记这个类是action层的一个处理类,类似于SpringMVC的Controller,这里我将它命名为Action:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {}

然后是请求URL注解,可以放在方法或者类上,类似RequestMapping,用path来记录url:

@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestPath {
    String path();
}

最后是对返回结果情况的一个标记,这里和ResponseBody注解的作用恰好相反,如果结果返回到一个jsp上,则用此注解标记,否则默认返回字符串:

@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponsePath {
    String path();
}

2.标记处理请求的类和方法

利用写好的注解标记方法:

@Action
@RequestPath(path = "/employee")
public class EmployeeAction {
    public EmployeeAction(){
    }
    @RequestPath(path="/manu")
    @ResponsePath(path="/employee/manu")
    public List info(HttpSession session){
    	List list = new ArrayList();
        return list;
    }
    @RequestPath(path = "/login")
    public String login(HttpSession session,String id,String password){
        return "success";
    }
}

按这样的方式标记注解之后,当请求/employee/manu时,就会由info方法进行处理,返回jsp所需的对象之后将请求转发到manu.jsp上去。如果请求/employee/login,则会由login方法进行处理,并返回处理后的字符串,这里是"success";

3.声明拦截器

这里声明一个拦截器拦截处jsp之外的所有请求:

@WebServlet("/")
public class BaseAction extends HttpServlet {
    // 所有的地址-对象映射 第一个元素放类或方法主体,后面的元素方法子映射或者参数列表
    public static Map<String,Object> urls = new HashMap<>();
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }

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

这里放了一个全局静态字典去保存url映射,但由于现在还没有处理好url的映射问题,所以还不能实现具体的处理逻辑。

这里静态资源也是需要放行的,但配置拦截之后可能会把静态资源也拦截下来,查找资料之后,我发现在web.xml里这样配置就可以放行静态资源了:
当servlet项目中有@WebServlet(“/”)拦截所有请求时,如何对静态资源放行

<servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.js</url-pattern>
    </servlet-mapping>

4.形成URL-方法映射

这里我假设最多有两层URL,第一层是放在类上的,第二层是放在方法上的,然后在一个Listener中实现映射的方法,最后配置让项目一启动就自动执行Listener中的方法完成映射。

首先是如何反射包的问题,通过百度我找到了两种解决方案:

java利用反射获取某个包下的所有获取对象属性、方法、并实例化

不过使用解析路径的方式实在是太费劲了,所以我决定使用org.reflections这个包来实现。

首先找到action包里所有有Action层注解的类,然后拿到类中的各方法,并用RequestPath标签上配置好的URL对URL与对象或方法进行映射,这里我保存的顺序是:

BaseAction.urls: 一级URL-列表firstList
		firstList[0] 某Action的实例化对象
		firstList[1] 该Action的方法字典methodMap
				methodMap: 二级URL-列表secondList
						secondList[0] 某方法的Method类型对象
						secondList[1:] 方法参数列表list

实现方法,并在Listener的contextInitialized中调用。

public class InitListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        InitListener initListener = new InitListener();
        try {
            initListener.resolveAction();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {}

    /**
     * 解析所有action
     */
    public void resolveAction() throws IllegalAccessException, InstantiationException {
        // 反射获取整个包
        Reflections reflections = new Reflections("com.hrm.action.*");
        // 获取包中所有类
        Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Action.class);
        // 遍历每一个类
        for(Class<?> cl:classes){
            // 获取类上的注解
            RequestPath requestPath = cl.getAnnotation(RequestPath.class);
            // 获取注解的顶层路径
            String firstPath = requestPath.path();
            // 类层级的list列表
            List<Object> objects = new ArrayList<>();
            // 将list放在列表中
            BaseAction.urls.put(firstPath,objects);
            // list的第一个元素放类实例
            objects.add(cl.newInstance());
            // 新建一个方法字典
            Map<String,Object> methodMap = new HashMap<>();
            // list的第二个元素放方法字典
            objects.add(methodMap);
            // 获取该类中所有方法
            Method[] methods = cl.getMethods();
            // 遍历所有方法
            for(Method method:methods){
                // 获取方法上的注解
                requestPath = method.getDeclaredAnnotation(RequestPath.class);
                // 跳过没有注解的方法
                if(requestPath == null)
                    continue;
                // 获取二级路径
                String secondPath = requestPath.path();
                // 新建方法层级的list
                objects = new ArrayList<>();
                // 把object放在字典中
                methodMap.put(secondPath,objects);
                // list第一个元素放方法本身
                objects.add(method);
                // 解析方法参数列表 names
                Parameter parameters[] = method.getParameters();
                for(Parameter parameter:parameters){
                    // 后面的添加参数列表
                    objects.add(parameter);
                }
            }
        }
    }
}

然后在web.xml中配置使listener生效:

<listener>
	<listener-class>com.hrm.action.InitListener</listener-class>
</listener>

这样项目一启动就可以将所有处理方法解析完毕了。

5.实现请求的处理

这里只需要从request中取得URI,解析找到对应的action实例对象和方法,然后从request里调用getParameter得到参数,(这里如果参数是HttpSession类型需要单独处理,从request中拿到session传入),最后调用方法。

这里获得参数就必须要变量名,但反射取得的都是arg0,arg1这样没有意义的变量名,如果想要指定变量名,要么增加注解,但这种方式过于复杂,要么就通过配置使class里面存储真实变量名,我选择了第二种。

java通过反射获取方法的参数名,Idea/Eclipse/Maven的配置

当成功调用方法并获得返回值之后,就是最后对结果的处理了,这里需要判断一下方法上是否有ResponsePath标签,如果没有的话,直接调用getWriter输出结果,否则,就利用request的setAttribute方法设置参数,然后转发到jsp上去,所有的jsp都放在了WEB-INF的page目录中,在jsp中利用如下的方法拿到参数:

<%
        Envelope envelopes[] = (Envelope[]) request.getAttribute("param");
        List<EmployeeEntity> employeeEntities = (List<EmployeeEntity>) envelopes[0].getObj();
        List<EmployerEntity> employerEntities = (List<EmployerEntity>) envelopes[1].getObj();
    %>

BaseAction完整代码如下:

@WebServlet("/")
public class BaseAction extends HttpServlet {
    // 所有的地址-对象映射 第一个元素放类或方法主体,后面的元素方法子映射或者参数列表
    public static Map<String,Object> urls = new HashMap<>();
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        // 获取地址的各部分
        String reqUrls[] = req.getRequestURI().split("/");
        // 获取切割总长度
        int length = reqUrls.length;
        // 获取一级地址列表
        List list = (List) urls.get("/"+reqUrls[length-2]);
        // 如果无法处理请求
        if(list == null){
            return;
        }
        // 获取一级地址的Action对象
        Object action = list.get(0);
        // 获取方法字典
        Map<String,Object> map = (Map<String, Object>) list.get(1);
        // 获取二级地址列表
        list = (List) map.get("/"+reqUrls[length-1]);
        // 获取处理方法
        Method method = (Method) list.get(0);
        // 方法参数列表
        Object[] params = new Object[list.size()-1];
        // 处理方法的所有参数
        for(int i=1;i<list.size();i++){
            // 获取参数
            Parameter parameter = (Parameter) list.get(i);
            // 如果是session类型的参数
            if(parameter.getType().equals(HttpSession.class)){
                // 需要从request中去获取
                params[i-1] = req.getSession();
            }else{
                // 获取前端传来的实际参数
                String res = req.getParameter(parameter.getName());
                // 如果参数是int类型
                if(parameter.getType().equals(int.class)){
                    params[i-1] = Integer.parseInt(res);
                }else{
                    // 放置参数
                    params[i-1] = parameter.getType().cast(res);
                }
            }
        }
        Object res = null;
        try {
            res = method.invoke(action,params);
        } catch (Exception e) {
            e.printStackTrace();
            res = new Envelope(-2,"action层方法错误",null).toString();
        }
        // 看方法上有没有转移jsp标签
        ResponsePath responsePath = method.getAnnotation(ResponsePath.class);
        // 没有说明直接返回字符串
        if(responsePath == null){
            // 设置响应编码
            resp.setHeader("content-type", "text/html;charset=UTF-8");
            resp.setCharacterEncoding("utf-8");
            // 打印结果
            resp.getWriter().println(res.toString());
        }else{
            // 获取jsp的路径
            String path = responsePath.path();
            // 给请求放上处理完的结果
            req.setAttribute("param",res);
            // 发送请求到jsp上
            req.getRequestDispatcher("/WEB-INF/page" + path + ".jsp").forward(req,resp);
        }
    }
}

通过这样的方式,就可以简单高效的完成Action层的开发,避免了写太多的Servlet,当然和真正的SpringMVC比,还是不够通用,而且功能太少,但是对于简单的业务逻辑来说已经足够了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值