为了账号安全,请及时绑定邮箱和手机立即绑定

如何设计一个JavaWeb MVC框架

标签:
Java

通过使用Java语言实现一个完整的框架设计,这个框架中主要内容有第一小节介绍的Web框架的结构规划,例如采用MVC模式来进行开发,程序的执行流程设计等内容;第二小节介绍框架的第一个功能:路由,如何让访问的URL映射到相应的处理逻辑;第三小节介绍处理逻辑,如何设计一个公共的 调度器,对象继承之后处理函数中如何处理response和request;第四小节至第六小节介绍如何框架的一些辅助功能,例如配置信息,数据库操作等;最后介绍如何基于Web框架实现一个简单的增删改查,包括User的添加、修改、删除、显示列表等操作。

通过这么一个完整的项目例子,我期望能够让读者了解如何开发Web应用,如何搭建自己的目录结构,如何实现路由,如何实现MVC模式等各方面的开发内容。在框架盛行的今天,MVC也不再是神话。经常听到很多程序员讨论哪个框架好,哪个框架不好, 其实框架只是工具,没有好与不好,只有适合与不适合,适合自己的就是最好的,所以教会大家自己动手写框架,那么不同的需求都可以用自己的思路去实现。

项目源码:https://github.com/junicorn/mario
示例代码:https://github.com/junicorn/mario-sample


目录
  • 项目规划
  • 路由设计
  • 控制器设计
  • 配置设计
  • 视图设计
  • 数据库操作
  • 增删改查

项目规划

做任何事情都需要做好规划,那么我们在开发博客系统之前,同样需要做好项目的规划,如何设置目录结构,如何理解整个项目的流程图,当我们理解了应用的执行过程,那么接下来的设计编码就会变得相对容易了。

创建一个maven项目

约定一下框架基础信息

  • 假设我们的web框架名称是 mario
  • 包名是 com.junicorn.mario

命令行创建

mvn archetype:create -DgroupId=com.junicorn -DartifactId=mario -DpackageName=com.junicorn.mario

Eclipse创建

图片描述

图片描述

创建好的基本结构是这样的

图片描述

初始化一下 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.junicorn</groupId>
    <artifactId>mario</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>mario</name>
    <url>https://github.com/junicorn/mario</url>

    <properties>
        <maven.compiler.source>1.6</maven.compiler.source>
        <maven.compiler.target>1.6</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <servlet.version>3.0.1</servlet.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

OK,项目创建好了,这个将是我们的框架。

框架流程

web程序是基于M(模型)V(视图)C(控制器) 设计的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从 Java 中分离了出来,所以它允许你的网页中只包含很少的脚本。

  • 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。
  • 视图 (View)是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Java中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个RSS 页面,或其它类型的“页面”,Jsp已经很好的实现了View层中的部分功能。
  • 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。
设计思路

mario 是基于servlet实现的mvc,用一个全局的Filter来做核心控制器,使用sql2o框架作为数据库基础访问。 使用一个接口 Bootstrap 作为初始化启动,实现它并遵循Filter参数约定即可。

建立路由、数据库、视图相关的包和类,下面是结构:

图片描述

路由设计

现代 Web 应用的 URL 十分优雅,易于人们辨识记忆。 路由的表现形式如下:

/resources/:resource/actions/:action
http://bladejava.com
http://bladejava.com/docs/modules/route

那么我们在java语言中将他定义一个 Route 类, 用于封装一个请求的最小单元, 在Mario中我们设计一个路由的对象如下:

/**
 * 路由
 * @author biezhi
 */
public class Route {

    /**
     * 路由path
     */
    private String path;

    /**
     * 执行路由的方法
     */
    private Method action;

    /**
     * 路由所在的控制器
     */
    private Object controller;

    public Route() {
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public Method getAction() {
        return action;
    }

    public void setAction(Method action) {
        this.action = action;
    }

    public Object getController() {
        return controller;
    }

    public void setController(Object controller) {
        this.controller = controller;
    }

}

所有的请求在程序中是一个路由,匹配在 path 上,执行靠 action,处于 controller 中。

Mario使用一个Filter接收所有请求,因为从Filter过来的请求有无数,如何知道哪一个请求对应哪一个路由呢? 这时候需要设计一个路由匹配器去查找路由处理我们配置的请求, 有了路由匹配器还不够,这么多的路由我们如何管理呢?再来一个路由管理器吧,下面就创建路由匹配器和管理器2个类:

/**
 * 路由管理器,存放所有路由的
 * @author biezhi
 */
public class Routers {

    private static final Logger LOGGER = Logger.getLogger(Routers.class.getName());

    private List<Route> routes = new ArrayList<Route>();

    public Routers() {
    }

    public void addRoute(List<Route> routes){
        routes.addAll(routes);
    }

    public void addRoute(Route route){
        routes.add(route);
    }

    public void removeRoute(Route route){
        routes.remove(route);
    }

    public void addRoute(String path, Method action, Object controller){
        Route route = new Route();
        route.setPath(path);
        route.setAction(action);
        route.setController(controller);

        routes.add(route);
        LOGGER.info("Add Route:[" + path + "]");
    }

    public List<Route> getRoutes() {
        return routes;
    }

    public void setRoutes(List<Route> routes) {
        this.routes = routes;
    }

}

这里的代码很简单,这个管理器里用List存储所有路由,公有的 addRoute 方法是给外部调用的。

/**
 * 路由匹配器,用于匹配路由
 * @author biezhi
 */
public class RouteMatcher {

    private List<Route> routes;

    public RouteMatcher(List<Route> routes) {
        this.routes = routes;
    }

    public void setRoutes(List<Route> routes) {
        this.routes = routes;
    }

    /**
     * 根据path查找路由
     * @param path  请求地址
     * @return      返回查询到的路由
     */
    public Route findRoute(String path) {
        String cleanPath = parsePath(path);
        List<Route> matchRoutes = new ArrayList<Route>();
        for (Route route : this.routes) {
            if (matchesPath(route.getPath(), cleanPath)) {
                matchRoutes.add(route);
            }
        }
        // 优先匹配原则
        giveMatch(path, matchRoutes);

        return matchRoutes.size() > 0 ? matchRoutes.get(0) : null;
    }

    private void giveMatch(final String uri, List<Route> routes) {
        Collections.sort(routes, new Comparator<Route>() {
            @Override
            public int compare(Route o1, Route o2) {
                if (o2.getPath().equals(uri)) {
                    return o2.getPath().indexOf(uri);
                }
                return -1;
            }
        });
    }

    private boolean matchesPath(String routePath, String pathToMatch) {
        routePath = routePath.replaceAll(PathUtil.VAR_REGEXP, PathUtil.VAR_REPLACE);
        return pathToMatch.matches("(?i)" + routePath);
    }

    private String parsePath(String path) {
        path = PathUtil.fixPath(path);
        try {
            URI uri = new URI(path);
            return uri.getPath();
        } catch (URISyntaxException e) {
            return null;
        }
    }

}

路由匹配器使用了正则去遍历路由列表,匹配合适的路由。当然我不认为这是最好的方法, 因为路由的量很大之后遍历的效率会降低,但这样是可以实现的,如果你有更好的方法可以告诉我 :)

在下一章节我们需要对请求处理做设计了~

控制器设计

一个MVC框架里 C 是核心的一块,也就是控制器,每个请求的接收,都是由控制器去处理的。 在Mario中我们把控制器放在路由对象的controller字段上,实际上一个请求过来之后最终是落在某个方法去处理的。

简单的方法我们可以使用反射实现动态调用方法执行,当然这对性能并不友好,你可以用缓存Method或者更高明的技术去做。 在这里我们不提及太麻烦的东西,因为初步目标是实现MVC框架,所以给大家提醒一下有些了解即可。

控制器的处理部分放在了核心Filter中,代码如下:

/**
 * Mario MVC核心处理器
 * @author biezhi
 *
 */
public class MarioFilter implements Filter {

    private static final Logger LOGGER = Logger.getLogger(MarioFilter.class.getName());

    private RouteMatcher routeMatcher = new RouteMatcher(new ArrayList<Route>());

    private ServletContext servletContext;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Mario mario = Mario.me();
        if(!mario.isInit()){

            String className = filterConfig.getInitParameter("bootstrap");
            Bootstrap bootstrap = this.getBootstrap(className);
            bootstrap.init(mario);

            Routers routers = mario.getRouters();
            if(null != routers){
                routeMatcher.setRoutes(routers.getRoutes());
            }
            servletContext = filterConfig.getServletContext();

            mario.setInit(true);
        }
    }

    private Bootstrap getBootstrap(String className) {
        if(null != className){
            try {
                Class<?> clazz = Class.forName(className);
                Bootstrap bootstrap = (Bootstrap) clazz.newInstance();
                return bootstrap;
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        throw new RuntimeException("init bootstrap class error!");
    }

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

        // 请求的uri
        String uri = PathUtil.getRelativePath(request);

        LOGGER.info("Request URI:" + uri);

        Route route = routeMatcher.findRoute(uri);

        // 如果找到
        if (route != null) {
            // 实际执行方法
            handle(request, response, route);
        } else{
            chain.doFilter(request, response);
        }
    }

    private void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Route route){

        // 初始化上下文
        Request request = new Request(httpServletRequest);
        Response response = new Response(httpServletResponse);
        MarioContext.initContext(servletContext, request, response);

        Object controller = route.getController();
        // 要执行的路由方法
        Method actionMethod = route.getAction();
        // 执行route方法
        executeMethod(controller, actionMethod, request, response);
    }

    /**
     * 获取方法内的参数
     */
    private Object[] getArgs(Request request, Response response, Class<?>[] params){

        int len = params.length;
        Object[] args = new Object[len];

        for(int i=0; i<len; i++){
            Class<?> paramTypeClazz = params[i];
            if(paramTypeClazz.getName().equals(Request.class.getName())){
                args[i] = request;
            }
            if(paramTypeClazz.getName().equals(Response.class.getName())){
                args[i] = response;
            }
        }

        return args;
    }

    /**
     * 执行路由方法
     */
    private Object executeMethod(Object object, Method method, Request request, Response response){
        int len = method.getParameterTypes().length;
        method.setAccessible(true);
        if(len > 0){
            Object[] args = getArgs(request, response, method.getParameterTypes());
            return ReflectUtil.invokeMehod(object, method, args);
        } else {
            return ReflectUtil.invokeMehod(object, method);
        }
    }

}

这里执行的流程是酱紫的:

  1. 接收用户请求
  2. 查找路由
  3. 找到即执行配置的方法
  4. 找不到你看到的应该是404

看到这里也许很多同学会有点疑问,我们在说路由、控制器、匹配器,可是我怎么让它运行起来呢? 您可说到点儿上了,几乎在任何框架中都必须有配置这项,所谓的零配置都是扯淡。不管硬编码还是配置文件方式, 没有配置,框架的易用性和快速开发靠什么完成,又一行一行编写代码吗? 如果你说机器学习,至少现在好像没人用吧。

扯淡完毕,下一节来进入全局配置设计 ->

配置设计

Mario中所有的配置都可以在 Mario 全局唯一对象完成,将它设计为单例。

要运行起来整个框架,Mario对象是核心,看看里面都需要什么吧!

  • 添加路由
  • 读取资源文件
  • 读取配置
  • 等等

由此我们简单的设计一个Mario全局对象:

/**
 * Mario
 * @author biezhi
 *
 */
public final class Mario {

    /**
     * 存放所有路由
     */
    private Routers routers;

    /**
     * 配置加载器
     */
    private ConfigLoader configLoader;

    /**
     * 框架是否已经初始化
     */
    private boolean init = false;

    private Mario() {
        routers = new Routers();
        configLoader = new ConfigLoader();
    }

    public boolean isInit() {
        return init;
    }

    public void setInit(boolean init) {
        this.init = init;
    }

    private static class MarioHolder {
        private static Mario ME = new Mario();
    }

    public static Mario me(){
        return MarioHolder.ME;
    }

    public Mario addConf(String conf){
        configLoader.load(conf);
        return this;
    }

    public String getConf(String name){
        return configLoader.getConf(name);
    }

    public Mario addRoutes(Routers routers){
        this.routers.addRoute(routers.getRoutes());
        return this;
    }

    public Routers getRouters() {
        return routers;
    }

    /**
     * 添加路由
     * @param path          映射的PATH
     * @param methodName    方法名称
     * @param controller    控制器对象
     * @return              返回Mario
     */
    public Mario addRoute(String path, String methodName, Object controller){
        try {
            Method method = controller.getClass().getMethod(methodName, Request.class, Response.class);
            this.routers.addRoute(path, method, controller);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        return this;
    }

}

这样在系统中永远保持一个Mario实例,我们用它来操作所有配置即可。

Boostrapinit方法中使用

@Override
public void init(Mario mario) {
    Index index = new Index();
    mario.addRoute("/", "index", index);
    mario.addRoute("/html", "html", index);
}

这样,一个简单的MVC后端已经形成了!接下来我们要将结果展现在JSP文件中,要做视图的渲染设计 LET'S GO!

视图设计

我们已经完成了MVC中的C层,还有M和V没有做呢。这一小节来对视图进行设计,从后台到前台的渲染是这样的 后台给定一个视图位置,输出到前端JSP或者其他模板引擎上,做一个非常简单的接口:

/**
 * 视图渲染接口
 * @author biezhi
 *
 */
public interface Render {

    /**
     * 渲染到视图
     * @param view      视图名称
     * @param writer    写入对象
     */
    public void render(String view, Writer writer);

}

具体的实现我们先写一个JSP的,当你在使用Servlet进行开发的时候已经习惯了这句语法:

servletRequest.getRequestDispatcher(viewPath).forward(servletRequest, servletResponse);

那么一个JSP的渲染实现就很简单了

/**
 * JSP渲染实现
 * @author biezhi
 *
 */
public class JspRender implements Render {

    @Override
    public void render(String view, Writer writer) {

        String viewPath = this.getViewPath(view);

        HttpServletRequest servletRequest = MarioContext.me().getRequest().getRaw();
        HttpServletResponse servletResponse = MarioContext.me().getResponse().getRaw();
        try {
            servletRequest.getRequestDispatcher(viewPath).forward(servletRequest, servletResponse);
        } catch (ServletException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private String getViewPath(String view){
        Mario mario = Mario.me();
        String viewPrfix = mario.getConf(Const.VIEW_PREFIX_FIELD);
        String viewSuffix = mario.getConf(Const.VIEW_SUFFIX_FIELD);

        if (null == viewSuffix || viewSuffix.equals("")) {
            viewSuffix = Const.VIEW_SUFFIX;
        }
        if (null == viewPrfix || viewPrfix.equals("")) {
            viewPrfix = Const.VIEW_PREFIX;
        }
        String viewPath = viewPrfix + "/" + view;
        if (!view.endsWith(viewSuffix)) {
            viewPath += viewSuffix;
        }
        return viewPath.replaceAll("[/]+", "/");
    }

}

配置 JSP 视图的位置和后缀可以在配置文件或者硬编码中进行,当然这看你的习惯, 默认设置了 JSP 在 /WEB-INF/ 下,后缀是 .jsp 你懂的!

怎么用可以参考 mario-sample 这个项目,因为真的很简单 相信你自己。

在下一节中我们就要和数据库打交道了,尝试新的旅程吧 :)

数据库操作

这一小节是对数据库操作做一个简单的封装,不涉及复杂的事务操作等。

我选用了Sql2o作为底层数据库框架作为支持,它的简洁易用性让我刮目相看,后面我们也会写如何实现一个ORM框架。

/**
 * 数据库支持
 * @author biezhi
 *
 */
public final class MarioDb {

    private static Sql2o sql2o = null;

    private MarioDb() {
    }

    /**
     * 初始化数据库配置
     * @param url
     * @param user
     * @param pass
     */
    public static void init(String url, String user, String pass){
        sql2o = new Sql2o(url, user, pass);
    }

    /**
     * 初始化数据库配置
     * @param dataSource
     */
    public static void init(DataSource dataSource){
        sql2o = new Sql2o(dataSource);
    }

    /**
     * 查询一个对象
     * @param sql
     * @param clazz
     * @return
     */
    public static <T> T get(String sql, Class<T> clazz){
        return get(sql, clazz, null);
    }

    /**
     * 查询一个列表
     * @param sql
     * @param clazz
     * @return
     */
    public static <T> List<T> getList(String sql, Class<T> clazz){
        return getList(sql, clazz, null);
    }

    /**
     * 查询一个对象返回为map类型
     * @param sql
     * @return
     */
    public static Map<String, Object> getMap(String sql){
        return getMap(sql, null);
    }

    /**
     * 查询一个列表并返回为list<map>类型
     * @param sql
     * @return
     */
    public static List<Map<String, Object>> getMapList(String sql){
        return getMapList(sql, null);
    }

    /**
     * 插入一条记录
     * @param sql
     * @param params
     * @return
     */
    public static int insert(String sql, Object ... params){
        StringBuffer sqlBuf = new StringBuffer(sql);
        sqlBuf.append(" values (");

        int start = sql.indexOf("(") + 1;
        int end = sql.indexOf(")");
        String a = sql.substring(start, end);
        String[] fields = a.split(",");

        Map<String, Object> map = new HashMap<String, Object>();

        int i=0;
        for(String name : fields){
            sqlBuf.append(":" + name.trim() + " ,");
            map.put(name.trim(), params[i]);
            i++;
        }

        String newSql = sqlBuf.substring(0, sqlBuf.length() - 1) + ")";

        Connection con = sql2o.open();
        Query query = con.createQuery(newSql);

        executeQuery(query, map);

        int res = query.executeUpdate().getResult();

        con.close();

        return res;
    }
    /**
     * 更新
     * @param sql
     * @return
     */
    public static int update(String sql){
        return update(sql, null);
    }

    /**
     * 带参数更新
     * @param sql
     * @param params
     * @return
     */
    public static int update(String sql, Map<String, Object> params){
        Connection con = sql2o.open();
        Query query = con.createQuery(sql);
        executeQuery(query, params);
        int res = query.executeUpdate().getResult();
        con.close();
        return res;
    }

    public static <T> T get(String sql, Class<T> clazz, Map<String, Object> params){
        Connection con = sql2o.open();
        Query query = con.createQuery(sql);
        executeQuery(query, params);
        T t = query.executeAndFetchFirst(clazz);
        con.close();
        return t;
    }

    @SuppressWarnings("unchecked")
    public static Map<String, Object> getMap(String sql, Map<String, Object> params){
        Connection con = sql2o.open();
        Query query = con.createQuery(sql);
        executeQuery(query, params);
        Map<String, Object> t = (Map<String, Object>) query.executeScalar();
        con.close();
        return t;
    }

    public static List<Map<String, Object>> getMapList(String sql, Map<String, Object> params){
        Connection con = sql2o.open();
        Query query = con.createQuery(sql);
        executeQuery(query, params);
        List<Map<String, Object>> t = query.executeAndFetchTable().asList();
        con.close();
        return t;
    }

    public static <T> List<T> getList(String sql, Class<T> clazz, Map<String, Object> params){
        Connection con = sql2o.open();
        Query query = con.createQuery(sql);
        executeQuery(query, params);
        List<T> list = query.executeAndFetch(clazz);
        con.close();
        return list;
    }

    private static void executeQuery(Query query, Map<String, Object> params){
        if (null != params && params.size() > 0) {
            Set<String> keys = params.keySet();
            for(String key : keys){
                query.addParameter(key, params.get(key));
            }
        }
    }
}

设计MVC框架部分已经完成,下一节是一个增删改查的例子。

增删改查
/**
 * 用户控制器
 */
public class UserController {

    /**
     * 用户列表
     * @param request
     * @param response
     */
    public void users(Request request, Response response){
        List<User> users = MarioDb.getList("select * from t_user", User.class);
        request.attr("users", users);
        response.render("users");
    }

    /**
     * 添加用户界面
     * @param request
     * @param response
     */
    public void show_add(Request request, Response response){
        response.render("user_add");
    }

    /**
     * 保存方法
     * @param request
     * @param response
     * @throws ParseException
     */
    public void save(Request request, Response response) throws ParseException{
        String name = request.query("name");
        Integer age = request.queryAsInt("age");
        String date = request.query("birthday");

        if(null == name || null == age || null == date){
            request.attr("res", "error");
            response.render("user_add");
            return;
        }

        Date bir = new SimpleDateFormat("yyyy-MM-dd").parse(date);

        int res = MarioDb.insert("insert into t_user(name, age, birthday)", name, age, bir);
        if(res > 0){
            String ctx = MarioContext.me().getContext().getContextPath();
            String location = ctx + "/users";
            response.redirect(location.replaceAll("[/]+", "/"));
        } else {
            request.attr("res", "error");
            response.render("user_add");
        }
    }

    /**
     * 编辑页面
     * @param request
     * @param response
     */
    public void edit(Request request, Response response){
        Integer id = request.queryAsInt("id");
        if(null != id){
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("id", id);
            User user = MarioDb.get("select * from t_user where id = :id", User.class, map);
            request.attr("user", user);
            response.render("user_edit");
        }
    }

    /**
     * 修改信息
     * @param request
     * @param response
     */
    public void update(Request request, Response response){
        Integer id = request.queryAsInt("id");
        String name = request.query("name");
        Integer age = request.queryAsInt("age");

        if(null == id || null == name || null == age ){
            request.attr("res", "error");
            response.render("user_edit");
            return;
        }

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("id", id);
        map.put("name", name);
        map.put("age", age);

        int res = MarioDb.update("update t_user set name = :name, age = :age where id = :id", map);
        if(res > 0){
            String ctx = MarioContext.me().getContext().getContextPath();
            String location = ctx + "/users";
            response.redirect(location.replaceAll("[/]+", "/"));
        } else {
            request.attr("res", "error");
            response.render("user_edit");
        }
    }

    /**
     * 删除
     * @param request
     * @param response
     */
    public void delete(Request request, Response response){
        Integer id = request.queryAsInt("id");
        if(null != id){
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("id", id);
            MarioDb.update("delete from t_user where id = :id", map);
        }

        String ctx = MarioContext.me().getContext().getContextPath();
        String location = ctx + "/users";
        response.redirect(location.replaceAll("[/]+", "/"));
    }
}

作者:biezhi
原文出处:https://github.com/biezhi/java-bible/blob/master/mvc/index.md

点击查看更多内容
28人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
2504
获赞与收藏
8.4万

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消