修改spring源码重写classloader实现项目加密
(一)操作方法和spring源码添加修改部分
事先说明:spring源码要下载好,会有修改spring的源码操作,本文和本作者所依赖的spring项目的版本是3.1.1,spring4及以上源码对应的类路径可能有所改变,需要自己找到要修改的类哦,类名应该是不会变的。望理解~~
操作步骤: ~具体说明看类文件注释~
1.运行ProduceKey.java的main方法 生成 key.key文件
2.ClassesEncryption类中,修改项目路径,修改key文件路径
3.执行ClassesEncryption的main方法加密文件
4.将OverrideClassLoader项目生成jar包,放在tomcat/lib下(自己网上看操作)
5.在tomcat/conf/context.xml 中Context 节点中添加
<Loader loaderClass="com.loader.ReBuildClassLoader" delegate="false"></Loader>
6. 替换tomcat的文件,比如com/ 下的除了(com/webapp/entity)下所有文件
7.启动tomcat
加密修改spring 源码
1.org.springframework.context-3.1.1.RELEASE.jar
2.org.springframework.core-3.1.1.RELEASE.jar
其中org.springframework.context-3.1.1.RELEASE.jar 修改类
(1)ClassPathScanningCandidateComponentProvider.java
line:230 加入
packageSearchPath = packageSearchPath.replace(".class", ".zdywjmjw");
System.out.println("packageSearchPath"+packageSearchPath);
Resource[] resourcesZdywjmjw = this.resourcePatternResolver.getResources(packageSearchPath);
Resource[] resources = new Resource[resourcesClass.length+resourcesZdywjmjw.length];
System.arraycopy(resourcesClass, 0, resources, 0, resourcesClass.length);
System.arraycopy(resourcesZdywjmjw, 0, resources, resourcesClass.length, resourcesZdywjmjw.length);
其中org.springframework.core-3.1.1.RELEASE.jar 修改类
(1)ClassPathResource.java
在类下面添加下列方法
private static boolean isWindowsOS() {
boolean isWindowsOS = false;
String osName = System.getProperty("os.name");
if (osName.toLowerCase().indexOf("windows") > -1) {
isWindowsOS = true;
}
return isWindowsOS;
}
// 把文件读入byte数组
private byte[] readFile(String filename) throws IOException {
File file = new File(filename);
long len = file.length();
byte data[] = new byte[(int) len];
FileInputStream fin = new FileInputStream(file);
int r = fin.read(data);
if (r != len)
throw new IOException("Only read " + r + " of " + len + " for " + file);
fin.close();
return data;
}
// 把byte数组写出到文件
private void writeFile(String filename, byte data[]) throws IOException {
FileOutputStream fout = new FileOutputStream(filename);
fout.write(data);
fout.close();
}
line 168行 修改
String basepath =this.getClass().getResource("/").getPath();
if(isWindowsOS()){
basepath = basepath.substring(1,basepath.length());//window下增加
}
String reloadedClass = basepath+this.path.replace(".class", ".zdywjmjw");
File file = new File(reloadedClass);
if (file.exists()) {
// 下面是定制部分
try {
String keyFilename = basepath+"key.key";
// 读取密匙
String algorithm = "DES";
byte rawKey[] = readFile(keyFilename);
DESKeySpec dks = new DESKeySpec(rawKey);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
SecretKey key = keyFactory.generateSecret(dks);
SecureRandom sr = new SecureRandom();
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key, sr);
// 读取经过加密的类文件
byte classData[] = readFile(pclass);
if (classData != null) {
byte decryptedClassData[] = cipher.doFinal(classData); // 解密
InputStream sbs = new ByteArrayInputStream(decryptedClassData);
return sbs;
}
} catch (Exception fnfe) {
}
}
(2).SimpleMetadataReader.java
添加方法
private static boolean isWindowsOS() {
boolean isWindowsOS = false;
String osName = System.getProperty("os.name");
if (osName.toLowerCase().indexOf("windows") > -1) {
isWindowsOS = true;
}
return isWindowsOS;
}
public static byte[] readFile(String filename) throws IOException {
File file = new File(filename);
long len = file.length();
byte data[] = new byte[(int) len];
FileInputStream fin = new FileInputStream(file);
int r = fin.read(data);
if (r != len)
throw new IOException("Only read " + r + " of " + len + " for " + file);
fin.close();
return data;
}
修改构造类
SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException {
InputStream is = resource.getInputStream();
ClassReader classReader = null;
try {
String filename = "";
if (resource.getURI().toString().indexOf("jar:file") == -1 ) {
String checkPath="";
if(isWindowsOS()){
checkPath = "classes\\com";
}else{
checkPath = "classes/com";
}
if(resource.getFile().getAbsolutePath().indexOf(checkPath)!=-1){
filename = resource.getFile().getAbsolutePath().replace(".class", ".zdywjmjw");
File file = new File(filename);
if(file.exists()){
//进行解密
String basepath =this.getClass().getResource("/").getPath();
if(isWindowsOS()){
basepath = basepath.substring(1,basepath.length());
}
String keyFilename = basepath+"key.key";
String algorithm = "DES";
try{
// 生成密匙
SecureRandom sr = new SecureRandom();
byte rawKey[] = readFile(keyFilename);
DESKeySpec dks = new DESKeySpec(rawKey);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
SecretKey key = keyFactory.generateSecret(dks);
// 创建用于实际加密操作的Cipher对象
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key, sr);
byte classData[] = readFile(name);
byte decryptedClassData[] = cipher.doFinal(classData);
classReader = new ClassReader(decryptedClassData);
} catch(NoSuchAlgorithmException e){
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidKeySpecException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
}else{
classReader = new ClassReader(is);
}
}else{
classReader = new ClassReader(is);
}
}else{
classReader = new ClassReader(is);
}
}finally {
is.close();
}
AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
classReader.accept(visitor, true);
this.annotationMetadata = visitor;
// (since AnnotationMetadataReader extends ClassMetadataReadingVisitor)
this.classMetadata = visitor;
this.resource = resource;
}
(3)LocalVariableTableParameterNameDiscoverer.java
line 103 行修改
String cName = clazz.getResource("/").getPath()+clazz.getName().replace(".", "/")+".class";
is = clazz.getClassLoader().getResourceAsStream(cName);
if(is == null){
// We couldn't load the class file, which is not fatal as it
// simply means this method of discovering parameter names won't work.
if (logger.isDebugEnabled()) {
logger.debug("Cannot find '.class' file for class [" + clazz
+ "] - unable to determine constructors/methods parameter names");
}
return NO_DEBUG_INFO_MAP;
}
(二)项目相关
项目名称:OverrideClassLoader
项目打包方式:jar,是普通Java工程
项目结构:
com.key
com.loader
com.util
其中 key包下面文件有:ClassesEncryption.java、ProduceKey.java、GenerateEncryptedClassMain
loader包下面有文件:ReBuildClassLoader.java
util包下面有文件:FileUtil.java
(1)ProduceKey.java
package com.key;
import java.security.SecureRandom;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import com.util.FileUtil;
/**
* 生成key文件类
* @author win7
*
*/
public class ProduceKey {
public static void main(String args[]) throws Exception {
String keyFilename = null;
boolean windowsOS = isWindowsOS();//检查是否是window系统
System.out.println(windowsOS);
if (windowsOS) {
keyFilename = "D://developer/apache-tomcat7/webapps/项目名称/WEB-INF/classes/key.key";//是本地开发的项目,tomcat的路径
} else {
keyFilename = "/usr/local/tomcat/webapps/项目名称/WEB-INF/classes/key.key";
}
String algorithm = "DES";
// 生成密匙
SecureRandom sr = new SecureRandom();
KeyGenerator kg = KeyGenerator.getInstance(algorithm);
kg.init(sr);
SecretKey key = kg.generateKey();
// 把密匙数据保存到文件,放入指定位置
FileUtil.writeFile(keyFilename, key.getEncoded());
}
private static boolean isWindowsOS() {
boolean isWindowsOS = false;
String osName = System.getProperty("os.name");
if (osName.toLowerCase().indexOf("windows") > -1) {
isWindowsOS = true;
}
return isWindowsOS;
}
}
(2)ClassesEncryption.java
package com.key;
import java.io.File;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import com.util.FileUtil;
/**
* class文件加密类
* @author win7
*
*/
public class ClassesEncryption {
public static List<File> filelist = new ArrayList<>();
public static void main(String[] args) throws Exception {
System.out.println("search start");
// 得到要加密的项目下com目录下的class文件
getFileList("D://developer/apache-tomcat7/webapps/项目名称/WEB-INF/classes/com");
System.out.println("文件数量" + filelist.size());
// 获取文件名秘钥
String keyFilename = "D://developer/apache-tomcat7/webapps/项目名称/WEB-INF/classes/key.key";
// 算法
String algorithm = "DES";
// -------下面开始生成密匙-------
// 强加密随机数生成器
SecureRandom sr = new SecureRandom();
// 将加密后的key.key转成字节数组
byte rawKey[] = FileUtil.readFile(keyFilename);
// 创建一个 DESKeySpec 对象,使用 rawKey 中的前 8 个字节作为 DES 密钥的密钥内容。
DESKeySpec dks = new DESKeySpec(rawKey);
// 得到转换指定算法的秘密密钥的SecretKeyFactory对象。
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
// 根据提供的密钥规范(密钥材料)生成 SecretKey 对象
SecretKey key = keyFactory.generateSecret(dks);
// 创建用于实际加密操作的Cipher对象 , Cipher:此类为加密和解密提供密码功能
Cipher ecipher = Cipher.getInstance(algorithm);
// 参数:加密的常量,加密密钥, 算法参数
ecipher.init(Cipher.ENCRYPT_MODE, key, sr);
// 加密命令行中指定的每一个类
for (int i = 0; i < filelist.size(); i++) {
String filename = filelist.get(i).getAbsolutePath();
byte classData[] = FileUtil.readFile(filename); // 读入类文件
byte encryptedClassData[] = ecipher.doFinal(classData); // 加密
String projectFile = filename.replace(".class", ".zdywjmjw");//zdyljzq:自定义文件名结尾
FileUtil.writeFile(projectFile, encryptedClassData); // 保存加密后的内容
System.out.println("Encrypted " + projectFile);
File classFile = new File(filename);
classFile.delete();
}
}
public static void encrypt(String tomcat_home) throws Exception {
System.out.println("search start");
getFileList(tomcat_home + "webapps/项目名称/WEB-INF/classes/com");
System.out.println("文件数量" + filelist.size());
String keyFilename = tomcat_home + "webapps/项目名称/WEB-INF/classes/key.key";
String algorithm = "DES";// 算法
// -------下面开始生成密匙-------
// 强加密随机数生成器
SecureRandom sr = new SecureRandom();
// 将加密后的key.key转成字节数组
byte rawKey[] = FileUtil.readFile(keyFilename);
// 创建一个 DESKeySpec 对象,使用 rawKey 中的前 8 个字节作为 DES 密钥的密钥内容。
DESKeySpec dks = new DESKeySpec(rawKey);
// 得到转换指定算法的秘密密钥的SecretKeyFactory对象。
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
// 根据提供的密钥规范(密钥材料)生成 SecretKey 对象
SecretKey key = keyFactory.generateSecret(dks);
// 创建用于实际加密操作的Cipher对象 , Cipher:此类为加密和解密提供密码功能
Cipher ecipher = Cipher.getInstance(algorithm);
// 参数:加密的常量,加密密钥, 算法参数
ecipher.init(Cipher.ENCRYPT_MODE, key, sr);
// 加密命令行中指定的每一个类
for (int i = 0; i < filelist.size(); i++) {
String filename = filelist.get(i).getAbsolutePath();
byte classData[] = FileUtil.readFile(filename); // 读入类文件
byte encryptedClassData[] = ecipher.doFinal(classData); // 加密
String projectFile = filename.replace(".class", ".zdywjmjw");//zdyljzq:自定义文件名结尾
FileUtil.writeFile(projectFile, encryptedClassData); // 保存加密后的内容
System.out.println("Encrypted " + projectFile);
File classFile = new File(filename);
classFile.delete();
}
}
/**
* 注意:加密的时候不能加密实体,要不然项目就启动不成功或者异常
* @param strPath
* @return
*/
public static List<File> getFileList(String strPath) {
File dir = new File(strPath);
File[] files = dir.listFiles(); // 该文件目录下文件全部放入数组
if (files != null) {
for (int i = 0; i < files.length; i++) {
String fileName = files[i].getName();
if (files[i].isDirectory()) { // 判断是文件还是文件夹
getFileList(files[i].getAbsolutePath()); // 获取文件绝对路径
} else if (fileName.endsWith(".class")) { // 判断文件名是否以.class结尾
String strFileName = files[i].getAbsolutePath();
// System.out.println("---" + strFileName);
if (isWindowsOS()) {
if (strFileName.indexOf("com\\项目名称\\webapp\\entity") == -1) {
filelist.add(files[i]);
}
} else {
if (strFileName.indexOf("com/项目名称/webapp/entity") == -1) {
filelist.add(files[i]);
}
}
} else {
continue;
}
}
}
return filelist;
}
public static boolean isWindowsOS() {
boolean isWindowsOS = false;
String osName = System.getProperty("os.name");
if (osName.toLowerCase().indexOf("windows") > -1) {
isWindowsOS = true;
}
return isWindowsOS;
}
}
(3)GenerateEncryptedClassMain.java
package com.key;
import java.util.Scanner;
/**
* main方法生成加密class文件
* @author win7
*
*/
public class GenerateEncryptedClassMain {
public static void main(String[] args) {
try {
Scanner s = new Scanner(System.in);
String str = null;
System.out.println("请tomcat路径以'/'结尾:");//如: /usr/local/tomcat/
str = s.next();
if (str == null || str.trim().equals("")) {
System.out.println("tomcat路径不能为空");
return;
}
String tomcat_home = str;
ClassesEncryption.encrypt(tomcat_home);
} catch (Exception e) {
e.printStackTrace();
}
}
}
(4)ReBuildClassLoader.java
package com.loader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import org.apache.catalina.loader.WebappClassLoader;
import com.util.FileUtil;
/**
* 自定义类加载器
* WebappClassLoader:是tomcat的jar包提供,所以要添加 tomcat server Runtime
*
* @author win7
*
*/
public class ReBuildClassLoader extends WebappClassLoader {
// 这些对象在构造函数中设置,以后loadClass()方法将利用它们解密类
private SecretKey key;
private Cipher cipher;
public ReBuildClassLoader() {
super();
}
public ReBuildClassLoader(ClassLoader parent) {
super(parent);
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
try {
// 要创建的Class对象
Class clasz = null;
// 必需的步骤1:如果类已经在系统缓冲之中,不必再次装入它
clasz = findLoadedClass(name);
if (clasz != null)
return clasz;
// 下面是定制部分
try {
String basepath = "";
if (isWindowsOS()) {
basepath = "D://developer/apache-tomcat7/webapps/项目名称/WEB-INF/classes/";// 项目物理地址
} else {
basepath = "/usr/local/tomcat/webapps/项目名称/WEB-INF/classes/";
}
String cname = basepath + name.replace('.', '/') + ".zdywjmjw";
File file = new File(cname);
if (file.exists()) {
String keyFilename = basepath + "key.key";
// 读取密匙
String algorithm = "DES";
// System.err.println("[DecryptStart: reading key]");
byte rawKey[] = FileUtil.readFile(keyFilename);
DESKeySpec dks = new DESKeySpec(rawKey);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
key = keyFactory.generateSecret(dks);
SecureRandom sr = new SecureRandom();
// System.err.println("[DecryptStart: creating cipher]");
cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key, sr);
// 读取经过加密的类文件
byte classData[] = FileUtil.readFile(cname);
if (classData != null) {
byte decryptedClassData[] = cipher.doFinal(classData); // 解密
clasz = defineClass(name, decryptedClassData, 0, decryptedClassData.length); // 再把它转换成一个类
// System.err.println("[DecryptStart: decrypting class + name + "]");
}
}
} catch (FileNotFoundException fnfe) {
}
// 必需的步骤2:如果上面没有成功
// 尝试用默认的ClassLoader装入它
if (clasz == null)
return super.loadClass(name, resolve);
// 必需的步骤3:如有必要,则装入相关的类
if (resolve && clasz != null)
resolveClass(clasz);
return clasz;// 把类返回给调用者
} catch (IOException ie) {
throw new ClassNotFoundException(ie.toString());
} catch (GeneralSecurityException gse) {
throw new ClassNotFoundException(gse.toString());
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
@Override
public InputStream getResourceAsStream(String arg0) {
String pclass = arg0.replace(".class", ".zdywjmjw");
File file = new File(pclass);
if (file.exists()) {
// 下面是定制部分
try {
String basepath = "";
if (isWindowsOS()) {
basepath = "D://developer/apache-tomcat7/webapps/项目名称/WEB-INF/classes/";// 项目物理地址
} else {
basepath = "/usr/local/tomcat/webapps/项目名称/WEB-INF/classes/";
}
String keyFilename = basepath + "key.key";
// 读取密匙
String algorithm = "DES";
byte rawKey[] = FileUtil.readFile(keyFilename);
DESKeySpec dks = new DESKeySpec(rawKey);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
key = keyFactory.generateSecret(dks);
SecureRandom sr = new SecureRandom();
cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key, sr);
// 读取经过加密的类文件
byte classData[] = FileUtil.readFile(pclass);
if (classData != null) {
byte decryptedClassData[] = cipher.doFinal(classData); // 解密
InputStream sbs = new ByteArrayInputStream(decryptedClassData);
return sbs;
}
} catch (Exception fnfe) {
}
}
return super.getResourceAsStream(arg0);
}
private boolean isWindowsOS() {
boolean isWindowsOS = false;
String osName = System.getProperty("os.name");
if (osName.toLowerCase().indexOf("windows") > -1) {
isWindowsOS = true;
}
return isWindowsOS;
}
}
FileUtil.java
package com.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtil {
// 把文件读入byte数组
static public byte[] readFile(String filename) throws IOException {
File file = new File(filename);
long len = file.length();
byte data[] = new byte[(int) len];
FileInputStream fin = new FileInputStream(file);
int r = fin.read(data);
if (r != len)
throw new IOException("Only read " + r + " of " + len + " for " + file);
fin.close();
return data;
}
// 把byte数组写出到文件
static public void writeFile(String filename, byte data[]) throws IOException {
FileOutputStream fout = new FileOutputStream(filename);
fout.write(data);
fout.close();
}
}
修改spring源码重写classloader实现项目加密的更多相关文章
- spring源码学习之:项目公共配置项解决方案
一:项目中有一些key,value的简单配置 org.apache.commons.configuration.DatabaseConfiguration可以轻松解决 二:配置项目的xml中bean ...
- 修改和编译spring源码,构建jar(spring-context-4.0.2.RELEASE)
上周在定位问题时,发现Spring容器实例化Bean的时候抛出异常,为了查看更详细的信息,决定修改spring-context-4.0.2.RELEASE.jar中的CommonAnnotationB ...
- Spring源码 02 项目搭建
参考源 https://www.bilibili.com/video/BV1tR4y1F75R?spm_id_from=333.337.search-card.all.click https://ww ...
- 剑指Spring源码(二)
这是春节后的第一篇博客,我在构思这篇博客的时候,一度想放弃,想想要不要换个东西写,因为毕竟个人水平有限,Spring源码实在博大精深,不是我这个菜的抠脚的菜鸡可以驾驭的,怕误人子弟,还有就是源码分析类 ...
- Spring源码分析 之浅谈设计模式
一直想专门写个Spring源码的博客,工作了,可以全身性的投入到互联网行业中.虽然加班很严重,但是依然很开心.趁着凌晨有时间,总结总结. 首先spring,相信大家都很熟悉了. 1.轻量级 零配置, ...
- Spring源码深度解析之Spring MVC
Spring源码深度解析之Spring MVC Spring框架提供了构建Web应用程序的全功能MVC模块.通过策略接口,Spring框架是高度可配置的,而且支持多种视图技术,例如JavaServer ...
- Spring源码阅读一
引导: 众所周知,阅读spring源码最开始的就是去了解spring bean的生命周期:bean的生命周期是怎么样的呢,见图知意: 大致流程: 首先后通过BeanDefinitionReader读取 ...
- spring源码分析(一)IoC、DI
创建日期:2016.08.06 修改日期:2016.08.07 - 2016.08.12 交流QQ:992591601 参考书籍:<spring源码深度解析>.<spring技术内幕 ...
- 【Spring源码分析】Bean加载流程概览
代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. 很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事 ...
随机推荐
- Sysbench Sysbench在centos系统下的安装
Sysbench在centos系统下的安装 by:授客 QQ:1033553122 测试环境: CentOS-7-x86_64-DVD-1503-01.iso 下载地址: http:/ ...
- svn基本常见操作设置
代码管理工具一开始用的确会有点懵,但是永久了就会发现都是那几下套路,记录下来 托管好了代码一般起冲突了还是想重新搞一下,有个万能的重置操作,那就是重新关联svn项目,以前有时更换地址也是,发现遇到很多 ...
- ajax简单登录(踩过的坑)
登陆页面: <%@ page language="java" contentType="text/html; charset=UTF-8" pageEnc ...
- 四. Redis事务处理
Redis目前对事务的支持还是比较简单,Redis能保证一个Client发起的事务中的命令可以连续执行,而中间不会插入其他Client的命令:当一个Client在连接中发起一个multi命令的时候,这 ...
- HTML+JS+JQuery不可以使用status
可能是JQuery的内部定义了status的原因!在HTML中的元素如果声明了ID为status的话,脚本中是不能访问这个对象的,会成为一个字符串对象.
- 基于pygame实现飞机大战【面向过程】
一.简介 pygame 顶级pygame包 pygame.init - 初始化所有导入的pygame模块 pygame.quit - uninitialize所有pygame模块 pygame.err ...
- myeclipse10 blue版激活码
http://blog.itpub.net/27042095/viewspace-1164998/
- css图片垂直水平居中及放大(实现水平垂直居中的效果有哪些方法?)
实现水平垂直居中方法有很多种: 一.万能法: 1.已知高度宽度元素的水平垂直居中,利用绝对定位和负边距实现. <style type="text/css"> .wrap ...
- Spring入门详细教程(二)
前言 本篇紧接着spring入门详细教程(一),建议阅读本篇前,先阅读第一篇.链接如下: Spring入门详细教程(一) https://www.cnblogs.com/jichi/p/1016553 ...
- MySQL多表更新的一个坑
简述 MySQL支持update t1,t2 set t1.a=2;这种语法,别的关系数据库例如oracle和sql server都不支持.这种语法有时候写起来挺方便,但他有一个坑. 测试脚本 dro ...