(转)mblog解读(一)

(二期)11、开源博客项目mblog解读(一)

【课程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类在执行过程中抛出的未被处理的异常。

本文演示如何使用以上多种处理异常的方式,最后演示以不同的方式将异常结果返回给调用者

 

  • 返回 modelAndView
  • 返回一个页面的地址
  • 返回 JSON
  • 返回 http 错误码
系统默认实现的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

第三步、使用容器发布事件(订阅事件、通知事件)

项目运用
  • 定义通知事件NotifyEvent。
  • 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;
    }

}

发布事件

//guava test
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容器初始化的时候执行该方法。

 

从这个文件开始看:

  • UploadController
  • 图片上传的入口,通过upload方法上传图片,并返回上传结果
  • FileRepo
  • 图片上传接口,定义图片上传应该拥有的所有相关方法
  • AbstractFileRepo
  • 实现FileRepo接口,把实现类基础公用部分的方法实现。
  • FileRepoImpl
  • 继承抽象类AbstractFileRepo,重写getRoot()方法,为绝对路径保存图片
DateFormatUtils.format(new Date(), YYYYMMDDHHMMSS);
//输出值格式例如:/2018/0527/27160051
图片压缩关键类-- Thumbnails
  • 指定大小进行缩放
  • 按照比例进行缩放
  • 不按照比例,指定大小进行缩放
  • 旋转
  • 水印
  • 裁剪
  • 转化图像格式
  • 输出到OutputStream
  • 输出到BufferedImage
<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安全使用建议

资源提供方:

  • 对client_id和回调地址做严格校验
  • 获取access token的code仅能使用一次,且与授权用户关联
  • 尽量避免直接读取当前用户session进行绑定
  • 有效使用client_secret参数

资源使用方:

  • 使用Authorization Code方式进行授权
  • 授权过程使用state随机哈希,并在服务端进行判断
  • 尽量使用HTTPS保证授权过程的安全性
  • 最后,对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

 

项目接入:

  • QQ登录按钮链接
  • http://localhost:8080/oauth/callback/call_qq
  • 获取Authorization Code
  • 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=&client_id=&client_secret=&grant_type=&authorization_code&redirect_uri=
  • 通过accessToken获取openid
  • 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登录。
posted @ 2018-10-24 09:46  free_wings  阅读(722)  评论(0编辑  收藏  举报