邓奔成

邓奔成 查看完整档案

成都编辑西南科技大学  |  理学院 编辑33  |  33 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

邓奔成 发布了文章 · 11月9日

spring扫描自定义注解对应的类并装入IOC容器

前言

本文继笔者学习mybatis源码后,仿照mybatis利用spring的拓展点将Mapper接口的代理类装入IOC的方法,实现自定义注解,让spring将指定包中有此注解的类装配进IOC容器来管理之,最终是放入容器的单例池中。

实现

1. 自定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface XxgComponent {
}

2. XxgComponentScannerConfigurer

2.1 实现BeanDefinitionRegistryPostProcessor

BeanDefinitionRegistryPostProcessor,是spring给我们的一个拓展点,其自身继承自BeanFactoryPostProcessor接口。
实现此接口的类,spring在初始化之后,会调用其以下两个方法:

void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
void postProcessBeanFactory(ConfigurableListableBeanFactory var1) throws BeansException;

故我们可以在其实现类对应的方法中,执行我们的业务逻辑--即让spring扫描我们自定义注解对应的类。

2.2 代码

public class XxgComponentScannerConfigurer implements BeanDefinitionRegistryPostProcessor {
     private String basePackages;
     public void setBasePackages(String basePackages) {
            this.basePackages = basePackages;
     }
     @Override
     public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
            // left intentionally blank
     }
     @Override
     public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
            XxgComponentScanner scanner = new XxgComponentScanner(registry);
         scanner.registerFilters();
         scanner.scan(basePackages);
     }
}

3.扫描类XxgComponentScanner

3.1 ClassPathBeanDefinitionScanner

此类是spring用于扫描bean的类,

  1. doScan()方法会根据配置的拦截器规则,来将需要的类装配成BeanDefinitionHolder对象并返回。
  2. 调用其scan()方法,其自身先调用doScan()方法,然后会将扫描到的BeanDefinitionHolder对象放入IOC容器

故,我们可以继承其类,然后:

  1. 不使用其默认拦截规则,而是根据我们自定义注解来拦截
  2. 重写其doScan()方法,首先调用父类的doScan()获取到有我们自定义注解的BeanDefinitionHolder对象们,然后可以遍历操作之,更改一些我们需要的参数,比如设置scope为单例等。

3.2 代码

public class XxgComponentScanner extends ClassPathBeanDefinitionScanner {
    public XxgComponentScanner(BeanDefinitionRegistry registry) {
        // 第二个参数:useDefaultFilters
        // 设置为false,不使用默认拦截器
        super(registry, false);
    }
    @Override
     public Set<BeanDefinitionHolder> doScan(String... basePackages) {
            // 利用spring的扫描方法,扫描mapper接口所在的包,获取所有BeanDefinitionHolder
         Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
         processBeanDefinitions(beanDefinitions);
         return beanDefinitions;
     }
     private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitionHolders) {
        for (BeanDefinitionHolder holder : beanDefinitionHolders) {
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition)holder.getBeanDefinition();
 beanDefinition.setScope("singleton");
        System.out.println(beanDefinition.getBeanClassName());
        }
    }
    // 定义拦截器,让spring装配我们需要的对象到IOC容器
    public void registerFilters() {
        // 设置 只扫描有@XxgComponent注解的类
     addIncludeFilter(new AnnotationTypeFilter(XxgComponent.class));
     // 设置 忽略 package-info.java addExcludeFilter((metadataReader, metadataReaderFactory) -> {
                String className = metadataReader.getClassMetadata().getClassName();
        return className.endsWith("package-info");
     });
   }
}

4.让spring管理我们的XxgComponentScannerConfigurer

<!-- 测试自定义装配bean注解-->
 <bean class="com.dbc.springMybatis.util.XxgComponentScannerConfigurer">
   <property name="basePackages" value="com.dbc.springMybatis.myBean"/>
 </bean>
查看原文

赞 0 收藏 0 评论 0

邓奔成 赞了文章 · 10月22日

总算把线程六种状态的转换说清楚了!






在我们接触编程时,就开始接触各种生命周期,比如对象的生命周期,程序的生命周期等等,对于线程来说也是存在自己的生命周期,而且这也是面试与我们深入了解多线程必备的知识,今天我们主要介绍线程的生命周期及其各种状态的转换。

线程的六种状态

线程的生命周期主要有以下六种状态:

  • New(新创建)
  • Runnable(可运行)
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed Waiting(计时等待)
  • Terminated(被终止)


在我们程序编码中如果想要确定线程当前的状态,可以通过 getState() 方法来获取,同时我们需要注意任何线程在任何时刻都只能是处于一种状态。


New 新建状态             

  • 首先我们展示一下整个线程状态的转换流程图,下面我们将进行详细的介绍讲解,如下图所示,我们可以直观的看到六种状态的转换,首先左侧上方是 NEW 状态,这是创建新线程的状态,相当于我们 new Thread() 的过程。

  • New 表示线程被创建但尚未启动的状态:当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,那么线程也就没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New。而一旦线程调用了 start(),它的状态就会从 New 变成 Runnable,进入到图中绿色的方框

Runnable 可运行状态

  • Java 中的 **Runable ** 状态对应操作系统线程状态中的两种状态,分别是 Running Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
  • 所以,如果一个正在运行的线程是 Runnable 状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。




阻塞状态

  • 上面认识了线程的关键状态 Runnable ,那么接下来我们来看一下下面的三个状态,这三个状态我们可以统称为阻塞状态,它们分别是 Blocked(被阻塞)Waiting(等待)Timed Waiting(计时等待) .


Blocked 被阻塞状态

  • 首先我们来认识一下 Blocked 状态,这是一个相对简单的状态,我们可以通过下面的图示看到,从 Runnable 状态进入到 Blocked 状态只有一种途径,那么就是当进入到 synchronized 代码块中时未能获得相应的 monitor 锁(关于 monitor 锁我们在之后专门来介绍,这里我们知道 synchronized 的实现都是基于 monitor 锁的),


  • 在右侧我们可以看到,有连接线从 Blocked 状态指向了 Runnable ,也只有一种情况,那么就是当线程获得 monitor 锁,此时线程就会进入 Runnable 状体中参与 CPU 资源的抢夺

Waiting 等待状态


上面我们看完阻塞状态,那么接下来我们了解一下 Waiting 状态,对于 Waiting 状态的进入有三种情况,如下图中所示,分别为:

  • 当线程中调用了没有设置 Timeout 参数的 Object.wait() 方法
  • 当线程调用了没有设置 Timeout 参数的 Thread.join() 方法
  • 当线程调用了 LockSupport.park() 方法

Waiting 等待状态

关于 LockSupport.park() 方法,这里说一下,我们通过上面知道 Blocked 是针对 synchronized monitor 锁的,但是在 Java 中实际是有很多其他锁的,比如 ReentrantLock 等,在这些锁中,如果线程没有获取到锁则会直接进入 Waiting 状态,其实这种本质上它就是执行了 LockSupport.park() 方法进入了Waiting 状态
  • **Blocked ****Waiting** 的区别

    • Blocked 是在等待其他线程释放 monitor
    • Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll()

Timed Waiting 计时等待状态

  • 最后我们来说说这个 Timed Waiting 状态,它与 Waiting 状态非常相似,其中的区别只在于是否有时间的限制,在 Timed Waiting 状态时会等待超时,之后由系统唤醒,或者也可以提前被通知唤醒如 notify




通过上述图我们可以看到在以下情况会让线程进入 Timed Waiting 状态。

  • 线程执行了设置了时间参数的 Thread.sleep(long millis) 方法;
  • 线程执行了设置了时间参数的 Object.wait(long timeout) 方法;
  • 线程执行了设置了时间参数的 Thread.join(long millis) 方法;
  • 线程执行了设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法。
通过这个我们可以进一步看到它与 waiting 状态的相同

线程状态间转换

上面我们讲了各自状态的特点和运行状态进入相应状态的情况 ,那么接下来我们将来分析各自状态之间的转换,其实主要就是 BlockedwaitingTimed Waiting 三种状态的转换 ,以及他们是如何进入下一状态最终进入 Runnable


Blocked 进入 Runnable

  • 想要从 Blocked 状态进入 Runnable 状态,我们上面说过必须要线程获得 monitor 锁,但是如果想进入其他状态那么就相对比较特殊,因为它是没有超时机制的,也就是不会主动进入。

如下图中紫色加粗表示线路:



Waiting 进入 Runnable

  • 只有当执行了 LockSupport.unpark(),或者 join 的线程运行结束,或者被中断时才可以进入 Runnable 状态。
  • 如下图标注


  • 如果通过其他线程调用 notify()notifyAll()来唤醒它,则它会直接进入 Blocked 状态,这里大家可能会有疑问,不是应该直接进入 Runnable 吗?这里需要注意一点 ,因为唤醒 Waiting 线程的线程如果调用 notify()notifyAll(),要求必须首先持有该 monitor 锁,这也就是我们说的 wait()notify 必须在 synchronized 代码块中。
  • 所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。

这里大家一定要注意这点,当我们通过 notify 唤醒时,是先进入阻塞状态的 ,再等抢夺到 monitor 锁喉才会进入 Runnable 状态!




Timed Waiting 进入 Runnable

  • 同样在 Timed Waiting 中执行 notify()notifyAll() 也是一样的道理,它们会先进入 Blocked 状态,然后抢夺锁成功后,再回到 Runnable 状态。


  • 但是对于 Timed Waiting 而言,它存在超时机制,也就是说如果超时时间到了那么就会系统自动直接拿到锁,或者当 join 的线程执行结束/调用了LockSupport.unpark()/被中断等情况都会直接进入 Runnable 状态,而不会经历 Blocked 状态


Terminated 终止


最后我们来说最后一种状态,Terminated 终止状态,要想进入这个状态有两种可能。

  • run() 方法执行完毕,线程正常退出。
  • 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。

总结

最后我们说一下再看线程转换的过程中一定要注意两点:

  • 线程的状态是按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。
  • 线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。
  • 所以一个线程只能有一次 New Terminated 状态,只有处于中间状态才可以相互转换。也就是这两个状态不会参与相互转化



本文由AnonyStar 发布,可转载但需声明原文出处。
欢迎关注微信公账号 :云栖简码 获取更多优质文章
更多文章关注笔者博客 :云栖简码 i-code.online
查看原文

赞 1 收藏 0 评论 0

邓奔成 赞了文章 · 10月22日

淦!为什么到处都是广告!

前言

微信搜【Java3y】关注这个有梦想的男人,点赞关注是对我最大的支持!

文本已收录至我的GitHubhttps://github.com/ZhongFuCheng3y/3y,有300多篇原创文章,最近在连载面试和项目系列!

我,三歪,最近要开始写项目系列文章。我给这个系列取了一个名字,叫做《揭秘

上一篇《揭秘!消息管理平台的实现原理》得到了不少同学的好评和赞赏,还没看过的同学可以去看看,那是我维护近一年的系统,我把核心都在文章中讲到了。

这篇文章主要讲广告相关的基础内容,不含任何技术相关的东西。广告系统会比消息管理平台这种系统有趣且复杂得多,消息管理平台可能几十个字就可以讲清楚它是做什么的,它有什么用,为什么需要它。

而广告系统不一样,如果你不了解任何广告相关的基础知识,你去看看广告相关的文章,你会发现:这些字你都认识,但你就是不知道它在讲什么。

出现这个问题的一个很重要的原因就是:广告的专业名词实在是多

这篇文章是来介绍广告的专业名词的?也不全是。如果你看过我一些文章,你会发现我经常推崇:在学习某项技术之前,首先要明白它能干什么,为什么要使用它,再去学习它的实现

了解广告的发展史对「明白它能干什么以及为什么要使用它」我觉得是很有帮助的,这篇文章以广告发展史为切入点,顺便科普广告相关的名词。

通过这篇文章,我认为你大概能明白日常在冲浪🏄的过程中,你所看到的广告可能是属于哪种类型的,会对广告有一个基础的认识和概念。

什么是广告?

初学编程时,我们习惯在百度上搜一下Java

在休息时刻打开了知乎

再随便逛一下知乎

没事再去博客论坛充实一下自己:

甚至打开三歪的历史文章:

你会发现:淦!怎么哪都有广告

画外音:

广告定义:一切付费的信息、产品或服务的传播渠道,都是广告

好戏开始

三歪这个小伙子,每天坚持努力写代码写文章,技术已经变得越来越弱了。原公司《广州精神小伙有限公司》决定抛弃三歪,但由于三歪这一年的努力工作,也成功赚得他的第一桶金。

三歪拿着自己的第一桶金和他富婆女朋友的2W8,凑齐3W块注册了一家公司,该公司名叫做《广州三歪神似林书豪技术有限公司》

三歪成立了自己的站点https://github.com/ZhongFuCheng3y/3y,到目前为止已经有11.3k的狂热粉丝用户每天在站点上看三歪写的文章。

三歪看着自己站点的日活量dau逐渐越来越高,越来越高...虽然自己的内心很开心,但是始终赚不了钱阿。即便有个富婆女朋友,但是人生梦想总得实现的,对不对?

画外音:DAU(daily active user):日活跃用户数量

网上大神总说:“流量就是王道”。于是三歪就想着:我的站点每天都有人访问,我租服务器每个月还得花几千块,我得赚点钱💰。

三歪思前想后,那变现的方式有什么呢?换个问法:“钱从哪里来?”。那无非两种:一、向用户收费。二、向企业收费。

  1. 向用户收费,怎么收?方式当然有很多种咯:一、加入我的VIP专属群,真人在线回答问题。二、开个淘宝店卖增发水。三、卖点程序员👩‍💻最喜欢的外设
  2. 向企业收费,怎么收?我这站点又不提供数据服务,没企业会来给我钱的咯。但我的站点有流量啊,我凭借自己写的文章通俗易懂,站点每天有11.3k的人在看,而现在有很多在线教育机构需要流量,他们会来我这投广告。他们给我钱,我来展示他的广告。

显然,三歪选择了最直接有效的变现方式:广告

画外音:

在线教育机构:需要投广告的人-->广告主(advertiser)

三歪的站点:能提供流量的人-->媒体(publisher)

广告的目的:广告主通过媒体达到低成本的用户接触

但实际上,一次的广告投放,参与的不仅仅是广告主和媒体,还有用户。三方的利益在广告中始终处在不断博弈的过程,三者的利益都需要被认真对待,这样才能使整个市场保持平衡和持续发展

用户:三歪这逼天天发广告,取关了...三歪这逼天天写干货,爱了爱了...三歪这逼...

广告主:投了三歪这个站点可还行...投了三歪这个站点血亏...

媒体:卧槽,我价格得低点,广告爸爸不来,我都吃不起饭了...卧槽,我接了个广告,我得写篇牛逼的文章,给他们更好的内容,不然用户都走光了...

画外音:

广告核心问题:为一系列用户与环境的组合找到最合适的广告投放策略优化整体广告活动的利润

广告可以分为两大类:

  • 追求长期收益的品牌广告。假设我去投品牌类广告:我不关心你短暂时间内是否来关注我,你学Java就得知道有Java3y。我想要做到就是,Java3y这个词,就由于宝马奔驰奥迪在汽车届的存在。
  • 追求购买或其他转化行为的效果广告。假设我去投效果广告:我在某里那投个广告,跑来关注我的人有5个,在某节投个广告,跑来关注我的人就有100个,某里的效果真辣鸡。

BB完了以后,广告主爸爸要来投广告啦。在最开始的时候,广告主爸爸说要买我的一个展示位:一个月30块,签合约

三歪表示没问题,一个月30块,平时就吃个晚饭就可以加个鸡腿了🍗

基于这种情况,媒体不需要有啥技术支持(顶多做个排期系统,排下每个月金主爸爸的时间),同样广告主也不需要啥技术,我给你钱,展示我的广告,完事。

画外音:

按时间结算(30块一个月)这种交易方式,专业术语叫做CPT:Cost Per Time

除了按时间结算的交易方式,常见的有以下几种:

不同的交易方式适用于不同的具体场景(所以才会共存)

随着关注站点的人越来越多,三歪发现看自己文章不仅仅有汉子,竟然还有妹子。

三歪想着:“广告位以固定的价格卖出去,所有的人都能看到那个广告,好像有点小亏啊。虽然我只有一个广告位,但我可以告诉金主爸爸不同属性的人进来,能看到不同的广告(看到他们家广告的人会更加精准),那我不是可以同时接过多个金主爸爸?”

三歪场景都已经想好了:“18~20岁的小伙子进来,我就给他推荐英雄联盟手办,18~20岁的小姐姐进来我就给她推荐面膜,30岁的老男人进来,我就给他推荐增发水...本来一个广告位一个月30块,我接3个广告主,一个广告主20块,那我一个月就是60块收入。”

把这个告诉了广告主,广告主表示可以,但每个月得给我保证我的广告曝光(展示)得在N次,如果达不到这个量,你得退钱,签合约吧。

基于这种情况,媒体就需要有受众定向的技术了,给用户打上不同的标签(比如上面提高的年龄),不同年龄段的用户看到不一样的广告,并且媒体得有广告投放的技术,页面上的广告不再固定,而是根据不同属性的用户展示不同的广告。而广告主仍然不需要什么技术,给钱就行。

三歪还在乐乎赚点小钱的时候,隔壁的做搜索的老王,已经玩起了竞价广告的模式。

画外音:

竞价广告:媒体把自己拥有的广告位(关键词标签)拿出来公开拍卖,谁的价格高谁就能获得广告位

这牛逼在哪呢?基于我上面的那种模式,我还得去给广告主去展示量:比如说,如果这个当前用户展示了这个广告,那下一个广告还给不给这个用户展示(这个用户都在这两个广告所属的人群内),如果不展示下一个广告,那展示量不够怎么办....

那老王是怎么做的呢?老王就告诉广告主:我这有个广告位,我给你们挑选好关键字了,你们来投,谁出价高,我就展示谁的广告。

三歪:这妙阿,反正媒体要做的就是把广告位给开放出去,谁来玩,就出价呗。哪个金主爸爸更有钱,那这个展示位就归给谁。还不用担心展示量达不到广告主的要求(这种模式下已经不保展示量了)

而且这还有个好处就是:搜索的广告要比正常的广告转化率要高。毕竟是用户想要有“意图”才去根据关键字搜索🔍,你根据用户感兴趣的内容给用户推荐了广告,转化一般都比较高。

三歪毕竟不是做搜索的,所以是吃不了搜索广告这碗饭的了。

随着发展,三歪发现其实自己的站点还有很多的位置可以售卖广告的,但以合约的方式很难卖出去(因为都是些边边角角的位置)。而市面上已经专门有人做了广告网络( AD Network,ADN)

说白了就是媒体把这些边边角角的剩余流量给广告网络( AD Network,ADN),它拿到我的广告位,帮我去包装出售,自己从中抽一份佣金。

那ADN是怎么包装的呢?它聚合了很多媒体的剩余流量,然后给这些位置都打上不同的人群标,也是以竞价的方式对这些打上人群标的广告位进行出售。

可以发现的是:上面提到的广告网络( AD Network,ADN)和搜索广告都是以竞价的方式来售卖广告位。

但这种的竞价并不是实时的,它是先通过关键字/人群的方式把广告位放出去,然后让广告主进行竞价,等到竞价完了以后再决定是否要展示这个广告,这里的出价展示决策并不是同步的。

在竞价广告发展的过程中,由于广告主不满足只能按照媒体提供的标签进行购买,广告主希望能够根据自身需求更加精确的挑选用户以达到更精准的广告效果,于是这种下需求就催生了实时竞价(Real Time Bidding,RTB)

虽然原始的竞价广告中会有人群/关键字(标签)给广告主进行挑选,但总会有一些完成不了的场景。比如:广告主希望找到自己流失的用户进行一次广告促销

对于媒体方而言,一般是没有广告主流失用户的数据的嘛,所以媒体方必然对上面这种需求是无能为力的但如果把这些人群/关键字(标签)实现开放式管理,然后每次媒体要展示广告的时候,广告主可以实时根据条件决定是否要出价,那不就很完美了吗?没错,这就是实时竞价。

实时竞价的好处就是:广告主可以根据自身的需求来投放更加精准的用户。

在实时竞价中,我们需要实时竞价广告,广告从哪来?从媒体那儿来。广告主需要直接对接媒体吗?不用。会有互联网广告交易平台(Ad Exchange),我们一般叫它ADX。广告主一般会有一个对接ADX的平台,叫做DSP(Demand-Side Platform),广告主要筛选到自己合适的数据,就一般会有一个数据管理平台(DMP),媒体也有可能不直接对接ADX,会有专门的供应方平台叫做SSP(Suply-Site Platform)。

它们的关系如下:

交互流程如下:

蓦然回首

站在媒体的角度,它需要保障用户体验的同时需要兼顾广告主的效果又得考虑自身的利益,在这三者的关系上得到一个平衡,媒体才能可持续发展。

广告业务经过不断的发展:从单个资源位按时间售卖,到后来的以合约的形式保展示量,再到后来以竞价的方式销售资源位,以及现在的实时竞价,这个发展可以看出在数据的驱动下变得越来越精细化

有需求就会有市场:在这发展的过程中就会出现各种平台来为媒体/广告主服务

  • 竞价广告

    • 媒体平台:竞价广告网络(ADN)
    • 广告主平台:交易终端(TD)
  • 实时竞价

    • 媒体方平台:供给方平台SSP
    • 广告主平台:需求方平台DSP和数据管理平台DMP

像我们公司,既作为媒体,又作为广告主。我们既提供各种的广告交易方式给站内的商家投放广告,同时需要对外购买流量

已经看到这里的同学,会不会想问:作为媒体,有这么多的广告主要接入,在何时何地展示某个广告比较合适呢

作为媒体方其实是按eCPM(effective cost per mile)每一千次展示可以获得的广告收入 这个指标来对广告进行排序的。

要怎么理解eCPM呢?看着好像是前面提到的CPM,只不过在前面多了个字符e

其实eCPMCPM没多大的关系:CPM是一种计价指标,而eCPM是衡量广告效果的指标

eCPM是媒体衡量广告效果的指标:如果以CPM的方式计费,那么eCPM=CPM,那如果以CPC的方式计费呢?eCPM=预估点击率*出价,如果以CPS的方式计费呢?eCPM=预估转化率*预估点击率*出价

从eCPM的公式我们可以发现的是:除了出价预估的点击率和转化率都有可能影响到广告的排序。预估的点击率或转化率较高的广告,这说明该广告的质量是符合媒体自身用户需求的,既可以赚取广告主更多的钱又提高了用户体验,这不美滋滋?

点击率、转化率在广告领域又有对应的专业名词:

在近期我们又可能会看到这个名词OCPX

OCPX 是一种以 转化成本为优化目的,根据单个流量的 点击率转化率 进行智能 动态出价的调整,帮助商家有效的控制转化成本,提升广告效率,最终达成目标的工具
eCPM=CPA*pCVR*pCTR*1000

总的来看,如果广告主使用OCPX的方式投放广告,一般都是需要回传「激活」「下单」等数据给媒体来进一步优化数据模型,最终获得更好的广告投放效果。

最后

如果没接触过广告的同学,可能还是会觉得:字都认识,但好像还是看不太懂。没办法,广告的术语实在是有点多,业务也的确比较复杂。

如果你看完这篇文章能对广告有个最基本的认识,我认为就值了。

广告的水还是很深很深的,我也仅仅在入门阶段,写下这篇文章希望对刚入门广告或者想了解这方面的同学有所帮助。

不知道你看完这篇文章是一种什么样的体验,可以在评论区里交流一下。

我后续会分享具体广告系统业务的内容,讲讲在生产环境下广告系统是如何搭建以及业务是怎么样的。感谢你的关注和点赞

参考资料:

三歪把【大厂面试知识点】、【简历模板】、【原创文章】全部整理成电子书,共有1263页!点击下方链接直接取就好了

PDF文档的内容均为手打,有任何的不懂都可以直接来问我

查看原文

赞 6 收藏 2 评论 1

邓奔成 收藏了文章 · 10月22日

8 种常见 SQL 错误用法

来源:yq.aliyun.com/articles/72501


1、LIMIT 语句

分页查询是最常用的场景之一,但也通常也是最容易出问题的地方。比如对于下面简单的语句,一般 DBA 想到的办法是在 type, name, create_time 字段上加组合索引。这样条件排序都能有效的利用到索引,性能迅速提升。

SELECT * 
FROM   operation 
WHERE  type = 'SQLStats' 
 AND name = 'SlowLog' 
ORDER  BY create_time 
LIMIT  1000, 10;

好吧,可能90%以上的 DBA 解决该问题就到此为止。但当 LIMIT 子句变成 “LIMIT 1000000,10” 时,程序员仍然会抱怨:我只取10条记录为什么还是慢?

要知道数据库也并不知道第1000000条记录从什么地方开始,即使有索引也需要从头计算一次。出现这种性能问题,多数情形下是程序员偷懒了。

在前端数据浏览翻页,或者大数据分批导出等场景下,是可以将上一页的最大值当成参数作为查询条件的。SQL 重新设计如下:

SELECT   * 
FROM     operation 
WHERE    type = 'SQLStats' 
AND      name = 'SlowLog' 
AND      create_time > '2017-03-16 14:00:00' 
ORDER BY create_time limit 10;

在新设计下查询时间基本固定,不会随着数据量的增长而发生变化。

2、隐式转换

SQL语句中查询变量和字段定义类型不匹配是另一个常见的错误。比如下面的语句:

mysql> explain extended SELECT * 
 > FROM   my_balance b 
 > WHERE  b.bpn = 14000000123 
 >       AND b.isverified IS NULL ;
mysql> show warnings;
| Warning | 1739 | Cannot use ref access on index 'bpn' due to type or collation conversion on field 'bpn'

其中字段 bpn 的定义为 varchar(20),MySQL 的策略是将字符串转换为数字之后再比较。函数作用于表字段,索引失效。

上述情况可能是应用程序框架自动填入的参数,而不是程序员的原意。现在应用框架很多很繁杂,使用方便的同时也小心它可能给自己挖坑。

3、关联更新、删除

虽然 MySQL5.6 引入了物化特性,但需要特别注意它目前仅仅针对查询语句的优化。对于更新或删除需要手工重写成 JOIN。

比如下面 UPDATE 语句,MySQL 实际执行的是循环/嵌套子查询(DEPENDENT SUBQUERY),其执行时间可想而知。

UPDATE operation o 
SET    status = 'applying' 
WHERE  o.id IN (SELECT id 
 FROM   (SELECT o.id, 
 o.status 
 FROM   operation o 
 WHERE  o.group = 123 
 AND o.status NOT IN ( 'done' ) 
 ORDER  BY o.parent, 
 o.id 
 LIMIT  1) t);

执行计划:

+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| id | select_type        | table | type  | possible_keys | key     | key_len | ref   | rows | Extra                                               |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| 1  | PRIMARY            | o     | index |               | PRIMARY | 8       |       | 24   | Using where; Using temporary                        |
| 2  | DEPENDENT SUBQUERY |       |       |               |         |         |       |      | Impossible WHERE noticed after reading const tables |
| 3  | DERIVED            | o     | ref   | idx_2,idx_5   | idx_5   | 8       | const | 1    | Using where; Using filesort                         |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+

重写为 JOIN 之后,子查询的选择模式从 DEPENDENT SUBQUERY 变成 DERIVED,执行速度大大加快,从7秒降低到2毫秒。

UPDATE operation o 
 JOIN  (SELECT o.id, 
 o.status 
 FROM   operation o 
 WHERE  o.group = 123 
 AND o.status NOT IN ( 'done' ) 
 ORDER  BY o.parent, 
 o.id 
 LIMIT  1) t
 ON o.id = t.id 
SET    status = 'applying'

执行计划简化为:

+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key   | key_len | ref   | rows | Extra                                               |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| 1  | PRIMARY     |       |      |               |       |         |       |      | Impossible WHERE noticed after reading const tables |
| 2  | DERIVED     | o     | ref  | idx_2,idx_5   | idx_5 | 8       | const | 1    | Using where; Using filesort                         |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+

4、混合排序

MySQL 不能利用索引进行混合排序。但在某些场景,还是有机会使用特殊方法提升性能的。

SELECT * 
FROM   my_order o 
 INNER JOIN my_appraise a ON a.orderid = o.id 
ORDER  BY a.is_reply ASC, 
 a.appraise_time DESC 
LIMIT  0, 20

执行计划显示为全表扫描:

+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
| id | select_type | table | type   | possible_keys     | key     | key_len | ref      | rows    | Extra 
+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
|  1 | SIMPLE      | a     | ALL    | idx_orderid | NULL    | NULL    | NULL    | 1967647 | Using filesort |
|  1 | SIMPLE      | o     | eq_ref | PRIMARY     | PRIMARY | 122     | a.orderid |       1 | NULL           |
+----+-------------+-------+--------+---------+---------+---------+-----------------+---------+-+

由于 is_reply 只有0和1两种状态,我们按照下面的方法重写后,执行时间从1.58秒降低到2毫秒。

SELECT * 
FROM   ((SELECT *
 FROM   my_order o 
 INNER JOIN my_appraise a 
 ON a.orderid = o.id 
 AND is_reply = 0 
 ORDER  BY appraise_time DESC 
 LIMIT  0, 20) 
 UNION ALL 
 (SELECT *
 FROM   my_order o 
 INNER JOIN my_appraise a 
 ON a.orderid = o.id 
 AND is_reply = 1 
 ORDER  BY appraise_time DESC 
 LIMIT  0, 20)) t 
ORDER  BY  is_reply ASC, 
 appraisetime DESC 
LIMIT  20;

5、EXISTS语句

MySQL 对待 EXISTS 子句时,仍然采用嵌套子查询的执行方式。如下面的 SQL 语句:

SELECT *
FROM   my_neighbor n 
 LEFT JOIN my_neighbor_apply sra 
 ON n.id = sra.neighbor_id 
 AND sra.user_id = 'xxx' 
WHERE  n.topic_status < 4 
 AND EXISTS(SELECT 1 
 FROM   message_info m 
 WHERE  n.id = m.neighbor_id 
 AND m.inuser = 'xxx') 
 AND n.topic_type <> 5

执行计划为:

+----+--------------------+-------+------+-----+------------------------------------------+---------+-------+---------+ -----+
| id | select_type        | table | type | possible_keys     | key   | key_len | ref   | rows    | Extra   |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+
|  1 | PRIMARY            | n     | ALL  |  | NULL     | NULL    | NULL  | 1086041 | Using where                   |
|  1 | PRIMARY            | sra   | ref  |  | idx_user_id | 123     | const |       1 | Using where          |
|  2 | DEPENDENT SUBQUERY | m     | ref  |  | idx_message_info   | 122     | const |       1 | Using index condition; Using where |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+

去掉 exists 更改为 join,能够避免嵌套子查询,将执行时间从1.93秒降低为1毫秒。

SELECT *
FROM   my_neighbor n 
 INNER JOIN message_info m 
 ON n.id = m.neighbor_id 
 AND m.inuser = 'xxx' 
 LEFT JOIN my_neighbor_apply sra 
 ON n.id = sra.neighbor_id 
 AND sra.user_id = 'xxx' 
WHERE  n.topic_status < 4 
 AND n.topic_type <> 5

新的执行计划:

+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
| id | select_type | table | type   | possible_keys     | key       | key_len | ref   | rows | Extra                 |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
|  1 | SIMPLE      | m     | ref    | | idx_message_info   | 122     | const    |    1 | Using index condition |
|  1 | SIMPLE      | n     | eq_ref | | PRIMARY   | 122     | ighbor_id |    1 | Using where      |
|  1 | SIMPLE      | sra   | ref    | | idx_user_id | 123     | const     |    1 | Using where           |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+

6、条件下推

外部查询条件不能够下推到复杂的视图或子查询的情况有:

  • 聚合子查询;
  • 含有 LIMIT 的子查询;
  • UNION 或 UNION ALL 子查询;
  • 输出字段中的子查询;

如下面的语句,从执行计划可以看出其条件作用于聚合子查询之后:

SELECT * 
FROM   (SELECT target, 
 Count(*) 
 FROM   operation 
 GROUP  BY target) t 
WHERE  target = 'rm-xxxx'
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
| id | select_type | table      | type  | possible_keys | key         | key_len | ref   | rows | Extra       |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
|  1 | PRIMARY     | <derived2> | ref   | <auto_key0>   | <auto_key0> | 514     | const |    2 | Using where |
|  2 | DERIVED     | operation  | index | idx_4         | idx_4       | 519     | NULL  |   20 | Using index |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+

确定从语义上查询条件可以直接下推后,重写如下:

SELECT target, 
 Count(*) 
FROM   operation 
WHERE  target = 'rm-xxxx' 
GROUP  BY target

执行计划变为:

+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+

关于 MySQL 外部条件不能下推的详细解释说明请参考文章:

http://mysql.taobao.org/month...

7、提前缩小范围

先上初始 SQL 语句:

SELECT * 
FROM   my_order o 
 LEFT JOIN my_userinfo u 
 ON o.uid = u.uid
 LEFT JOIN my_productinfo p 
 ON o.pid = p.pid 
WHERE  ( o.display = 0 ) 
 AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15

该SQL语句原意是:先做一系列的左连接,然后排序取前15条记录。从执行计划也可以看出,最后一步估算排序记录数为90万,时间消耗为12秒。

+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref             | rows   | Extra                                              |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
|  1 | SIMPLE      | o     | ALL    | NULL          | NULL    | NULL    | NULL            | 909119 | Using where; Using temporary; Using filesort       |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |      1 | NULL                                               |
|  1 | SIMPLE      | p     | ALL    | PRIMARY       | NULL    | NULL    | NULL            |      6 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+

由于最后 WHERE 条件以及排序均针对最左主表,因此可以先对 my_order 排序提前缩小数据量再做左连接。SQL 重写后如下,执行时间缩小为1毫秒左右。

SELECT * 
FROM (
SELECT * 
FROM   my_order o 
WHERE  ( o.display = 0 ) 
 AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15
) o 
 LEFT JOIN my_userinfo u 
 ON o.uid = u.uid 
 LEFT JOIN my_productinfo p 
 ON o.pid = p.pid 
ORDER BY  o.selltime DESC
limit 0, 15

再检查执行计划:子查询物化后(select_type=DERIVED)参与 JOIN。虽然估算行扫描仍然为90万,但是利用了索引以及 LIMIT 子句后,实际执行时间变得很小。

+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
| id | select_type | table      | type   | possible_keys | key     | key_len | ref   | rows   | Extra                                              |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL    | NULL    | NULL  |     15 | Using temporary; Using filesort                    |
|  1 | PRIMARY     | u          | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |      1 | NULL                                               |
|  1 | PRIMARY     | p          | ALL    | PRIMARY       | NULL    | NULL    | NULL  |      6 | Using where; Using join buffer (Block Nested Loop) |
|  2 | DERIVED     | o          | index  | NULL          | idx_1   | 5       | NULL  | 909112 | Using where                                        |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+

8、中间结果集下推

再来看下面这个已经初步优化过的例子(左连接中的主表优先作用查询条件):

SELECT    a.*, 
 c.allocated 
FROM      ( 
 SELECT   resourceid 
 FROM     my_distribute d 
 WHERE    isdelete = 0 
 AND      cusmanagercode = '1234567' 
 ORDER BY salecode limit 20) a 
LEFT JOIN 
 ( 
 SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
 FROM     my_resources 
 GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

那么该语句还存在其它问题吗?不难看出子查询 c 是全表聚合查询,在表数量特别大的情况下会导致整个语句的性能下降。

其实对于子查询 c,左连接最后结果集只关心能和主表 resourceid 能匹配的数据。因此我们可以重写语句如下,执行时间从原来的2秒下降到2毫秒。

SELECT    a.*, 
 c.allocated 
FROM      ( 
 SELECT   resourceid 
 FROM     my_distribute d 
 WHERE    isdelete = 0 
 AND      cusmanagercode = '1234567' 
 ORDER BY salecode limit 20) a 
LEFT JOIN 
 ( 
 SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
 FROM     my_resources r, 
 ( 
 SELECT   resourceid 
 FROM     my_distribute d 
 WHERE    isdelete = 0 
 AND      cusmanagercode = '1234567' 
 ORDER BY salecode limit 20) a 
 WHERE    r.resourcesid = a.resourcesid 
 GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

但是子查询 a 在我们的SQL语句中出现了多次。这种写法不仅存在额外的开销,还使得整个语句显的繁杂。使用 WITH 语句再次重写:

WITH a AS 
( 
 SELECT   resourceid 
 FROM     my_distribute d 
 WHERE    isdelete = 0 
 AND      cusmanagercode = '1234567' 
 ORDER BY salecode limit 20)
SELECT    a.*, 
 c.allocated 
FROM      a 
LEFT JOIN 
 ( 
 SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
 FROM     my_resources r, 
 a 
 WHERE    r.resourcesid = a.resourcesid 
 GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

总结

数据库编译器产生执行计划,决定着SQL的实际执行方式。但是编译器只是尽力服务,所有数据库的编译器都不是尽善尽美的。

上述提到的多数场景,在其它数据库中也存在性能问题。了解数据库编译器的特性,才能避规其短处,写出高性能的SQL语句。

程序员在设计数据模型以及编写SQL语句时,要把算法的思想或意识带进来。

编写复杂SQL语句要养成使用 WITH 语句的习惯。简洁且思路清晰的SQL语句也能减小数据库的负担 。

查看原文

邓奔成 赞了文章 · 10月22日

8 种常见 SQL 错误用法

来源:yq.aliyun.com/articles/72501


1、LIMIT 语句

分页查询是最常用的场景之一,但也通常也是最容易出问题的地方。比如对于下面简单的语句,一般 DBA 想到的办法是在 type, name, create_time 字段上加组合索引。这样条件排序都能有效的利用到索引,性能迅速提升。

SELECT * 
FROM   operation 
WHERE  type = 'SQLStats' 
 AND name = 'SlowLog' 
ORDER  BY create_time 
LIMIT  1000, 10;

好吧,可能90%以上的 DBA 解决该问题就到此为止。但当 LIMIT 子句变成 “LIMIT 1000000,10” 时,程序员仍然会抱怨:我只取10条记录为什么还是慢?

要知道数据库也并不知道第1000000条记录从什么地方开始,即使有索引也需要从头计算一次。出现这种性能问题,多数情形下是程序员偷懒了。

在前端数据浏览翻页,或者大数据分批导出等场景下,是可以将上一页的最大值当成参数作为查询条件的。SQL 重新设计如下:

SELECT   * 
FROM     operation 
WHERE    type = 'SQLStats' 
AND      name = 'SlowLog' 
AND      create_time > '2017-03-16 14:00:00' 
ORDER BY create_time limit 10;

在新设计下查询时间基本固定,不会随着数据量的增长而发生变化。

2、隐式转换

SQL语句中查询变量和字段定义类型不匹配是另一个常见的错误。比如下面的语句:

mysql> explain extended SELECT * 
 > FROM   my_balance b 
 > WHERE  b.bpn = 14000000123 
 >       AND b.isverified IS NULL ;
mysql> show warnings;
| Warning | 1739 | Cannot use ref access on index 'bpn' due to type or collation conversion on field 'bpn'

其中字段 bpn 的定义为 varchar(20),MySQL 的策略是将字符串转换为数字之后再比较。函数作用于表字段,索引失效。

上述情况可能是应用程序框架自动填入的参数,而不是程序员的原意。现在应用框架很多很繁杂,使用方便的同时也小心它可能给自己挖坑。

3、关联更新、删除

虽然 MySQL5.6 引入了物化特性,但需要特别注意它目前仅仅针对查询语句的优化。对于更新或删除需要手工重写成 JOIN。

比如下面 UPDATE 语句,MySQL 实际执行的是循环/嵌套子查询(DEPENDENT SUBQUERY),其执行时间可想而知。

UPDATE operation o 
SET    status = 'applying' 
WHERE  o.id IN (SELECT id 
 FROM   (SELECT o.id, 
 o.status 
 FROM   operation o 
 WHERE  o.group = 123 
 AND o.status NOT IN ( 'done' ) 
 ORDER  BY o.parent, 
 o.id 
 LIMIT  1) t);

执行计划:

+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| id | select_type        | table | type  | possible_keys | key     | key_len | ref   | rows | Extra                                               |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| 1  | PRIMARY            | o     | index |               | PRIMARY | 8       |       | 24   | Using where; Using temporary                        |
| 2  | DEPENDENT SUBQUERY |       |       |               |         |         |       |      | Impossible WHERE noticed after reading const tables |
| 3  | DERIVED            | o     | ref   | idx_2,idx_5   | idx_5   | 8       | const | 1    | Using where; Using filesort                         |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+

重写为 JOIN 之后,子查询的选择模式从 DEPENDENT SUBQUERY 变成 DERIVED,执行速度大大加快,从7秒降低到2毫秒。

UPDATE operation o 
 JOIN  (SELECT o.id, 
 o.status 
 FROM   operation o 
 WHERE  o.group = 123 
 AND o.status NOT IN ( 'done' ) 
 ORDER  BY o.parent, 
 o.id 
 LIMIT  1) t
 ON o.id = t.id 
SET    status = 'applying'

执行计划简化为:

+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key   | key_len | ref   | rows | Extra                                               |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| 1  | PRIMARY     |       |      |               |       |         |       |      | Impossible WHERE noticed after reading const tables |
| 2  | DERIVED     | o     | ref  | idx_2,idx_5   | idx_5 | 8       | const | 1    | Using where; Using filesort                         |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+

4、混合排序

MySQL 不能利用索引进行混合排序。但在某些场景,还是有机会使用特殊方法提升性能的。

SELECT * 
FROM   my_order o 
 INNER JOIN my_appraise a ON a.orderid = o.id 
ORDER  BY a.is_reply ASC, 
 a.appraise_time DESC 
LIMIT  0, 20

执行计划显示为全表扫描:

+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
| id | select_type | table | type   | possible_keys     | key     | key_len | ref      | rows    | Extra 
+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
|  1 | SIMPLE      | a     | ALL    | idx_orderid | NULL    | NULL    | NULL    | 1967647 | Using filesort |
|  1 | SIMPLE      | o     | eq_ref | PRIMARY     | PRIMARY | 122     | a.orderid |       1 | NULL           |
+----+-------------+-------+--------+---------+---------+---------+-----------------+---------+-+

由于 is_reply 只有0和1两种状态,我们按照下面的方法重写后,执行时间从1.58秒降低到2毫秒。

SELECT * 
FROM   ((SELECT *
 FROM   my_order o 
 INNER JOIN my_appraise a 
 ON a.orderid = o.id 
 AND is_reply = 0 
 ORDER  BY appraise_time DESC 
 LIMIT  0, 20) 
 UNION ALL 
 (SELECT *
 FROM   my_order o 
 INNER JOIN my_appraise a 
 ON a.orderid = o.id 
 AND is_reply = 1 
 ORDER  BY appraise_time DESC 
 LIMIT  0, 20)) t 
ORDER  BY  is_reply ASC, 
 appraisetime DESC 
LIMIT  20;

5、EXISTS语句

MySQL 对待 EXISTS 子句时,仍然采用嵌套子查询的执行方式。如下面的 SQL 语句:

SELECT *
FROM   my_neighbor n 
 LEFT JOIN my_neighbor_apply sra 
 ON n.id = sra.neighbor_id 
 AND sra.user_id = 'xxx' 
WHERE  n.topic_status < 4 
 AND EXISTS(SELECT 1 
 FROM   message_info m 
 WHERE  n.id = m.neighbor_id 
 AND m.inuser = 'xxx') 
 AND n.topic_type <> 5

执行计划为:

+----+--------------------+-------+------+-----+------------------------------------------+---------+-------+---------+ -----+
| id | select_type        | table | type | possible_keys     | key   | key_len | ref   | rows    | Extra   |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+
|  1 | PRIMARY            | n     | ALL  |  | NULL     | NULL    | NULL  | 1086041 | Using where                   |
|  1 | PRIMARY            | sra   | ref  |  | idx_user_id | 123     | const |       1 | Using where          |
|  2 | DEPENDENT SUBQUERY | m     | ref  |  | idx_message_info   | 122     | const |       1 | Using index condition; Using where |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+

去掉 exists 更改为 join,能够避免嵌套子查询,将执行时间从1.93秒降低为1毫秒。

SELECT *
FROM   my_neighbor n 
 INNER JOIN message_info m 
 ON n.id = m.neighbor_id 
 AND m.inuser = 'xxx' 
 LEFT JOIN my_neighbor_apply sra 
 ON n.id = sra.neighbor_id 
 AND sra.user_id = 'xxx' 
WHERE  n.topic_status < 4 
 AND n.topic_type <> 5

新的执行计划:

+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
| id | select_type | table | type   | possible_keys     | key       | key_len | ref   | rows | Extra                 |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
|  1 | SIMPLE      | m     | ref    | | idx_message_info   | 122     | const    |    1 | Using index condition |
|  1 | SIMPLE      | n     | eq_ref | | PRIMARY   | 122     | ighbor_id |    1 | Using where      |
|  1 | SIMPLE      | sra   | ref    | | idx_user_id | 123     | const     |    1 | Using where           |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+

6、条件下推

外部查询条件不能够下推到复杂的视图或子查询的情况有:

  • 聚合子查询;
  • 含有 LIMIT 的子查询;
  • UNION 或 UNION ALL 子查询;
  • 输出字段中的子查询;

如下面的语句,从执行计划可以看出其条件作用于聚合子查询之后:

SELECT * 
FROM   (SELECT target, 
 Count(*) 
 FROM   operation 
 GROUP  BY target) t 
WHERE  target = 'rm-xxxx'
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
| id | select_type | table      | type  | possible_keys | key         | key_len | ref   | rows | Extra       |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
|  1 | PRIMARY     | <derived2> | ref   | <auto_key0>   | <auto_key0> | 514     | const |    2 | Using where |
|  2 | DERIVED     | operation  | index | idx_4         | idx_4       | 519     | NULL  |   20 | Using index |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+

确定从语义上查询条件可以直接下推后,重写如下:

SELECT target, 
 Count(*) 
FROM   operation 
WHERE  target = 'rm-xxxx' 
GROUP  BY target

执行计划变为:

+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+

关于 MySQL 外部条件不能下推的详细解释说明请参考文章:

http://mysql.taobao.org/month...

7、提前缩小范围

先上初始 SQL 语句:

SELECT * 
FROM   my_order o 
 LEFT JOIN my_userinfo u 
 ON o.uid = u.uid
 LEFT JOIN my_productinfo p 
 ON o.pid = p.pid 
WHERE  ( o.display = 0 ) 
 AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15

该SQL语句原意是:先做一系列的左连接,然后排序取前15条记录。从执行计划也可以看出,最后一步估算排序记录数为90万,时间消耗为12秒。

+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref             | rows   | Extra                                              |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
|  1 | SIMPLE      | o     | ALL    | NULL          | NULL    | NULL    | NULL            | 909119 | Using where; Using temporary; Using filesort       |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |      1 | NULL                                               |
|  1 | SIMPLE      | p     | ALL    | PRIMARY       | NULL    | NULL    | NULL            |      6 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+

由于最后 WHERE 条件以及排序均针对最左主表,因此可以先对 my_order 排序提前缩小数据量再做左连接。SQL 重写后如下,执行时间缩小为1毫秒左右。

SELECT * 
FROM (
SELECT * 
FROM   my_order o 
WHERE  ( o.display = 0 ) 
 AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15
) o 
 LEFT JOIN my_userinfo u 
 ON o.uid = u.uid 
 LEFT JOIN my_productinfo p 
 ON o.pid = p.pid 
ORDER BY  o.selltime DESC
limit 0, 15

再检查执行计划:子查询物化后(select_type=DERIVED)参与 JOIN。虽然估算行扫描仍然为90万,但是利用了索引以及 LIMIT 子句后,实际执行时间变得很小。

+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
| id | select_type | table      | type   | possible_keys | key     | key_len | ref   | rows   | Extra                                              |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL    | NULL    | NULL  |     15 | Using temporary; Using filesort                    |
|  1 | PRIMARY     | u          | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |      1 | NULL                                               |
|  1 | PRIMARY     | p          | ALL    | PRIMARY       | NULL    | NULL    | NULL  |      6 | Using where; Using join buffer (Block Nested Loop) |
|  2 | DERIVED     | o          | index  | NULL          | idx_1   | 5       | NULL  | 909112 | Using where                                        |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+

8、中间结果集下推

再来看下面这个已经初步优化过的例子(左连接中的主表优先作用查询条件):

SELECT    a.*, 
 c.allocated 
FROM      ( 
 SELECT   resourceid 
 FROM     my_distribute d 
 WHERE    isdelete = 0 
 AND      cusmanagercode = '1234567' 
 ORDER BY salecode limit 20) a 
LEFT JOIN 
 ( 
 SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
 FROM     my_resources 
 GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

那么该语句还存在其它问题吗?不难看出子查询 c 是全表聚合查询,在表数量特别大的情况下会导致整个语句的性能下降。

其实对于子查询 c,左连接最后结果集只关心能和主表 resourceid 能匹配的数据。因此我们可以重写语句如下,执行时间从原来的2秒下降到2毫秒。

SELECT    a.*, 
 c.allocated 
FROM      ( 
 SELECT   resourceid 
 FROM     my_distribute d 
 WHERE    isdelete = 0 
 AND      cusmanagercode = '1234567' 
 ORDER BY salecode limit 20) a 
LEFT JOIN 
 ( 
 SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
 FROM     my_resources r, 
 ( 
 SELECT   resourceid 
 FROM     my_distribute d 
 WHERE    isdelete = 0 
 AND      cusmanagercode = '1234567' 
 ORDER BY salecode limit 20) a 
 WHERE    r.resourcesid = a.resourcesid 
 GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

但是子查询 a 在我们的SQL语句中出现了多次。这种写法不仅存在额外的开销,还使得整个语句显的繁杂。使用 WITH 语句再次重写:

WITH a AS 
( 
 SELECT   resourceid 
 FROM     my_distribute d 
 WHERE    isdelete = 0 
 AND      cusmanagercode = '1234567' 
 ORDER BY salecode limit 20)
SELECT    a.*, 
 c.allocated 
FROM      a 
LEFT JOIN 
 ( 
 SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
 FROM     my_resources r, 
 a 
 WHERE    r.resourcesid = a.resourcesid 
 GROUP BY resourcesid) c 
ON        a.resourceid = c.resourcesid

总结

数据库编译器产生执行计划,决定着SQL的实际执行方式。但是编译器只是尽力服务,所有数据库的编译器都不是尽善尽美的。

上述提到的多数场景,在其它数据库中也存在性能问题。了解数据库编译器的特性,才能避规其短处,写出高性能的SQL语句。

程序员在设计数据模型以及编写SQL语句时,要把算法的思想或意识带进来。

编写复杂SQL语句要养成使用 WITH 语句的习惯。简洁且思路清晰的SQL语句也能减小数据库的负担 。

查看原文

赞 14 收藏 12 评论 1

邓奔成 发布了文章 · 10月22日

常见包装类、List、Set、HashMap见解

一. Integer与String

1.1 Integer

包装类,就是对基本数据类型的一个包装,赋予一些常用的方法

比如类型装换等。

int的包装类,使用final修饰类,不可继承。

会自动装箱与自动拆箱。

特别注意:在-128到127之间,维护了一个数组(static初始化,服务器启动就存在。)

比较值时,如果是这个区间,则默认从数组中拿出值来进行判断。

Integer a = 1;
Integer b = 1;
Integer c = 128;
Integer d = 128;
sout(a == b); // true
sout(c == d); // false

源码部分

 private static class IntegerCache {
     static final int low = -128;
     static final int high;
     static final Integer cache[];
    ​
     static {
         // high value may be configured by property
         int h = 127;
         String integerCacheHighPropValue =
         sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
         if (integerCacheHighPropValue != null) {
            try {
                 int i = parseInt(integerCacheHighPropValue);
                 i = Math.max(i, 127);
                 // Maximum array size is Integer.MAX_VALUE
                 h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
             // If the property cannot be parsed into an int, ignore it.
             }
         }
         high = h;
        ​
         cache = new Integer[(high - low) + 1];
         int j = low;
         for(int k = 0; k < cache.length; k++)
         cache[k] = new Integer(j++);
        ​
         // range [-128, 127] must be interned (JLS7 5.1.7)
         assert IntegerCache.high >= 127;
 }
​
 private IntegerCache() {}
 } 
​
 public static Integer valueOf(int i) {
     if (i >= IntegerCache.low && i <= IntegerCache.high)
     return IntegerCache.cache[i + (-IntegerCache.low)];
     return new Integer(i);
 }

1.2 String

不可变长字符串。

使用String字符串拼接时,默认调用StringBulider的append方法

String str1 = "hello,";
String str2 = "dbc";
String str3 = str1 + str2;
// 实际: String str3 = (new StringBuilder()).append(str1).append(str2).toString();

String,StringBuilder,StringBuffer的区别

  1. String: 定长字符串
  2. StringBuilder: 可变长字符串、线性不安全、效率高
  3. StringBuffer: 可变长字符串、线性安全、效率低

二. List

有序,可重复

2.1 ArrayList

底层维护了一个数组

会根据存入数据个数和容量,来自动扩容

特点:查询快、添加和删除效率低(和LinkedList相比)。

源码

 transient Object[] elementData; // non-private to simplify nested class access
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
​
 // 带参的一个构造方法
 public ArrayList(int initialCapacity) {
     if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
     } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
     } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
     initialCapacity);
     }
 }
​
 //添加的一个方法
 public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
 }
​
 // 扩容相关
 private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
 }
​
 private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
​
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
    }
 private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容1.5倍
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
     // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
 }

2.2 LinkedList

底层维护了一个链表

特点:查询慢、添加和删除快(和ArrayList相比)。

源码

 // 链表的节点
 private static class Node<E> {
     E item;
     Node<E> next;
     Node<E> prev;
    ​
     Node(Node<E> prev, E element, Node<E> next) {
         this.item = element;
         this.next = next;
         this.prev = prev;
     }
 }

三. Map

key-value型存储格式

key不可重复

3.1 HashMap

底层是一个哈希表(数组+链表)

key和value可以为null

与HashTable的区别

  1. HashTable是线性安全的,效率低,而HashMap线性不安全。
  2. HashTable不允许值(key和value)为null,HashMap的值可以为null

源码

 /**
 * 默认容量(数组长度)
 */
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
​
 /**
 * 最大容量
 */
 static final int MAXIMUM_CAPACITY = 1 << 30;
​
 /**
 *  Jdk1.8后,数组中某个位置的链表长度为8,并且总元素个数>=64,则将该链表编程红黑树
 *  优化查询效率
 */
 static final int TREEIFY_THRESHOLD = 8;
​
 /**
 * 最小树化容量 
 * 数组中某个位置的链表长度为8,并且总元素个数>=64,则将该链表编程红黑树
 * 优化查询效率
 */
 static final int MIN_TREEIFY_CAPACITY = 64;
​
 /**
 *  当数据个数低于这个数,将其转换为链表
 */
 static final int UNTREEIFY_THRESHOLD = 6;
​
 /**
 * 负载因子,添加元素时,当哈希表中的元素个数为:数组长度*负载因子时,会扩容
 */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
​
​
 /**
 * 基本的哈希bin节点
 */
 static class Node<K,V> implements Map.Entry<K,V> {
     final int hash;
     final K key;
     V value;
     Node<K,V> next;
    ​
     Node(int hash, K key, V value, Node<K,V> next) {
         this.hash = hash;
         this.key = key;
         this.value = value;
         this.next = next;
    }
​
     public final K getKey()        { return key; }
     public final V getValue()      { return value; }
     public final String toString() { return key + "=" + value; }
​
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
​
     public final V setValue(V newValue) {
        V oldValue = value;
         value = newValue;
        return oldValue;
     }
​
    public final boolean equals(Object o) {
        if (o == this)
         return true;
     if (o instanceof Map.Entry) {
         Map.Entry<?,?> e = (Map.Entry<?,?>)o;
         if (Objects.equals(key, e.getKey()) &&
            Objects.equals(value, e.getValue()))
             return true;
         }
        return false;
     }
 }
​

四. Set

无序、不可重复

底层维护了一个Map

存值时,将添加的值存为Map的key, 而value就是一个Object

故要获取值,只能获取迭代器来遍历

4.1 HashSet

底层维护了一个HashMap

存值时,存的为Map的key

可以添加null(HashMap的key可以为null)

HashSet如何判断元素是否重复的?

  1. 先判断hashCode
  2. 再使用equals判断

如果hashCode的返回值相等 并且 equals返回true,则被认为是相同的元素

源码

 ...
 private transient HashMap<E,Object> map;
 // Dummy value to associate with an Object in the backing Map
 // 要与支持映射中的对象关联的伪值
 private static final Object PRESENT = new Object();
 ...
 
 // 构造方法 
 public HashSet() {
    map = new HashMap<>();
 }
 ...
 
 // 添加方法 
 public boolean add(E e) {
    return map.put(e, PRESENT)==null;
 }
​

4.2 LinkedHashSet

HashSet的子类

双向链表

4.3 TreeSet

底层维护了一个TreeMap

存值时,存的为Map的key

不能添加null

源码

 private transient NavigableMap<E,Object> m;
​
 TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
 }
​
 // 构造方法
 public TreeSet() {
    this(new TreeMap<E,Object>());
 }
​
 // 添加方法
 public boolean add(E e) {
    return m.put(e, PRESENT)==null;
 }
查看原文

赞 0 收藏 0 评论 0

邓奔成 发布了文章 · 10月20日

AutoWired属性上使用的简单实现

前言

本文在创建类的实例时,是直接操作的属性,而没有用setter或者constructor方法,这是不合理的(违背了JavaBean封装的思想)。故建议在阅读本文时,仅关注实现的思路,不要参考具体实现过程,而应该使用spring的setter/constructor两种方法

IOC控制反转

本文继java的springMvc框架简单实现后,就Service-ServiceImplDao--DaoImpl之间的依赖关系进行解耦,简单实现@AutoWired有关此部分的依赖注入(DI)。

思路

  1. 以Dao为例,在声明某个Dao时,我们不再赋初值:
@XxgAutoWired
private UserDao userDao;
  1. UserDaoImpl类上面加入@XxgComponent注解
@XxgComponent
public class UserDaoImpl extends BaseDaoImpl<User> implements UserDao {
 ...
}
  1. 服务器启动,

    3.1 扫描包过程中,加入:判断类中是否有@XxgComponent注解,如果有,则加入我们指定的容器

    3.2 扫描结束之后,给MethodDefinition创建所在类的实例

  2. 利用反射执行method时,传递上一步创建的所在类的实例

所在类的实例说明:

主要将该实例中有@AutoWired注解的属性,赋予初值,每次调用方法都使用该实例,就达到了预想的效果。

赋初值的方式:

  1. Map<Class, ComponentDefinition> componentMap容器的key存放依赖类实现的接口的class,value存放该类的相关属性。
  2. 获取到有@AutoWired注解的属性时,获取该属性的类型class作为key,在容器中查找该key
  3. 容器中查到的数据,即为其实现类的相关属性
  4. 将其赋值给该实现类

问题

这种实现模式,有一个最大的问题:

一个Dao的接口,只能有一个对应的实现类。

其次:

依赖注入仅在Controller类中使用了才有效

可以使用xml文件来存储接口与其实现类的依赖关系。

一个接口有多个实现类时,只在需要的实现类上添加@XxgComponent注解

代码实现

1.XxgAutoWired注解

/**
 * @Auther dbc
 * @Date 2020/10/15 14:40
 * @Description
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface XxgAutoWired {
}

2. XxgComponent注解

/**
 * @Auther dbc
 * @Date 2020/10/16 15:48
 * @Description
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface XxgComponent {
}

3.MethodDefinition中添加一个类的实例

private Object parentInstance; // 所属类的实例

4.扫描类的部分修改

添加Component容器

private static Map<Class, ComponentDefinition> componentMap = new ConcurrentHashMap<>(); // component类容器

ComponentDefinition

/**
 * @Auther dbc
 * @Date 2020/10/16 16:03
 * @Description 对有@XxgComponent注解的类 进行包装
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ComponentDefinition {
 private Class typeClazz; // 类对象
 private String typeName; // 类名
 private XxgComponent component; // component注解
 private Class implClazz; // 实现的父类
}

修改dealClass方法

private void dealClass(Class clazz) throws Exception {
 // 一开始就添加
 if (clazz.isAnnotationPresent(XxgComponent.class)) {
 // 将实现类添加到容器中
 addComponent(clazz);
 return ;
 }
 ...
} 

将实现类添加到容器中的方法

​
 /**
 * 将Component类添加到容器中
 */
 private void addComponent(Class clazz) throws Exception {
 ComponentDefinition componentDefinition = new ComponentDefinition();
 componentDefinition.setComponent((XxgComponent) clazz.getAnnotation(XxgComponent.class));
 componentDefinition.setTypeClazz(clazz);
 componentDefinition.setImplClazz(clazz.getInterfaces()[0]);
 componentDefinition.setTypeName(clazz.getName());
 if (componentMap.get(componentDefinition.getImplClazz()) != null) {
 throw new Exception("已存在相同的实现类" + componentDefinition.getImplClazz().getName());
 }
 componentMap.put(componentDefinition.getImplClazz(), componentDefinition);
 }
 
 /**
 * 处理controller的属性,自动注入值, 返回该类的一个实例
 */
 private Object dealClassField(Class clazz) {
 Field[] fields = clazz.getDeclaredFields();
 Object instance = null;
 try {
 instance = clazz.newInstance();
 } catch (InstantiationException | IllegalAccessException e) {
 e.printStackTrace();
 }
 for(Field field : fields) {
 if (field.isAnnotationPresent(XxgAutoWired.class)) {
 // 给有XxgAutoWired注解的属性,自动注入值
 field.setAccessible(true);
 try {
 ComponentDefinition componentDefinition = componentMap.get(field.getType());
 if (componentDefinition != null) {
 // 处理@XxgComponent 的自动注入
 field.set(instance, dealClassField(componentDefinition.getTypeClazz()));
 }
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
 }
 return instance;
 }

5.调用方法时,传递扫描时存入的实例

DispatcherServlet中,当获取到请求执行的方法,以及装配好参数之后,调用该方法时,传递创建好的实例

try {
  Object result = methodDefinition.getMethod().invoke(
  methodDefinition.getParentInstance(), // 所属类的实例
  params.toArray());
  sendResponse(result, req, resp);
} catch (IllegalAccessException | InvocationTargetException e) {
  e.printStackTrace();
  sendResponse(e.getMessage(), req, resp);
}

后续

真正的@AutoWired太强大了,可以用在属性上,还可以用在方法上等,博主就不再深究了。

下文博主将使用XML配置文件的方式实现依赖注入,并对单例模式、依赖的值类型等进行处理。

查看原文

赞 0 收藏 0 评论 0

邓奔成 发布了文章 · 10月19日

java的springMvc框架简单实现

前言

在本文中,博主一步步地从servlet到controller层实现一个简单的框架。通过此框架,我们可以像spring那样使用以下基础注解:

  • @XxgController
  • @XxgRequestMapping
  • @XxgParam
  • @XxgRequestBody

观看本文之前,你或许应该先了解以下内容:

  • BeanUtils
  • ObjectMapper
  • Servlet相关知识

思路:拦截器实现路由分发。利用注解?

思考:

  1. 拦截器可以在servlet之前拦截所有请求路径
  2. 可以找到注解中路径与请求路径相匹配的那个方法
  3. 然后将req,resp转发给该方法来执行

问题:

拦截器如何找到使用了该注解的方法?包扫描?如何实现?

分析:

包扫描,就涉及IO流, 而File类可以递归查询其下面所有的文件,我们

可以过滤一下:

  1. 只要后缀名为.class的文件,并获取其className(包括包路径)
  2. 通过反射获取这个类,判断其是否有指定的注解进而再次过滤

这样在拦截器拦截到请求路径,我们可以进行匹配并调用该方法。

偷个懒:

因为MVC设计模式,我们一般把api接口都放在同一个包下,所以我们可以直接指定要扫描包,其它包就不管

一.扫描类1.0版的实现

public class FileScanner {
 private final String packetUrl = "com.dbc.review.controller";
 private final ClassLoader classLoader = FileScanner.class.getClassLoader();
 private List<Class> allClazz = new ArrayList<>(10); //存该包下所有用了注解的类
​
 public List<Class> getAllClazz(){
   return this.allClazz;
 }
​
 public String getPacketUrl(){
   return this.packetUrl;
 }
​
 // 查询所有使用了给定注解的类
 // 递归扫描包,如果扫描到class,则调用class处理方法来收集想要的class
 public void loadAllClass(String packetUrl) throws Exception{
     String url = packetUrl.replace(".","/");
     URL resource = classLoader.getResource(url);
     if (resource == null) {
        return;
    }
     String path = resource.getPath();
     File file = new File(URLDecoder.decode(path, "UTF-8"));
     if (!file.exists()) {
        return;
     }
     if (file.isDirectory()){
     File[] files = file.listFiles();
     if (files == null) {
        return;
     }
     for (File f : files) {
         String classname = f.getName().substring(0, f.getName().lastIndexOf("."));
         if (f.isDirectory()) {
            loadAllClass(packetUrl + "." + classname);
         }
         if (f.isFile() && f.getName().endsWith(".class")) {
             Class clazz = Class.forName(packetUrl + "." + classname);
             dealClass( clazz);
         }
    }
     }
 }
​
 private void dealClass(Class clazz) {
 if ((clazz.isAnnotationPresent(Controller.class))) {
 allClazz.add(clazz);
 }
 }
​
 // 真正使用的时候,根据请求路径及请求方法来获取处理的方法
 public boolean invoke(String url,String requestMethod) {
     for (Class clazz : allClazz){
     Controller controller = (Controller) clazz.getAnnotation(Controller.class);
     Method[] methods = clazz.getDeclaredMethods();
     for (Method method : methods) {
         RequestMapping requestMapping = method.getDeclaredAnnotation(RequestMapping.class);
         if (requestMapping == null) {
         continue;
         }
         for (String m : requestMapping.methods()) {
         m = m.toUpperCase();
         if (!m.toUpperCase().equals(requestMethod.toUpperCase())) {
         continue;
         }
         StringBuilder sb = new StringBuilder();
         String urlItem = sb.append(controller.url()).append(requestMapping.url()).toString();
         if (urlItem.equals(url)) {
         // 获取到用于处理此api接口的方法
         try {
        //                            method.getGenericParameterTypes() // 可以根据此方法来判断该方法需要传哪些参数
         method.invoke(clazz.newInstance());
         return true;
         } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
         e.printStackTrace();
         return false;
         }
     }
     }
     }
     }
     return false;
 }
​
 @Test
 public void test() throws Exception {
     // 1. 在Filter的静态代码块中实例化
     FileScanner fileScanner = new FileScanner();
     // 2. 启动扫描
     fileScanner.loadAllClass(fileScanner.getPacketUrl());
     // 3. 拦截到请求后,调用此方法来执行
     // 若该包下没有定义post请求的/test/post 的处理方法,则返回false
     // 执行成功返回true
     fileScanner.invoke("/test/post","post");
     // 4. 执行失败,返回false,则抛出405 方法未定义。
    ​
     // 最后 :对于controller的传参,本类未实现
     //       暂时想到:根据method获取其参数列表,再传对应参数,就是不太好实现
     }
}

TestController

@Controller(url = "/test")
public class TestController {
​
 @RequestMapping(url = "/get",methods = "GET")
 public void get(){
 System.out.println(111);
 }
 @RequestMapping(url = "/post",methods = {"POST","get"})
 public void post(){
 System.out.println(22);
 }
​
 public void test(HttpServletRequest req, HttpServletResponse res){
 System.out.println(req.getPathInfo());
 }
}

扫描类2.0版

通过1.0版,我们初步实现递归扫描包下的所有controller,并能通过路径映射实现访问。但很明显有至少以下问题:

  1. 执行方法时,方法不能有参数。不符合业务需求
  2. 每次访问,都要反复处理Class反射来找到路径映射的方法,效率低。

针对以上2个问题,我们在2.0版进行一下修改:

  1. 将controller、requestmapping对应方法,方法对应参数的可能用到的相关信息存放在一个容器中。在服务器初次启动时进行扫描,并装配到容器中。这样在每次访问时,遍历这个容器,比1.0版的容器更方便。
  2. 定义参数类型,通过注解@XxgRequestBody以及@XxgParam区分参数从请求体拿或者从url的?后面拿。从而获取前端传来的数据
  3. 通过ObjectMapper进行不同类型参数的装配,最后调用方法的invoke实现带参/不带参的方法处理。

BeanDefinition

/**
 * 用来存放controller类的相关参数、方法等
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BeanDefinition {
 private Class typeClazz; // 类对象
 private String typeName; // 类名
 private Object annotation; // 注解
 private String controllerUrlPath; // controller的path路径
 private List<MethodDefinition> methodDefinitions; // 带有RequestMapping的注解
}

MethodDefinition

/**
 * 描述方法的类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MethodDefinition {
 private Class parentClazz; // 所属父类的class
 private Method method; // 方法
 private String methodName; // 方法名
 private Object annotation; // 注解类
 private String requestMappingUrlPath; // url
 private String[] allowedRequestMethods; // allowedRequestMethods
 private List<ParameterDefinition> parameterDefinitions;  // 参数列表
 private Object result;  // 返回数据
}

ParameterDefinition

/**
 * 描述参数的类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ParameterDefinition {
 private Class paramClazz; // 参数类对象
 private String paramName; // 参数名称
 private Object paramType; // 参数类型
 private boolean isRequestBody; // 是否是获取body中数据
}

单例模式的容器

赋予扫描包及根据uri获取对应方法的方法

/**
 * 用于存放请求路径 与 controller对应关系的类
 * 设计成单例模型
 */
public class RequestPathContainer {
 private static List<BeanDefinition> requestList = new ArrayList<>();
 private static final ClassLoader classLoader = RequestPathContainer.class.getClassLoader();
 private static volatile RequestPathContainer instance = null;
​
 public static RequestPathContainer getInstance() {
     if (instance == null) {
        synchronized(RequestPathContainer.class){
            if (instance == null) {
                instance = new RequestPathContainer();
            }
         }
     }
     return instance;
 }
​
 private RequestPathContainer() {
​
 }
​
 public List<BeanDefinition> getRequestList() {
    return requestList;
 }
​
 // 扫描包
 public void scanner(String packetUrl) throws UnsupportedEncodingException, ClassNotFoundException {
     String url = packetUrl.replace(".", "/");
     URL resource = classLoader.getResource(url);
     if (resource == null) {
         return;
     }
     String path = resource.getPath();
     File file = new File(URLDecoder.decode(path, "UTF-8"));
     if (!file.exists()) {
         return;
     }
     if (file.isDirectory()){
     File[] files = file.listFiles();
     if (files == null) {
        return;
     }
     for (File f : files) {
     if (f.isDirectory()) {
        scanner(packetUrl + "." + f.getName());
     }
     if (f.isFile() && f.getName().endsWith(".class")) {
         String classname = f.getName().replace(".class", ""); // 去掉.class后缀名
         Class clazz = Class.forName(packetUrl + "." + classname);
         dealClass(clazz);
         }
     }
     }
 }
​
 // 筛选包中的类,并添加到List中
 private void dealClass(Class clazz) {
     if (!clazz.isAnnotationPresent(XxgController.class)) {
     // 没有controller注解
        return;
     }
     List<MethodDefinition> methodDefinitions = new ArrayList<>();
     Method[] methods = clazz.getDeclaredMethods();
     for (Method method : methods) {
         // 方法转 方法描述类
         MethodDefinition methodDefinition = convertMethodToMethodDefinition(method, clazz);
         if (methodDefinition != null) {
            methodDefinitions.add(methodDefinition);
         }
         }
         if (methodDefinitions.size() == 0) {
            return;
         }
         // 设置类描述类
         BeanDefinition beanDefinition = convertBeanToBeanDefinition(clazz, methodDefinitions);
         requestList.add(beanDefinition);
    }
​
 // 根据uri 和 请求方法 获取执行方法
 public MethodDefinition getMethodDefinition(String uri, String method) {
     for (BeanDefinition beanDefinition: requestList) {
     if (!uri.contains(beanDefinition.getControllerUrlPath())) {
        continue;
     }
     List<MethodDefinition> methodDefinitions = beanDefinition.getMethodDefinitions();
     for (MethodDefinition methodDefinition: methodDefinitions) {
     StringBuilder sb = new StringBuilder().append(beanDefinition.getControllerUrlPath());
     sb.append(methodDefinition.getRequestMappingUrlPath());
     if (!sb.toString().equals(uri)) {
     continue;
     }
     String[] allowedRequestMethods = methodDefinition.getAllowedRequestMethods();
     for (String str : allowedRequestMethods) {
     if (str.toUpperCase().equals(method.toUpperCase())) {
     // 请求路径 与 请求方法 均满足,返回该方法描述类
     return methodDefinition;
     }
     }
     }
     }
     return null;
 }
​
 /**
 * 将controller类 转换为 类的描述类
 */
 private BeanDefinition convertBeanToBeanDefinition(Class clazz, List<MethodDefinition> methodDefinitions) {
     BeanDefinition beanDefinition = new BeanDefinition();
     beanDefinition.setTypeName(clazz.getName());
     beanDefinition.setTypeClazz(clazz);
     XxgController controller = (XxgController) clazz.getAnnotation(XxgController.class);
     beanDefinition.setAnnotation(controller);
     beanDefinition.setControllerUrlPath(controller.value());
     beanDefinition.setMethodDefinitions(methodDefinitions);// 增加方法体
     return beanDefinition;
 }
​
 /**
 * 将方法 转换为 方法描述类
 */
 private MethodDefinition convertMethodToMethodDefinition(Method method, Class clazz) {
 if (!method.isAnnotationPresent(XxgRequestMapping.class)) {
 // 没有RequestMapping注解
 return null;
 }
 method.setAccessible(true);
 Parameter[] parameters = method.getParameters();
 // 设置参数描述类
 List<ParameterDefinition> parameterDefinitions = new ArrayList<>();
 for ( Parameter parameter : parameters) {
 ParameterDefinition parameterDefinition = convertParamToParameterDefinition(parameter);
 parameterDefinitions.add(parameterDefinition);
 }
 // 设置方法描述类
 MethodDefinition methodDefinition = new MethodDefinition();
 methodDefinition.setParameterDefinitions(parameterDefinitions);  // 增加参数列表
 methodDefinition.setMethod(method);
 methodDefinition.setMethodName(method.getName());
 methodDefinition.setResult(method.getReturnType());
 XxgRequestMapping requestMapping = method.getAnnotation(XxgRequestMapping.class);
 methodDefinition.setRequestMappingUrlPath(requestMapping.value());
 methodDefinition.setAnnotation(requestMapping);
 methodDefinition.setAllowedRequestMethods(requestMapping.methods());
 methodDefinition.setParentClazz(clazz);
 return methodDefinition;
 }
​
 /**
 * 将参数 转换为 参数描述类
 */
 private ParameterDefinition convertParamToParameterDefinition(Parameter parameter) {
 ParameterDefinition parameterDefinition = new ParameterDefinition();
 if ( parameter.isAnnotationPresent(XxgParam.class)) {
 parameterDefinition.setParamName(parameter.getAnnotation(XxgParam.class).value());
 } else {
 parameterDefinition.setParamName(parameter.getName());
 }
 parameterDefinition.setParamClazz(parameter.getType());
 parameterDefinition.setParamType(parameter.getType());
 parameterDefinition.setRequestBody(parameter.isAnnotationPresent(XxgRequestBody.class));
 return parameterDefinition;
 }
​
}

全局servlet

不使用拦截器,仍然使用servlet来进行路由分发。此servlet监听/

public class DispatcherServlet extends HttpServlet {
 private ObjectMapper objectMapper = new ObjectMapper();
​
 @Override
 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
​
 // 编码设置
 resp.setContentType("text/json;charset=utf-8");
 RequestPathContainer requestPathContainer = RequestPathContainer.getInstance();
 MethodDefinition methodDefinition = requestPathContainer.getMethodDefinition(req.getRequestURI(), req.getMethod());
​
 if (methodDefinition == null) {
 resp.setStatus(404);
 sendResponse(R.failed("请求路径不存在"), req, resp);
 return;
 }
​
 List<ParameterDefinition> parameterDefinitions = methodDefinition.getParameterDefinitions();
 List<Object> params = new ArrayList<>(parameterDefinitions.size());
 for (ParameterDefinition parameterDefinition : parameterDefinitions) {
 try {
 Object value = dealParam(parameterDefinition, req, resp);
 params.add(value);
 } catch (ParamException e) {
 resp.setStatus(404);
 sendResponse(R.failed(e.getMessage()), req, resp);
 return ;
 }
 }
​
 try {
 Object result = methodDefinition.getMethod().invoke(methodDefinition.getParentClazz().newInstance(), params.toArray());
 sendResponse(result, req, resp);
 } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
 e.printStackTrace();
 sendResponse(e.getMessage(), req, resp);
 }
​
 }
​
 /**
 * 处理参数
 * @param parameterDefinition
 * @param req
 * @param resp
 */
 private Object dealParam(ParameterDefinition parameterDefinition, HttpServletRequest req, HttpServletResponse resp) throws ParamException, IOException {
 Object value;
 String data = "";
 if (parameterDefinition.isRequestBody()) {
 // 从请求体(request的输入流)中获取数据
 data = getJsonString(req);
 } else if (parameterDefinition.getParamType() == HttpServletRequest.class) {
 return req;
 } else if (parameterDefinition.getParamType() == HttpServletResponse.class) {
 return resp;
 } else if (isJavaType(parameterDefinition)) {
 // 从url中取出参数
 data = req.getParameter(parameterDefinition.getParamName());
 if(data == null) {
 throw new ParamException("服务器无法拿到请求数据,请检查请求头等");
 }
 } else {
 // 将请求url中的参数封装成对象
 try {
 Object obj = parameterDefinition.getParamClazz().newInstance();
 ConvertUtils.register(new DateConverter(), Date.class);
 BeanUtils.populate(obj, req.getParameterMap());
 return obj;
 } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
 throw new ParamException("未找到参数'" + parameterDefinition.getParamName() + "'对应的值");
 }
 }
 try {
 value = objectMapper.readValue(data, parameterDefinition.getParamClazz());
 } catch (JsonProcessingException e) {
 String errMsg = "参数'" + parameterDefinition.getParamName() +
 "'需要'" + parameterDefinition.getParamType() +
 "类型";
 throw new ParamException(errMsg);
 }
 return value;
 }
​
 private void sendResponse(Object result, HttpServletRequest req, HttpServletResponse resp) throws IOException {
 if (result == null) {
 return;
 }
 resp.setContentType("text/json;charset=utf-8");
 objectMapper.writeValue(resp.getWriter(), result);
 }
​
 /**
 * 判断参数是否是普通类型
 * @return
 */
 private boolean isJavaType(ParameterDefinition parameterDefinition) {
 Object[] javaTypes = MyJavaType.getJavaTypes();
 for (Object item : javaTypes) {
 if (item.equals(parameterDefinition.getParamClazz())) {
 return true;
 }
 }
 return false;
 }
​
 /**
 * 获取请求头的json字符串
 */
 private String getJsonString(HttpServletRequest req) throws IOException {
 BufferedReader br = new BufferedReader(new InputStreamReader(req.getInputStream(), "utf-8"));
 char[] chars = new char[1024];
 int len;
 StringBuilder sb = new StringBuilder();
 while ((len = br.read(chars)) != -1) {
 sb.append(chars, 0, len);
 }
 return sb.toString();
 }
}

servletcontext监听器初始化容器

@Override
 public void contextInitialized(ServletContextEvent servletContextEvent) {
 RequestPathContainer requestPathContainer = RequestPathContainer.getInstance();
 String configClassName = servletContextEvent.getServletContext().getInitParameter("config");
 Class appListenerClass = null;
 try {
 appListenerClass = Class.forName(configClassName);
 XxgScanner xxgScanner = (XxgScanner)appListenerClass.getAnnotation(XxgScanner.class);
 if (xxgScanner != null) {
 try {
 requestPathContainer.scanner(xxgScanner.value()); // 扫描controller类,初始化List
 } catch (UnsupportedEncodingException | ClassNotFoundException e) {
 e.printStackTrace();
 }
 }
 } catch (ClassNotFoundException e) {
 e.printStackTrace();
 }
 }

遗留的问题

静态资源也被拦截了

处理静态资源

default servlet

打开tomcat的conf/web.xml文件,可以发现tomcat默认有个default servlet,有如下配置:

 <servlet>
 <servlet-name>default</servlet-name>
 <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
 <init-param>
 <param-name>debug</param-name>
 <param-value>0</param-value>
 </init-param>
 <init-param>
 <param-name>listings</param-name>
 <param-value>false</param-value>
 </init-param>
 <load-on-startup>1</load-on-startup>
 </servlet>

但是他并没有匹配servlet-mapping,即处理的路径,那么可以在我们项目的web.xml中做以下配置来处理静态资源:

<!--  将全局拦截器的匹配/* 改成 / 。必须-->
<!--  /表示只处理其他的servlet不能匹配的路径-->
<servlet-mapping>
 <servlet-name>DispatcherServlet</servlet-name>
 <url-pattern>/</url-pattern>
 </servlet-mapping>
<!--  静态资源-->
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.html</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.js</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.css</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.jpg</url-pattern>
 </servlet-mapping>

最后

一.本文其实主要做了以下两个操作

  1. 服务器启动时,扫描controller包,将符合我们预期的类、方法、参数装配到容器中。
  2. 前端访问服务器,获取容器中指定路径对应的方法
    2.1 将访问参数按不同类型装配到参数列表中
    2.2 执行对应方法
    2.3 处理方法返回数据

二.参考说明

  • 项目实现过程

1.0版是博主自己思考并完成的。
2.0版是博主的小高老师给博主讲了思路,写出来后又看了小高老师的实现,然后综合着完善的。

  • 在写了文章后,博主对项目中不同类进行了解耦等操作,代码重构了一番,主要为了应付开闭原则、单一职责原则等。
  • 代码:https://github.com/dengbenche...

传送门

下一节:AutoWired属性上使用的简单实现

查看原文

赞 0 收藏 0 评论 0

邓奔成 发布了文章 · 9月14日

有效的数独

算法介绍

判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/probl...
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

   public boolean isValidSudoku(char[][] board) {

 for(int i = 0;i < board.length;i++){

 // 判断该行

 ArrayList<Integer> olds = new ArrayList<>();

 for (int j = 0;j < board[i].length;j++){

 if(i % 3 == 0 && j % 3 == 0){

 ArrayList<Integer> newOlds = new ArrayList<>();

 //判断该单元格

 for (int m = 0;m < 3;m++){

 for (int k = 0;k < 3;k++){

 char item = board[i+m][k+j];

 if(item == '.') continue;

 if(newOlds.indexOf((int)item) >= 0){

 return false;

 }

 newOlds.add((int)item);

 }

 }

 }

 char item = board[i][j];

 if(item == '.') continue;

 if(olds.indexOf((int)item) >= 0) return false;

 olds.add((int) item);

 }

 // 判断该列

 olds.clear();

 for (int j = 0;j < board.length;j++){

 char item = board[j][i];

 if(item == '.') continue;

 if(olds.indexOf((int)item) >= 0) return false;

 olds.add((int)item);

 }

 }

 return true;

 }
查看原文

赞 0 收藏 0 评论 0

邓奔成 发布了文章 · 9月11日

简单实现DbUtil的query与update方法

写在前面

在阅读本文之前,你或许应该先了解:

  • JDBC的实现步骤(注册驱动、建立连接、发送SQL、处理结果集)
  • 数据库连接池的使用(本文使用c3p0连接池)

DbUtils是Apache的一个对于数据库操作的封装类,本文重写了DbUtils的query与update方法,而代码着重于封装的实现原理,对于很多异常等都未做详细的处理。

实现步骤

  1. 首先,通过c3p0连接池建立数据库连接,并获取连接对象。
  • 导入c3p0-0.9.2-pre1.jar
  • 创建c3p0.propertiesc3p0-config.xml文件,下面是.xml文件主要内容:
<?xml version="1.0" encoding="UTF-8"?>  
<c3p0-config>  
 <!-- 默认配置,当使用ComboPooledDataSource无参构造器时,使用的就是这个配置 -->  
 <default-config>  
 <!-- 基本配置 -->  
 <property name="jdbcUrl">jdbc:mysql://127.0.0.1:3306/share?serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;characterEncoding=utf8</property>  
 <property name="driverClass">com.mysql.cj.jdbc.Driver</property>  
 <property name="user">root</property>  
 <property name="password">123456</property>  
 <!-- 每次增量,当需要创建Connection对象时,一次创建几个 -->  
 <property name="acquireIncrement">3</property>  
 <!-- 当创建池对象后,池中应该有几个Connection对象 -->  
 <property name="initialPoolSize">10</property>  
 <!-- 池中最少Connection个数,如果少于这个值,就会创建Connection -->  
 <property name="minPoolSize">2</property>  
 <!-- 池中最大连接个数 -->  
 <property name="maxPoolSize">15</property>  
 </default-config>
</c3p0-config> 
  • 创建数据库连接池,获取连接对象的包装类JdbcUtil
public class JdbcUtil {  
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource();  
  
 public static Connection getConnection(){  
        Connection conn = null;  
 try {  
            conn = dataSource.getConnection();  
 } catch (SQLException e) {  
            e.printStackTrace();  
 }  
        return conn;  
 }  
  
    public static DataSource getDataSource(){  
        return dataSource;  
 }  
}
  1. 数据库增删查改的类MyRunner
public class MyRunner {  
    private DataSource dataSource;  
  
 public MyRunner(DataSource dataSource){  
        this.dataSource = dataSource;  
 }  
  
    public <T> T query(String sql, MyResultSetHandler<T> handler, Object... params){  
        T result = null;  
 try {  
            Connection conn = dataSource.getConnection();  
 PreparedStatement pstmt = conn.prepareStatement(sql);  
 generateParams(pstmt, params); // 给pstmt赋值  
 result = handler.handle(pstmt.executeQuery());  
 } catch (SQLException e) {  
            e.printStackTrace();  
 }  
        return result;  
 }  
  
    /**  
 * 数据操作 包括增、删、改  
 * @param sql sql语句  
 * @param params 参数列表  
 */  
 public void update(String sql, Object... params){  
        try {  
            Connection conn = dataSource.getConnection();  
 PreparedStatement pstmt = conn.prepareStatement(sql);  
 generateParams(pstmt, params); // 给pstmt赋值  
 pstmt.executeUpdate();  
 } catch (SQLException e) {  
            e.printStackTrace();  
 }  
    }  
  
    /**  
 * 给指定的pstmt 预处理的? 赋值  
 * @param pstmt PreparedStatement  
 * @param params 参数列表  
 * @throws SQLException  
 */ private static void generateParams(PreparedStatement pstmt, Object[] params) throws SQLException {  
        for(int i = 1; i <= params.length; i++){  
            pstmt.setObject(i, params[i-1]);  
 }  
    }  
}
  1. 创建统一的处理ResultSet结果集的接口类MyResultSetHandler
/**  
 * @Auther dbc  
 * @Date 2020/9/11 17:04  
 */
public interface MyResultSetHandler<T> {  
    T handle(ResultSet resultSet) throws SQLException;  
}
  1. 实现上一步的接口MyBeanHandler
public class MyBeanHandler<T> implements MyResultSetHandler<T> {  
  
    private Class<T> clazz;  
  
 public MyBeanHandler(Class<T> T){  
        this.clazz = T;  
 }  
  
  
    @Override  
 public T handle(ResultSet resultSet) throws SQLException {  
        T t = null;  
 if(resultSet.next()){  
            try {  
                t = clazz.newInstance();  
 ResultSetMetaData metaData = resultSet.getMetaData();  
 int columnCount = metaData.getColumnCount();  
 for(int i = 1; i <= columnCount; i++){  
                    String columnName = metaData.getColumnName(i);  
 Object columnValue = resultSet.getObject(columnName);  
 try {  
                        Field field = clazz.getDeclaredField(columnName);  
 field.setAccessible(true); // 获取访问操作权限  
 field.set(t, columnValue);  
 }catch (NoSuchFieldException e){  
                        e.printStackTrace();  
 }  
                }  
            } catch (InstantiationException | IllegalAccessException e) {  
                e.printStackTrace();  
 }  
        }  
        return t;  
 }  
  
}
  1. 对于结果集为多个,需要用List来接收,同样用一个类来实现MyResultSetHandler,传入List<T>即可,本文就不再做演示。
  2. 增加测试类Account,数据库也应当存在此表即一些测试数据。主要有以下字段:
private int id;  
private String name;  
private Double money;
//注意:mysql中定义decimal字段,应由BigDecimal来接收
  1. 测试
public static void main(String[] args)  {  
    MyRunner myRunner= new MyRunner(JdbcUtil.getDataSource());  
 Account account = myRunner.query("SELECT * FROM account WHERE id =?;",new MyBeanHandler<Account>(Account.class),1);  
 System.out.println(account);  
 List<Account> accounts = myRunner.query("SELECT * FROM account",new MyBeanListHandler<>(Account.class));  
 System.out.println(accounts);  
}
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 7 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-07-15
个人主页被 658 人浏览