自定义@WebServlet注解
Servlet3.0提供了注解来注册实例化servlet,使得不再需要在web.xml中进行servlet的部署描述,简化开发。本文根据@WebServlet的功能,通过开发自定义注解和注解处理器来模仿一个@WebServlet,以此来了解@WebServlet的大致流程。
1、开发用于配置Servlet的相关注解
/**
* 自定义WebServlet注解,模拟Servlet3.0的WebServlet注解
* @Target 注解的属性值表明了 @WebServlet注解只能用于类或接口定义声明的前面,
* @WebServlet注解有一个必填的属性 value 。
* 调用方式为: @WebServlet(value="/xxxx") ,
* 因语法规定如果属性名为 value 且只填 value属性值时,可以省略 value属性名,即也可以写作:@WebServlet("/xxxx")
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WebServlet {
//Servlet的访问URL
String value();
//Servlet的访问URL
String[] urlPatterns() default {""};
//Servlet的描述
String description() default "";
//Servlet的显示名称
String displayName() default "";
//Servlet的名称
String name() default "";
//Servlet的init参数
WebInitParam[] initParams() default {};
}
2、开发WebInitParam注解,用于配置Servlet初始化时使用的参数
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WebInitParam {
//参数名
String paramName() default "";
//参数的值
String paramValue() default "";
}
对于@WebServlet
里面定义的@WebInitParam
类型变量的初始化,通过如下方式
@WebServlet(value = "*.do",initParams = {@WebInitParam(name = "basePackage", value = "servlet")})
3、编写注解处理器
/**
* Created by WuJiXian on 2020/10/25 19:44
* 注意复合注解比如initParams,这种形式的如何初始化?
* WebInitParam[] initParams() default {};
*/
@WebFilter(value = "*.do",initParams = {@WebInitParam(name = "basePackage", value = "servlet")})
public class AnnotationHandlerFilter implements Filter {
private static final String CLS = AnnotationHandlerFilter.class.getName();
private ServletContext servletContext;
/**
* 读出web.xml配置的filter初始化参数(指定扫描的包)
* 多个包逗号分隔
* 扫描指定包下带有@WebServlet注解的类,将字节码加入Map
* 将map存入servletContext
* @param filterConfig 过滤器配置对象
* @throws ServletException
* 难点:
* 如何扫描指定包下的所有类
* 类加载器+URL
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println(CLS + "---------init(FilterConfig filterConfig)---------");
//保留 key-路径 value-class的映射,保存web-init-param 键值
Map<String, Class<?>> clsMap = new HashMap<>();
String basePackage = filterConfig.getInitParameter("basePackage");
Set<Class<?>> classes = ScanClassUtil.getClasses(basePackage);
if (classes.size() > 0) {
for (Class<?> aClass : classes) {
if (aClass.isAnnotationPresent(WebServlet.class)) {
WebServlet annotation = aClass.getDeclaredAnnotation(WebServlet.class);
//value和urlPattern都代表了<servlet-pattern>中的url-pattern
String value = annotation.value();
String[] urlPatterns = annotation.urlPatterns();
if (!"".equals(value)) {
System.out.println("---------url-pattern->" + value);
clsMap.put(value, aClass);
}
for (String urlPattern : urlPatterns) {
System.out.println("---------url-pattern->" + urlPattern);
clsMap.put(urlPattern, aClass);
}
}
}
}
this.servletContext = filterConfig.getServletContext();
// clsMap存入servletContext域中
servletContext.setAttribute("clsMap", clsMap);
}
/**
* 根据请求的URI-map->class
* 判断请求方法 get or post
* cls->实例化对象,调用具体的方法
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
* 难点:路径url的多种情况,带参数
* 字符串截取到最后一个点之前的路径,这样
* 就参数就不会影响具体的servlet实例化
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println(CLS + "---------(ServletRequest request, ServletResponse response, FilterChain chain)---------");
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 强转之后req.getClass().getName()类名是强转之前的类名
// 如下打印:req:org.apache.catalina.connector.RequestFacade
System.out.println("----------req:"+req.getClass().getName() + "------resp:" + resp.getClass().getName());
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
String url = uri.substring(contextPath.length());
System.out.printf("--------uri:%s,contextPath:%s,url:%s--------", uri, contextPath, url);
Map<String, Class<?>> clsMap = (Map<String, Class<?>>) servletContext.getAttribute("clsMap");
Class<?> cls = clsMap.get(url);
// /xxx.do!sayHello 调用具体的方法
// /xxx.do 则根据请求方法来调用默认的方法
if (cls != null) {
Object o = null;
Method method = null;
try {
o = cls.newInstance();
//next statement error
//method = cls.getDeclaredMethod("get", req.getClass(), resp.getClass());
method = cls.getDeclaredMethod("get", HttpServletRequest.class, HttpServletResponse.class);
String reqMethod = req.getMethod();
if ("get".equalsIgnoreCase(reqMethod)) {
method.invoke(o, req, resp);
} else if ("post".equals(reqMethod)) {
method.invoke(o, req, resp);
}
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
// chain.doFilter放不放行无所谓,本质上所有的处理都在这个类完成了
//chain.doFilter(req, resp);
}
@Override
public void destroy() {
}
}
AnnotationHandleFilter过滤器初始化方法init(FilterConfig filterConfig)使用到了一个用于扫描某个包下面的类的工具类ScanClassUtil,ScanClassUtil的代码如下:
public class ScanClassUtil {
/**
* 从包package中读取所有的字节码Class
* @param pack
* @return
*/
public static Set<Class<?>> getClasses(String pack) {
Set<Class<?>> classes = new LinkedHashSet<>();
boolean recursive = true;
// 获取包的名字,并进行替换
String packageName = pack;
String packageDirName = packageName.replace('.', '/');
// 定义一个枚举类型的集合,循环处理目录下的所有文件
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
// url路径格式: file:/G:/studyWorkspace/javaWebProj/web03/web/WEB-INF/classes/ano
URL url = dirs.nextElement();
String protocol = url.getProtocol();
// 如果是以文件的形式保存在服务器上
if ("file".equals(protocol)) {
System.err.println("file类型的扫描");
// 获取包的物理路径,编码的原因是为了中文目录考虑
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
// 以文件的方式扫描整个包下的文件,并添加到集合中
findAndAddClassesInPackageByFile(pack, filePath, recursive, classes);
} else if ("jar".equals(protocol)) {
// 如果是jar包文件
System.err.println("jar类型的扫描");
JarFile jar;
jar = ((JarURLConnection) url.openConnection()).getJarFile();
// 从jar包得到一个枚举类
Enumeration<JarEntry> entries = jar.entries();
// 同样进行循环迭代
while (entries.hasMoreElements()) {
JarEntry entry;
entry = entries.nextElement();
String name = entry.getName();
// 如果是以/开头的,获取后面的字符串
if (name.charAt(0) == '/')
name = name.substring(1);
// 如果前半部分和定义的包名相同
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf("/");
if (idx != -1) {
packageName = name.substring(0, idx).replace('/', '.');
}
if ((idx != -1) || recursive) {
if (name.endsWith(".class") && !entry.isDirectory()) {
// 去掉后面的".class"获取真正的类名
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
// 将字节码添加到classes
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
/**
* 以文件的形式来获取包下的所有class
* @param pkg
* @param pkgPath
* @param recursive
* @param classes
*/
private static void findAndAddClassesInPackageByFile(String pkg, String pkgPath, boolean recursive, Set<Class<?>> classes) {
// 获取此包的目录 建立一个File
File dir = new File(pkgPath);
// 如果不存在 也不是目录就直接返回
if (!dir.exists() || !dir.isDirectory())
return;
// 如果存在 就获取包下的所有文件 包括目录
File[] dirFiles = dir.listFiles(f -> ((recursive && f.isDirectory()) ||
f.getName().endsWith(".class")));
// 循环所有文件
assert dirFiles != null;
for (File file : dirFiles) {
// 如果是目录 则继续扫描
if (file.isDirectory()) {
findAndAddClassesInPackageByFile(pkg + "." + file.getName(),
file.getAbsolutePath(), recursive, classes);
} else {
// 如果是java类文件 去掉后面的.class 只留下类名
String className = file.getName().substring(0, file.getName().length() - 6);
try {
// 添加到字节码集合中
classes.add(Thread.currentThread().getContextClassLoader().loadClass(pkg + "." + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
String pkg = "ano";
getClasses(pkg);
}
}
4、使用该注解
@WebServlet("/test.do")
public class WebServletTest {
public void get(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("get...");
resp.getWriter().write("sayHello...");
}
public void post(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("post...");
}
}
总结:
1)通过模拟@WebServlet注解的实现,了解了注解处理的大致流程。整个注解的核心就是注解+反射。
2)注解没有相应的注解处理器来处理的话,那么注解就没有灵魂,离开了注解处理器,注解什么都不是。
下面在上面的基础上进一步来模拟SpringMVC的请求注解实现。
JavaWeb学习总结(四十八)——模拟Servlet3.0使用注解的方式配置Servlet
简单模拟SpringMVC
我们都知道在SpringMVC开发web项目时,只需要给类添加上相应的@Controller
和@RequestMapping
注解这个类可以完成一个web请求处理的功能,比如当浏览器输入与@RequestMapping(value="path")
注解的值相同的路径时,这个url就能被相应的处理器处理并映射到一个类,然后根据请求执行具体的方法。
首先,看下项目整体结构
实现模拟SpringMVC web工程的核心就是围绕@Controller
和@RequestMapping
这两个注解,开发相应的注解处理器,这其中又用到了其他个类的辅助。
最核心的注解处理器AnnotationHandleServlet
的实现思路:
1)这里用Servlet完成的注解处理器,不同于Filter,当servlet被请求的时候才会去执行init方法,除非配置了load-on-startup属性。在init方法中,同上面开发@WebServlet注解的实现原理大致相同,只不过这里是将满足类上带有@Controller
和方法上存在@RequestMapping
的类才会被放入map中。
2)当用户请求时(无论是get还是post请求),会调用封装好的execute方法 ,execute会先获取请求的url,然后解析该URL,根据解析好的URL从Map集合中取出要调用的目标类 ,再遍历目标类中定义的所有方法,找到类中使用了RequestMapping注解的那些方法,判断方法上面的RequestMapping注解的value属性值是否和解析出来的URL路径一致,如果一致,说明了这个就是要调用的目标方法,此时就可以利用java反射机制先实例化目标类对象,然后再通过实例化对象调用要执行的方法处理用户请求。
方法处理完成之后需要给客户端发送响应信息,比如告诉客户端要跳转到哪一个页面,采用的是服务器端跳转还是客户端方式跳转,或者发送一些数据到客户端显示,那么该如何发送响应信息给客户端呢,在此,我们可以设计一个View(视图)类,对这些操作属性进行封装,其中包括跳转的路径 、展现到页面的数据、跳转方式。这就是AnnotationHandleServlet的实现思路。
/**
* AnnotationHandleServlet作为自定义注解的核心处理器以及负责调用目标业务方法处理用户请求<p>
* Created by WuJiXian on 2020/10/26 9:55
*/
@WebServlet(value = "*.do", initParams = {@WebInitParam(name = "basePackage", value = "controller")})
public class AnnotationHandleServlet extends HttpServlet {
// key-url value-classes 路径字节码对象映射
private static Map<String, Class<?>> clsMap = new HashMap<>();
/**
* 扫描包下特定注解的类将其加入Map
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
System.out.println("AnnotationHandleServlet...init...");
String basePackage = config.getInitParameter("basePackage");
Set<Class<?>> classes;
// 多个包“,”分隔
if (basePackage.contains(",")) {
String[] pgs = basePackage.split(",");
for (String pg : pgs) {
classes = ScanClassUtil.getClasses(pg);
add2ClassMap(classes, clsMap);
}
} else {
classes = ScanClassUtil.getClasses(basePackage);
add2ClassMap(classes, clsMap);
}
System.out.println("AnnotationHandleServlet...init finished...");
}
private void add2ClassMap(Set<Class<?>> classes, Map<String, Class<?>> clsMap) {
for (Class<?> cls : classes) {
if (cls.isAnnotationPresent(Controller.class)) {
Method[] declaredMethods = cls.getDeclaredMethods();
for (Method method : declaredMethods) {
// 只有当该类被Controller注解,并且存在RequestMapping标注的方法才将
// 该类放入map容器
if (method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping reqAnnotation = method.getAnnotation(RequestMapping.class);
String mapUrl = reqAnnotation.value();
System.out.println("-------mapUrl------->" + mapUrl);
clsMap.put(mapUrl, cls);
}
}
}
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.execute(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.execute(req, resp);
}
private void execute(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("---------------execute start ---------------");
// 获取到请求路径
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
String reqMap = uri.substring(contextPath.length());
// 这里并没有对后缀.do做处理,而是直接xxx.do作为key的
// String reqMap = uri.substring(contextPath.length(), url.lastIndexOf("."));
System.out.printf("--------uri:%s,contextPath:%s,url:%s--------\n", uri, contextPath, reqMap);
// 将当前线程的HttpServletRequest和HttpServletResponse对象存储到ThreadLocal中,以便在Controller中使用
WebContext.requestHolder.set(req);
WebContext.responseHolder.set(resp);
// 获取要使用的类
Class<?> clazz = clsMap.get(reqMap);
if (clazz != null) {
Object o;
Method method = null;
try {
o = clazz.newInstance();
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
if (m.isAnnotationPresent(RequestMapping.class)) {
String value = m.getAnnotation(RequestMapping.class).value();
if (reqMap.equalsIgnoreCase(value)) {
// 找到要执行的目标方法
method = m;
break;
}
}
}
if (method != null) {
// 执行目标方法处理用户请求,这里以无参为例
// Object rtn = method.invoke(o, req, resp);
Object rtn = method.invoke(o);
// 如果方法有返回值,那么就表示用户需要返回视图对象
if (rtn instanceof View) {
View view = (View) rtn;
String dispatchAction = view.getDispatchAction();
// 判断跳转的方式
if (dispatchAction.equals(DispatchActionConstant.FORWARD))
req.getRequestDispatcher(view.getUrl()).forward(req, resp);
else if (dispatchAction.equals(DispatchActionConstant.REDIRECT))
resp.sendRedirect(req.getContextPath() + "/" + view.getUrl());
else
req.getRequestDispatcher(view.getUrl()).forward(req, resp);
}
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | ServletException | IOException e) {
e.printStackTrace();
}
}
System.out.println("---------------execute finished ---------------");
}
}
ScanClassUtil
该类的代码不在展示,上文已经存在,BeanUtils工具类并没有用到,只是对类获取方法的简单的封装,这里利用反射获取类中的所有方法直接写在了上面代码中,DispatchActionConstant
定义了请求方法的常量,如下。
public class DispatchActionConstant {
public static final String FORWARD = "forward";
public static final String REDIRECT = "redirect";
}
WebContext类内部定义了ThreadLocal类型的引用,它将每个线程与特定的请求绑定,依靠requestHolder
能够拿到当前线程的request对象。
public class WebContext {
public static ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>();
public static ThreadLocal<HttpServletResponse> responseHolder = new ThreadLocal<>();
public static HttpServletRequest getRequest() {
return requestHolder.get();
}
public static HttpServletResponse getResponse() {
return responseHolder.get();
}
public static HttpSession getSession(){
return requestHolder.get().getSession();
}
public static ServletContext getServletContext(){
return requestHolder.get().getSession().getServletContext();
}
}
View视图对象,对url的简单封装
public class View {
//url前面不要有"/"
private String url;
private String dispatchAction = DispatchActionConstant.FORWARD;
public View(String url) {
this.url = url;
}
public View(String url, String dispatchAction) {
this.url = url;
this.dispatchAction = dispatchAction;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getDispatchAction() {
return dispatchAction;
}
public void setDispatchAction(String dispatchAction) {
this.dispatchAction = dispatchAction;
}
}
ModelAndView类,里面绑定了当前线程的request对象,当在程序中实例化一个该类型的对象后,调用put方法,是直接将属性放入到了域对象中。
这里有点SpringMVC的内味。细品。
public class ModelAndView {
private HttpServletRequest request;
public ModelAndView() {
this.request = WebContext.getRequest();
}
public void put(String name, Object value) {
request.setAttribute(name, value);
}
}
测试用例类
@Controller
public class ControllerTest {
@RequestMapping("/login.do")
public View loginTest() {
return new View("login.jsp");
}
@RequestMapping("/register.do")
public View registerTest() {
return new View("register.jsp");
}
}
当在浏览器中访问 http://localhost:8080/web/login.do
时,会跳转到login.jsp页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
Login.jsp
<form action="${pageContext.request.contextPath}/loginCheck.do" method="post">
用户名:<input type="text" value="${param.username}" name="username">
<br/>
密码:<input type="text" value="${param.pwd}" name="pwd">
<br/>
<input type="submit" value="登录"/>
</form>
<%--不要使用重定向,因为request域只对本次请求有效--%>
${msg}
</body>
</html>
当用户输入信息登录之后,被loginCheck映射的loginHandle方法处理。
@Controller
public class ControllerDemo {
@RequestMapping("/loginCheck.do")
public View loginHandle() {
ModelAndView modelAndView = new ModelAndView();
// 获取当前线程绑定的request对象
HttpServletRequest request = WebContext.getRequest();
String username = request.getParameter("username");
String pwd = request.getParameter("pwd");
if ("qqq".equals(username) && "123".equals(pwd)) {
modelAndView.put("msg", "登录成功");
modelAndView.put("curUser", username);
//url前面不要有"/"
return new View("index.jsp", DispatchActionConstant.FORWARD);
}
modelAndView.put("msg", "登录失败");
return new View("login.jsp", DispatchActionConstant.FORWARD);
}
}
访问成功跳转到首页,访问失败,跳转原页面给予用户提示信息。