JavaEE开发-第六节 JavaEE项目实战(三)

反射

自动生成表名

  1. 在BaseDaoImpl类中有个虚函数table方法让子类去实现,然后拿到子类要访问的表名

     private String table = table();
     //要求子类实现,告知表名
     protected abstract String table();
    
  2. 在子类中需要实现这个table虚函数

     @Override
     protected String table() {
         return "award";
     }
    
  3. 这样n个类都需要去实现这个方法,从代码从可以发现,这个表名其实就是子类继承父类BaseDaoImpl遵守的泛型的实际类

     public class AwardDaoImpl extends BaseDaoImpl<Award> implements AwardDao {
     }
     //AwardDaoImpl需要查询的表名就是award
     //一般情况,如果AwardDaoImpl遵守的泛型实际类为(MyName)那么对应的表名为my_name
    
  4. 因此根据3中的分析,可以在BaseDaoImpl类中用反射的API封装一个自动获取表名的方法

     protected String table() {
         //反射
         //拿到父类的泛型
         ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();
         //获取泛型中的参数
         Class beanCls = (Class) type.getActualTypeArguments()[0];
         //获取表名字符串
         return Strings.underlineCase(beanCls.getSimpleName());
     }
    
  5. Strings是自动封装的一个工具类在util文件夹下:

     package com.zh.xr.util;
     public class Strings {
         /**
          * @param str 传值可能是小驼峰(myAge)、大驼峰(MyAge)
          * @return 则返回表名为my_age
          */
         public static String underlineCase(String str) {
             if (str == null) return null;
             int len = str.length();
             if (len == 0) return str;
        
             StringBuilder sb = new StringBuilder();
             sb.append(Character.toLowerCase(str.charAt(0)));
             for (int i = 1; i < len; i++) {
                 char c = str.charAt(i);
        
                 if (Character.isUpperCase(c)) {
                     sb.append("_");
                     sb.append(Character.toLowerCase(c));
                 } else {
                     sb.append(c);
                 }
             }
             return sb.toString();
         }
     }
    
  6. 这样一来,如果子类没有实现父类BaseDaoImpl的table方法那就默认使用父类的获取方式,如果子类自己实现了,就用子类自己的表名

自动生成service

  1. 正常情况下是,service内部创建Dao,dao获取数据返回,service封装成servlet直接使用的数据
  2. 基类BaseServiceImpl内部提供了一个dao抽象方法,然后让其子类去实现这个方法,通过子类返回的具体Dao,才去真正调用dao的实际方法

     //父类BaseServiceImpl
     private BaseDao<T> dao = dao();
     protected abstract  BaseDao<T> dao();
     //子类EducationServiceImpl
     public class EducationServiceImpl extends BaseServiceImpl<Education> implements EducationService {
         @Override
         protected BaseDao<Education> dao() {
             return new EducationDaoImpl();
         }
     }
    
  3. 从上面可以发现只需要将子类的Servce自主成换成Dao即可

     protected BaseDao<T> dao = newDao();
     protected BaseDao<T> newDao() {
         //子类
         // com.zh.xr.service.impl.EducationServiceImpl
         //通过反射替换后的子类Dao类型,即子类原本要通过dao函数实现返回的的Dao子类
         // com.zh.xr.dao.impl.EducationDaoImpl
         try {
             String clsName = getClass().getName()
                     .replace(".service.", ".dao.")
                     .replace("Service", "Dao");
             //创建子类实例对象
             return (BaseDao<T>) Class.forName(clsName).newInstance();
         } catch (Exception e) {
             e.printStackTrace();
             return null;
         }
     }
    
  4. 同理子类可以选择实现父类的newDao方法

自动生成Servlet

  1. 每个子类Servlet都需要创建一个与其对应的Service,这样才能获取对应的数据,比如:

     public class AwardServlet extends BaseServlet{
         private AwardService service = new AwardServiceImpl();
         ...
     }
    
  2. 那么可不可以也自动创建,抽取到父类的BaseServlet方法中呢?

    1. 父类要添加泛型

       public class BaseServlet<T> extends HttpServlet {}
      
    2. 子类集成要填充泛型的实际类型

       public class AwardServlet extends BaseServlet<Award>{}
      
    3. 那么该子类对应的service为:AwardServiceImpl
    4. 因此在BaseServlet封装自动创建service的方法为:

       protected BaseService<T> service = newService();
       protected BaseService<T> newService() {
           //替换前
           // com.zh.xr.servlet.WebsiteServlet
           //替换后
           // com.zh.xr.service.impl.WebsiteServiceImpl
           try {
               String clsName = getClass().getName()
                       //包名替换
                       .replace(".servlet.", ".service.impl.")
                       //类名替换
                       .replace("Servlet", "ServiceImpl");
               return (BaseService<T>) Class.forName(clsName).newInstance();
           } catch (Exception e) {
               e.printStackTrace();
               return null;
           }
       }
      

模板tpl

  1. 可以看到如果新增一个业务模块“比如公司信息”
  2. 需要一次创建bean、dao、service、servlet
  3. 而且发现这些类里面的代码都很相似,重复操作
  4. 那么可以通过将这些类抽取成tpl模板,放到test文件夹中
  5. 然后通过编写代码,通过IO动态执行自动添加n个模块,就相当于脚本功能一样
  6. 本节视频略,在(24项目实战07_反射_模板)中讲述

自动生成单元测试

  1. 打开一个类,在代码中右击->Generate…->test->Testing library 选择JUnit4 ->Generate test methods for:下面勾选需要实现测试的方法->点击OK即可
  2. 会自动在test文件夹的java目录下生成一个当前类的测试类

工作经验模块、项目经验模块

  1. 工作经验中涉及复杂模型数据
  2. 一个模型bean中有个属性值的类型是另外一个bean类型
  3. 比如Experience工作经验bean有个公司companybean
  4. 此时需要考虑数据库联合查询、保存
  5. 添加经验的jsp页面公司信息选择展示等特殊功能

用户模块-登录

  1. 用户模块包括登录、退出、注册、用户信息页面
  2. 同理创建user一系列的模块(bean、dao、service、servlet)

登录

  1. 由于登录页面可以直接输入网址访问,因此login.jsp不能放在WEB-INF文件夹中
  2. 在webapp根目录下新建一个page文件夹,然后里面放入login.jsp,跟WEB-INF文件夹同级
  3. 前端登录的密码使用MD5加密,则引入下面js框架

     <script src="${ctx}/asset/plugin/JavaScript-MD5/md5.min.js"></script>
    
  4. 技巧:一个表单中有密码这个input,但是如何将加密后的值传递给服务器呢? 点击提交submit拿到的是明文,密文是在js中处理的,将表单做如下处理:

     //1. 密码表单新增input隐藏,然后设置name属性为password,真正的密码输入框不设置name
     <div class="form-line">
         <input type="hidden" name="password">
         <input id="originPassword" type="password" class="form-control" maxlength="20" placeholder="密码" required>
     </div>
     //js中处理加密后,赋值给name属性为password的表单input
     addValidatorRules('.form-validation', function () {
         //将属性值设置为md5加密后的值,设置给nama属性为password的input标签中,
         $('[name=password]').val(md5($('#originPassword').val()))
         return true
     })
    

登录-验证码

  1. 验证码(CAPTCHA)
    1. 是Completely Automated Public Turing test to tell Computers and Humans Apart的缩写
    2. 全自动区分计算机和人类的图灵测试
    3. 可以用于防止大规模注册、暴力破解密码、刷票、论坛灌水等
  2. 传统的验证码:由扭曲倾斜的文字、干扰线组成
    1. 由服务器端生成验证码图片,返回给客户端展示
  3. 在java中,可以使用Kaptcha库生成验证码
    1. Kaptcha产自Google
  4. Kaptcha库
    1. Kaptcha是一个可高度配置的验证码生成工具,产自Google
    2. 常用的配置有:字体、内容的范围、尺寸、边框、干扰线、样式等
    3. pom.xml中导入如下框架

       <dependency>
           <groupId>com.github.penggle</groupId>
           <artifactId>kaptcha</artifactId>
           <version>2.3.2</version>
       </dependency>
      
  5. UserServlet中添加captcha方法

     public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
         // 创建Katpcha对象
         DefaultKaptcha dk = new DefaultKaptcha();
    
         // 验证码的配置,配置内容存放到resources文件下的kaptcha.properties资源文件中
         try (InputStream is = getClass().getClassLoader().getResourceAsStream("kaptcha.properties")) {
             Properties properties = new Properties();
             properties.load(is);
    
             Config config = new Config(properties);
             dk.setConfig(config);
         }
    
         // 生成验证码字符串
         String code = dk.createText();
            
         // 存储到Session中(当这个客户端是首次请求服务器时,就会创建一个全新的Session)
         HttpSession session = request.getSession();
         session.setAttribute("code", code.toLowerCase());
            
         // 生成验证码图片
         BufferedImage image = dk.createImage(code);
    
         // 设置返回数据的格式
         response.setContentType("image/jpeg");
    
         // 将图片数据写回到客户端
         ImageIO.write(image, "jpg", response.getOutputStream());
     }
    
  6. 图片验证码kaptcha.properties文件中的配置属性如下:

     # 图片边框
     kaptcha.border=yes
     # 边框颜色
     kaptcha.border.color=105,179,90
     # 字体颜色
     kaptcha.textproducer.font.color=blue
     # 图片宽
     kaptcha.image.width=130
     # 图片高
     kaptcha.image.height=48
     # 字体大小
     kaptcha.textproducer.font.size=30
     # session key
     kaptcha.session.key=code
     # 验证码长度
     kaptcha.textproducer.char.length=4
     # 字体
     kaptcha.textproducer.font.names=微软雅黑
    
  7. 在login.jsp文件中,验证码图片标签img的src属性中设置请求

     //相当于get请求
     <img id="captcha" src="${ctx}/user/captcha" alt="验证码">
        
     //js代码,点击图片刷新验证码
     $('#captcha').click(function () {
         //time作用是让每次发送的请求都不一样,否则浏览器会有默认缓存,只发送一次
         //hide、fadeIn动画功能:将图片先隐藏,然后在慢慢显示
         $(this).hide().attr('src', '${ctx}/user/captcha?time=' + new Date().getTime()).fadeIn()
     })
    

登录-Session与cookie

  1. 疑问:
    1. 服务器获取图片验证码接口将验证码图片返回给客户端,客户端点击登录调用登录接口,服务器拿到用户输入的验证码数据,那么如何拿到验证码生成的数据进行比对呢?
    2. 此类为题归结为:多次请求之间的数据共享问题,即第n个请求用到其他请求的数据
    3. 常用例子:
      1. PC电商平台(京东),在用户不登录的情况下,也可以添加商品到购物车,每次添加都是向服务器发送了一个请求,那么是如何做到多个请求将数据共享的呢?
      2. 如果是用户登录情况下,可以通过用户id、商品id直接存入到数据库,然后多个接口进行共享,那么不登录是如何共享的呢?
    4. 以上方法可以通过Session来解决
    5. A浏览器连续添加几次商品到购物车,服务器会创建同一个Session对象存储这些数据,当B浏览器添加时会重新创建另外一个Session对象。Session针对不同的客户端创建不同的session对象
    6. n台电脑用同一个浏览器发送,也会创建不同的session对象
  2. 会话跟踪
    1. HTTP是一种“无状态”(stateless)的协议
      1. 每次客户端访问网页时,客户端都会打开与web服务器的单独连接
      2. 并且服务器不会自动保留之前客户端请求的任何记录
      3. 所以服务器无法识别多个请求是否来自同一个客户端(比如浏览器)
    2. 在很多应用场景中,都有以下需求
      1. 服务器能够识别出多个请求是否来自同一个客户端
      2. 在来自同一个客户端的多个请求之间共享数据
    3. 以上需求可以使用会话跟踪技术来完成。在Java中,实现会话跟踪的常用方案是
      1. Cookie
      2. Session
  1. 简介
    1. Cookie是直接存储在浏览器本地的一小串数据
      1. 使用document.cookie访问Cookie(存取)
      2. 修改Cookie时, 只会修改其中提到的Cookie
      3. name=value必须被编码(encodeURIComponent),就是value为中文时要通过编码后赋值
      4. 一个Cookie最大为4kb,每个网站最多有20+个左右的Cookie(具体取决于浏览器)
       <script>
           document.cookie = 'name=hh';
           console.log(document.cookie);
       </script>
      
    2. Windows中Chrome浏览器的Cookie存放位置
      1. C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Cookies
      2. 使用SQLite数据库进行存储
  2. Cookie的有效期
    1. 如果没有设置Cookie的过期时间,则当浏览器关闭时,Cookie就失效了
    2. expires
      1. 必须完全采用GMT时区的格式,可以使用date.toUTCString来获取
      2. 例如:expires=Tue, 19 Jan 2038 03:14:07 GMT
    3. max-age
      1. 过期时间距离当前时间的秒数
      2. 例如:max-age=60
  3. Cookie的作用域
    1. domain和path标识定义了Cookie的作用域,即Cookie应该发送给哪些URL
      1. 浏览器是可以将cookie自动发送给服务器
      2. domain和path可以设置浏览器是否将当前网站的cookie发送给服务器
    2. domain
      1. 标识指定了哪些主机可以接受Cookie
      2. 如果不指定,默认为当前文档的主机(不包含子域名);如果指定了domain,则一般包含子域名
      3. 例如:如果设置domain=321it.com,则Cookie也包含在子域名中(如bbs.321it.com)
    3. path
      1. 标识指定了主机下的哪些路径可以接受Cookie,子路径也会被匹配
      2. 例如:设置path=/docs, 则以下地址都会匹配

         /docs
         /docs/one/
         /docs/one/img
        
    4. 举例:

       //js代码
       document.cookie = 'name=hh; max-age=6; domain=321it.coms; path=/docs;';
      
    5. 服务端获取客户端发送过来的Cookie

       request.getCookies();
      
  4. Cookie的参考资料
    1. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies
    2. https://zh.javascript.info/cookie
  5. 服务器设置Cookie
    1. 上面讲的是客户端通过JS代码主动保存到Cookie,其实服务器也可以将数据保存的客户端的Cookie中
    2. Cookie通常是由Web服务器使用响应头Set-Cookie设置的

       //本质是设置到响应头的Set-Cookie字段中,返回给客户端,告诉浏览器存储到本地
       Cookie cookie = new Cookie("name","zh");
       response.addCookie(cookie);
      
    3. 关于max-age
      1. 在JavaScript中:如果设置为0或者负数, 会立即删除Cookie
      2. 在Java中:如果设置为0, 是立即删除Cookie; 如果设置为负数, 按默认情况处理

Session

  1. getSession的原理
    1. 检查客户端是否有发送一个叫做JSESSIONID的Cookie
      1. 如果没有
        1. 创建一个新的Session对象, 并且这个Session对象会有一个id
        2. 这个Session对象会保留在服务器的内存中
        3. 在响应的时候,会添加一个Cookie(JSESSIONID=Session对象的id) 给客户端
      2. 如果有
        1. 返回id为JSESSIONID的Session对象
    2. 分析:
      1. 这样就可以确定是否为同一个客户端发送请求、是否为同一台电脑发送请求
      2. 如果不是同一个浏览器那么就不会有JSESSIONID的cookie,服务器就会创建新的Session对象
      3. 如果不是同一台电脑同理。
  2. JSESSIONID
    1. 默认情况下,当用户关闭浏览器时,Cookie中存储的JSESSIONID就会被销毁
    2. 可以通过以下代码延长JSESSIONID在客户端的寿命

       Cookie cookie = new Cookie("JSESSIONID",request.getSession().getId());
       cookie.setMaxAge(3600);
       response.addCookie(cookie);
      
  3. Session的有效期
    1. Session的有效期默认是30分钟
    2. 可以在web.xml中配置失效时间(单位是分钟)

       <session-config>
           <session-timeout>30</session-timeout>
       </session-config>
      

总结

  1. Cookie
    1. 数据存储在浏览器客户端
    2. 数据有大小和数量的限制
    3. 适合存储一些小型、不敏感的数据
    4. 默认情况下,关闭浏览器后就会销毁
  2. Session
    1. 数据存储在服务器端
    2. 数据没有大小和数量的限制
    3. 可以存储大型、敏感的数据(比如用户信息)
    4. 默认情况下,未使用30分钟后就会销毁

登录-代码示例

public void login(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 设置编码
    response.setContentType("text/json; charset=UTF-8");
    Map<String, Object> result = new HashMap();

    // 检查验证码
    String captcha = request.getParameter("captcha").toLowerCase();

    // 从Session中取出验证码
    String code = (String) request.getSession().getAttribute("code");

    if (!captcha.equals(code)) {
        // forwardError(request, response, "验证码不正确");
        result.put("success", false);
        result.put("msg", "验证码不正确");
    } else {
        // 检查用户名、密码
        User user = new User();
        BeanUtils.populate(user, request.getParameterMap());
        user = ((UserService) service).get(user);
        if (user != null) { // 用户名、密码正确
            // 登录成功后,将User对象放入Session中
            request.getSession().setAttribute("user", user);
            // redirect(request, response, "user/admin");
            result.put("success", true);
        } else { // 用户名、密码有问题
            // forwardError(request, response, "邮箱或密码不正确");
            result.put("success", false);
            result.put("msg", "邮箱或密码不正确");
        }
    }

    //设置客户端cookie的寿命,7天内有效
    Cookie cookie = new Cookie("JSESSIONID", request.getSession().getId());
    cookie.setMaxAge(3600 * 24 * 7);
    response.addCookie(cookie);

    response.getWriter().write(new ObjectMapper().writeValueAsString(result));
}

登录-Filter

  1. 疑问:
    1. 做一个网站,有些页面是必须登录才能访问的,否则直接访问要跳转到登录页面,该如何拦截呢?
    2. 通常可以设置一个Servlet基类,然后设置访问路径为/*,其他类继承该类,但是该方法仍然不是最终方法
    3. 可以通过Filter实现
  2. Filter:译为“过滤器”
    1. 用来拦截、过滤客户端的请求和服务器的响应
    2. 有注解、XML两种使用方式
      1. 注解指类前面加上这个@WebFilter("/*"),这样才是拦截请求
      2. XML在web.xml中配置,等价于上面那注解

          <!-- 1. 配置Filter -->
         <filter>
             <!-- 给这个类com.zh.xr.filter.LoginFilter取一个别名叫LoginFilter-->
             <filter-name>LoginFilter</filter-name>
             <filter-class>com.zh.xr.filter.LoginFilter</filter-class>
         </filter>
         <!-- 2. 上面配置的Filter要映射到哪一个路径-->
         <filter-mapping>
             <filter-name>LoginFilter</filter-name>
             <url-pattern>/*</url-pattern>
         </filter-mapping>
         <!-- 配置其他Filter -->
        

    图1

  3. Filter执行的顺序
    1. 如果有多个Filter那么会按顺序执行,那么如何排序呢?
    2. 如果是xml方式,哪个Filter写在前面,哪个先执行
    3. 如果是注解方式,就根据两个Filter类名的ASCII码字母大小进行,小的先执行

       //后执行
       com.zh.xr.filter.LoginFilter
       //先执行
       com.zh.xr.filter.CharsetFilter
      
  4. Filter-生命周期方法
    1. init:将Filter添加到Web容器中时调用(Web容器指的就是服务器软件比如Tomcat),一般用来加载资源
    2. destroy:将Filter从Web容器中移除时调用,一般用来销毁资源
  5. Filter-dispatcherTypes属性的常用值
    1. REQUEST: 默认值,只拦截客户端直接发送的请求
    2. FORWARD: 只拦截转发的请求
    3. 举例:
      1. 在web.xml中配置

         <filter-mapping>
             <filter-name>LoginFilter</filter-name>
             <url-pattern>/*</url-pattern>
             //只拦截客户端请求
             <dispatcher>REQUEST</dispatcher>
             //只拦截转发请求
             <dispatcher>FORWARD</dispatcher>
         </filter-mapping>
        
      2. 注解:

         @WebFilter(value = "/*",dispatcherTypes = DispatcherType.REQUEST)
         //二者都拦截
         @WebFilter(value = "/*",dispatcherTypes = {DispatcherType.REQUEST,DispatcherType.FORWARD})
        
  6. 举例使用
    1. 在com.zh.xr下新建一个文件夹filter,右击->new->Create New Filter -> LoginFilter
    2. 代码举例:

       package com.zh.xr.filter;
       import javax.servlet.*;
       import javax.servlet.annotation.WebFilter;
       import javax.servlet.http.HttpServletRequest;
       import javax.servlet.http.HttpServletResponse;
       import java.io.IOException;
       //拦截所有请求
       @WebFilter("/*")
       public class LoginFilter implements Filter {
           //只有调用chain.doFilter(request, response);才算放行
           public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
               //任何请求来都执行,做请求之前的处理
               System.out.println("Filter1-doFilter-1");
                      
               //透传给服务端的Servlet去执行服务器代码,然后响应
               chain.doFilter(req, resp);
                      
               //服务器代码执行完调用,做服务器响应之后的处理
               System.out.println("Filter1-doFilter-2");
           }
           /**
            * 当项目部署到Web容器时调用(当Filter被加载到Web容器中),Web容器指的就是服务器软件比如Tomcat
            * @throws ServletException
            */
           public void init(FilterConfig config) throws ServletException {
               // 适合做一些资源的一次性加载、初始化
               System.out.println("LoginFilter - init");
           }
              
           /**
            * 当项目从Web容器中取消部署时调用(当Filter从Web容器中移除时调用)
            */
           public void destroy() {
               // 适合做一些资源的回收操作
               System.out.println("LoginFilter - destroy");
           }
       }
      
    3. 拦截其他页面必须登录的过滤

       //只有调用chain.doFilter(request, response);才算放行
       public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
           HttpServletRequest request = (HttpServletRequest) req;
           HttpServletResponse response = (HttpServletResponse) resp;
           String uri = request.getRequestURI();
          
           // 优先放开的请求,静态资源css、js、图片等资源
           if (uri.contains("/asset/")
                   || uri.contains("/contact/save")) {
               chain.doFilter(request, response);
           } else if (uri.contains("/admin")
                   || uri.contains("/save")
                   || uri.contains("/remove")
                   || uri.contains("/user/password")
                   || uri.contains("/user/updatePassword")) {
               // 需要作登录验证的请求
               Object user = request.getSession().getAttribute("user");
               if (user != null) { // 登录成功过,放行
                   chain.doFilter(request, response);
               } else { // 没有登录成功过,重定向到登录页面
                   response.sendRedirect(request.getContextPath() + "/page/login.jsp");
               }
           } else {
               chain.doFilter(request, response);
           }
       }
      

登录-Listener

  1. Listener:译为“监听器”
    1. 比较常用的是ServletContextListener, 用来监听ServletContext的创建和销毁
      1. contextInitialized:ServletContext创建的时候调用
      2. contextDestroyed:ServletContext销毁的时候调用
    2. 一个项目就有一个ServletContext对象,即ServletContext就代表一个项目
      1. 当部署的时候,Tomcat就会为这个项目创建一个ServletContext对象
      2. 当取消部署的时候救护销毁ServletContext对象
  2. 有注解、XML两种使用方式
    1. 注解:

       @WebListener()
      
    2. xml,在web.xml

       <listener>
           <listener-class>com.zh.xr.listener.ContextListener</listener-class>
       </listener>
      
  3. 代码举例:

     @WebListener()
     public class ContextListener implements ServletContextListener {
         @Override
         public void contextInitialized(ServletContextEvent sce) {
             // 在项目启动(部署)的时候做一些一次性的操作(资源加载)
             System.out.println("contextInitialized------------------");
        
             //将客户端输入到服务端的日期字符串,自动转化为Date类型,项目全局代码只需要执行一次
             // null参数表示允许值为null
             DateConverter dateConverter = new DateConverter(null);
             dateConverter.setPatterns(new String[]{"yyyy-MM-dd"});
             ConvertUtils.register(dateConverter, Date.class);
         }
        
         @Override
         public void contextDestroyed(ServletContextEvent sce) {
             // 回收资源
             System.out.println("contextDestroyed------------------");
         }
     }
    
Table of Contents