Wt Blog


  • 首页

  • 归档

problem-solving

发表于 2019-07-20

常见问题

Tomcat启动失败的解决方案

首先,如果启动时,弹出对话框,对话框中提示了端口号,多是因为端口号冲突,例如此前已经启动了Tomcat却没有关闭,仍再次启动,就会出现冲突。如果要解决问题,可以先停止此前占用端口的程序,例如在Eclipse找到停止按钮,或者在Tomcat的bin目录下执行shutdown指令,如果不知道如何,可以重启电脑!

如果启动过程中报告异常信息,提示ClassNotFoundException或者FileNotFoundException,就检查对应的类是否存在,如果提示的类是依赖的jar包中的类,可能是jar包文件已经损坏,需要重新下载(可以删除.m2中的jar包或者更换版本)。

如果启动过程中报告的异常信息是ZipException,一定是某个jar包损坏,则检查最近添加的依赖的jar包并重新下载。

如果启动过程中报告的异常信息是LifeCycleException,通常是由于缓存问题导致的,则需要Clean项目及Tomcat。

关于GET和POST的区别

GET请求会将请求参数体现在URL中,POST请求会将请求参数封装在请求体中,并不会体现在URL中。

GET请求不适用于涉及隐私、安全方面的数据,也不适用于传输数据量较大的数据,通常限制值是2K,该值既取决于客户端的浏览器,也取决于服务器端。

所以,在涉及隐私、安全的数据提交,或者较大数据的提交(特别是上传文件),都应该优先考虑POST方式提交请求。

由于POST方法将请求参数封装在请求体中,没有体现在URL中,所以,如果涉及URL分享等操作,必须使用GET方式提交请求。

在发生请求时,如果使用GET请求,将一次性将请求的URL提交到服务器,所以,请求参数也就直接提交到了服务器,如果使用POST请求,会先向服务器提交第1次请求,此次请求并不携带请求参数,当服务器响应100后,客户端发出第2次请求,再将请求参数提交到服务器。所以,GET请求的访问速度比POST请求更快。

关于转发和重定向

无论是转发还是重定向,都是客户端请求的第1个目标无法实现请求的响应,需要配合服务器端的其它组件来完成响应!

转发的原因是因为使用控制器可以处理用户请求,但是,当得到数据结果后,存在不便于显示的问题,毕竟控制器是Java类,不便于组织HTML代码,所以,会将数据结果转发给JSP页面,由JSP页面来完成数据的呈现,当然,JSP也存在不便于处理数据逻辑的问题,即与HTML高度相似的代码结构中不便于编写Java代码,所以,推荐的做法就是控制器负责处理数据,得到数据结果后把这些数据结果转发给JSP,由JSP呈现,整个过程是发生在服务器内部的,对于客户端来说是不可见的,所以,在转发时,客户端只发出了1次请求,请求的URL就是控制器的URL,并且,即使WEB-INF文件夹的内容是不可以被客户端直接访问的,也不影响把JSP放在这个文件夹下。

重定向是客户端发出第1次请求后,服务器端无法完成最终的响应,所以,只能给出302(通常是302)响应码,让客户端请求另一个URL来完成最终响应,在整个过程中,客户端是发出了2次请求的,同时,客户端也明确第2次的请求目标,所以,在客户端的浏览器中,会显示第2次请求的URL,由于客户端共发出2次请求,所以,在没有经过特殊的处理方式时,第1次请求的数据不可以直接应用到第2次请求中。

解决乱码问题

计算机能够直接识别并处理的都是二进制数,也就是由0和1组成的序列,每个存储0或者1的空间称为“位(bit)”,由于每个二进制位只能存储1个0或者1个1,只能表达2种可能性,就不足以表示更多的内容,在计算机中,使用了更大的单位“字节(byte)”作为基本单位,每个字节由8个二进制位组成。

在ASCII编码表中指定了人类生活使用的字符与二进制数的对应关系,例如a对应的就是110 0001,假设输入了1个a,其实计算机处理的是110 0001,当计算机运算得到110 0001,就会显示为a。

由于ASCII编码表只制定了1个字节的对应关系,但是,中文的汉字种类太多,1个字节无法表达,就需要更多的字节数,例如使用2个字节,就可以表示更多种对应关系,Java语言在处理字符时,内存中就使用的Unicode编码。

当数据需要传输时,如果传输的是a,只需要1个字节就够了,如果传输的是中这个汉字,至少需要2个字节,所以,如果单纯直接传输二进制数的序列,接收方可能接收到1110 0001 1101 1100 1011 1010,却不知道如何进行分隔!所以,为了保证能够正确的分隔这些二进制的序列,就产生了传输编码,例如UTF-8。

在UTF-8中,如果某个字符是2个字节的,则使用的格式是:

1
110x xxxx   10xx xxxx

如果某个字符是3个字节的,则使用的格式是:

1
1110 xxxx   10xx xxxx   10xx xxxx

如果某个字符是4个字节的,则使用的格式是:

1
1111 0xxx   10xx xxxx   10xx xxxx   10xx xxxx

通常,UTF-8分为常用版本(utf8mb3)和包括了不常用字符的版本(utf8mb4),一般默认指的是常用版本。

当然,除了UTF-8以外,还有其它的编码格式,例如GBK、GB2312、ISO-8859-1、latin1等,不同的编码格式的编码规范是不相同的,甚至有些编码格式并不支持中文!所以,如果发出和接收使用的是不同的编码,甚至使用了不支持中文的编码,就会导致无法解读,进而出现乱码!

所以,乱码问题都是由于“使用的编码不统一”所导致的,而解决方案就是“使用统一的编码”,在项目中,常见的需要指定编码的位置有:项目的源代码、网络传输和接收的编码、界面、其它网络连接、数据库等存储位置……可以简单的理解为:只要能够指定编码的位置,统统指定同一种编码,就不会出现乱码,如果没有指定,就可能出现乱码。

如何快速的向数据表中插入已知的1000万条数据

假设这1000万条数据在1个List集合中,将这个集合进行遍历,循环1000万次,结合数据库编程技术,就可以将这些数据插入到数据库中。

这样做的缺陷:

  1. 在实际工作环境中,应用服务器(程序运行所在的Tomcat服务器)与MySQL数据库服务器并不是同一台服务器,当需要执行数据操作时,会由应用服务器将SQL语句发送到MySQL数据库服务器,发送过程中就需要建立网络连接,才可以发送SQL语句,如果使用以上原始做法,就需要连接1000万次,每次发送1条SQL语句,效率非常低下!
  2. 每次执行1条SQL语句之前,MySQL服务器还会对SQL语句进行词法分析、语义分析、编译等过程,才可以执行,假设有1000万条SQL语句,则这些词法分析、语义分析、编译等过程就需要经历1000万次!

针对问题1,可以使用批处理来解决,批处理可以一次性发送多条SQL语句到数据库服务器,减少传递SQL语句的次数,从而提高运行效率;

针对问题2,可以把INSERT INTO xx () VALUES ();这种语法调整为INSERT INTO xx () VALUES (值列表1), (值列表2), ..., (值列表N),这种做法可以使得1条SQL语句插入多条数据,假设每条SQL语句插入了1000条数据,则只需要1万条SQL语句即可!

所以,总的来说,可以使用每条SQL语句插入100条数据,批处理时每次发送1000条这样的SQL语句,整体循环100次,就可以完成所有数据的插入!

理论上来说,批处理时,不建议一次性处理超过5000条SQL语句。另外,每条SQL语句也不是插入越多数据就越快,拼接这样的SQL语句也是需要耗时的。

Spring-MVC

发表于 2019-07-20

Spring-MVC

1. SpringMVC的作用

SpringMVC主要解决了View-Controller交互的问题。

传统的Controller具体表现为一个个的Servlet类,在一个普通的项目中,需要实现的功能至少有50个以上,假设是50个,则项目中就需要创建50个Servlet类去处理这50个功能对应的请求,在web.xml中每个Servlet类至少需要8行代码进行配置,如果是一个更加复杂的系统,就会导致:Servlet实例过多,类文件太多,配置文件太长,所导致的代码开发难度和维护难度大的问题。另外,还存在Servlet或其它Java EE中的API功能较弱的问题。

2. SpringMVC中的核心组件

  • DispatcherServlet:前端控制器,用于接收所有请求,并进行分发;
  • HandlerMapping:记录了请求路径与实际处理请求的控制器之间的对应关系;
  • Controller:实际处理请求的控制器;
  • ModelAndView:控制器的处理结果,其中Model表示处理请求时得到的即将响应给客户端数据,View表示负责响应时显示的视图的名称;
  • ViewResolver:根据视图的名称,确定视图组件。

3. SpringMVC HelloWorld

3.1. 案例目标

在浏览器中,通过http://localhost:8080/项目名称/hello.do进行访问,页面中将显示“Hello, SpringMVC!!!”字样。

3.2. 创建项目

创建Maven Project,Packaging必须选择war。
创建完成后,在web.xml;添加spring-webmvc依赖;复制Spring的配置文件到项目中;添加Tomcat运行环境。

3.3. 通过DispatcherServlet接收所有请求

SpringMVC中已经定义好了DispatcherSerlvet类,如果需要该Servlet能够接收并处理请求,首先,就需要在web.xml中进行配置:

1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>

在配置时,如果不明确DispatcherServlet的包名,可以随意创建某个Java类,在类中声明该Servlet,由Eclipse完成导包,则import语句中就有了该类的全名。

至此,DispatcherServlet就可以接收所有以.do为后缀的请求。

默认情况下,所有的Servlet都是第一次接收并处理请求时才完成的初始化操作,并且,SpringMVC框架是基于Spring框架基础之上的,在项目启动初期,就应该加载整个Spring环境,以保证SpringMVC框架能正常运行!所以,可以通过配置去实现:当启动Tomcat时,就直接初始化DispatcherServlet,在DispatcherServlet中,已经定义了某个属性,值是Spring配置文件的位置,当DispatcherServlet被初始化时,就会读取该配置文件!

在配置时,可以通过<load-on-startup>使得DispatcherServlet是默认直接初始化的!

在DispatcherServlet的父类FrameworkServlet中,有contextConfigLocation属性,其作用就是制定Spring配置文件的位置,一旦创建了DispatcherServlet对象,就会自动读取所配置的文件,以加载Spring的环境!则配置为:

1
2
3
4
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>

DispatcherServlet的完整配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>

以上配置实现的效果就是:当Tomcat启动时,就会创建DispatcherServlet的对象,并且,DispatcherServlet会读取所配置的spring.xml文件,以加载Spring环境,后续,DispatcherServlet将接收所有以.do为后缀的请求!

验证现在的配置结果,可以自定义cn.tedu.spring.User类,然后,在类中显式的定义构造方法:

1
2
3
4
5
public class User {
public User() {
System.out.println("创建了User的对象!");
}
}

在Spring的配置文件中进行配置:

1
<bean class="cn.tedu.spring.User" />

由于Tomcat启动时就会初始化DispatcherServlet,进而加载Spring配置文件,就会由Spring框架创建User对象,导致构造方法被执行,所以,当启动Tomcat时,就会看到构造方法中输出的内容!

3.4. 使用控制器接收请求

首先创建cn.tedu.spring.HelloController控制器类,在类之前添加@Controller注解:

1
2
3
4
5
6
7
8
package cn.tedu.spring;

import org.springframework.stereotype.Controller;

@Controller
public class HelloController {

}

然后,在Spring的配置文件设置组件扫描:

1
2
<!-- 组件扫描 -->
<context:component-scan base-package="cn.tedu.spring" />

控制类中需要添加处理请求的方法,方法的设计原则是:

  1. 应该使用public权限;
  2. 暂时使用String作为返回值类型;
  3. 方法名称自由定义;
  4. 方法中暂时不添加参数。

方法之前添加@RequestMapping注解,用于配置请求路径,以确定请求路径与处理请求的方法的对应关系:

1
2
3
4
5
6
@RequestMapping("hello.do")
public String showHello() {
//测试代码
System.out.println("HelloController.showHello()");
return null;
}

打开浏览器,输入http://localhost:8080/SPRINGMVC-01/hello.do,在浏览器中会提示404错误,因为目前尚未处理页面,在Eclipse的控制台应该可以看到以上输出的内容!每访问一次,就会出现1次输出语句!

3.5. 响应页面

处理请求的方法的参数是String类型时,默认表示响应给客户端的视图的名称,后续,会经过ViewResolver进行处理,得到具体的视图。

首先,创建jsp页面,例如在WEB-INF下创建index.jsp文件,页面内容随意。

然后,设计处理请求的方法的返回值,并配置ViewResolver!ViewResolver是一个接口,具体使用的实现类是InternalResourceViewResolver,需要在Spring的配置文件中配置它的prefix和suffix属性,分别表示“前缀”和“后缀”,InternalResourceViewResolver的工作模式是以webapp文件夹为根路径,将“前缀 + 处理请求的方法的返回值 + 后缀”得到具体的JSP文件的位置,刚才创建的JSP文件在webapp/WEB-INF/index.jsp,则可以:

1
2
3
/WEB-INF/   +   index   +   .jsp

"" + /WEB-INF/index.jsp + ""

还可以是其他组合方式,只要能组合出文件的路径都是正确的,可以在处理请求的方法中返回"index",配置为:

1
2
3
4
5
<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/" />
<property name="suffix" value=".jsp" />
</bean>

开发完成,再次测试访问。

1. 使用Session

当需要向Session中存入数据时,可以使用ModelMap对象将数据进行封装,操作方式与封装转发的数据完全相同,例如:

1
modelMap.addAttribute("username", username);

需要在当前控制器类之前添加@SessionAttributes注解,并且,在注解中显式的指定ModelMap中封装的哪些数据是需要存储在Session中的,例如:

1
2
3
4
5
6
@Controller 
@RequestMapping(value="user")
@SessionAttributes("username")
public class UserController {
// ...
}

当添加了以上注解后,如果ModelMap中被存入了名为username的数据,该数据就在Session中,而ModelMap中的其它数据依然只能用于转发,也就是数据的作用域只在Request级别。

关于@SessionAttributes注解,其属性的配置可以参考该注解的源代码:

1
2
3
4
5
6
7
@AliasFor("names")
String[] value() default {};

@AliasFor("value")
String[] names() default {};

Class<?>[] types() default {};

value和names属性的作用是完全相同,用于指定ModelMap中的哪些名称对应的数据需要存放到Session中,可以使用字符串数组表示多个属性,另外,还可以配置types属性用于指定Session的数据的数据类型,也可以是数组类型,与配置的names保持一致即可。

使用这种做法操作Session非常简单,但是,也存在一系列的问题:

  1. 默认情况下,重定向时会把Session中的数据暴露在URL中;
  2. 通过ModelMap存放的数据一定会在Request的作用域中,所以,通过这种方式存放到Session中的数据,其实在Request中也是存在的;
  3. 通过这种方式存放到Session中的数据,不可以通过Session对象的invalidate()方法清除!只能通过SessionStatus类的setComplete()方法进行清除!

更加简单的操作Session的方式就是直接在处理请求的方法中添加HttpSession类型的参数,然后在方法体中直接操作即可,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RequestMapping("handle_login.do") 
public String handleLogin(String username, String password,
ModelMap modelMap, HttpSession session) {
// 日志
System.out.println("UserController.handleLogin()");
System.out.println("\tusername=" + username);
System.out.println("\tpassword=" + password);

// 判断用户名是否正确
if ("root".equals(username)) {
// 是:判断密码是否正确
if ("1234".equals(password)) {
// 是:登录成功,将用户名存入到Session
// modelMap.addAttribute("username", username);
session.setAttribute("username", username);
// 重定向到主页
return "redirect:../index.do";
} else {
// 否:密码错误
modelMap.addAttribute("errorMessage", "ModelMap:密码错误");
return "error";
}
} else {
// 否:用户名错误
modelMap.addAttribute("errorMessage", "ModelMap:用户名错误");
return "error";
}
}

使用这种做法并不存在以上使用@SessionAttributes时的各种问题,操作也非常简单,缺点就是不易于执行单元测试!

可以忽略“不易于执行单元测试”,甚至“不使用@SessionAttributes”的原因可能是:可以使用专门的测试工具去测试控制器,所以,在控制器中的方法本身是不需要执行单元测试的,甚至在大型项目中根本就不会使用Session,那各种使用方式都是不需要的!

2. SpringMVC的拦截器(Interceptor)

  • 如果项目中有多个请求需要执行相同的数据处理方案,就可以使用拦截器来实现。
  • 拦截器的作用并不一定是要把请求“拦截下来,不允许向后执行”,其主要特征是:若干种不同的请求都需要先执行拦截器中的代码,才可以向后执行。

当然,拦截器也确实具备“拦截”的功能,即:可以将请求拦截下来,不允许向后执行。

假设需要定义一个“登录拦截器”,实现“如果用户已经登录,则放行,如果未登录,则拦截,不允许向后执行”。

  1. 需要自定义cn.tedu.spring.LoginInterceptor拦截器类,实现HandlerInterceptor (低版本可以通过报错实现方法,高版本不会报错,需要手动实现方法)

  2. 拦截器需要在Spring的配置文件中进行配置才可以使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 配置拦截器链 -->
    <mvc:interceptors>
    <!-- 配置第1个拦截器 -->
    <mvc:interceptor>
    <!-- 拦截的路径 -->
    <mvc:mapping path="/index.do"/>
    <!-- 拦截器类 -->
    <bean class="cn.tedu.spring.LoginInterceptor" />
    </mvc:interceptor>
    </mvc:interceptors>
  3. 如果需要实现“验证登录以决定是否拦截或者放行”的功能,需要重写拦截器类中的preHandle()方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {
    System.out.println("LoginInterceptor.preHandle()");
    // 获取HttpSession对象
    HttpSession session = request.getSession();
    // 判断Session中有没有登录的数据
    if (session.getAttribute("username") == null) {
    // 没有登录数据,即未登录,则重定向到登录页
    // http://localhost:8080/XX/index.do
    // http://localhost:8080/XX/user/password.do
    String projectName = request.getServletContext().getContextPath();
    response.sendRedirect(projectName + "/user/login.do");
    // 返回false表示拦截,不允许向后执行
    return false;
    }
    // 返回true表示放行,允许向后执行
    return true;
    }

注意:即使调用了response.sendRedirect()进行重定向,如果要阻止继续向后运行,仍然需要return false;。

  • 在配置拦截器时,使用的是<mvc:mapping />配置需要拦截的路径,每个拦截器都可以配置1~N个该节点。
  • 在配置路径时,还可以使用*作为通配符,例如配置为<mvc:mapping path="/product/*" />,则表示例如/product/index.do、/product/add.do、/product/list.do等路径都会被拦截!但是,1个*只能通配1层资源,例如/product/*就不会匹配上/product/x/y.do,如果需要通配若干层路径,可以使用2个*,即配置为/product/**,使用2个*是无视层级的,无论是/product/list.do,还是/product/x/y.do,甚至更多层级的,都可以通配!
  • 另外,还可以添加<mvc:exclude-mapping />用于配置例外路径,也就是“白名单”,被添加在白名单中的路径将不被拦截器处理,与<mvc:mapping />的配置方式完全相同,可以有多个配置白名单的节点,在配置时,路径中也可以使用通配符。
  • 在配置整个<mvc:interceptor>节点时,其子级的节点必须先配置<mvc:mapping />,再配置<mvc:exclude-mapping />,最后配置<bean>,不可以颠倒顺序!

3. 在SpringMVC中统一处理异常

在Java中,异常的继承体系是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Throwable
Error
OutOfMemoryError
Exception
SQLException
IOException
FileNotFoundException
RuntimeException
NullPointerException
ClassCastException
ArithmeticException
NumberFormatException
IndexOutOfBoundsException
ArrayIndexOutOfBoundsException
StringIndexOutOfBoundsException

在Exception中,RuntimeException及其子孙类异常是比较特殊的异常,完全不受Java处理异常的语法约束,因为这些异常可能出现的频率极高,并且,这些异常是可以通过更加严谨的编程来杜绝异常的发生的!

常见的处理异常的做法是捕获(try…catch)或者声明抛出(throw/throws),在实际处理时,如果当前类适合处理异常,就应该使用try…catch捕获并处理,如果当前类不适合处理异常,则应该声明抛出,然后续调用这个方法的角色进行处理。

在服务器端的项目中,通常适合处理异常的都是控制器,但是,某些异常可能在多个不同的功能中都会出现,在处理不同的请求时采取相同的代码进行处理,就会导致代码冗余,不便于统一管理,所以,在SpringMVC框架中就提供了统一处理异常的机制。

可以在控制器类中添加统一处理异常的方法,关于该方法:

  1. 应该使用public权限;
  2. 返回值的意义与处理请求的方法完全相同;
  3. 方法名称可以自定义;
  4. 方法中必须包含异常类型的参数,且参数的类型能包括所有可能需要处理的异常,例如可能处理NullPointerException,则参数的类型可以是NullPointerException或者RuntimeException或者Exception甚至Throwable,如果同时还需要处理NumberFormatException,参数类型就不可以是NullPointerException,简单来说,写Throwable绝对错不了;
  5. 与处理请求的方法不同,不可以随心所欲的添加参数,如果需要转发数据,只能添加HttpServletRequest参数,不可以使用ModelMap;
  6. 必须添加@ExceptionHandler注解。

所以,处理请求的方法可以是:

1
2
3
4
5
6
7
8
9
10
11
12
@ExceptionHandler
public String handleException(Throwable ex, HttpServletRequest request) {
if (ex instanceof NullPointerException) {
request.setAttribute("errorMessage", "空指针异常!");
} else if (ex instanceof ArrayIndexOutOfBoundsException) {
request.setAttribute("errorMessage", "数据下标越界异常异常!");
} else {
request.setAttribute("errorMessage", "未知异常:" + ex.getClass().getName());
}

return "error";
}

一旦添加了该方法,当前类中任何处理请求的方法都不必处理相关异常,等同于这些方法把异常抛出了,将由以上方法进行统一处理!

需要注意的是:该方法只能处理当前类处理请求时出现的异常,如果其他控制器类的方法抛出了异常,是不会被处理的!可以把该处理异常的方法放在所有控制器类公共的父类中!

@ExceptionHandler 该注解可以指定需要被处理的异常的种类!参数可以是数组,即同时指定多种异常都将被该方法进行处理!

拦截器(Interceptor)和过滤器(Filter)的区别

拦截器和过滤器都是可以设置在若干种不同的请求处理之前的,都可以实现“拦截”和“放行”的做法,项目中,都可以存在若干个拦截器或者过滤器形成拦截器链或者过滤器链。

过滤器是JavaEE中的组件,拦截器是SpringMVC中的组件,只要是使用Java语言做服务器端开发都可以使用过滤器,但是,只有使用了SpringMVC框架才可以使用拦截器,并且,如果使用SpringMVC时,DispatcherServlet映射的路径是*.do,则只有以.do为后缀的请求才可能被拦截器处理,也就是说,只有被DispatcherServlet映射到的路径才可能被拦截器处理。

过滤器是执行在所有Servlet组件之前的,而拦截器是执行在DispatcherServlet之后、且在各Controller控制器之前及之后的组件!

过滤器是需要在web.xml中进行配置的,其过滤的路径只能通过<url-pattern>节点配置1个路径,配置非常不灵活,拦截器可以通过若干个<mvc:mapping />节点配置若干个黑名单,还可以通过若干个<mvc:exclude-mapping />节点配置若干个白名单,配置就非常灵活!

虽然拦截器和过滤器可以实现的的功能几乎相同,且拦截器的配置更加灵活,但是,由于执行时间节点的差异,拦截器也并不能完全取代过滤器!

Java-question-2

发表于 2019-07-18

晨考题

OOP

  1. 什么是方法的重载?构造方法可以重载吗?
  • 重载(Overload):发生在一个类中,方法名相同,参数列表不同,方法体不同,与返回值类型无关,重载遵循”编译期”绑定,构造方法可以重载。
  1. 什么是重写?

    • 重写(Override):
    1. 发生在父子类中,方法名称相同,参数列表相同,方法体不同
  2. 遵循”运行期绑定”,看对象的类型来调用方法

  3. this关键字的作用

    • this表示当前对象:
      • this.属性 区分成员变量和局部变量
      • this.() 调用本类的某个方法
      • this()表示调用本类构造方法,只能用在构造方法的第一行语句。
    • this关键字只能出现在非static修饰的代码中
  4. 抽象类与接口的区别

    • 接口是抽象类的变体,接口中所有的方法都是抽象的。而抽象类是声明方法的存在而不去实现它的类。
    • 接口可以多继承,抽象类不行
    • 接口定义方法,不能实现,而抽象类可以实现部分方法。
  • 接口中基本数据类型为static 而抽类象不是的。
  1. &&和&的区别?可举例说明。

    • &和&&都可以执行关系判断,
    • &: 按位与,当&操作符两边的表达式不是boolean类型时,a&b是把a和b都转换成二进制数然后再进行与的运算,不管前面的条件是否正确,后面都执行.
      • 4&7=100&111=100
  • &&:逻辑与,前面条件正确时,才执行后面,不正确时,就不执行,就效率而言,这个更好
  1. final关键字的用法。(具体说明)

    1. 修饰变量:变量不可被改变
    2. 修饰方法:方法不可被重写
    3. 修饰类:类不可被继承
  • final关键字修饰成员变量,意为初始化后不可改变。该成员变量必需在初始化时赋值,对象一旦创建即不可改变。可以在声明时初始化或在构造方法中进行初始化。
  1. 分别写出break、continue、return的作用是什么?

    • break: 常用在嵌套循环和判断语句中,作用是跳出当前循环(写在哪一层,就跳出哪一层)
    • continue: 跳过当前循环中的剩余语句而开始下一次的循环
    • return: 有返回值时,把方法的返回值返回给调用方,无返回值时,结束方法
  2. static 关键字的用法?

    1. 用来修饰成员变量,将其变为类的成员,从而实现所有对象对于该成员的共享。
    2. 用来修饰成员方法,将其变为类方法,可以直接使用“类名.方法名”的方式调用,常用于工具类,静态方法可以访问静态成员变量,不能访问非静态成员变量。
    3. 静态块用法,将多个类成员放在一起初始化,使得程序更加规整,其中理解对象的初始化过程非常关键。
    4. static final用来修饰成员变量和成员方法,可简单理解为“全局常量”。
  3. 请写出JAVA中的8大基本类型和其在内存中所占用的字节数

    1
    2
    3
    4
    5
    6
    7
    8
    byte	1字节
    short 2字节
    int 4字节
    long 8字节
    float 4字节
    double 8字节
    char 2字节
    boolean 1字节
  4. 面向对象三大特征:

    1. 封装:
      1. 类:封装的是对象的属性和行为
      2. 方法:封装的是具体的业务逻辑功能
      3. 访问控制修饰符:封装的是访问的权限
    2. 继承:
      1. 作用:代码的复用
      2. 父类:所有子类共有的属性和行为
        子类:子类所特有的属性和行为
      3. 子继承父后,子具有:父类+子类
      4. 单一继承、多接口实现,传递性
    3. 多态:
      1. 意义:行为的多态、对象的多态
      2. 向上造型、强制类型转换、instanceof
      3. 多态的表现形式:
        • 重写:主要是依据对象
        • 重载:主要是依据参数

SE

  1. 已知如下定义,String s = ”stary”,下面那个表达式是合法的?
    A. s += ”books”;
    B. char c = s[1];
    C. int len = s.length;
    D. String t = s.toLowerCase();

    答案:AD
    解析:
    选项B s为字符串对象,char c=s.toCharArray()[1];
    选项C int len=s.length();

  2. 对传入的字符串参数进行对比,如果是以 “db _ “ 开头的,则返回”匹配检验成功”,否则则返回”没找到” 。(写出主要代码即可)

    答案:

    1
    2
    3
    4
    5
    6
    7
    public String check(String str){
    if(str != null && str.startsWith("db_") ){
    return "匹配检验成功";
    }else{
    retrun "没找到";
    }
    }
  3. 已知表达式 int[] m= {0,1,2,3,4,5,6}; 下面哪个表达式的值与数组下标量总数相等?
    A .m.length() B.m.length C.m.length()+1 D.m.length+1

    答案:B
    解析:
    数组下标是从零开始的,但是数据下标的总量和数据长度相同。

  4. 如何将数值型字符转换为数字?
    参考答案:
    调用数值类型相应包装类中的方法parse”…” (String) 或 valueOf(String) 即可返回相应基本类型或包装类型数值

    • 例如:
    1. 如何将字串 String 转换成整数 int?

      1
      2
      int i = Integer.valueOf(str).intValue();
      或int i=Integer.parseInt(str);
    2. 如何将字串 String 转换成Integer ?

      1
      Integer integer=Integer.valueOf(str);
  5. 写一个线程,每隔10秒钟输出到控制台一个”hello world”。打印10次以后退出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Test extends Thread{
    public void run(){
    for(int i=0;i<10;i++){
    System.out.println("hello world");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

    }
    public static void main(String[] args) {
    Test t=new Test();
    t.start();
    }
    }
  1. 斐波那契数列,它指的是这样一个数列:1,1,2,3,5,8,13……用递归算法求前10项的系数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static void main(String[]args){
    System.out.println("系数前20项是:");
    for(int j=1;j<=10;j++){
    System.out.print(getFibo(j)+"\t");
    }
    }
    public static int getFibo(int i){
    if(i<=0){
    return 0;
    }else if(i==1||i==2){
    return 1;
    }else{
    return getFibo(i-1)+getFibo(i-2);
    }
    }

Spring-框架

发表于 2019-07-18

Spring

Spring简介

Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作Expert One-On-One J2EE Development and Design中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架。Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。

  • 通过设计模式中的工厂模式来创建对象:

    1
    2
    3
    4
    5
    public class UserDaoFactory {
    public static IUserDao newInstance() {
    return new UserDao2();
    }
    }
  • 然后,代码可以进一步调整为:

    1
    public IUserDao userDao = UserDaoFactory.newInstance();
  • 则后续需要使用IUserDao对象时,声明成以上代码即可,如果需要更换具体的实现类对象,也只需要修改工厂类中的返回值,类似以上声明的代码,无论在项目中出现过多少次,都是不需要调整的!

  • 所以,总的看来,通过工厂来创建对象,前期编写的代码会更多一些,但是,可以实现“解耦”的效果,后期的管理难度会大大的降低!
  • 在实际项目开发中,不可能为所有的类或者大量的类去设计专属工厂,而Spring框架就相当于一个大工厂,可以生产我们配置的、希望它去生产的所有对象!后续,当需要获取对象时,直接通过Spring获取即可,并不需要自己创建对象,所以,Spring框架也被称之为“Spring容器”。

1、Spring 示例:

创建Maven Project,Packaging选择war
创建出来的项目默认没有web.xml文件,作为WEB项目来说是错误的,所以,需要先生成该文件。
需要添加所以依赖的框架,在pom.xml中添加:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>

如果下载的jar包有问题,可以尝试换个版本,目前,对使用的Spring的要求是不低于4.2版本。

当项目创建完成后,下载http://doc.tedu.cn/config/spring-context.zip文件,将压缩包中的文件**applicationContext.xml**复制到项目中的**src/main/resources**中。

假设希望通过Spring创建java.util.Date类的对象,则应该在applicationContext.xml中添加配置:

1
2
3
<!-- id:当从Spring容器中获取该对象时使用的名称 -->
<!-- class:需要创建的类 -->
<bean id="date" class="java.util.Date"></bean>

通过单元测试查看配置效果,需要先添加单元测试的依赖:

1
2
3
4
5
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

然后,创建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestCase {
@Test
public void getDate() {
// 1. 加载Spring配置文件,获得Spring容器
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2. 从Spring容器中获取对象
Date date = (Date) ac.getBean("date");
// 3. 测试所获取的对象
System.out.println(date);
// 4. 关闭Spring容器,释放资源
ac.close();
}
}

通过Spring创建对象的方式

(a) 类中存在无参数的构造方法(实用,掌握)
以java.util.Date类为例,当Spring创建对象时,会自动调用其无参数构造方法。
在Spring的配置文件中,配置方式为:

1
<bean id="date" class="java.util.Date"></bean>

如果某个类中并不存在无参数的构造方法,则不可以通过这种方式进行配置。

(b) 类中存在静态工厂方法(不实用,了解)
如果某个类中存在某个static修饰的方法,且方法的返回值类型就是当前类的类型,例如java.util.Calendar类就是如此:

1
2
3
4
5
public abstract class Calendar {
public static Calendar getInstance() {
// ...
}
}

符合这种条件的类,可以配置为:

1
2
3
4
5
  <!-- (b) 类中存在静态工厂方法 -->
<!-- factory-method:工厂方法的名称 -->
<bean id="calendar" class="java.util.Calendar"
factory-method="getInstance">
</bean>

(c) 存在实例工厂方法(更不实用,了解)
在其他类中,存在某个工厂方法,可以创建指定的类的对象,例如:

1
2
3
4
5
6
7
8
9
10
public class Phone {
public Phone(String name) {
}
}

public class PhoneFactory {
public Phone newInstance() {
return new Phone("HuaWei");
}
}

当需要Spring创建Phone对象时,可以让Spring先创建PhoneFactory的对象(实例),然后调用该对象的newInstance()方法,即可获取Phone的对象,从而完成对象的创建过程。

具体配置为:

1
2
3
4
5
6
7
8
9
<!-- (c) 存在实例工厂方法 -->
<!-- factory-bean:配置为工厂类的bean-id -->
<bean id="phoneFactory" class="cn.tedu.spring.PhoneFactory">
</bean>
<bean id="phone"
class="cn.tedu.spring.Phone"
factory-bean="phoneFactory"
factory-method="newInstance">
</bean>

由Spring管理的对象的作用域与生命周期

(a) 由Spring管理的对象的作用域
单例:单一实例,在某个时刻,可以获取到的同一个类的对象将有且仅有1个,如果反复获取,并不会得到多个实例。
单例是一种设计模式,其代码格式可以是:

1
2
3
4
5
6
7
8
9
10
public class King {
private static King king = new King();

private King() {
}

public static King getInstance() {
return king;
}
}

以上单例模式也称之为“饿汉式单例模式”,其工作特性为“程序运行初期就已经创建了该类的实例,随时获取实例时,都可以直接获取”,还有另外一种单例模式是“懒汉式单例模式”,其工作特性为“不到逼不得已不会创建类的对象”,其代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class King {
private static final Object LOCK = new Object();
private static King king;
private King() {
}
public static King getInstance() {
if (king == null) {
synchronized (LOCK) {
if (king == null) {
king = new King();
}
}
}
return king;
}
}

  • 理论上来说,懒汉式的单例模式可能可以节省一部分性能消耗,例如整个系统运行了30分钟之后才获取对象的话,则前30分钟是不需要创建对象的!但是,这种优势只是理论上的优势,在实际应用时,2种单例模式的差异表现的并不明显!
  • 通过以上单例模式的设计方式,可以发现单例的对象都是使用了static修饰的!所以,具有“唯一、常驻”的特性!

在Spring管理对象时,可以配置lazy-init属性,以配置是否为懒汉式的单例模式:

1
2
3
4
5
<!-- lazy-init:是否懒加载,取值为true或false(默认) -->
<bean id="user"
class="cn.tedu.spring.User"
lazy-init="true">
</bean>

在Spring管理对象时,可以配置scope属性,以配置类的对象是否是单例的:

1
2
3
4
5
<!-- scope:是否单例,取值为singleton(默认,单例)或prototype(非单例) -->
<bean id="user"
class="cn.tedu.spring.User"
scope="prototype">
</bean>

(b) 由Spring管理的对象的生命周期

  • 生命周期描述了某个对象从创建到销毁的整个历程,在这个历程中,会被调用某些方法,这些方法就是生命周期方法,学习生命周期的意义在于了解这些方法的调用特征,例如 何时调用、调用次数,以至于开发功能时,可以将正确的代码重写在正确的方法中!
  • 以Servlet为例,其生命周期方法主要有init()、service()、destroy(),其中,init()和destroy()都是只执行1次的方法,而service()在每次接收到请求时都会被调用,且init()方法是创建对象之后就会执行的方法,destroy()是销毁对象的前一刻执行的方法,所以,如果有某个流对象需要初始化,初始化的代码写在destroy()方法中就是不合适的!反之,如果要销毁流对象,也不能把代码写在init()中!

由Spring管理的单例对象也并不是开发人员完全可控的,即不知道何时创建的,何时销毁的,所以,Spring提供了“允许自定义初始化方法和销毁方法”的做法,开发人员可以自行定义2个方法分别表示初始化方法和销毁方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {
public User() {
super();
System.out.println("创建User对象……");
}

public void init() {
System.out.println("初始化方法被调用……");
}

public void destroy() {
System.out.println("销毁方法被调用……");
}
}

配置:

1
2
3
4
5
6
7
<!-- init-method:初始化方法的名称 -->
<!-- destroy-method:销毁方法的名称 -->
<bean id="user"
class="cn.tedu.spring.User"
init-method="init"
destroy-method="destroy">
</bean>

注意:生命周期方法的配置是建立在“单例”基础之上的,如果对象并不是单例的,讨论生命周期就没有意义了。

2、Spring IOC

1、通过SET方式注入属性的值

假设存在:

1
2
3
4
5
6
7
8
public class UserDao {
//如果需要为`driver`注入值,首先需要为该属性添加SET方法
public String driver; // com.mysql.jdbc.Driver

public void setDriver(String driver) {
this.driver = driver;
}
}

Spring的配置文件中,添加<property>节点以进行配置:

1
2
3
<bean id="userDao" class="cn.tedu.spring.UserDao">
<property name="driver" value="com.mysql.jdbc.Driver" />
</bean>

另外,如果某个属性的值并不是基本值(在Spring中,把基本数据类型的值和字符串统一称为基本值),例如:

1
2
3
public class UserServlet {
public UserDao userDao;
}

以上属性的值应该是前序创建的UserDao类的对象,则,在配置时,可以通过<property>节点的ref属性引用那个<bean>的id:

1
2
3
<bean id="userServlet" class="cn.tedu.spring.UserServlet">
<property name="userDao" ref="userDao" />
</bean>

  • 综合来看,无论属性的值是什么类型,只要是通过SET方式注入属性值,首先都必须为属性添加SET方法,然后在<bean>节点下级通过<property>节点进行配置,如果属性的值是基本值,则使用value属性直接编写对应的值,如果属性的值不是基本值,则使用ref属性引用另一个<bean>的id(如果没有所说的另一个<bean>,就想办法配出这样一个<bean>)。

2、通过构造方法注入属性的值

假设存在:

1
2
3
4
5
6
7
8
public class AdminDao {
public String url;

public AdminDao(String url) {
super();
this.url = url;
}
}

Spring配置文件中:

1
2
3
4
5
<!-- 通过构造方法注入属性值 -->
<bean id="adminDao" class="cn.tedu.spring.AdminDao">
<!-- index:参数的序号,即第几个参数 -->
<constructor-arg index="0" value="jdbc:mysql://..." />
</bean>

小结

  • 通过SET方式注入必须为属性添加规范的SET方法,在配置时,使用<property>节点注入属性的值,该节点中,name属性可以理解为就是属性名称;
  • 通过构造方法注入必须自定义带参数的构造方法,且构造方法会基于参数为属性赋值;
  • 无论通过哪种方式,如果注入的值是基本值,通过value属性配置,如果注入的值是引用其他Bean,通过ref引用那个<bean>的id。
  • 通常推荐为绝大部分类提供无参数的构造方法,所以,通过SET方式注入是比较实用的做法,而通过构造方法注入的使用频率就非常低。

3、注入集合类型的属性值

1. List
假设存在:

1
2
3
// Frank, Andy, Lucy, Kate
//通过SET方式注入属性的值,需要先 生成SET方法
public List<String> names;

配置为:

1
2
3
4
5
6
7
8
9
<!-- 注入List类型的值:Frank, Andy, Lucy, Kate -->
<property name="names">
<list>
<value>Frank</value>
<value>Andy</value>
<value>Lucy</value>
<value>Kate</value>
</list>
</property>

Spring框架在处理时,会使用ArrayList封装List类型的属性的值。

2. Set
假设存在:

1
2
// Beijing, Shanghai, Guangzhou, Shenzhen
public Set<String> cities;

配置为:

1
2
3
4
5
6
7
8
9
<!-- 注入Set类型的值:Beijing, Shanghai, Guangzhou, Shenzhen -->
<property name="cities">
<set>
<value>Beijing</value>
<value>Shanghai</value>
<value>Guangzhou</value>
<value>Shenzhen</value>
</set>
</property>

Spring框架在处理时,会使用LinkedHashSet封装Set类型的属性的值。

3. 数组
假设存在:

1
2
// { 9, 5, 2, 7 } 
public int[] numbers;

配置为:

1
2
3
4
5
6
7
8
9
<!-- 注入数组类型的值:{ 9, 5, 2, 7 } -->
<property name="numbers">
<array>
<value>9</value>
<value>5</value>
<value>2</value>
<value>7</value>
</array>
</property>

在Spring框架中,注入List集合类型的值和数组类型的值时,使用<list>节点或者<array>节点都是可以的,即:数据是List类型的,使用<array>来配置,或者数据是数组类型的,使用<list>来配置,都是正确的。

4. Map
假设存在:

1
2
// username=root, password=1234, from=Hangzhou, age=26
public Map<String, String> session;

配置为:

1
2
3
4
5
6
7
8
9
<!-- 注入Map类型的值:username=root, password=1234, from=Hangzhou, age=26 -->
<property name="session">
<map>
<entry key="username" value="root" />
<entry key="password" value="1234" />
<entry key="from" value="Hangzhou" />
<entry key="age" value="26" />
</map>
</property>

5. Properties
在配置Properties类型的属性值时,其配置的节点结构是:

1
2
3
4
5
6
7
<!-- 注入Properties类型的值 -->
<property name="config">
<props>
<prop key="username">root</prop>
<prop key="password">1234</prop>
</props>
</property>

也可以准备.properties文件。创建db.properties文件:

1
2
3
4
url=jdbc:mysql://localhost:3306/db_name?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
driver=com.mysql.jdbc.Driver
username=root
password=1234

在Spring的配置文件中,使用<util:properties>节点就可以直接读取.properties文件:

1
2
<util:properties id="config" 
location="classpath:db.properties" />

以上<util:properties>节点也是一种<bean>节点,所以,注入值时,可以通过ref引用这个节点:

1
2
<!-- 注入Properties类型的值 -->
<property name="config" ref="config" />

Spring表达式

Spring表达式是使用占位符的方式定义在Spring的配置文件中的,作用是用于获取其他<bean>中的属性的值!

假设存在:

1
2
3
4
5
6
7
8
public class ValueBean {
// 值是SampleBean中的names中的第2个
public String name;
//通过SET方式注入,也可以通过构造方法注入
public void setName(String name) {
this.name = name;
}
}

在Spring的配置文件中进行配置:

1
2
3
4
5
6
<!-- 使用Spring表达式 -->
<bean id="valueBean" class="cn.tedu.spring.ValueBean">
<!-- 值是SampleBean中的names中的第2个 -->
<property name="name"
value="#{sampleBean.names[1]}" />
</bean>

  1. Spring表达式的基本格式是使用#{}进行占位,其内部语法是:

    1
    #{bean的id.属性}
  2. 如果属性的类型是List集合、Set集合或者数组,则在属性右侧使用[]写出索引或者下标:

    1
    #{bean的id.属性[索引或者下标]}
  3. 如果属性的类型是Map集合或者Properties,可以使用的语法:

    1
    2
    1. #{bean的id.属性.key}
    2. #{bean的id.属性['key']}

自动装配(autowire)

自动装配:不需要在Spring的配置文件中进行属性值的注入,只需要配置允许自动装配,Spring就会自动的完成属性值的注入。

假设存在StudentServlet依赖于StudentDao:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StudentServlet {
public StudentDao studentDao;

public void setStudentDao(StudentDao studentDao) {
this.studentDao = studentDao;
}

public void reg() {
System.out.println("StudentServlet.reg()");
studentDao.reg();
}
}

public class StudentDao {

public void reg() {
System.out.println("StudentDao.reg()");
}
}

配置为:

1
2
3
4
5
6
<bean id="studentDao"
class="cn.tedu.spring.StudentDao" />
<bean id="studentServlet"
class="cn.tedu.spring.StudentServlet"
autowire="byName">
</bean>

以上配置中,autowire属性就是用于配置自动装配的。

  • 当取值是byName时,表示“按照名称自动装配”,在这个过程中,Spring会先检测到在StudentServlet中有名为studentDao的属性,会根据该属性得到setStudentDao这个方法名称,然后,尝试找到与SET方法名称对应的bean的id,即查找某个id为studentDao的<bean>,如果找到,则自动调用setStudentDao方法来完成自动装配,如果没有找到匹配的bean-id,则不会尝试自动装配。简单的来说,就是SET方法的名称需要与bean-id相对应,属性的名称可以和bean-id不对应。自动装配是一种“尝试性”的操作,能装就装,装不上也不会报错!
  • 还可以取值为byType,表示“按照类型自动装配”,在这个过程,Spring会检测StudentServlet中以set作为前缀的方法,并尝试调用这些方法,调用时,会在Spring容器中查找与参数类型相符合的bean,如果没有匹配的对象,则不自动装配,如果找到1个,则执行该方法,以完成自动装配,如果找到2个或者更多,则直接报错错误。

Spring注解

1、 通用注解

使用注解的方式来管理对象时,就不必在Spring的配置文件中使用<bean>节点进行配置了,但是,需要先配置一项“组件扫描”,使得Spring框架知道需要管理的类在哪里:

1
2
<!-- 配置组件扫描的根包 -->
<context:component-scan base-package="cn.tedu.spring" />

为需要创建对象的类添加@Component注解即可:

1
2
3
@Component
public class UserDao {
}

“组件扫描 + 注解”就可以实现Spring创建对象!

在配置组件扫描时,base-package表示“根包”,假设类都在cn.tedu.spring包中,可以直接配置为这个包。例如实际使用的是cn.tedu.spring.dao、cn.tedu.spring.servlet等,可以把根包设置为cn.tedu.spring。!

关于使用的注解,可以是:

  • @Component:通用注解
  • @Controller:添加在控制器之前的注解
  • @Service:添加在业务类之前的注解
  • @Repository:添加在处理持久层的类之前的注解

    在配置Spring创建和管理对象时,在类之前添加以上4个注解中的任意1个即可,以上4个注解的作用相同,使用方式相同,语义不同。

在使用以上注解后,由Spring创建的对象的bean-id默认就是类名首字母改为小写的名称,例如UserDao类的默认bean-id就是userDao,如果需要自定义bean-id,可以对注解进行配置,例如:

1
2
3
@Component("dao")
public class UserDao {
}

2、关于作用域与生命周期的注解

  • 使用@Scope注解可以配置某类的对象是否是单例的,可以在该注解中配置属性为singleton或prototype,当配置为@Scope("prototype")时表示非单例的,如果希望是单例,则不需要配置该注解,默认就是单例的。
  • 在单例的前提下,默认是饿汉式的单例,如果希望是懒汉式的单例模式,可以在类之前添加@Lazy注解,在该注解中还可以配置true或false,例如@Lazy(false),但是,没有必要这样配置,如果希望是饿汉式的,根本就不需要添加该注解,如果希望是懒汉式的,只需要配置@Lazy即可,而不需要写成@Lazy(true)。

在被Spring管理的类中,可以自定义方法,作为初始化方法和销毁时调用的方法,需要添加@PostConstruct和@PreDestroy注解,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class UserDao {

public UserDao() {
System.out.println("创建UserDao的对象!");
}

@PostConstruct
public void init() {
System.out.println("UserDao.init()");
}

@PreDestroy
public void destroy() {
System.out.println("UserDao.destroy()");
}

}

以上2个注解是javax包中的注解,使用时,需要为项目添加Tomcat运行环境,以使用Java EE相关的jar包,才可以识别以上2个注解!

3.、自动装配

假设存在:

1
2
3
4
5
6
7
8
@Repository
public class UserDao {

public void reg() {
System.out.println("UserDao.reg()");
}

}

如果存在UserServlet需要依赖于以上UserDao,则在UserServlet中的属性之前添加@Autowired注解即可,例如:

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class UserServlet {

@Autowired
public UserDao userDao;

public void reg() {
System.out.println("UserServlet.reg()");
userDao.reg();
}

}

当然,以上2个类都必须是被Spring所管理的,即:都在组件扫描的包下,且都添加了@Component或等同功能的注解。

通过注解实现自动装配时,并不需要属性有SET方法!Spring框架就是将值直接赋值过去的!

使用@Resource注解也可以实现自动装配,它是Java EE中的注解,它的装配模式是:优先byName实现装配,如果装配失败,会尝试byType实现装配,且,如果byType装配,要求匹配类型的对象必须有且仅有1个,无论是0个还是超过1个,都会报告错误!

使用@Resource时还可以配置需要注入的bean的id,例如:

1
@Resource(name="ud1")

使用@Autowired时,会优先byType,如果找到1个匹配类型的对象,则直接装配,如果没有匹配类型的对象,则直接报告错误,如果找到多个匹配类型的对象,则会尝试byName,如果byName装配失败,则报告错误!

Java-reflect-Annotations

发表于 2019-07-17

反射,注解解析

目标:实现JUnit4的原型,执行某个类中标注了 @Test 注解的方法。
反射: Java提供的动态执行API
可以动态解析对象的类型,动态解析类的结构(属性、方法、构造器等)

案例1,动态检查对象的类型和其类的内部结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Demo03 {
public static void main(String[] args) {
test("123");
test(123);
test(123.0);
}

public static void test(Object obj) {
//obj变量的类型是Object,obj引用的对象是什么类型?
//在程序中检查 obj 引用的对象的类型?
//obj.getClass() 是java在Object类型上定义的
//方法, 可以动态获取当前对象的类型!
Class cls = obj.getClass();
System.out.println(cls);

//Class 类型上提供检查类的内部结构方法
//Field: 字段,就是类中声明的成员变量
//Declared: 声明的
// getDeclaredFields 获取cls类型的上声明的
// 全部成员变量,包含静态变量和实例变量
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
// Method: 方法
// getDeclaredMethods 返回cls类的定义的全部方法
Method[] methods = cls.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
//cls.getDeclaredConstructors() 全部构造器
System.out.println("------------");
//
}
}

案例2,利用反射API 动态加载类,动态创建对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo04 {
public static void main(String[] args)
throws Exception {
/**
* 动态加载类,动态创建对象
* 运行期间才加载类,创建对象
*/
Scanner in = new Scanner(System.in);
System.out.print("输入类名:");
String className = in.nextLine();
//根据类名className动态加载类
//如果类名错误,会出现类没有找到异常
Class cls = Class.forName(className);
System.out.println(cls);
//动态创建 cls 对应的对象,cls必须包含无参数构造器
//如果没有无参数构造器,则抛出 无此方法异常
Object obj = cls.newInstance();
System.out.println(obj);
}
}

案例3, 利用反射API找到方法上标注的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// Retention 用于声明注解 @Test 的保留范围
@Retention(RetentionPolicy.RUNTIME)
//Target 用于声明 注解的标注位置,当设置为METHOD以后
// 注解 @Test 就只能在方法上标注使用
@Target(ElementType.METHOD)
public @interface Test {

}

public class TestCase {

public void t() {
System.out.println("t");
}

@Test //自定义注解
public void testHello() {
System.out.println("Hello World!");
}

@Test
public void demo() {
System.out.println("demo!");
}
}


public class Demo05 {
public static void main(String[] args) throws Exception {
/**
* 利用反射API找到方法上标注的注解
*/
String className = "day12.TestCase";
//动态加载类
Class cls=Class.forName(className);
//动态获得类上声明的全部方法
Method [] methods=cls.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
//Method 的API,可以检测方法上标注的注解
Annotation[] anns=method.getAnnotations();
for (Annotation ann : anns) {
System.out.println(ann);
}
}
}
}

案例4,利用反射API动态执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Demo06 {
public static void main(String[] args)throws Exception{
/**
* 利用反射API动态执行方法
* 根据动态输入的类名和方法名, 利用反射API
* 加载类,找到方法,创建对象,调用方法。
*/
//动态输入类名和方法名
Scanner in = new Scanner(System.in);
System.out.print("输入类名:");
String className = in.nextLine();
System.out.print("输入方法名:");
String methodName=in.nextLine();
//加载类
Class cls = Class.forName(className);
//利用API在cls上找到要调用的方法
//getDeclaredMethod 在类上根据methodName寻找
//一个 Method 对象,如果找不到抛出异常
Method method=
cls.getDeclaredMethod(methodName);
//动态创建一个cls的对象
Object obj = cls.newInstance();
//执行方法, invoke 就是执行方法,但是必须提供
//包含方法的对象, 如果对象上不包含方法则抛出异常
Object val = method.invoke(obj);
System.out.println(val);
}
}

案例5, JUnit4 原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class JUnit4Demo07 {
public static void main(String[] args)throws Exception{
/**
* JUnit4 的原型: 执行某个类中标注了 @Test 注解的方法。
*/
Scanner in = new Scanner(System.in);
System.out.print("输入测试案例类名:");
String className = in.nextLine();
Class cls = Class.forName(className);
Method[] methods = cls.getDeclaredMethods();
Object obj = cls.newInstance();
for (Method method : methods) {
//查找方法上的某个注解getAnnotation(注解类)
//如果找到指定注解,则返回一个注解对象
//如果找不到指定注解,则返回null
Annotation ann=method.getAnnotation(Test.class);
if(ann==null) continue;
method.invoke(obj);
}
}
}

解析控制器上的注解

加载控制器类,读取每个标注了@RequestMapping的方法,并且读取注解上标注的URL
案例:

  1. 声明注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Retention(RUNTIME)
    @Target(METHOD)
    /**
    * 标注在控制器方法上
    * 用途:将请求url地址映射到当前的方法上
    * 英文翻译: Request 请求 Mapping 映射
    */
    public @interface RequestMapping {
    //为注解定义参数
    public String value();
    }
  2. 在控制器上标注注解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    /**
    * 控制器类, 用于封装业务功能,
    */
    public class BizController {
    /**
    * 第一个业务功能,Hello World!
    * @return 目标页面名称
    */
    public String execute(HttpServletRequest request) {
    System.out.println("Hello World!");
    request.setAttribute("msg", "Hello");
    return "hello";
    }
    /**
    * 负责处理 /test/list.do 显示员工列表
    * 在Web程序运行期间, 将/test/list.do请求映射到
    * list() 方法上,也就是请求/test/list.do时候,
    * 执行list()方法
    */
    @RequestMapping("/test/list.do")
    public String list(HttpServletRequest request) {
    return "list";
    }
    /**
    * 负责处理 /test/add.do 显示添加员工界面
    */
    @RequestMapping("/test/add.do")
    public String add(HttpServletRequest request) {
    return "add";
    }
    // ...
    }
  3. 利用Java反射API解析 @RequestMapping注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class Demo01 {
    public static void main(String[] args) throws Exception{
    /*
    * 解析 BizController类的@RequestMapping注解
    */
    String className="mvc.BizController";
    //动态加载类
    Class cls = Class.forName(className);
    //动态找到类的上全部方法
    Method[] methods=cls.getDeclaredMethods();
    //遍历全部方法,查找标注的 @RequestMapping 注解
    for(Method method:methods) {
    //利用RequestMapping.class类型找到的对象
    //是 RequestMapping 类型的对象,包含value方法
    RequestMapping ann=method.getAnnotation(
    RequestMapping.class);
    if(ann!=null) {
    //调用注解对象上的 value方法就能取到标注的值
    String val = ann.value();
    System.out.println(val);
    System.out.println(method);
    }
    }
    }
    }

解析控制器注解存储到Map

控制器RequestMapping注解的目的是将用户的请求映射到对应的业务方法:

RequestHandler

  1. Map 每次只能存储一对 key-value
    1. key 是请求路径
    2. value 包含 方法 和 包含方法对象
  2. 利用 RequestHandler 将包含方法的控制器对象和控制器方法封装为一个value,这样就可以将 控制器对象和控制器方法一起作为Value的存储到 map 对象中。
  3. 利用 map 存储 请求路径(key) 和 RequestHandler(控制器对象和方法),这样在根据请求路径查找对应的控制器和方法时候会很快找到。

案例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Handler 处理器
* 请求处理器,用处理用户请求
* 封装控制器对象和控制器上的业务方法
*/
public class RequestHandler {
private Object controller;
private Method method;

public RequestHandler() {
}

public RequestHandler(Object controller, Method method) {
super();
this.controller = controller;
this.method = method;
}

public Object getController() {
return controller;
}

public Method getMethod() {
return method;
}

@Override
public String toString() {
return "RequestHandler [controller=" + controller + ", method=" + method + "]";
}

}

HandlerMapping

用于解析 控制器 类将请求URL和控制器方法的对应关系缓存到Map中,并且提供根据url找到对应控制器方法,并且执行控制方法的功能
案例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class HandlerMapping {
/**
* map 用于映射 请求 path 到对应的请求处理器
* 如:将 /emp/list.do 映射到 bizColtroller.list()方法
*/
private Map<String, RequestHandler> map=new HashMap<>();

/**
* 用于根据 path 执行对应控制器方法
* @param path 请求路径
* @param request 执行控制器时候的参数
* @return 控制器执行以后的返回值,一般是目标JSP页面
*/
public String execute(String path, HttpServletRequest request) throws Exception {
RequestHandler handler = get(path);
Method method = handler.getMethod();
Object controller = handler.getController();
Object val = method.invoke(
controller, request);
String value = (String)val;
return value;
}

public RequestHandler get(String path) {
return map.get(path);
}

/**
* 初始化方法:解析控制器中的注解,将注解和注解
* 标注的方法添加到 map中
* @param className 控制器类名
*/
public void init(String className) throws Exception{
Class cls = Class.forName(className);
Method[] methods = cls.getDeclaredMethods();
Object controller = cls.newInstance();
for(Method method:methods) {
RequestMapping ann = method.getAnnotation(RequestMapping.class);
if(ann!=null) {
//找到注解上标注的路径
String path=ann.value();
//创建请求处理器对象,封装控制器对象和方法
RequestHandler handler=
new RequestHandler(
controller, method);
//请求路径和对应的“请求处理器”添加到map
map.put(path, handler);
System.out.println(path+":"+handler);
}
}
}
}

测试案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo02 {
public static void main(String[] args) throws Exception {
/**
* 测试 HandlerMapping的初始化方法是否可用
*/
HandlerMapping mapping = new HandlerMapping();
mapping.init("mvc.BizController");

//执行 /test/add.do 对应的方法
String val = mapping.execute("/test/add.do", null);
System.out.println(val);
}
}

增加 ContextConfigListener 加载配置文件

将配置控制器类名登记到XML文件中,这样可以灵活配置控制器的类名,以及控制器数量等

  1. 编写配置文件 resources/beans.xml:

    1
    2
    3
    4
    <?xml version="1.0" encoding="UTF-8"?>
    <beans>
    <bean class="mvc.BizController"></bean>
    </beans>
  2. ContextConfigListener

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
     /**
    * 初始化 URL 到 控制器 的映射表
    */
    public class ContextConfigListener implements ServletContextListener {
    /**
    * 读取 beans.xml 配置文件,
    * 解析配置文件中定义的控制器类
    * 将控制器类的url映射到对应的控制器方法
    * @param xml
    * @return 包含url到控制器方法映射关系的
    * HandlerMapping对象
    */
    public HandlerMapping loadControlers(String xml)
    throws Exception {
    SAXReader reader = new SAXReader();
    InputStream in = ContextConfigListener.class
    .getClassLoader().getResourceAsStream(xml);
    System.out.println(in);
    Document doc = reader.read(in);
    in.close();
    Element root = doc.getRootElement();//beans
    List<Element> list=root.elements("bean");
    HandlerMapping mapping = new HandlerMapping();
    for(Element e:list) {
    //在bean元素上获取class属性的值作为类名
    String className=e.attributeValue("class");
    System.out.println(className);
    //利用类名初始化 HandlerMapping 中的map
    mapping.init(className);
    }
    return mapping;
    }

    public void contextInitialized(
    ServletContextEvent e) {
    try {
    ServletContext ctx = e.getServletContext();
    //HandlerMapping mapping=new HandlerMapping();
    //mapping.init("mvc.BizController");
    HandlerMapping mapping=loadControlers("beans.xml");

    String path = ctx.getContextPath();
    ctx.setAttribute("root", path);

    ctx.setAttribute("handlerMapping", mapping);
    System.out.println("初始化了handlerMapping");
    }catch(Exception ex) {
    ex.printStackTrace();
    throw new RuntimeException(ex);
    }
    }
    public void contextDestroyed(ServletContextEvent arg0) {
    }

    }
  3. 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Demo04 {
    public static void main(String[] args)throws Exception {
    /**
    * 测试: 读取配置文件,初始化 HandlerMapping
    */
    ContextConfigListener l = new ContextConfigListener();
    HandlerMapping mapping=l.loadControlers("beans.xml");
    }
    }
  4. 配置监听器:

    1
    2
    3
    <listener>
    <listener-class>mvc.ContextConfigListener</listener-class>
    </listener>

重构DispatcherServlet

根据用户的URL请求执行对应的 控制器,处理软件功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 单一前端控制器
* 负责接收请求,处理与HTTP协议有关逻辑
* 同时处理 get 和 post请求
* 为了增加广泛的实用性, 可以处理任何的*.do 请求
* 将 请求URL设置为 *.do
*/
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
//创建 HandlerMapping 对象
//找到对应的业务方法,执行业务方法
//HandlerMapping mapping = new HandlerMapping();
//mapping.init("mvc.BizController");

//从ServletContext获取已经创建并初始化完成的
//HandlerMapping 对象,这样可以避免每次都初始化
//可以提高软件的性能
HandlerMapping mapping=(HandlerMapping)
getServletContext().getAttribute("handlerMapping");

//获取用户发起的请求
String pth = request.getServletPath();
System.out.println(pth);

//处理编码问题
request.setCharacterEncoding("UTF-8");

//执行 URL 路径对应的业务方法
String target=mapping.execute(pth, request);
//target代表需要显示是目标网页,
//约定target是以 redirect:为前缀
//则进行重定向,如果控制器返回的字符串
//以 redirect: 为开头,则重定向到
//redirect: 以后的URL地址
if(target.startsWith("redirect:")) {
String path = target.substring(9);
response.sendRedirect(path);
}else {
String path = "/WEB-INF/jsp/"+target+".jsp";
request.getRequestDispatcher(path).forward(request, response);
}
}catch(Exception e) {
e.printStackTrace();
//抛出一个 ServletException,这个异常是
//作用是,将异常e抛给Web容器,Web容器会
//显示 500 错误页面到浏览器
throw new ServletException(e);
}
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

}

配置:

1
2
3
4
5
6
7
8
9
10
<servlet>
<description></description>
<display-name>DispatcherServlet</display-name>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>mvc.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>

添加控制器方法并且测试:

1
2
3
4
5
@RequestMapping("/test/hello.do")
public String hello(HttpServletRequest request) {
request.setAttribute("msg", "HI");
return "hello";
}

Smart-MVC

发表于 2019-07-17

SmartMVC 是Spring MVC 框架克隆版。
设计目标: 将复杂的Web编程封装起来,使开发更加快速便捷。

控制器与业务功能拆分:

  1. 创建前端控制器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class DispatcherServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //检查request\response引用的对象类型
    Class cls1 = request.getClass();
    Class cls2 = response.getClass();
    System.out.println(cls1);
    System.out.println(cls2);

    //调用业务方法,根据业务方法的返回值转发到JSP
    BizController controller=new BizController();
    //执行业务方法,得到转发的目标页面名称
    String target = controller.execute(request);
    //将目标页面名称前后增加 JSP文件的位置和后缀
    String path = "/WEB-INF/jsp/"+target+".jsp";
    request.getRequestDispatcher(path)
    .forward(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    doGet(request, response);
    }
    }
  2. 配置 web.xml,使前端控制器可以处理任何 * .do 请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <servlet>
    <description></description>
    <display-name>DispatcherServlet</display-name>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>mvc.DispatcherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <url-pattern>*.do</url-pattern>
    </servlet-mapping>
  3. 编写控制器类,封装业务功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 控制器类, 用于封装业务功能,
    */
    public class BizController {
    /**
    * 第一个业务功能,Hello World!
    * @return 目标页面名称
    */
    public String execute(HttpServletRequest request) {
    System.out.println("Hello World!");
    request.setAttribute("msg", "Hello");
    return "hello";
    }
    }
  4. 编写 /WEB-INF/jsp/hello.jsp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <%@ page 
    language="java"
    contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Hello</title>
    </head>
    <body>
    <h1>Hello World!</h1>
    <p>${msg}</p>
    </body>
    </html>
  5. 测试:

    1
    http://localhost:8080/Servlet12/test.do

JSP-case-5

发表于 2019-07-16

重构员工项目

  1. 添加 emp.EmpContoller 控制器类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    public class EmpController {
    @RequestMapping("/emp/list.do")
    public String list(HttpServletRequest request) {
    EmpDao dao = new EmpDao();
    List<Emp> list = dao.findAll();
    Map<Integer, String> names=new HashMap<>();
    for (Emp emp : list) {
    names.put(emp.getEmpno(), emp.getEname());
    }
    //利用request对象将数据共享到JSP页面
    request.setAttribute("list", list);
    request.setAttribute("names", names);
    //转发的员工列表页面
    return "list-emp2";
    }

    @RequestMapping("/emp/add.do")
    public String add(HttpServletRequest request) {
    EmpDao dao = new EmpDao();
    List<Emp> mgrs = dao.findMgrs();
    request.setAttribute("mgrs", mgrs);
    return "add-emp";
    }
    @RequestMapping("/emp/save.do")
    public String save(HttpServletRequest request) {
    try {
    request.setCharacterEncoding("UTF-8");
    String ename=request.getParameter("ename");
    String mgrId=request.getParameter("mgr");
    String date =request.getParameter("hiredate");
    String deptId=request.getParameter("deptno");
    String salarys=request.getParameter("salary");
    String comms = request.getParameter("comm");

    //数据类型转换: 将字符串转换为目标数据类型
    SimpleDateFormat fmt= new SimpleDateFormat("yyyy-MM-dd");
    int mgr = Integer.parseInt(mgrId);
    Date hiredate = new Date(fmt.parse(date).getTime());
    int deptno = Integer.parseInt(deptId);
    double salary = Double.parseDouble(salarys);
    double comm = Double.parseDouble(comms);
    //保存到数据库
    Emp emp = new Emp(0, ename, mgr, hiredate, deptno, salary, comm);
    EmpDao dao = new EmpDao();
    int n = dao.save(emp);
    if(n==1) {
    //设置绝对重定向路径
    String url= request.getContextPath()+"/emp/list.do";
    return "redirect:"+url;
    }else {
    //失败
    request.setAttribute("message", "添加失败!");
    return "message";
    }
    }catch(Exception e) {
    e.printStackTrace();
    throw new RuntimeException(e);
    }
    }
    @RequestMapping("/emp/delete.do")
    public String delete(HttpServletRequest request) {
    String no = request.getParameter("empno");
    //请自行打桩测试接收到的参数是否有效
    int empno = Integer.parseInt(no);
    //调用EmpDao删除数据
    EmpDao dao = new EmpDao();
    int n = dao.delete(empno);
    if(n==1) {
    //重定向到 员工列表页面, 显示结果
    String path=request.getContextPath()+"/emp/list.do";
    return "redirect:"+path;
    } else {
    //转发到message.jsp显示错误消息
    request.setAttribute("message", "删除失败");
    return "message";
    }

    }
    @RequestMapping("/emp/edit.do")
    public String edit(HttpServletRequest request) {
    String no = request.getParameter("empno");
    int empno = Integer.parseInt(no);
    EmpDao dao = new EmpDao();
    Emp emp = dao.findByEmpno(empno);

    List<Emp> mgrs = dao.findMgrs();

    request.setAttribute("mgrs", mgrs);
    request.setAttribute("emp", emp);

    return "edit-emp";
    }
    @RequestMapping("/emp/update.do")
    public String update(HttpServletRequest request) {
    try {
    request.setCharacterEncoding("UTF-8");
    String no = request.getParameter("empno");
    String ename=request.getParameter("ename");
    String mgrno=request.getParameter("mgr");
    String hire=request.getParameter("hiredate");
    String dept=request.getParameter("deptno");
    String sly =request.getParameter("salary");
    String com =request.getParameter("comm");
    int empno = Integer.parseInt(no);
    int mgr = Integer.parseInt(mgrno);
    //java.sql.Date 提供了将字符串转换为日期的方法
    Date hiredate = Date.valueOf(hire);
    int deptno = Integer.parseInt(dept);
    double salary = Double.parseDouble(sly);
    double comm = Double.parseDouble(com);

    Emp emp = new Emp(empno, ename, mgr, hiredate, deptno, salary, comm);
    EmpDao dao = new EmpDao();
    //更新数据
    int n = dao.updateEmp(emp);
    if(n==1) {
    String path=request.getContextPath()+"/emp/list.do";
    return "redirect:"+path;
    } else {
    request.setAttribute("message", "更新失败!");
    return "message";
    }
    } catch (Exception e) {
    e.printStackTrace();
    throw new RuntimeException(e);
    }
    }
    }
  2. 添加 emp.UserContoller 控制器类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public class UserController {
    @RequestMapping("/user/login.do")
    public String login(HttpServletRequest request) {
    //读取表单参数
    String name = request.getParameter("name");
    String password = request.getParameter("password");
    String save = request.getParameter("save");

    // 访问数据层,检查用户信息
    UserDao dao = new UserDao();
    User user = dao.findUserByName(name.trim());

    //将用户名保存在 request中
    request.setAttribute("name",name);

    //String contextPath = request.getContextPath();
    //request.setAttribute("root", contextPath);

    //如果没有找到用户信息,表示用户名是错误的
    if(user==null) {
    //转回到登录页面显示错误消息
    request.setAttribute("message", "用户名或者密码错误");
    return "login";
    }
    //找到用户信息,则判断密码是否正确
    //哪个变量不可能为null放到前面
    if( user.getName().equals(name) &&
    user.getPassword().equals(password)) {
    //如果密码相等, 则可以登录
    //将登录结果保存在 session
    HttpSession session = request.getSession();
    session.setAttribute("loginUser", user);

    //转发到消息页面显示登录成功消息
    request.setAttribute("message", "登录成功");
    return "message";
    }
    //执行到这个位置?密码不一致的情形
    request.setAttribute("message", "用户名或者密码错误");
    return "login";
    }
    @RequestMapping("/user/start-login.do")
    public String startLogin(HttpServletRequest request) {
    return "login";
    }
    }
  3. 配置 beans.xml

    1
    2
    3
    4
    5
    6
    <?xml version="1.0" encoding="UTF-8"?>
    <beans>
    <bean class="mvc.BizController"></bean>
    <bean class="emp.EmpController"></bean>
    <bean class="emp.UserController"></bean>
    </beans>
  4. 更新 所有JSP页面,将链接增加 .do

  5. 更新 AccessFilter
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    /**
    * 访问控制过滤器:只有登录的用户才能通过访问
    */
    public class AccessFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request,ServletResponse response, FilterChain chain)throws IOException, ServletException {
    //转换request和response对象的类型,这样可以
    //使用更多的API方法
    HttpServletRequest req=(HttpServletRequest)request;
    HttpServletResponse res=(HttpServletResponse)response;
    HttpSession session = req.getSession();
    User loginUser = (User)session.getAttribute("loginUser");
    if(loginUser==null) {
    System.out.println("没有登录,转到登录");
    //没有登录,重定向到登录页面,不执行后续链节
    String login=req.getContextPath()+"/user/start-login.do";
    res.sendRedirect(login);
    return; //不执行后续链节
    }
    //执行后续链节,就是执行后续的Servlet
    System.out.println("登录成功,继续执行");
    chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
    }

    @Override
    public void destroy() {
    }
    }

Servlet-Filter-2

发表于 2019-07-16

监听器

1、什么是监听器?

Java EE 设计了一系列回调函数接口,在Web服务器的特定工作节点时候,执行回调方法。 利用这些方法可以在服务器特定工作点插入功能。

监听器是Servlet规范中定义的一种特殊类,用于监听ServletContext、HttpSession和ServletRequest等域对象的创建和销毁事件,它还可以监听域对象的属性发生修改的事件,可以在事件发生前或者发生后做一些必要的处理。

在Servlet中要创建监听器类首先需要新建一个类并继承相应的监听器接口,实现接口中定义的方法,然后在web.xml文件中注册相应的监听器即可。如果一个web.xml文件中注册了多个监听器,则监听器的启动顺序按照在web.xml中的注册顺序启动。如果一个web.xml文件中同时定义了监听器、过滤器和Servlet,那么web容器会先加载监听器、再加载过滤器最后加载Servlet。

示例:
利用监听器检测 ServletContext 对象何时创建,何时销毁:
使用监听器, ServletContextListener 监听器的使用。

  1. 实现 ServletContextListener 接口;
  2. 在web.xml 中配置 Listener,使监听器生效。

  3. 创建监听器对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ContextDemoListener implements ServletContextListener{
    // context 上下文, Initialized 初始化完成了
    // 此方法会在 ServletContext对象初始化完成以后执行
    @Override
    public void contextInitialized(ServletContextEvent e) {
    System.out.println("初始化完成");
    }
    // Destroyed 销毁完成了
    // 此方法会在ServletContext对象销毁完成以后执行
    @Override
    public void contextDestroyed(ServletContextEvent e) {
    System.out.println("销毁以后");
    }
    }
  4. 配置 web.xml

    1
    2
    3
    <listener>
    <listener-class>day11.ContextDemoListener</listener-class>
    </listener>
  5. 测试: 由于 ServletContext对象在容器启动时候创建,在容器关闭时候销毁,所以在容器启动时候和关闭时候能够看到打桩语句的输出。
    ServletContextListener 可以用于初始化或者销毁全局资源。

  6. 如初始化:在ServletContext对象初始化以后,立即绑定 root=ContextPath 更加合理。
  7. 如销毁:在ServletContext对象销毁以后,关闭数据库连接池,保证数据库连接池可靠关闭所有连接。

  8. 创建Listener

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * 初始化和销毁应用程序的全局资源
    * 1. ServletContext 创建时候 初始化 root=ContextPath
    * 2. ServletContext 销毁时候 关闭数据库连接池
    */
    public class ResourceInitDestroyListener implements ServletContextListener {
    public void contextInitialized(ServletContextEvent e) {
    ServletContext ctx = e.getServletContext();
    String contextPath = ctx.getContextPath();
    ctx.setAttribute("root", contextPath);
    System.out.println("初始化了 root="+contextPath);
    }

    public void contextDestroyed(ServletContextEvent e) {
    //关闭数据库连接池
    DBUtil.close();
    System.out.println("关闭了数据库连接池!");
    }
    }
  9. 配置 web.xml

    1
    2
    3
    <listener>
    <listener-class>day11.ResourceInitDestroyListener</listener-class>
    </listener>

Servlet线程安全问题

  1. Servlet对象是单例的,其service(包含被其调用的doGet、doPost)方法是被Tomcat线程并发调用的。
  2. service方法是存在线程安全问题。
  3. 一般情况下,不要在Servlet中定义可读写变量!保持Servlet是无状态的类。 “定义无状态的Servlet”
    1. 无状态的类: 没有可以读写变量的类称为无状态类!可以有只读变量。
  4. 如果定义了有状态的Servlet,进行适当同步(加锁),解决线程安全问题。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SetNameServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

private String name;
/**
* 线程并发安全性演示
*/
protected void doGet(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
//获取执行当前doGet方法的线程,这个线程
//由Tomcat的线程池管理,用于处理当前http请求
Thread t = Thread.currentThread();
System.out.println(t);

//解决线程安全问题
synchronized (this) {
String n = request.getParameter("n");
name = n;

try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("name:"+name);
}
}
}

测试:打开两个浏览器:

1
2
localhost:8080/Servlet/SetNameServlet?name=Tom
//name分别输入不同的值

Servlet-Filter

发表于 2019-07-15

过滤器

  • JSP 和 Servlet 中的过滤器都是 Java 类。
  • 过滤器可以动态地拦截请求和响应,以变换或使用包含在请求或响应中的信息。
  • 可以将一个或多个过滤器附加到一个 Servlet 或一组 Servlet。过滤器也可以附加到 JSP 文件和 HTML 页面。
  • 过滤器是可用于 Servlet 编程的 Java 类,可以实现以下目的:
    • 在客户端的请求访问后端资源之前,拦截这些请求。
    • 在服务器的响应发送回客户端之前,处理这些响应。
  • 过滤器通过 Web 部署描述符(web.xml)中的 XML 标签来声明,然后映射到您的应用程序的部署描述符中的 Servlet 名称或 URL 模式。
  • 当 Web 容器启动 Web 应用程序时,它会为您在部署描述符中声明的每一个过滤器创建一个实例。
  • Filter 的执行顺序与在 web.xml 配置文件中的配置顺序一致,一般把 Filter 配置在所有的 Servlet 之前。

1、过滤器的一个实例

  • 利用Filter可以实现如:权限检查、编码过滤、请求日志等功能
  1. 编写Servlet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class TargetServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    /**
    * 被过滤器调用的目标 (Target)Servlet
    * 请求URL /target
    */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("Target");
    response.setContentType("text/html;charset=UTF-8");
    response.getWriter().println("OK");
    }
    }
  2. 编写 Filter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class DemoFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {
    //chain 链节、链
    System.out.println("Hello");
    //执行后续的链节,就是执行后续的 Servlet
    chain.doFilter(request, response);
    System.out.println("World!");
    }
    @Override
    public void destroy() {
    }
    @Override
    public void init(FilterConfig arg0) throws ServletException {
    }
    }
  3. 配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <servlet>
    <description></description>
    <display-name>TargetServlet</display-name>
    <servlet-name>TargetServlet</servlet-name>
    <servlet-class>day10.TargetServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>TargetServlet</servlet-name>
    <url-pattern>/target</url-pattern>
    </servlet-mapping>
    <!-- 配置过滤器 -->
    <filter>
    <filter-name>demo</filter-name>
    <filter-class>day10.DemoFilter</filter-class>
    </filter>
    <filter-mapping>
    <filter-name>demo</filter-name>
    <url-pattern>/target</url-pattern>
    </filter-mapping>
  4. 测试:

    1
    http://localhost:8080/Servlet10/target

2、过滤器的生命周期

过滤器的生命周期分为四个阶段:实例化、初始化、过滤和销毁:实例化是指在Web工程的web.xml文件里声明一个过滤器,在声明了过滤器之后,Web容器会创建一个过滤器的实例。

  • Servlet对象从创建到使用,以及最后销毁的过程 实例:
  1. 创建Servlet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class InitRootServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    public void init() throws ServletException {
    ServletContext ctx=getServletContext();
    String contextPath = ctx.getContextPath();
    //将ContextPath存储到ServletContext中
    ctx.setAttribute("root", contextPath);
    System.out.println("初始化了");
    }
    @Override
    public void destroy() {
    System.out.println("销毁!");
    }
    /**
    * 在 ServletContext中初始化 root 变量
    * 请求URL: /init-root
    */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");
    response.getWriter().print("OK");
    }
    }
  2. 配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <servlet>
    <description></description>
    <display-name>InitRootServlet</display-name>
    <servlet-name>InitRootServlet</servlet-name>
    <servlet-class>day11.InitRootServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
    <servlet-name>InitRootServlet</servlet-name>
    <url-pattern>/init-root</url-pattern>
    </servlet-mapping>

注意: 设置1可以在Web容器启动时候执行Servlet init()方法。 实现容器启动时候初始化ContextPath。

  1. 删除所有Servlet中初始化 ContextPath 的代码。
  2. 启动应用测试软件功能,在控制台观察打桩输出结果。
  • JSP 也是Servlet,其生命周期类似
    1. JSP在第一次请求时候先翻译为Servlet 源文件
    2. 然后编译为.class
    3. 与Servlet生命周期相同

3、过滤器的常用方法

  • 过滤器最常用的方法有三个:init()、doFilter()和destory()。
  1. init()方法:这是过滤器的初始化方法,在Web容器创建了过滤器实例之后将调用这个方法进行一些初始化的操作,这个方法可以读取web.xml中为过滤器定义的一些初始化参数。
  2. doFilter()方法:这是过滤器的核心方法,会执行实际的过滤操作,当用户访问与过滤器关联的URL时,Web容器会先调用过滤器的doFilter方法进行过滤。
  3. destory()方法:这是Web容器在销毁过滤器实例前调用的方法,主要用来释放过滤器的资源等。

过滤器解决授权问题(权限检查)

目标: 对于 / emp / * 等路径的请求进行拦截,如果登录了才能使用。
Servlet、Filter的路径匹配规则:

  1. 精确匹配:精确匹配一个具体的URL
    1. /emp/list
    2. /emp/add
  2. 路径匹配:匹配路径下一系列URL
    1. / emp / *
    2. / user / *
    3. / dept / *
  3. 后缀匹配或扩展名匹配: 满足后缀条件的被匹配上
      • .html
      • .do
      • .png
  4. 任意匹配: 全部都匹配
    1. /
    2. / *

案例步骤:

  1. 添加过滤器检查权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * 访问控制过滤器:只有登录的用户才能通过访问
    */
    public class AccessFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request,ServletResponse response, FilterChain chain)throws IOException, ServletException {
    //转换request和response对象的类型,这样可以
    //使用更多的API方法
    HttpServletRequest req=(HttpServletRequest)request;
    HttpServletResponse res=(HttpServletResponse)response;
    HttpSession session = req.getSession();
    User loginUser = (User)session.getAttribute("loginUser");
    if(loginUser==null) {
    System.out.println("没有登录,转到登录");
    //没有登录,重定向到登录页面,不执行后续链节
    String login=req.getContextPath()+"/user/start-login";
    res.sendRedirect(login);
    return; //不执行后续链节
    }
    //执行后续链节,就是执行后续的Servlet
    System.out.println("登录成功,继续执行");
    chain.doFilter(req, res);
    }
    @Override
    public void init(FilterConfig arg0) throws ServletException {
    }
    @Override
    public void destroy() {
    }
    }
  2. 配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <filter>
    <filter-name>acl</filter-name>
    <filter-class>day11.AccessFilter</filter-class>
    </filter>
    <filter-mapping>
    <filter-name>acl</filter-name>
    <url-pattern>/emp/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
    <filter-name>acl</filter-name>
    <url-pattern>/dept/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
    <filter-name>acl</filter-name>
    <url-pattern>/photo/*</url-pattern>
    </filter-mapping>
  3. 测试

过滤器可以叠加使用,方便灵活的重构

1
2
3
4
5
6
7
8
9
10
11
12
<filter>
<filter-name>demo</filter-name>
<filter-class>day10.DemoFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>demo</filter-name>
<url-pattern>/target</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>demo</filter-name>
<url-pattern>/emp/*</url-pattern>
</filter-mapping>

ServletContext/Application

  • ServletContext: Servlet上下文,也就是当前Servlet的运行环境,就代表当前的容器对象,ServletContext API方法提供了与当前容器有关的方法。
  • SertvletContext提供了数据共享功能,其共享范围是全部的Servlet和JSP

案例 获取当前服务器的版本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ContextDemoServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* 演示 ServletContext 的功能
* 请求URL /context-demo
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/*
* 1. 获得ServletContext: getServletContext()方法是在HttpServlet类上定义
* 调用这个方法可以获得ServletContext对象, ServletContext对象是单例对象
* 任何方法得到的ServletContext对象都是同一个对象
*/
ServletContext ctx = getServletContext();
/*
* ServletContext 代表Servlet的工作环境,也就是代表当前的Web Server
* getServerInfo() 方法可以获得当前服务器的 名称和版本
*/
String str = ctx.getServerInfo();
System.out.println(str);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().print("OK");
}
}

数据共享范围(scope)

共享数据方法: setAttribute, getAttribute,removeAttribute

  1. pageContext 可以共享数据
  2. request 可以共享数据
  3. session 可以共享数据
  4. servletContext == application 可以共享数据
    如上4种对象其共享数据范围不同。

  5. pageContext 简称 page 范围,其共享范围仅限于 JSP 页面内部,相当于JSP内部的局部变量。

  6. request 范围,是一次请求过程范围
  7. session 范围,是一次用户会话过程范围,浏览器不关闭与服务器的多次请求的范围。
  8. application 范围,是最大的共享范围,一个Web应用的全体Servlet、JSP共享的范围。 适合保存公共静态数据。

使用建议:

  1. 数据共享范围越小越好,最常用的是 request
  2. 与当前用户有关的数据,共享到 session,比如用户登录状态
  3. 全局数据,可以共享到 application

EL表达式,会自动的按照由小到大搜索数据:

1
2
3
4
5
${root} 检索顺序
pageContext.getAttribute("root");
request.getAttribute("root");
session.getAttribute("root");
application.getAttribute("root");

利用session数据范围,在页面上显示登录用户信息:
更新header.jsp:

1
${loginUser.name}

Filter流程总述:

  • 请求发起时,Web容器先判断是否存在过滤器和这个请求的资源相关,如果有存在关联就把请求交给过滤器去处理,在过滤器中可以对请求的内容做出改变,然后再将请求转交给被请求的资源。当被请求的资源做出响应时,Web容器同样会将响应先转发给过滤器,在过滤器中可以对响应做出处理然后再将响应发送给客户端。在这整个过程中客户端和目标资源是不知道过滤器的存在的。
  • 过滤器对请求做了两次(对request和response)过滤,其实Filter是对请求中的Request和Response进行了拦截。拦截到了进行处理,处理完后再返回到其原来的调用流程上去。这点体现了责任链模式。
  • 在一个Web应用程序中可以配置多个过滤器,从而形成过滤器链。
  • 在请求资源时,过滤器链中的过滤器依次对请求作出处理。在接受到响应时再按照相反的顺序对响应作出处理。
  • 多个过滤器的执行顺序是按照web.xml中filter的配置的上下顺序来决定的。

使用Filter的好处:

  1. 在Filter执行的整个过程中客户端和目标资源是不知道过滤器的存在的。Filter提供的是一种声明式的服务,即在不用在原程序上做任何修改,只需要编写Filter,原程序想用Filter,只需要在XML文件中声明一下即可。他具有可插拔的能力,用的时候配上web.XML,不用的时候只需要修改web.xml,对整个系统没有影响,这种声明式的服务非常方便,也非常强大。

  2. 其次,使用Filter进行控制业务也非常方便,比如验证用户是否登录,是否有操作权限,判断Session,字符集等,放到Filter里,可以省去大量重复的代码和繁琐的控制。

JSP-case-4

发表于 2019-07-14

实现登录功能

利用Session可以传递登录状态,实现登录权限检查

构建登录表单

  1. 创建StartLoginServlet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class StartLoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    /**
    * 显示登录界面的Servlet
    * 请求URL /user/start-login
    */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String contextPath=request.getContextPath();
    request.setAttribute("root", contextPath);

    //记住用户名功能,可以在这里实现

    String path="/WEB-INF/jsp/login.jsp";
    request.getRequestDispatcher(path)
    .forward(request, response);
    }
    }
  2. 配置 web.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <servlet>
    <description></description>
    <display-name>StartLoginServlet</display-name>
    <servlet-name>StartLoginServlet</servlet-name>
    <servlet-class>day10.StartLoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>StartLoginServlet</servlet-name>
    <url-pattern>/user/start-login</url-pattern>
    </servlet-mapping>
  3. 将 /pages/examples/login.html 复制到/WEB-INF/jsp/login.jsp

  4. 更新JSP页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    <%@ page
    language="java"
    contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    %>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/core"
    prefix="c"%>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>AdminLTE 2 | Log in</title>
    <!-- Tell the browser to be responsive to screen width -->
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
    <!-- Bootstrap 3.3.7 -->
    <link rel="stylesheet" href="${root}/bower_components/bootstrap/dist/css/bootstrap.min.css">
    <!-- Font Awesome -->
    <link rel="stylesheet" href="${root}/bower_components/font-awesome/css/font-awesome.min.css">
    <!-- Ionicons -->
    <link rel="stylesheet" href="${root}/bower_components/Ionicons/css/ionicons.min.css">
    <!-- Theme style -->
    <link rel="stylesheet" href="${root}/dist/css/AdminLTE.min.css">
    <!-- iCheck -->
    <link rel="stylesheet" href="${root}/plugins/iCheck/square/blue.css">

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->

    <!-- Google Font -->
    <!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic"> -->
    </head>
    <body class="hold-transition login-page">
    <div class="login-box">
    <div class="login-logo">
    <h1>系统登录</h1>
    </div>
    <!-- /.login-logo -->
    <div class="login-box-body">
    <!-- empty 空, not empty 非空
    not empty message 检查message是否为空
    -->
    <c:choose>
    <c:when test="${not empty message}">
    <p class="login-box-msg">${message}</p>
    </c:when>
    <c:otherwise>
    <p class="login-box-msg">Sign in to start your session</p>
    </c:otherwise>
    </c:choose>

    <form action="${root}/user/login" method="post">
    <div class="form-group has-feedback">
    <input type="text" class="form-control"
    placeholder="用户名" name="name"
    value="${name}">
    <span class="glyphicon glyphicon-user form-control-feedback"></span>
    </div>
    <div class="form-group has-feedback">
    <input type="password" class="form-control"
    placeholder="密码" name="password">
    <span class="glyphicon glyphicon-lock form-control-feedback"></span>
    </div>
    <div class="row">
    <div class="col-xs-8">
    <div class="checkbox icheck">
    <label>
    <input type="checkbox"> 记住用户名
    </label>
    </div>
    </div>
    <!-- /.col -->
    <div class="col-xs-4">
    <button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
    </div>
    <!-- /.col -->
    </div>
    </form>

    <a href="${root}/user/register"
    class="text-center">注册新用户</a>

    </div>
    <!-- /.login-box-body -->
    </div>
    <!-- /.login-box -->

    <!-- jQuery 3 -->
    <script src="${root}/bower_components/jquery/dist/jquery.min.js"></script>
    <!-- Bootstrap 3.3.7 -->
    <script src="${root}/bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
    <!-- iCheck -->
    <script src="${root}/plugins/iCheck/icheck.min.js"></script>
    <script>
    $(function () {
    $('input').iCheck({
    checkboxClass: 'icheckbox_square-blue',
    radioClass: 'iradio_square-blue',
    increaseArea: '20%' /* optional */
    });
    });
    </script>
    </body>
    </html>
  5. 测试:

    1
    http://localhost:8080/Servlet10/user/start-login

实现检查登录功能

  1. 建表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    create table t_user(
    id int auto_increment primary key,
    name varchar(100),
    password varchar(100),
    email varchar(200)
    );

    insert into t_user (id, name, password, email )
    values (null, 'Tom', '123', 'tom@tedu.cn');
  2. 创建 User 类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    public class User {
    private int id;
    private String name;
    private String password;
    private String email;

    public User() {
    }

    public User(int id, String name, String password, String email) {
    super();
    this.id = id;
    this.name = name;
    this.password = password;
    this.email = email;
    }

    public int getId() {
    return id;
    }

    public void setId(int id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public String getPassword() {
    return password;
    }

    public void setPassword(String password) {
    this.password = password;
    }

    public String getEmail() {
    return email;
    }

    public void setEmail(String email) {
    this.email = email;
    }

    @Override
    public String toString() {
    return "User [id=" + id + ", name=" + name + ", password=" + password + ", email=" + email + "]";
    }
    }
  3. 创建 UserDao 类,封装根据用户名获取用户信息的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class UserDao {
    public User findUserByName(String name) {
    String sql="select id, name, password, email from t_user where name=?";
    try(Connection conn = DBUtil.getConnection()){
    PreparedStatement ps=conn.prepareStatement(sql);
    ps.setString(1, name);
    ResultSet rs=ps.executeQuery();
    User user=null;
    while(rs.next()) {
    user = row2user(rs);
    }
    return user;
    }catch(Exception e) {
    e.printStackTrace();
    throw new RuntimeException(e);
    }
    }

    private User row2user(ResultSet rs)
    throws SQLException{
    int id = rs.getInt("id");
    String name = rs.getString("name");
    String password = rs.getString("password");
    String email = rs.getString("email");
    return new User(id, name, password, email);
    }
    }
  4. 测试

    1
    2
    3
    4
    5
    6
    @Test
    public void testFindUserByName() {
    UserDao dao = new UserDao();
    User user = dao.findUserByName("Tom");
    System.out.println(user);
    }
  5. 编写LoginServlet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    /**
    * 检查用户登录表单Servlet
    * 请求URL /user/login
    * 处理 Post 请求
    */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //读取表单参数
    request.setCharacterEncoding("UTF-8");
    String name = request.getParameter("name");
    String password = request.getParameter("password");
    // 访问数据层,检查用户信息
    UserDao dao = new UserDao();
    User user = dao.findUserByName(name.trim());

    //将用户名保存在 request中
    request.setAttribute("name",name);

    String contextPath = request.getContextPath();
    request.setAttribute("root", contextPath);

    //如果没有找到用户信息,表示用户名是错误的
    if(user==null) {
    //转回到登录页面显示错误消息
    request.setAttribute("message", "用户名或者密码错误");
    String path = "/WEB-INF/jsp/login.jsp";
    request.getRequestDispatcher(path).forward(request, response);
    return;
    }
    //找到用户信息,则判断密码是否正确
    //哪个变量不可能为null放到前面
    if(user.getPassword().equals(password)) {
    //如果密码相等, 则可以登录
    //将登录结果保存在 session
    HttpSession session = request.getSession();
    session.setAttribute("loginUser", user);
    //转发到消息页面显示登录成功消息
    request.setAttribute("message", "登录成功");
    String path = "/WEB-INF/jsp/message.jsp";
    request.getRequestDispatcher(path).forward(request, response);
    return;
    }
    //执行到这个位置?密码不一致的情形
    request.setAttribute("message", "用户名或者密码错误");
    String path = "/WEB-INF/jsp/login.jsp";
    request.getRequestDispatcher(path).forward(request, response);
    }
    }
  6. 配置web.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <servlet>
    <description></description>
    <display-name>LoginServlet</display-name>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>day10.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/user/login</url-pattern>
    </servlet-mapping>

记住用户名功能

利用Cookie实现记住用户名功能
实现步骤:

  1. 重构 LoginServlet 在用户登录成功时候保存用户名到Cookie

    1
    2
    3
    4
    5
    6
    7
    //将用户名存储到 Cookie中
    if("true".equals(save)) {
    Cookie cookie = new Cookie("_username_", URLEncoder.encode(name,"UTF-8"));
    cookie.setMaxAge(60*60*24*30);
    cookie.setPath("/");
    response.addCookie(cookie);
    }
  2. 重构 StartLoginServlet 检查Cookie,如果有保存的用户名就读取出来,送到login.jsp 显示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //从全部Cookie中查找,如果Cookie中包含 用户名
    //就利用request送到login.jsp中显示
    Cookie[] cookies = request.getCookies();
    if(cookies!=null) {
    for (Cookie cookie : cookies) {
    String name=cookie.getName();
    if("_username_".equals(name)) {
    String username=cookie.getValue();
    username = URLDecoder.decode(username,"UTF-8");
    request.setAttribute("name", username); }
    }
    }
123…7

Alone5

70 日志
1 标签
© 2020 Alone5
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4