大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: 从JSP到Spring 修改讲义

说明:接下来的课程都会尽量结合实战进行,一步一步的展示代码的进化……


dbFactory

复习:为什么需要

项目中新建一个文件夹dbFactory,里面新建一个带main()方法的BuildFactory类,

public class BuildFactory{
	public static void main(String[] args) {
		System.out.println("begin create database……");
可以直接F11运行,选择run as - Java Application即可运行:

hibernate配置

引入控制台中hibernate建库建表代码,注意一样需要在java path下新建文件夹META-INF,放入persistence.xml文件,即:项目中会有两个persistence.xml:

  • 用于建库建表的:设置schema-generation.database.action:
  • 用于开发/测试的:不设置建库建表内容

不要忘了给entity添加注解,其中作为基类的BaseEntity:

@MappedSuperclass
public class BaseEntity<T>{
	@Id
	@GeneratedValue( strategy = GenerationType.IDENTITY )	
	protected T id;

id要设置成protected(@试一试@:不这样做仍然是private会发生什么情况);另外id是数据库生成,所以不需要setter。

组织结构

为便于管理,

  • 引入UserFactory和ArticleFactory等类,每个类中都有一个静态create()方法,供main()调用
  • create()方法传入公用的EntityManager对象,
  • 且将所有的操作放在一个统一的transaction
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();

try {
	UserFactory.create(em);
	ArticleFactory.create(em);
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
	throw e;
}

在UserFactory.create()方法中:

public static void create(EntityManager em) {
	User fg = new User();
	fg.setUsername("fg");
	fg.setPwd(password);  //使用统一的密码1234
	//不要忘记这个方法的调用
	fg.Register();
	em.persist(fg);
}

这样我们就成功注册了一个新用户(演示:跑一跑)

静态字段

接下来我们发布一篇文章。但注意这里有一个问题,文章作者怎么办?

应该用以前的注册用户哟!怎么用呢?

public class UserFactory {
	static User fg;
public static void create(EntityManager em) {
	Article java = new Article();
	java.setTitle("SpringMVC");
	java.setBody("SpringMVC is good, good, very good");
	java.setAuthor(UserFactory.fg);
	//一样不要忘了这一行代码!
	java.Publish();
	em.persist(java);
}
演示:运行效果

生成时间

很多时候,我们的entity会带一个createTime,而且它会在entity实例化时自动赋值:

protected LocalDateTime createTime;
public BaseEntity() {
	createTime = LocalDateTime.now();
}

和id一样,createTime也只有getter没有setter(@想一想@:为什么?)

但这样的话,我在dbFactory的时候怎么将其设置为指定的时间呢?两个方案:

  1. 反射:略
  2. 封装:新建一个SystemTime类
    private static LocalDateTime time;
    
    public static LocalDateTime getNow() {
    	if (time == null) {
    		return LocalDateTime.now();
    	}
    	return time;
    }
    
    public static void setNow(LocalDateTime time) {
    	SystemTime.time = time;
    }
    public static void reset() {
    	SystemTime.time = null;
    }
    entity中这样赋值:
    createTime = SystemTime.getNow();
    在dbFactory中:
    SystemTime.setNow(LocalDateTime.of(2021, 11, 11, 7, 23, 45));
    
    //正常new entity()、调用entity方法、persist()……
    
    //不要忘了重置系统时间!
    SystemTime.reset();


注册

就是将用户名和密码存入数据库。(@想一想@:真的这样吗?)

但是,其中又有一些细节:

MD5加密

密码明文存储安全吗?怎么加密呢?

新建一个Md5Util类,暴露一个静态方法Md5():

public class Md5Util {
	public static String Md5(String input){

因为这个方法是公用(至少dbFactory要用)的,所以放在glb文件夹中

将字符串转换成字节数组

byte[] md5 = input.getBytes("utf-8");

使用MessageDigest对字节数组进行MD5加密

byte[] md5Encrypted = MessageDigest.getInstance("md5").digest(md5);

以16进制字符串格式输出:

for (byte b : md5Encrypted) {
	sb.append(Integer.toHexString(b & 0xff));
}

注意这里的:&0xff,主要是解决md5Encrypted中负数补码的问题。

检查用户名重复

mock service中已经实现,现在我们需要在prod中实现。核心就是UserRepository中的

public User GetByName(String name) {
在拿到EntityManager em之后:
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> criteria = builder.createQuery(User.class);		
Root<User> root = criteria.from(User.class);
TypedQuery<User> query = em.createQuery(
		criteria.where(builder.equal(root.get(User_.username) , name)));
return query.getSingleResult();	

注意:这里的User_.username由jpamodelgen生成(复习),在当前SpringMVC项目中你可能在项目属性中找不到JPA选项,需要在project facets中先勾选

然后,prod.UserService中:

public boolean HasName(String username) {
	User user = userRepository.GetByName(username);
	return user != null;
}

PS:实际上所有查询都同一个套路……

#体会:SRV对BLL的调用和封装……

@想一想@:既然在业务逻辑中已经进行了检查,还有必要在数据库上添加唯一约束么?)

注册成功

Controller中注意if...else,能不能省略else?
if (usernameExists) {
	//报错,略……
} else {
	userService.Register(model);
}
UserRepository中(省略了事务声明等):
em.persist(user);

但这里有一个问题:密码的MD5加密应该写哪里?首先是肯定不应该放在repository和UI层的!他们的职责是很清晰的,这明显不是他们的职责。剩下可选的包括:

  • entity中:User.Register(),在用户注册的时候加密他的密码,将其归为业务逻辑,这是说得过去的。
  • srv中:首先在model和entity的映射过程中忽略password,避免后面的覆盖:
    @Mapping(target = "pwd", ignore = true)	
    User toUser(RegisterModel model);
    然后顺势赋值加密过后的密码,也是OK的:
    User user = userConvert.toUser(model);
    user.setPwd(Md5Util.Md5(model.getPassword()));

最后,前面使用getSingleResult()是有问题的,当没有查询到结果的时候,居然是抛异常而不是返回null值!

所以我们要么try...catch(不推荐,@想一想@:为什么?);要么修改GetByName()的接口返回List<User>:

List<User> GetByName(String name);
return query.getResultList();
这样,在service中:
List<User> users = userRepository.GetByName(username);
return users.size() > 0;

@想一想@:这样每次

  1. 获取EntityManager烦不烦?
  2. 声明事务烦不烦?
烦的话,怎么办呢?

注册成功之后呢?一般会自动登录。登录的过程,实际上就是生成一个证明用户身份的


cookie

复习:cookie和session

生成

可以在handler method中直接new一个javax.servlet.http.Cookie:
Cookie cookie = new Cookie("user", String.valueOf(userId));

构造函数第一个参数是cookie的名称,第二个是cookie中存储的值,这里是新注册用户的id。

然后要将生成的cookie发送给客户端。这需要javax.servlet.http.HttpServletResponse的addCookie()方法:

response.addCookie(cookie);
但response对象从哪里来呢?在handler method中添加这样一个参数即可!
public ModelAndView input(@ModelAttribute("command") @Validated RegisterModel model,
		BindingResult result,
		HttpServletResponse response) {//额外添加的repsonse
cookie对象中还可以设置:
  • 过期时间,以秒为单位,
    cookie.setMaxAge(90 * 24 * 60 * 60);
    如果是0话,就是告诉客户端理解删除该cookie
    cookie.setMaxAge(0);
    如果是负数的话,将cookie设置session模式(关闭浏览器后删除,你没看错我也没写错,就这么反人类)
    cookie.setMaxAge(-100); // 改成session模式
  • 重新设值
    cookie.setValue("986");
  • domain和path
    cookie.setDomain("sample.17bang.ren");
    cookie.setPath("/article")

其他不常用方法略……

获取

常用的有两种方式:

(和获得response对象类似)在handler method中添加javax.servlet.http.HttpServletRequest参数,从request中拿到cookie:

@RequestMapping(method = RequestMethod.GET)
public ModelAndView input(HttpServletRequest request,
	Cookie[] cookies = request.getCookies();
但这样的问题是拿到的是一个数组cookie,定位我们想要的cookie比较麻烦。可以引入一个类CookieUtil,并封装一个方法:
public class CookieUtil {	
	public static Cookie getCookie(Cookie[] cookies, String name) {
		if (cookies == null) {    //防御式
			return null;
		}
		for (int i = 0; i < cookies.length; i++) {
			//使用equals而不是==
			if (cookies[i].getName().equals(name)) {
				return cookies[i];
			}
		}
		return null;
	}
演示:拿到cookie之后删除
Cookie cookie = CookieUtil.getCookie(request.getCookies(), CookieKeys.UserId);
if (cookie != null) {
	cookie.setMaxAge(0);	//删除cookie	
	response.addCookie(cookie);		
}

所以SpringMVC为我们提供了另一种方式:在handler method的参数前面标记@CookieValue:

public ModelAndView input(
		@CookieValue(name = "userId", required = false) Integer id) {	
	System.out.println(id);

这其实就是model bind的一种,制定了数据源是cookie而已

  • name指示cookie的name,即取哪一个cookie
  • required指示是否一定要从cookie中取到这个参数值。
    • 默认为true,如果取不到的话抛异常,返回400页面;
    • 设置为false后,如果取不到值,参数值为null:这时候不要设参数类型为不可为null的primitive type,比如int

@想一想@:就凭cookie中的一个id,就可以确定用户的身份吗?


登录状态栏

功能实现:页面能根据当前用户是否已登录分别显示:欢迎你,xxx;或者,注册/登录链接……

srv中后端不相信前端

cookie是可以伪造的!其实前端无论什么都是可以伪造的,所以一定要注意安全……

一种常见的方式就是在cookie中存放userId的同时,还要存放user(经过加密过后)的password,或者其他身份识别凭据(token,比如随机生成的一个数字)

但现在的问题是,当我们在controller中设置cookie的时候:

Cookie cPwd = new Cookie(CookieKeys.UserPwd, /*加密过后的密码怎么来?*/)
一种方式是userService.Register(model)之后返回一个包含了id和password的对象;但另外一种思路是:在userService.Register(model)中完成cookie的操作?

ui vs srv?

反对方的意见:cookie是和web相关的,应该属于UI层;srv层应该和ui层彻底解耦,这样它才能够服务于非web的项目……这是很有道理的!

但是,这样就会带来一个比较麻烦的问题,ui层不得不频繁的、大量的向srv层传递cookie信息。比如,很多功能都需要知道当前用户(current user)是谁:

  • 发布内容,作者是谁
  • 点赞/踩,投票人是谁
  • 申请权限,申请人是谁
  • ……

这样就会出现大量这样的service方法:

  • publish(articleModel, userId, userAuth)
  • agree(articleld, userId, userAuth)
  • apply(admin, userId, userAuth)
  • ……

userId和userAuth参数就显得非常累赘!

如果我们能够确定,项目一定是基于web的,其实可以考虑在srv层直接通过cookie(有节制地)完成部分功能!

这样做的另一个理由是:ui始终不涉及密码加密等过程。

依赖注入

srv中怎么拿到cookie呢?

利用依赖注入,可以在任何地方通过@Autowired获得:ServletRequest、ServletResponse、HttpSession和WebRequest

private HttpServletRequest request;
private HttpServletResponse response;

@Autowired
public UserService(HttpServletRequest request, HttpServletResponse response) {
	this.request = request;
	this.response = response;
}

注意这里能是接口ServletRequest/Response,而必须是实现类HttpServletRequest/Response,因为后面重构注册功能时要用到:

public int Register(RegisterModel model) {
	Cookie cId = new Cookie(CookieKeys.UserId, String.valueOf(user.getId()));
	cId.setMaxAge(90 * 24 * 60 * 60); 
	Cookie cPwd = new Cookie(CookieKeys.UserPwd, user.getPwd());
	cPwd.setMaxAge(90 * 24 * 60 * 60);
	response.addCookie(cId);
	response.addCookie(cPwd);
ServletRequest中没有addCookie()方法,也不能这样强转:
((HttpServletResponse)response).addCookie(cId);
因为这时候的response是一个Spring框架生成的proxy对象,不是ServletResponse直接的实现类对象!

GetCurrentUser()

接下来我们通过读取cookie获取当前用户:

  1. 先从cookie中获取userId
  2. 然后根据userId查找其pwd
  3. 对比cookie中的pwd和数据库中的
protected User GetCurrentUser() {		
	Cookie cUserId = CookieUtil.getCookie(request.getCookies(), CookieKeys.UserId);		
	if (cUserId == null) {
		return null;
	}
	Cookie cUserPwd = CookieUtil.getCookie(request.getCookies(), CookieKeys.UserPwd);		
	if (cUserPwd == null) {
		//TODO: 是不是还应该做点啥?
		return null;
	}
	try {
		Integer userId = Integer.valueOf(cUserId.getValue());			
		User current = userRepository.Load(userId);
		if (current == null) {
			return null;
		}
		if (!cUserPwd.getValue().equals(current.getPwd())) {
			//TODO:
			return null;
		}
		return current;
		
	} catch (Exception e) {
		return null;
	}
}

#体会:层层防御

该方法会被大量使用,所以放置在BaseService基类中

@想一想@:如果只有userId,没有pwd;或者cookie中的pwd和数据库中的不一致,怎么办?

LoginStatus

这次我们从页面开始考虑这个问题。在_header.ftl中应该有这样的逻辑:
<#if loginStatus?? >
	<span>源栈欢迎你, <a href="/User/${loginStatus.id}">${loginStatus.username}</a> </span>
<#else>
	<a href="/Log/On">登录</a> | <a  href="/Register">注册</a>
</#if>
loginStatus应该是一个对象,包含id和username两个字段。他们从何而来?当然应该是controller:
LoginStatusModel loginStatus = userService.GetLoginStatus();
model.addAttribute("loginStatus", loginStatus);
controller又是调用srv,srv直接利用GetCurrentUser():
public LoginStatusModel GetLoginStatus() {
	return userConvert.toLoginStatusModel(GetCurrentUser());
}


和cookie相提并论的,还有

session

从request获得

HttpSession session = request.getSession(true);
  • 如果当前有session,返回当前session
  • 否则,如果传入参数为
    • true:创建一个session并返回(默认)
    • false:返回null值

通过handler method参数:

  • 直接声明
    public ModelAndView input(HttpSession session, 
  • 注解,同cookie
    public ModelAndView input(@SessionAttribute(required = false) LoginStatusModel user

依赖注入,同cookie章节的Request/Response……

和cookie不同,session中:

  • 可以放置任意类型的对象
    void setAttribute(String name,Object value)
  • 设置的不是过期时间,而是间隔/空闲时间
    session.setMaxInactiveInterval(20*60);  //单位:秒

演示:session依赖于cookie


页面跳转

演示:未登录用户,访问发布求助页面,自动跳转到登录页面

复习:30X和重定向

redirect

一样有2种方式:

1、利用response对象,同JSP中内容

response.sendRedirect("/17bang/");
return null;
注意参数中路径以域名后根路径为起始路径,现一般很少使用

2、path前缀redirect:

return new ModelAndView("redirect:/");
//或者:
return "redirect:/";
@想一想@:有无前缀的区别是什么?对比:
return "/";

继续建议:使用字符串常量……

return new ModelAndView(URLMapping.Redirect + URLMapping.Register.Base);
//或:
return URLMapping.Redirect + URLMapping.Register.Base;

PRG模式

Post-Redirect-Get,即:所有的Post请求,经处理之后都要Redirect,再经Get处理。

这种模式想要解决两个问题:(演示)

  • POST之后再F5刷新,某些浏览器(比如IE)会重复提交
  • 需要同时在Get handler和Post handler中为model赋值(比如页面有下拉列表需要填充options)

但带来的问题就是:如果Post中验证未通过,如何显示错误消息?只需要在Post handler method中将BindingResult随着redirect传递到Get handler method即可

在handler method中添加参数:

public ModelAndView input(RedirectAttributes redirectAttrs
方法中调用redirectAttrs.addFlashAttribute():
redirectAttrs.addFlashAttribute("error", result);
最后,在Get handler method中获取并解析传入的error:
model.addAttribute("org.springframework.validation.BindingResult.command", 
		model.asMap().get("error"));
知识点:
  • addFlashAttribute()会将数据存放在session中,但是“用后即仍”,只能用一次
  • asMap()方法将model解析成Map<String,?>

演示:

  • 另外一个方法addAttribute()将数据存放在url参数里
    redirectAttrs.addAttribute("id",35);
    redirectAttrs.addAttribute("dfg");
  • 验证结果(BindingResult)实际上通过org.springframework.validation.BindingResult.{modelname}存放

或许你觉得这样更麻烦……不要急,我们还有后续手段!



作业

  1. 补充以下文章信息:摘要、关键字、分类、广告和评论,使用dbFactory创建数据,显示
    • 文章单页
    • 文章列表,能分页、过滤和排序
  2. 补充完成注册页面邀请人功能:
    • 检查邀请人是否存在,邀请码是否正确并予以提示
    • 将邀请人信息一并存入数据库
    不是利用cookie,而是利用session,完成注册成功后自动登录(@想一想@:session中存什么?)
  3. 完成登录功能,包括:
    1. 验证:用户名不存在、用户名或密码错误
    2. “记住我”:登录凭据保存30天
  4. 完成退出登录功能,退出登录后刷新页面(即:仍留在当前页面
  5. (利用位运算赋权限)将用户分为:管理员(admin)、博主(blogger)和注册用户(registered),然后按一起帮完成导航栏
  6. 在任意一个页面点击页头的注册/登录链接,完成注册/登录后都会“跳转到之前页面”(演示:注册/登录的链接中都带有prepage参数,记录的是当前页面
  7. 实现功能:文章发布页需要登录用户才能访问,
    1. 未登录用户访问该页面,自动跳转到登录页面
    2. 用户可以:
      1. 在当前页完成登录,也可以
      2. 从登录页面转到注册页面完成注册
    3. 然后自动跳转到文章发布页面

  8. 利用PRG模式,完成个人资料页面功能
学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 从JSP到Spring 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码