还是接着上一篇博客的话题,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比,还是不够通用,而且功能太少,但是对于简单的业务逻辑来说已经足够了。