社区项目中的登录模块功能开发。主要包含内容:利用Spring Email发送邮件(发送邮件、开发注册功能),Cookie、Session的使用(会话管理、生成验证码、登录和退出),Kaptcha生成验证码工具的使用(生成验证码),拦截器的使用(显示登录信息、检查登录状态),TheadLocal的使用(显示登录信息),MultipartFile上传文件(账号设置)。
发送邮件
邮箱设置:启用客户端SMTP服务
Spring Email:
导入jar包
邮箱参数配置
域名、端口号、邮箱账号、邮箱密码、邮箱协议(smtps表示安全的smtp)、邮箱采用安全连接
使用JavaMailSender发送邮件
src/main/java/community/util/包下新建工具类:MailClient.class
变量:
- mailSender: JavaMailSender类型,Spring Framework 中的一个接口。
- form:注入发送人的邮箱。
函数:
sendMail函数:
调用mailSender的
createMimeMessage()
创建邮件主体。MimeMessageHelper是辅助类,用来构建Mime邮件消息。分别设置了发件人,收件人,邮件主题,邮件内容。最后调用mailSender的
send()
函数发送邮件。
模板引擎:使用Thymeleaf发送HTML邮件
html模板:
利用模板引擎格式化html邮件:
开发注册功能
dao层:数据访问层
dao层属于一种比较底层,比较基础的操作,具体到对于某个表的增删改查,也就是说某个DAO一定是和数据库的某一张表一 一对应的,其中封装了增删改查基本操作,建议DAO只做原子操作,增删改查。
负责与数据库进行联络的一些任务都封装在此,dao层的设计首先是设计dao层的接口,然后在Spring的配置文件中定义此接口的实现类,然后就可以再模块中调用此接口来进行数据业务的处理,而不用关心此接口的具体实现类是哪个类,显得结构非常清晰,dao层的数据源配置,以及有关数据库连接参数都在Spring配置文件中进行配置。
service层:服务层
粗略的理解就是对一个或多个DAO进行的再次封装,封装成一个服务,所以这里也就不会是一个原子操作了,需要事物控制。
service层主要负责业务模块的应用逻辑应用设计。同样是首先设计接口,再设计其实现类,接着再Spring的配置文件中配置其实现的关联。这样我们就可以在应用中调用service接口来进行业务处理。service层的业务实,具体要调用已经定义的dao层接口,封装service层业务逻辑有利于通用的业务逻辑的独立性和重复利用性。程序显得非常简洁。
controller层:
Controler负责请求转发,接受页面过来的参数,传给Service处理,接到返回值,再传给页面。
controller层负责具体的业务模块流程的控制,在此层要调用service层的接口来控制业务流程,控制的配置也同样是在Spring的配置文件里进行,针对具体的业务流程,会有不同的控制器。我们具体的设计过程可以将流程进行抽象归纳,设计出可以重复利用的子单元流程模块。这样不仅使程序结构变得清晰,也大大减少了代码量。
三层的关系:
Service层是建立在DAO层之上的,建立了DAO层后才可以建立Service层,而Service层又是在Controller层之下的,因而Service层应该既调用DAO层的接口,又要提供接口给Controller层的类来进行调用,它刚好处于一个中间层的位置。每个模型都有一个Service接口,每个接口分别封装各自的业务处理方法。
访问注册页面:点击顶部区域内的链接打开注册页面
在src/main/java/community/Controller包下新建LoginController.class类,声明注册功能访问路径(get方法)
提交注册数据(实现UserService中的register方法):
服务端验证账号是否已经存在,邮箱是否已经注册
src/main/java/community/Service包下新建UserService.class类
public Map<String,Object> register(User user)
函数:验证提交数据是否为空:
验证账号合法性:
注册用户:
服务端发送激活邮件
还是在
public Map<String,Object> register(User user)
函数内:
通过表单提交数据(LoginController中实现post方法):
激活注册账号(UserService中activation方法和LoginCotroller中activation方法):
点击邮件中的链接,访问服务端激活服务:
UserService中activation方法提供判断用户是否应该被激活以及激活用户的功能:
LoginCotroller中activation方法利用服务端返回的激活状态显示不同的文字:
会话管理
HTTP基本性质:
- HTTP是简单的
- HTTP是可扩展的
- HTTP是无状态,有会话的
Cookie
是服务器发送到浏览器并保存在浏览器端的一小块数据
浏览器下次访问该服务器时,会自动携带该块数据,将其发送给服务器
Session
是JavaEE的标准,用于在服务端记录客户端信息,依赖于Cookie
数据存放在服务端更加安全,但是也会增加服务端的内存压力
生成验证码
Kaptcha
编写Kaptcha配置类
src/main/java/community/Config包下新建KaptchaConfig.class
生成随机字符、生成图片
LoginController.class类中新建
getKaptcha(HttpServletResponse response, HttpSession session)
方法验证码不能存在客户端,容易被盗取,所以要存在服务器。利用Session存取Kaptcha生成的验证码
login.html文件中需要做如下改动:
实现的js方法:
登录和退出功能
Dao层需要实现LoginTicketMapper.class类的增改查功能:
访问登录页面
- 点击顶部区域内链接,打开登陆页面
登录:实现UserService类中的
Map<String,Object> login(String username,String password,long expiredSeconds)
方法以及LoginController类中的String login(String username,String password,String code,boolean rememberme,Model model,HttpSession session,HttpServletResponse response)
方法验证账号、密码、验证码
UserService中的login方法验证账号密码:
LoginController中的login方法验证验证码和账号密码:
成功时生成登陆凭证(ticket),发放给客户端
UserService中的login方法:
失败时跳回登录页
失败后跳回登录页表单中还包含上次填写的数据,html文件中部分逻辑如下:
退出
将登陆凭证修改为失效状态
UserService类中实现
void logout(String ticket)
方法修改用户状态:跳转至网站首页
loginController类中实现
String logout(@CookieValue("ticket") String ticket)
方法跳转到首页:
显示登录信息
主要使用拦截器来实现功能。此处注意如果同时配置一个类继承WebMvcConfigurationSupport
和一个类实现WebMvcConfigurer
或者WebMvcConfigurerAdapter
,就会导致只有一个生效。解决办法:将这些配置都在一个类中设置。
拦截器:
spring中拦截器主要分两种,一个是HandlerInterceptor
,一个是MethodInterceptor
。
HandlerInterceptor
是springMVC项目中的拦截器,它拦截的目标是请求的地址,比MethodInterceptor
先执行。
实现一个HandlerInterceptor
拦截器可以直接实现HandlerInterceptor
接口,也可以继承HandlerInterceptorAdapter
类。这两种方法殊途同归,其实HandlerInterceptorAdapter
也就是声明了HandlerInterceptor
接口中所有方法的默认实现,而我们在继承他之后只需要重写必要的方法。
preHandle: 用来拦截处理器的执行,preHandle方法将在Controller处理之前调用的。SpringMVC里可以同时存在多个interceptor,它们基于链式方式调用,每个interceptor都根据其声明顺序依次执行。这种链式结构可以中断,当方法返回false时整个请求就结束了。
方法的返回值是布尔类型,方法如果返回false,那后面的interceptor和Controller都不会执行(通常都会响应一个自定义的 Http 错误码给客户端)。如果返回值为true,则接着调用下一个interceptor的preHandle方法。如果当前是最后一个interceptor,接下来就会直接调用Controller的处理方法。
postHandle: postHandle 会在Controller方法调用之后,但是在DispatcherServlet 渲染视图之前调用。因此我们可以在这个阶段,对将要返回给客户端的ModelAndView进行操作。
afterCompletion: afterCompletion在当前interceptor的preHandle方法返回true时才执行。该方法会在整个请求处理完成后被调用,就是DispatcherServlet渲染视图完成以后,主要是用来进行资源清理工作。需要注意的是,afterCompletion在interceptor链式结构中以相反的顺序执行,也就是说先申明的interceptor返回会后调用。
拦截器内方法的执行顺序依次是:preHandle -> postHandle -> afterCompletion。
配置拦截器:为拦截器指定拦截、排除的路径
Config包下新建
WebMvcConfig
类实现WebMvcConfigurer
接口。拦截器应用
在Controller包下新建Interceptor包,在Interceptor包中新建
LoginTicketInterceptor
类实现HandlerInterceptor
接口。在请求开始时查询登录用户
在LoginTicketInterceptor类中实现
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
方法:注意:hostHolder用于代替Session对象来作为一个容器存储user对象,hostHolder中利用ThreadLocal实现线程隔离。
ThreadLocal:
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的线程中有不同的副本。这里有几点需要注意:
因为每个线程内有自己的实例副本,且该副本只能由当前Thread使用。这是也是ThreadLocal命名的由来。 既然每个Thread有自己的实例副本,且其它Thread不可访问,那就不存在多线程间共享的问题。 ThreadLocal提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal相对的实例副本都可被回收。
ThreadLocal与Synchronized的区别: ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,Threadlocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值 Entry(Threadlocal,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。 ThreadLocal用法和原理:
ThreadLocal中主要的方法就是set()、get()、clear()。
set()
ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
get()
clear()
ThreadLocal常用场景:
ThreadLocal主要用于:
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
常见的情况有:
- 存储用户Session
- 数据库连接,处理数据库事务
- 数据跨层传递(controller,service, dao)
- Spring使用ThreadLocal解决线程安全问题
在本次请求中持有用户数据
在LoginTicketInterceptor类中实现
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
方法:在模板视图上显示用户数据
在请求结束时清理用户数据
账号设置
上传文件
- 请求:必须是POST请求
- 表单:enctype="multipart/form-data"
- Spring MVC:通过MultipartFile处理上传文件
开发步骤
访问账号设置页面
在Controller包下新建UserController类,声明
String getSettingPage()
方法:上传头像
配置上传头像的资源位置:
在application.properties中增加如下配置:
在UserService类中增加
int updateHeader(int userId,String headerUrl)
方法:在UserController类中增加
String uploadHeader(MultipartFile headerImage, Model model)
方法上传文件:获取头像
在UserController类中增加
void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response)
方法获取用户头像:并在setting.html文件中做上传头像部分相应的修改:
检查登录状态
使用拦截器
在/user/setting的功能中使用拦截器,如果检测到当前页面不是登录状态,则不允许访问该页面,直接跳转到设置页面。
在方法前标注自定义注解
在Community下新建annotation包,在该包下新建LoginRequired接口类,声明注解的位置、生效时间:
拦截所有请求,只处理带有该注解的方法
在UserController类中的getSettingPage和uploadHeader方法上声明两个注解用来拦截请求:
在Interceptor包下新建LoginRequiredInterceptor类读取注解并做处理:
并且在Config包下的WebMvcConfig类中声明拦截器作用范围:
自定义注解
常用的元注解:
@Target、@Retention、@Document、@Inherited
如何读取注解:
Method.getDeclaredAnnotations()
Method.getAnnotation(Class
annotationClass)