说明:接下来的课程都会尽量结合实战进行,一步一步的展示代码的进化……
复习:为什么需要?
项目中新建一个文件夹dbFactory,里面新建一个带main()方法的BuildFactory类,
public class BuildFactory{ public static void main(String[] args) { System.out.println("begin create database……");可以直接F11运行,选择run as - Java Application即可运行:
引入控制台中hibernate建库建表代码,注意一样需要在java path下新建文件夹META-INF,放入persistence.xml文件,即:项目中会有两个persistence.xml:
不要忘了给entity添加注解,其中作为基类的BaseEntity:
@MappedSuperclass public class BaseEntity<T>{ @Id @GeneratedValue( strategy = GenerationType.IDENTITY ) protected T id;
id要设置成protected(@试一试@:不这样做仍然是private会发生什么情况);另外id是数据库生成,所以不需要setter。
为便于管理,
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的时候怎么将其设置为指定的时间呢?两个方案:
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();
就是将用户名和密码存入数据库。(@想一想@:真的这样吗?)
但是,其中又有一些细节:
密码明文存储安全吗?怎么加密呢?
新建一个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的调用和封装……
@想一想@:既然在业务逻辑中已经进行了检查,还有必要在数据库上添加唯一约束么?)
if (usernameExists) { //报错,略…… } else { userService.Register(model); }UserRepository中(省略了事务声明等):
em.persist(user);
但这里有一个问题:密码的MD5加密应该写哪里?首先是肯定不应该放在repository和UI层的!他们的职责是很清晰的,这明显不是他们的职责。剩下可选的包括:
@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;
@想一想@:这样每次
注册成功之后呢?一般会自动登录。登录的过程,实际上就是生成一个证明用户身份的
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) {//额外添加的repsonsecookie对象中还可以设置:
cookie.setMaxAge(90 * 24 * 60 * 60);如果是0话,就是告诉客户端理解删除该cookie
cookie.setMaxAge(0);如果是负数的话,将cookie设置session模式(关闭浏览器后删除,你没看错我也没写错,就这么反人类)
cookie.setMaxAge(-100); // 改成session模式
cookie.setValue("986");
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而已
@想一想@:就凭cookie中的一个id,就可以确定用户的身份吗?
cookie是可以伪造的!其实前端无论什么都是可以伪造的,所以一定要注意安全……
一种常见的方式就是在cookie中存放userId的同时,还要存放user(经过加密过后)的password,或者其他身份识别凭据(token,比如随机生成的一个数字)
但现在的问题是,当我们在controller中设置cookie的时候:
Cookie cPwd = new Cookie(CookieKeys.UserPwd, /*加密过后的密码怎么来?*/)一种方式是userService.Register(model)之后返回一个包含了id和password的对象;但另外一种思路是:在userService.Register(model)中完成cookie的操作?
反对方的意见:cookie是和web相关的,应该属于UI层;srv层应该和ui层彻底解耦,这样它才能够服务于非web的项目……这是很有道理的!
但是,这样就会带来一个比较麻烦的问题,ui层不得不频繁的、大量的向srv层传递cookie信息。比如,很多功能都需要知道当前用户(current user)是谁:
这样就会出现大量这样的service方法:
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直接的实现类对象!
接下来我们通过读取cookie获取当前用户:
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和数据库中的不一致,怎么办?
<#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相提并论的,还有
从request获得
HttpSession session = request.getSession(true);
通过handler method参数:
public ModelAndView input(HttpSession session,
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和重定向
一样有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;
Post-Redirect-Get,即:所有的Post请求,经处理之后都要Redirect,再经Get处理。
这种模式想要解决两个问题:(演示)
但带来的问题就是:如果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"));知识点:
演示:
redirectAttrs.addAttribute("id",35); redirectAttrs.addAttribute("dfg");
或许你觉得这样更麻烦……不要急,我们还有后续手段!
然后自动跳转到文章发布页面
多快好省!前端后端,线上线下,名师精讲
更多了解 加: