JavaEE开发-第六节 JavaEE项目实战(三)
反射
自动生成表名
-
在BaseDaoImpl类中有个虚函数table方法让子类去实现,然后拿到子类要访问的表名
private String table = table(); //要求子类实现,告知表名 protected abstract String table();
-
在子类中需要实现这个table虚函数
@Override protected String table() { return "award"; }
-
这样n个类都需要去实现这个方法,从代码从可以发现,这个表名其实就是子类继承父类BaseDaoImpl遵守的泛型的实际类
public class AwardDaoImpl extends BaseDaoImpl<Award> implements AwardDao { } //AwardDaoImpl需要查询的表名就是award //一般情况,如果AwardDaoImpl遵守的泛型实际类为(MyName)那么对应的表名为my_name
-
因此根据3中的分析,可以在BaseDaoImpl类中用反射的API封装一个自动获取表名的方法
protected String table() { //反射 //拿到父类的泛型 ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass(); //获取泛型中的参数 Class beanCls = (Class) type.getActualTypeArguments()[0]; //获取表名字符串 return Strings.underlineCase(beanCls.getSimpleName()); }
-
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(); } }
-
这样一来,如果子类没有实现父类BaseDaoImpl的table方法那就默认使用父类的获取方式,如果子类自己实现了,就用子类自己的表名
自动生成service
- 正常情况下是,service内部创建Dao,dao获取数据返回,service封装成servlet直接使用的数据
-
基类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(); } }
-
从上面可以发现只需要将子类的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; } }
- 同理子类可以选择实现父类的newDao方法
自动生成Servlet
-
每个子类Servlet都需要创建一个与其对应的Service,这样才能获取对应的数据,比如:
public class AwardServlet extends BaseServlet{ private AwardService service = new AwardServiceImpl(); ... }
-
那么可不可以也自动创建,抽取到父类的BaseServlet方法中呢?
-
父类要添加泛型
public class BaseServlet<T> extends HttpServlet {}
-
子类集成要填充泛型的实际类型
public class AwardServlet extends BaseServlet<Award>{}
- 那么该子类对应的service为:AwardServiceImpl
-
因此在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
- 可以看到如果新增一个业务模块“比如公司信息”
- 需要一次创建bean、dao、service、servlet
- 而且发现这些类里面的代码都很相似,重复操作
- 那么可以通过将这些类抽取成tpl模板,放到test文件夹中
- 然后通过编写代码,通过IO动态执行自动添加n个模块,就相当于脚本功能一样
- 本节视频略,在(24项目实战07_反射_模板)中讲述
自动生成单元测试
- 打开一个类,在代码中右击->Generate…->test->Testing library 选择JUnit4 ->Generate test methods for:下面勾选需要实现测试的方法->点击OK即可
- 会自动在test文件夹的java目录下生成一个当前类的测试类
工作经验模块、项目经验模块
- 工作经验中涉及复杂模型数据
- 一个模型bean中有个属性值的类型是另外一个bean类型
- 比如Experience工作经验bean有个公司companybean
- 此时需要考虑数据库联合查询、保存
- 添加经验的jsp页面公司信息选择展示等特殊功能
用户模块-登录
- 用户模块包括登录、退出、注册、用户信息页面
- 同理创建user一系列的模块(bean、dao、service、servlet)
登录
- 由于登录页面可以直接输入网址访问,因此login.jsp不能放在WEB-INF文件夹中
- 在webapp根目录下新建一个page文件夹,然后里面放入login.jsp,跟WEB-INF文件夹同级
-
前端登录的密码使用MD5加密,则引入下面js框架
<script src="${ctx}/asset/plugin/JavaScript-MD5/md5.min.js"></script>
-
技巧:一个表单中有密码这个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 })
登录-验证码
- 验证码(CAPTCHA)
- 是Completely Automated Public Turing test to tell Computers and Humans Apart的缩写
- 全自动区分计算机和人类的图灵测试
- 可以用于防止大规模注册、暴力破解密码、刷票、论坛灌水等
- 传统的验证码:由扭曲倾斜的文字、干扰线组成
- 由服务器端生成验证码图片,返回给客户端展示
- 在java中,可以使用Kaptcha库生成验证码
- Kaptcha产自Google
- Kaptcha库
- Kaptcha是一个可高度配置的验证码生成工具,产自Google
- 常用的配置有:字体、内容的范围、尺寸、边框、干扰线、样式等
-
pom.xml中导入如下框架
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
-
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()); }
-
图片验证码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=微软雅黑
-
在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
- 疑问:
- 服务器获取图片验证码接口将验证码图片返回给客户端,客户端点击登录调用登录接口,服务器拿到用户输入的验证码数据,那么如何拿到验证码生成的数据进行比对呢?
- 此类为题归结为:多次请求之间的数据共享问题,即第n个请求用到其他请求的数据
- 常用例子:
- PC电商平台(京东),在用户不登录的情况下,也可以添加商品到购物车,每次添加都是向服务器发送了一个请求,那么是如何做到多个请求将数据共享的呢?
- 如果是用户登录情况下,可以通过用户id、商品id直接存入到数据库,然后多个接口进行共享,那么不登录是如何共享的呢?
- 以上方法可以通过Session来解决
- A浏览器连续添加几次商品到购物车,服务器会创建同一个Session对象存储这些数据,当B浏览器添加时会重新创建另外一个Session对象。Session针对不同的客户端创建不同的session对象
- n台电脑用同一个浏览器发送,也会创建不同的session对象
- 会话跟踪
- HTTP是一种“无状态”(stateless)的协议
- 每次客户端访问网页时,客户端都会打开与web服务器的单独连接
- 并且服务器不会自动保留之前客户端请求的任何记录
- 所以服务器无法识别多个请求是否来自同一个客户端(比如浏览器)
- 在很多应用场景中,都有以下需求
- 服务器能够识别出多个请求是否来自同一个客户端
- 在来自同一个客户端的多个请求之间共享数据
- 以上需求可以使用会话跟踪技术来完成。在Java中,实现会话跟踪的常用方案是
- Cookie
- Session
- HTTP是一种“无状态”(stateless)的协议
Cookie
- 简介
- Cookie是直接存储在浏览器本地的一小串数据
- 使用document.cookie访问Cookie(存取)
- 修改Cookie时, 只会修改其中提到的Cookie
- name=value必须被编码(encodeURIComponent),就是value为中文时要通过编码后赋值
- 一个Cookie最大为4kb,每个网站最多有20+个左右的Cookie(具体取决于浏览器)
<script> document.cookie = 'name=hh'; console.log(document.cookie); </script>
- Windows中Chrome浏览器的Cookie存放位置
C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Cookies
- 使用SQLite数据库进行存储
- Cookie是直接存储在浏览器本地的一小串数据
- Cookie的有效期
- 如果没有设置Cookie的过期时间,则当浏览器关闭时,Cookie就失效了
- expires
- 必须完全采用GMT时区的格式,可以使用date.toUTCString来获取
- 例如:expires=Tue, 19 Jan 2038 03:14:07 GMT
- max-age
- 过期时间距离当前时间的秒数
- 例如:
max-age=60
- Cookie的作用域
- domain和path标识定义了Cookie的作用域,即Cookie应该发送给哪些URL
- 浏览器是可以将cookie自动发送给服务器
- domain和path可以设置浏览器是否将当前网站的cookie发送给服务器
- domain
- 标识指定了哪些主机可以接受Cookie
- 如果不指定,默认为当前文档的主机(不包含子域名);如果指定了domain,则一般包含子域名
- 例如:如果设置
domain=321it.com
,则Cookie也包含在子域名中(如bbs.321it.com
)
- path
- 标识指定了主机下的哪些路径可以接受Cookie,子路径也会被匹配
-
例如:设置
path=/docs
, 则以下地址都会匹配/docs /docs/one/ /docs/one/img
-
举例:
//js代码 document.cookie = 'name=hh; max-age=6; domain=321it.coms; path=/docs;';
-
服务端获取客户端发送过来的Cookie
request.getCookies();
- domain和path标识定义了Cookie的作用域,即Cookie应该发送给哪些URL
- Cookie的参考资料
- 服务器设置Cookie
- 上面讲的是客户端通过JS代码主动保存到Cookie,其实服务器也可以将数据保存的客户端的Cookie中
-
Cookie通常是由Web服务器使用响应头Set-Cookie设置的
//本质是设置到响应头的Set-Cookie字段中,返回给客户端,告诉浏览器存储到本地 Cookie cookie = new Cookie("name","zh"); response.addCookie(cookie);
- 关于max-age
- 在JavaScript中:如果设置为0或者负数, 会立即删除Cookie
- 在Java中:如果设置为0, 是立即删除Cookie; 如果设置为负数, 按默认情况处理
Session
- getSession的原理
- 检查客户端是否有发送一个叫做JSESSIONID的Cookie
- 如果没有
- 创建一个新的Session对象, 并且这个Session对象会有一个id
- 这个Session对象会保留在服务器的内存中
- 在响应的时候,会添加一个Cookie(JSESSIONID=Session对象的id) 给客户端
- 如果有
- 返回id为JSESSIONID的Session对象
- 如果没有
- 分析:
- 这样就可以确定是否为同一个客户端发送请求、是否为同一台电脑发送请求
- 如果不是同一个浏览器那么就不会有JSESSIONID的cookie,服务器就会创建新的Session对象
- 如果不是同一台电脑同理。
- 检查客户端是否有发送一个叫做JSESSIONID的Cookie
- JSESSIONID
- 默认情况下,当用户关闭浏览器时,Cookie中存储的JSESSIONID就会被销毁
-
可以通过以下代码延长JSESSIONID在客户端的寿命
Cookie cookie = new Cookie("JSESSIONID",request.getSession().getId()); cookie.setMaxAge(3600); response.addCookie(cookie);
- Session的有效期
- Session的有效期默认是30分钟
-
可以在web.xml中配置失效时间(单位是分钟)
<session-config> <session-timeout>30</session-timeout> </session-config>
总结
- Cookie
- 数据存储在浏览器客户端
- 数据有大小和数量的限制
- 适合存储一些小型、不敏感的数据
- 默认情况下,关闭浏览器后就会销毁
- Session
- 数据存储在服务器端
- 数据没有大小和数量的限制
- 可以存储大型、敏感的数据(比如用户信息)
- 默认情况下,未使用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
- 疑问:
- 做一个网站,有些页面是必须登录才能访问的,否则直接访问要跳转到登录页面,该如何拦截呢?
- 通常可以设置一个Servlet基类,然后设置访问路径为
/*
,其他类继承该类,但是该方法仍然不是最终方法 - 可以通过Filter实现
- Filter:译为“过滤器”
- 用来拦截、过滤客户端的请求和服务器的响应
- 有注解、XML两种使用方式
- 注解指类前面加上这个
@WebFilter("/*")
,这样才是拦截请求 -
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 -->
- 注解指类前面加上这个
- Filter执行的顺序
- 如果有多个Filter那么会按顺序执行,那么如何排序呢?
- 如果是xml方式,哪个Filter写在前面,哪个先执行
-
如果是注解方式,就根据两个Filter类名的ASCII码字母大小进行,小的先执行
//后执行 com.zh.xr.filter.LoginFilter //先执行 com.zh.xr.filter.CharsetFilter
- Filter-生命周期方法
- init:将Filter添加到Web容器中时调用(Web容器指的就是服务器软件比如Tomcat),一般用来加载资源
- destroy:将Filter从Web容器中移除时调用,一般用来销毁资源
- Filter-dispatcherTypes属性的常用值
- REQUEST: 默认值,只拦截客户端直接发送的请求
- FORWARD: 只拦截转发的请求
- 举例:
-
在web.xml中配置
<filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> //只拦截客户端请求 <dispatcher>REQUEST</dispatcher> //只拦截转发请求 <dispatcher>FORWARD</dispatcher> </filter-mapping>
-
注解:
@WebFilter(value = "/*",dispatcherTypes = DispatcherType.REQUEST) //二者都拦截 @WebFilter(value = "/*",dispatcherTypes = {DispatcherType.REQUEST,DispatcherType.FORWARD})
-
- 举例使用
- 在com.zh.xr下新建一个文件夹filter,右击->new->Create New Filter -> LoginFilter
-
代码举例:
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"); } }
-
拦截其他页面必须登录的过滤
//只有调用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
- Listener:译为“监听器”
- 比较常用的是ServletContextListener, 用来监听ServletContext的创建和销毁
- contextInitialized:ServletContext创建的时候调用
- contextDestroyed:ServletContext销毁的时候调用
- 一个项目就有一个ServletContext对象,即ServletContext就代表一个项目
- 当部署的时候,Tomcat就会为这个项目创建一个ServletContext对象
- 当取消部署的时候救护销毁ServletContext对象
- 比较常用的是ServletContextListener, 用来监听ServletContext的创建和销毁
- 有注解、XML两种使用方式
-
注解:
@WebListener()
-
xml,在web.xml
<listener> <listener-class>com.zh.xr.listener.ContextListener</listener-class> </listener>
-
-
代码举例:
@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------------------"); } }