【课程11】图片上传模块.xmind54.6KB
【课程11】消息发...通知.xmind55.2KB
【课程11】异常处理分析.xmind95.4KB
【课程11预习】多...解读.xmind0.4MB
【课程11】第三方...模块.xmind0.2MB
【课程11】多人博...解读.xmind0.3MB
项目简介
mblog (mtons blog)开源免费的Java多人博客系统。
技术选型
项目结构
项目模块切分
我们先来想一下,我们能不能一个项目就用一个模块。这样开起来很方便,简单明了,那么做起来呢,接下来我们分析一下。
假设我们有这么一个项目,整个项目构建一个war包,而每一层放到各自的Package里面。如下:
Itoo-Exam
com.tgb.itoo.exam.dao —–负责与数据库的交互,封装了hibernate的交互类
com.tgb.itoo.exam.service—-负责处理业务逻辑,放Service接口及其实现类
com.tgb.itoo.exam.web——-负责与客户端的交互,主要放action/controller,jsp等等
com.tgb.itoo.exam.util——–工具类
那么随着我们项目的扩大,Maven项目也会越来越大,那么会遇到下面的几个问题:
1、首先build整个项目的时间越来越长,尽管你一直在web层工作,但你不得不build整个项目。
2、模块化体现不出来,如果我们只负责维护某个模块,因为我们所有的模块都在一个war包中,那么我们可以随意修改其他模块(权限的控制),导致版本管理混乱,冲突。同时因为模块太多,太大,不好维护。很多人都在同时修改这个war包,将导致工作无法顺利进行。
3、pom.xml本来是可以继承、复用的,但是如果我们新建一个项目,只能依赖于这个war包,那么会把这个war包的相关的前台的东西依赖过来,导致项目管理混乱。
这样的管理是混乱的,没有遵守一个设计模式原则:“高内聚,低耦合”。相反在代码内部,所有的东西都耦合在了一起。因此我们需要划分模块。
另外,随着技术的飞速发展和各类用户对软件的要求越来越高,软件本身变得越来越复杂,设计人员开始采用各种方式进行开发,于是就有了我们的分层架构、分层模块来提高代码的清晰和重用。从而实现了系统内部的高内聚、低耦合。
模块化的好处
1、方便重用,当我们再开发一条teacher线的时候,我们只需要引用itoo-base,itoo-exam-api,这些包都是复用的,称为我们平台复用的基础类库,供所有的项目使用。这是模块化最重要的一个目的。
2、灵活性。比如我们这些公共的jar包,itoo-base,itoo-tool,itoo-exam-api等这些jar包,我们不需要再当源码,只需要deploy到nexus,其他人从nexus下载即可。代码的可维护性、可扩展性好,并且保证了项目独立性与完整性。
使用模块化配置,复用性强,防止pom变得过于庞大,方便构建;针对项目的管理更方便,每一个模块都是独立的,抽象出一个父类来管理第三方jar的版本,开发人员只需要开发自己的线,其他的都不用管,灵活;基于此种基础我们还可以做分布式。
聚合:
Maven聚合:当我们的模块非常多的时候,我们想要一次构建多个项目,而不是到多个模块的目录下分别执行命令。Maven的聚合特性就是为该需求服务的。
Maven构建: Maven首先解析聚合模块pom、分析要构建的模块、并计算出一个反应堆构建顺序,然后根据这个顺序依次构建各个模块。反应堆是所有模块组成的一个构建结构。
继承
Maven继承也是为了防止重复,让项目的jar包版本一致,在项目管理上起了很大的作用。
比如说相同的jar包我们每个人都需要依赖一遍,并且每个人引用的版本号不同,势必造成项目混乱,运行出问题。
1、子模块省略grouopId和version,都会从父模块依赖下来。
2、子模块元素pom.xml。
总之,聚合是为了方便快速构建项目,继承是为了消除重复配置,在简化pom的同时还能促进各个模块配置的一致性。
异常处理机制
关键类-HandlerExceptionResolver
Spring MVC通过HandlerExceptionResolver处理程序的异常,包括Handler的映射、数据绑定以及目标方法的执行。HandlerExceptionResolver时一个接口,该接口的实现类都有处理异常的功能。HandlerExceptionResolver是该接口应用广泛的一个实现类,并且DispatcherServlet默认装配了HandlerExceptionResolver 的Bean。
SpringMVC 提供的异常处理主要有两种方式:
- 一种是直接实现自己的HandlerExceptionResolver
通过注解的方式实现处理异常主要有以下两种方式:
- 1 @ControllerAdvice+@ExceptionHandler:配置对全局异常进行处理
- 2 @Controller + @ExceptionHandler:配置对当前所在Controller的异常进行处理
在SpringMVC中,处理异常类实际上是HandlerExceptionResolver子类。HandlerExceptionResolver处理所有controller类在执行过程中抛出的未被处理的异常。
本文演示如何使用以上多种处理异常的方式,最后演示以不同的方式将异常结果返回给调用者
系统默认实现的HandlerExceptionResolver
以下是系统默认加载到spring mvc容器中的HandlerExceptionResolver
- ExceptionHandlerExceptionResolver: 根据@ExceptionHandler注解的方法处理对应的异常。其实上文的通过注解的方式处理异常,实际就是在这个类中实现
- ResponseStatusExceptionResolver: 根据@ResponseStatus注解的方法处理异常
- DefaultHandlerExceptionResolver: 将异常转化为特定的HTTP的状态码
- HandlerExceptionResolverComposite:此类通过列表包含以上3个HandlerExceptionResolver,当捕获异常时,会循环调用以上3个HandlerExceptionResolver进行处理
ExceptionHandlerExceptionResolver处理过程总结一下:
- 根据用户调用Controller中相应的方法得到HandlerMethod,之后构造ExceptionHandlerMethodResolver,
- 构造ExceptionHandlerMethodResolver有2种选择,
- 1.通过HandlerMethod拿到Controller,找出Controller中带有@ExceptionHandler注解的方法(局部)
- 2.找到@ControllerAdvice注解配置的类中的@ExceptionHandler注解的方法(全局)。
- 这2种方式构造的ExceptionHandlerMethodResolver中都有1个key为Throwable,value为Method的缓存。之后通过发生的异常找出对应的Method,然后调用这个方法进行处理。
- 这里异常还有个优先级的问题,比如发生的是NullPointerException,但是声明的异常有Throwable和Exception,这时候ExceptionHandlerMethodResolver找Method的时候会根据异常的最近继承关系找到继承深度最浅的那个异常,即Exception。
发布与通知
常见场景
在实际开发过程中,常常遇到这种场景:
做完某一件事情以后,需要广播一些消息或者通知,告诉其他的模块进行一些事件处理,一般来说,可以一个一个发送请求去通知,但是这种方式比较消耗业务时间。那种更好解决方法呢,那就是事件监听,事件监听也是设计模式中 发布-订阅模式、观察者模式的一种实现。
观察者模式:在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。
Spring的事件为Bean和Bean之间的消息传递提供支持。当一个对象处理完某种任务后,通知另外的对象进行某些处理,常用的场景有进行某些操作后发送通知,消息,操作记录,发送短信、邮件等情况。
Spring的事件遵循的流程
自定义事件,继承ApplicationEvent(org.springframework.context.ApplicationEvent)
定义监听事件,实现ApplicationListener(org.springframework.context.ApplicationListener)
使用容器触发事件。
发布事件,使用applicationContext发布事件。
ApplicationEvent中,在自定义事件的构造方法中除了第一个source参数,其他参数都可以去自定义,
可以根据项目实际情况进行监听传参,这里就只定义个简单的String字符串的透传。
逻辑整理
这里使用一种简单的单机实现Spring的事件模型ApplicationEvent。
第一步、分别自定义订阅和通知事件,继承ApplicationEvent
第二步、分别定义事件监听器,实现ApplicationListener
第三步、使用容器发布事件(订阅事件、通知事件)
项目运用
- FollowController.sendNotify()-->发送关注通知。
- NotifyEventHandler,关注事件监听处理类,保存通知到数据库
spring中的运用
知识拓展--@EventListener
有条件的事件处理
为了使注释@EventListener的功能更强大,Spring 4.2支持用SpEL表达式表达事件类型的方式
定义事件
public class TestEvent extends ApplicationEvent {
public boolean isImport;
public TestEvent(Object source, boolean isImport) {
super(source);
this.isImport = isImport;
}
public boolean isImport() {
return isImport;
}
public void setImport(boolean anImport) {
isImport = anImport;
}
@Override
public String toString() {
return "TestEvent{" +
"isImport=" + isImport +
'}';
}
}
定义监听
@Component
public class EventHandler {
@EventListener(condition="#testEvent.isImport")
public void TestEventTest(TestEvent testEvent) {
System.out.println("==============TestEvent==============" + testEvent.toString());
}
}
知识拓展--guava的EventBus
定义事件
public class GuavaEvent {
private final int message;
public GuavaEvent(int message) {
this.message = message;
System.out.println("event message:"+message);
}
public int getMessage() {
return message;
}
}
定义事件监听
public class GuavaEventListener {
public int lastMessage = 0;
@Subscribe
public void listen(GuavaEvent event) {
lastMessage = event.getMessage();
System.out.println("guava--------Message:"+lastMessage);
}
public int getLastMessage() {
return lastMessage;
}
}
发布事件
EventBus eventBus = new EventBus();
GuavaEventListener listener = new GuavaEventListener();
eventBus.register(listener);
eventBus.post(new GuavaEvent(200));
eventBus.post(new GuavaEvent(300));
System.out.println("LastMessage:"+listener.getLastMessage());
事件监听者[Listeners]
监听特定事件(如,CustomerChangeEvent)
- 传统实现:定义相应的事件监听者类,如CustomerChangeEventListener;
- EventBus实现:以CustomerChangeEvent为唯一参数创建方法,并用Subscribe注解标记。
把事件监听者注册到事件生产者:
- 传统实现:调用事件生产者的registerCustomerChangeEventListener方法;这些方法很少定义在公共接口中,因此开发者必须知道所有事件生产者的类型,才能正确地注册监听者;
- EventBus实现:在EventBus实例上调用EventBus.register(Object)方法;请保证事件生产者和监听者共享相同的EventBus实例。
按事件超类监听(如,EventObject甚至Object):
- EventBus实现:EventBus自动把事件分发给事件超类的监听者,并且允许监听者声明监听接口类型和泛型的通配符类型(wildcard,如 ? super XXX)。
检测没有监听者的事件:
- 传统实现:在每个事件分发方法中添加逻辑代码(也可能适用AOP);
- EventBus实现:监听DeadEvent;EventBus会把所有发布后没有监听者处理的事件包装为DeadEvent(对调试很便利)。
事件生产者[Producers]
管理和追踪监听者:
- 传统实现:用列表管理监听者,还要考虑线程同步;或者使用工具类,如EventListenerList;
- EventBus实现:EventBus内部已经实现了监听者管理。
向监听者分发事件:
- 传统实现:开发者自己写代码,包括事件类型匹配、异常处理、异步分发;
- EventBus实现:把事件传递给 EventBus.post(Object)方法。异步分发可以直接用EventBus的子类AsyncEventBus。
图片上传模块
@PostContruct
是spring框架的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
从这个文件开始看:
- 图片上传的入口,通过upload方法上传图片,并返回上传结果
- 实现FileRepo接口,把实现类基础公用部分的方法实现。
- 继承抽象类AbstractFileRepo,重写getRoot()方法,为绝对路径保存图片
DateFormatUtils.format(new Date(), YYYYMMDDHHMMSS);
图片压缩关键类-- Thumbnails
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
使用例子:
Oauth2.0协议运用
传统授权方式缺点:
(1)网站为了后续的服务,会保存用户的密码,这样很不安全。
(2)Google不得不部署密码登录,而我们知道,单纯的密码登录并不安全。
(3)网站拥有了获取用户储存在Google所有资料的权力,用户没法限制网站获得授权的范围和有效期。
(4)用户只有修改密码,才能收回赋予网站的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。
(5)只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。
OAuth就是为了解决上面这些问题而诞生的。
协议原理
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
OAuth的校验流程为什么这么复杂,直接授权之后redirect回accessToken不就结了吗?为什么还要返回auth_code之后请求accessToken?
首先,redirect是不安全的,随时可以暂停回调而拿到accessToken,拿到了accessToken也就意味着拿到了授权,但是auth_code是和client相对应的,那么即使拿到了auth_code还需要再次申请accessToken,申请accessToken时需要校验Client和state。同时协议设计的原则就是只有Client能拿到accessToken而用户是拿不到的。
Oauth2.0安全使用建议
资源提供方:
- 获取access token的code仅能使用一次,且与授权用户关联
资源使用方:
- 使用Authorization Code方式进行授权
- 授权过程使用state随机哈希,并在服务端进行判断
- 最后,对oauth2.0有详细的了解,严格按照流程进行开发。
QQ授权登录
QQ登录OAuth2.0总体处理流程如下:
Step1:申请接入,获取appid和apikey;
Step2:开发应用,并设置协作者帐号进行测试联调;
Step3:放置QQ登录按钮;
Step4:通过用户登录验证和授权,获取Access Token;
Step5:通过Access Token获取用户的OpenID;
Step6:调用OpenAPI,来请求访问或修改用户授权的资源。
QQ互联官网开发攻略:http://wiki.connect.qq.com/%E5%BC%80%E5%8F%91%E6%94%BB%E7%95%A5_server-side
项目接入:
- http://localhost:8080/oauth/callback/call_qq
- https://graph.qq.com/oauth2.0/authorize?response_type=code&redirect_uri=&state=0vnuc37nwskcs9cr3yo1wvaq&client_id=
- 通过Authorization Code获取Access Token
- https://graph.qq.com/oauth2.0/token?code
- https://graph.qq.com/oauth2.0/me?access_token=
- 通过accessToken和openid获取用户信息
- https://graph.qq.com/user/get_user_info?access_token=&oauth_consumer_key=&openid=&format=json
- 判断是否已经注册,为注册跳转到/bind_oauth方法进行账号注册与绑定。然后使用shiro登录。