Java注解实现拦截

2024-06-10 23:38
文章标签 java 注解 拦截 实现

本文主要是介绍Java注解实现拦截,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Java注解实现拦截

一个很常见的场景是对方法进行拦截,比如计算方法的执行时间或者判断是否有执行该方法的权限。常用的拦截框架有AOP和aspectj,这两种拦截器使用不同的原理。AOP使用动态代理Bean来实施拦截,而aspectj使用扫描将字节码写入class文件。在这里我想要实现的是一种比较优雅的拦截方式:使用注解静态拦截,但是并不使用扫描。比如我们想要实现一个注解Lock。

预期的代码应该是这样:

	@Lock(name = "mylock")public static void test() {System.out.println("just a test");}

当给某个方法加上注解如Lock时,我们会让这个方法在执行前调用某个锁线程的方法,而在方法结束的时候则调用解锁方法。这样这个test()方法就在某种程度上变成了线程同步的方法。这个锁同时还可以有一个名字,其他方法采用Lock注解时只有锁的名字一样才会与test方法线程同步。

我们的Lock注解定义如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface Lock { final long DEFAULT_ELAPSE_TIME = 3L;String name();long time() default DEFAULT_ELAPSE_TIME;
}

 注解中有Retention,Target等配置,这些属于注解的基本知识,这里不多罗嗦。我们在注解中定义了一个name作为锁的名称,以及time作为锁的最大有效时间。

注解的处理器

我们在Java代码中,见过各种各样的注解,比如@Override,或者在Spring中常见的@Autowire等注解。这些注解使我们可以把非业务的代码分离出去,让我们的代码更加整洁和易读。在Java中,注解的使用原理一般有两种:使用处理器在编译期处理和在运行时使用反射进行动态处理。我们先来看一个使用反射进行注解处理的例子:

public class Model {@Lock(name = "my lock")public static void run() {	System.out.println("running!!!!!");}public static void main(String args[]) throws NoSuchMethodException, SecurityException {Class clazz = Model.class;Method method = clazz.getMethod("run",new Class[]{});Lock lock = method.getAnnotation(Lock.class);System.out.println(lock.name() + ", " + lock.time());}
}

 上面的代码非常简单,我们也会看到程序输出正确打印了“my lock, 3”。运用反射可以很方便的处理注解以及很多其他任务。事实上在Spring中,采用动态代理的方式完成的aop就是用的反射。由于在Spring中,几乎所有的类都使用Spring的bean的形式进行加载,亦即每个类都会生成一个单例,因此使用动态代理可以很好的完成拦截的任务。在Spring中,要完成我们这里的锁的任务,甚至不需要在代码中有任何修改,连注解都不需要。但是Spring的aop只能完成对非静态方法的拦截,这限制了它的使用场景,况且我们不一定就是使用Spring的加载方式进行类的加载。

当然,我们可以在程序启动时进行一个扫描,将所有类中包含指定注解的类的class文件改写,然后我们真正加载到的会是一个全新的类,Aspectj正是这么做的。但是我们不想使用扫描,因为这样感觉效率会比较低,而且不够高大上。同时使用类似aspectj的框架限制了我们的自定义注解的种类,我们所有需要拦截的方法所标注的注解都是一样,如@aspect,这也是我们不想要的。

这时候我们在Java 6中,找到了可以在编译期间处理注解的方法:java.annotation.processing.*(在Java 5以及之前,也是可以处理的,但会更加复杂,参见:

http://www.infoq.com/cn/articles/cf-java-annotation)。

你可以使用下面这篇文章来对使用注解处理器有个大致的了解:

http://hannesdorfmann.com/annotation-processing/annotationprocessing101/

以及相应的中文翻译版:

http://www.race604.com/annotation-processing/

能在编译期间做一些事情,甚至修改java和class文件,这很神奇也很酷,有点像C++的模板元编程。我们试图让事情尽量的简单,我们先看一下我们的LockProcessor第一个版本的代码,它的主要功能是获取被@Lock注解的方法的详细信息,包括所在类名,方法的参数和返回值类型等:

 @Overridepublic boolean process(Set<? extends TypeElement> annotations,RoundEnvironment roundEnv) {for (Element element : roundEnv.getElementsAnnotatedWith(Lock.class)) {Element classElement = element.getEnclosingElement();String name = null;String time = null;List<? extends AnnotationMirror> anns = element.getAnnotationMirrors();for (AnnotationMirror mirror : anns) {if (mirror.getAnnotationType().asElement().equals(lockElement)) {Map<? extends ExecutableElement, ? extends AnnotationValue> values = mirror.getElementValues();name = (String) getAnnotationValue(values, "name");time = (String) getAnnotationValue(values, "time");if (time == null) {time = DEFAULT_DURATION;}}}processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,classElement.toString() + "#" + element.toString() + "#"+ element.getKind() + "#"+ element.asType().toString() + "#" + name + "#"+ NumberUtils.toLong(time));}return true;}private Object getAnnotationValue(Map<? extends ExecutableElement, ? extends AnnotationValue> values,String annotationFiledName) {for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : values.entrySet()) {if (entry.getKey().getSimpleName().contentEquals(annotationFiledName)) {return entry.getValue().getValue();}}return null;}

 简要介绍一下这个Processor类,首先类的顶部的@AutoService注解表示会自动生成META-INF/services/javax.annotation.processing.Processor首先类的顶部的@AutoService注解表示会自动生成META-INF/services/javax.annotation.processing.Processor文件。这个文件如果不能自动生成,则手动生成也可以,但是路径和文件名都是必须严格匹配的。@SupportedAnnotationTypes 注解表示该Processor支持的注解类型,只有在这里标明的注解才会被Processor的process方法处理到,多个注解类型使用大括号分开标识。@SupportedSourceVersion 表示该Processor最大支持的Java版本,当标明为Java 7时,表示Java 7及以下的版本的代码会被支持。

在该Processor的成员变量中,我们定义了一个lockElement,其类型为TypeElement。TypeElement类型用来表示一个类或者接口的信息。在init方法中,我们将lockElement设置为Lock注解的信息,在后面的处理中,用来比较我们获得的注解是否为Lock注解。虽然我们的Processor只支持Lock注解,但是包含Lock注解的元素却可能同时包含其他注解,因此存储一个Lock注解的TypeElement是必要的。

在继续往下讨论process方法的细节前,或许你对注解处理器工作的原理有些好奇。我们可以从Java编译过程一窥究竟,事实上在明白了Java的编译过程后,你会对process方法有更深的理解。在[4]介绍中,Java的编译过程分为三个阶段:抽象语法树生成,注解处理和生成字节码。抽象语法树自然就是扫描Java代码,记录所有类型、变量、方法、注解等,存储在树状结构中,这在编译原理中非常常见。而后,Java编译器开始根据 WE-INF目录下的processor文件进入相应的注解处理器代码,如下图的中间步骤。

535098

注意注解处理结束后,如果生成了新的Java文件或者class文件,则需要重新回到第一步,解析与输入,然后再进行注解处理,如此往复,直到没有新的文件生成。从中我们也可以看出,编译器并不是看到一个注解就去寻址指定的注解处理器,而是在所有代码都扫描完后才会去做。

在process方法中,有两个循环,外层的循环遍历所有带有Lock注解的元素,这个元素可能是类,包,方法,变量,这取决于注解的定义中所支持的种类。当然在编写注解处理器时应该很清楚注解会用在哪些地方。内层循环获取带有Lock注解的元素上的Lock注解的信息。其中用到了一些Java的API,应该很容易明白。这里取注解的默认值的方法不是很优雅,我没有找到其他好方法,如果你知道烦请告知我。在外层循环的末尾,使用基类AbstractProcessor的成员变量processingEnv打印了一些信息,包括被注解元素所在类的完整类名,以及被注解方法的完整签名和注解的值。这些信息会被当作编译信息打印出来,有了这些信息我们可以做一些操作,比如写一个Java或者Class文件。注意process方法的返回值,当为true时,表示对于该注解不需要额外的处理器处理,为false时,则表示需要。

现在我们已经可以在编译期间获得注解以及处理注解的所有信息,我们应该怎样实现拦截呢?动态代理是行不通的,那么修改类的字节码呢?看起来是不行的,至少暂时是不行的,因为注解处理的时间是比生成原始class文件早的,我们需要寻找其他方法,但是注解处理器所得到的注解信息是有用的,这些信息让我们不需要去扫描代码寻找注解,我们应该把这些信息存在某个临时文件里,后面或许有用。

熟悉Java类加载过程的人应该知道,Java程序启动时,并不是所有类都会加载,只有当这个类被用到时,才会被加载。因此我们或许可以在程序启动的时候去修改类的class文件,让它添加我们想要的功能。也许你会问,修改class文件的方式,行得通吗?代码已经编译好了,这样能行吗?当然行得通,aspectj就是这么做的。那么aspectj是怎么做的呢,它用的是asm框架。

asm框架

关于asm框架的介绍很多,网上也有很多教程,但是其源代码反而不是那么容易读,因为注释太少了。下面的链接是asm官方的解释文档,看完该文档你就应该能够明白asm里面每一个类、方法、变量以及常量的含义。如果没有,说明你看的不够仔细^_^。

http://download.forge.objectweb.org/asm/asm4-guide.pdf

简单来说,asm就是一个用来操作字节码的框架,它提供了很多封装好的访问class文件,提取类型,方法和变量的方法。用它会比较容易的修改class文件。在进行实际的操作前,我们先来了解一下class文件的内容。以上面的Model类为例,我们把main函数里的代码换为调用run方法:

public class Model {public static void run() {	System.out.println("running!!!!!");}public static void main(String args[]) {run();}
}

 这个类足够简单了吧,我们用javac命令来编译一下该文件,看看Model.class文件的内容:

// Compiled from Model.java (version 1.7 : 51.0, super bit)
public class com.dewmobile.test.Model {// Method descriptor #9 ()V// Stack: 1, Locals: 1public Model();0  aload_0 [this]1  invokespecial java.lang.Object() [1]4  returnLine numbers:[pc: 0, line: 9]// Method descriptor #9 ()V// Stack: 2, Locals: 0public static void run();0  getstatic java.lang.System.out : java.io.PrintStream [2]3  ldc <String "running!!!!!"> [3]5  invokevirtual java.io.PrintStream.println(java.lang.String) : void [4]8  returnLine numbers:[pc: 0, line: 12][pc: 8, line: 13]// Method descriptor #14 ([Ljava/lang/String;)V// Stack: 0, Locals: 1public static void main(java.lang.String[] arg0);0  invokestatic com.dewmobile.test.Model.run() : void [5]3  returnLine numbers:[pc: 0, line: 17][pc: 3, line: 18]
}

 找到run方法的定义,里面只执行了一句打印”running!!!!!”的代码,在class文件也能看到相应的调用,稍微多了一些步骤:

public static void run();0  getstatic java.lang.System.out : java.io.PrintStream [2]3  ldc <String "running!!!!!"> [3]5  invokevirtual java.io.PrintStream.println(java.lang.String) : void [4]8  return

 第一句获取静态对象out,其类型为PrintStream;第二句将参数字符串放到栈里面;第三句调用方法println,第四句返回。我们如果熟悉class文件结构,完全可以用文本编辑器直接修改class,但是会比较麻烦且容易出错。而asm框架就提供了相应的修改class的方法。我们的目标是在run方法的开头和结尾各执行一个语句,这要用到asm中的ClassReader、ClassWriter、ClassAdaptor以及MethodAdaptor。ClassReader用来读取一个class文件,生成一个对象加载到内存中。ClassWriter用来将操作完的class对象转为byte数组,这样我们就可以使用文件IO库的方法生成一个新的class文件。连接ClassReader和ClassWriter的是ClassAdaptor,ClassWriter的构造方法包含一个ClassAdaptor的参数,而ClassReader有一个accept方法来接受一个ClassAdaptor对象。因此真正对class进行操作的是ClassAdaptor,而对于我们要修改某个方法而言,MethodAdaptor也是需要的。详细的资料在上面提到的官方文档里有解释,寓教于练,我们直接看看需要的代码。下面这段代码是一个继承了ClassAdaptor的LockAdaptor类:

public class LockAdaptor extends ClassAdapter {private String methodName;private String lockName;private long lockDuration;public LockAdaptor(ClassVisitor cv) {super(cv);}public LockAdaptor(ClassVisitor cv, String methodName, String lockName, long lockDuration) {super(cv);this.methodName = methodName;this.lockName = lockName;this.lockDuration = lockDuration;}public MethodVisitor visitMethod(final int access, final String name,final String desc, final String signature, final String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);MethodVisitor wrappedMv = mv;if (mv != null && name.equals(methodName)) {wrappedMv = new LockMethodAdaptor(mv, lockName, lockDuration);}return wrappedMv;}
}

 上面的类包含了三个成员变量:要锁住的方法的名称、锁的名称和锁的最大持续时间。注意如果使用注解实现锁,那么锁的名称是必须的,因为我们希望不同方法采用该注解时,不会被其他方法影响。ClassAdaptor中有一个visitMethod方法,这个方法的参数分别的含义是:

    • access:访问方法的可见性,public、private或者protected等,参见OpCodes类里面以ACC_打头的所有字段;
    • name:访问方法的名称;
    • desc:方法的描述符,包含方法的参数和返回值,但是信息不是很详细;
    • signature:方法的签名,如果方法参数、返回值和异常声明都是基本类型的话,则为null;
    • exceptions:方法的异常声明。

为了测试desc和signature的差异,我们给run方法加上参数:List<String>。然后再上面的vistiMethod方法中打印出它的参数,我们得到:

access:9, name:run, desc:(Ljava/util/List;)V, signature:(Ljava/util/List<Ljava/lang/String;>;)V, exceptions:null

 如上所示,如果我们想完全匹配一个方法的话,需要比较方法名称和方法签名,当然在我们的测试阶段,我就只判断了方法的名称是否一致。在if判断为true后,我们把原来的MethodAdaptor换为我们的LockMethodAdaptor,我们来看LockMethodAdaptor的代码:

public class LockMethodAdaptor extends MethodAdapter {private String lockName;private long lockDuration;public LockMethodAdaptor(MethodVisitor mv) {super(mv);}public LockMethodAdaptor(MethodVisitor mv, String lockName,long lockDuration) {super(mv);this.lockName = lockName;this.lockDuration = lockDuration;}@Overridepublic void visitCode() {mv.visitLdcInsn(lockName);mv.visitLdcInsn(lockDuration);mv.visitMethodInsn(Opcodes.INVOKESTATIC,Type.getInternalName(Util.class), "lock", "(Ljava/lang/String;J)V");}@Overridepublic void visitInsn(int opcode) {if (opcode == Opcodes.RETURN) {mv.visitLdcInsn(lockName);mv.visitMethodInsn(Opcodes.INVOKESTATIC,Type.getInternalName(Util.class), "unlock", "(Ljava/lang/String;)V");}mv.visitInsn(opcode);}
}

 同样的LockMethodAdaptor有两个成员变量:锁的名称和锁的时间。然后覆盖了visitCode和visitInsn两个方法。visitCode方法用来在目标方法的起始位置加入指令,我们先将两个参数压入栈,然后调用Util.lock方法。visitInsn用来在方法的中间加入指令,我们只需要在方法返回之前再次调用解锁,注意这里仅仅判断opcode是否为RETURN是不够的,还需要将OpCodes里定义的所有其他RETURN类型一起匹配才行。然后也是参数压栈,调用方法。

接下来我们来看看AsmGenerator的实现:

public class AsmGenerator {public AsmGenerator() {}public void generateLockedMethod(String className, String methodName,String lockName, long lockDuration) {ClassReader cr;try {cr = new ClassReader(className);ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);ClassAdapter classAdapter = new LockAdaptor(cw, methodName,lockName, lockDuration);cr.accept(classAdapter, ClassReader.SKIP_DEBUG);byte[] data = cw.toByteArray();File file = new File("src/main/java/" + className.replace('.', '/') + ".class");FileOutputStream fout = new FileOutputStream(file);fout.write(data);fout.close();} catch (IOException e) {e.printStackTrace();}}
}

 AsmGenerator首先读取指定的类,然后新建一个ClassWriter和ClassAdaptor,调用ClassReader的accept方法将指定的类输入给ClassAdaptor和ClassWriter。最后将ClassWriter生成的byte数组写入文件。

最后是我们调用asm的地方以及lock方法所在的类:

public class App {public static void main(String[] args) {AsmGenerator generator = new AsmGenerator();generator.generateLockedMethod("Model", "run", "hahaha", 3000L);	}
}
public class Util {public static void lock(String name, long duration) {System.out.println("lock " + name + "#" + duration);}public static void unlock(String name) {System.out.println("unlock " + name);}
}

我们来看修改后的Model.class文件:

//  (version 1.7 : 51.0, super bit)
public class com.dewmobile.test.Model {// Method descriptor #6 ()V// Stack: 1, Locals: 1public Model();0  aload_0 [this]1  invokespecial java.lang.Object() [8]4  return// Method descriptor #6 ()V// Stack: 3, Locals: 0public static void run();0  ldc <String "hahaha"> [11]2  ldc2_w  [12]5  invokestatic Util.lock(java.lang.String, long) : void [19]8  getstatic java.lang.System.out : java.io.PrintStream [25]11  ldc <String "running!!!!!"> [27]13  invokevirtual java.io.PrintStream.println(java.lang.String) : void [33]16  ldc <String "hahaha"> [11]18  invokestatic Util.unlock(java.lang.String) : void [36]21  return// Method descriptor #38 ([Ljava/lang/String;)V// Stack: 0, Locals: 1public static void main(java.lang.String[] arg0);0  invokestatic Model.run() : void [40]3  return}

已经正确加入了lock和unlock。注意如果你在eclipse中运行Model类,你不会得到加锁的运行结果,因为eclipse没有使用我们生成的Model.class。

我们已经有了如何记录需要拦截的方法的类的LockProcessor,也有了将指定方法的字节码修改的途径,那么问题来了,我们在编译阶段获取的需要拦截的方法的信息需要在一个合适的时机去调用asm的修改类字节码的方法。解决方法可以有多种,比如可以在程序启动的时候进行修改,因为熟悉ClassLoader的人应该会了解,Java程序在启动时并不会加载所有的类,而是等到需要加载的时候才会去读取class文件,不过这对于将程序打成jar包后再运行可能会麻烦一些。在这种情况下,我们可以再编译后运行一个额外的java程序,来将需要修改的class文件修改完成。这样就解决了程序打包的问题。

好的,现在我们的目标似乎已经完成了。故事结束了,王子和公主幸福的生活在一起。不过我在写这篇文章的时候有了一个额外的发现:lombok。

lombok

lombok是一个在编译期间修改Java代码的框架,它的官方地址在:https://projectlombok.org/

lombok提供了一些注解用来简化冗长的Java代码,比如在某个字段上添加@Getter注解,则会自动生成该字段的get方法。通过一些资料的说明,可以看到lombok利用的正是注解处理器可以再编译期间运行一些代码的原理。不过上面已经说过,注解处理器可以获取当前注解的元素,却无法直接修改其代码,因为注解处理器处理的时候,代码还没有生成。那么lombok是怎么实现的呢?[3]这篇文章值得好好阅读,在这篇文章里,作者解释了lombok是如何工作的,以及一些实用的用来创建我们的自定义注解的例子。不错,如果你看了这篇文章就会知道,lombok修改的是我们Java代码的抽象语法树(AST)。修改了AST之后,需要再从Java编译的第一步走起,然后再注解处理,最后生成class文件。这是一种类似于黑客的行为。

由于javac编译器和eclipse编译器虽然输出几乎是一样的,但是内部实现方式却差异很大,因此lombok针对每种编译器进行了适配。比如lombok提供的类JavacAnnotationHandler主要针对的是javac编译器,在[3]中有详细的实例。最难的部分在于如何适配,在此贴出[3]中的部分代码,看看有多复杂:

private JCMethodDecl createHelloWorld(JavacNode type) {TreeMaker treeMaker = type.getTreeMaker();JCModifiers modifiers = treeMaker.Modifiers(Modifier.PUBLIC);List methodGenericTypes = List.nil();JCExpression methodType = treeMaker.TypeIdent(TypeTags.VOID);Name methodName = type.toName("helloWorld");List methodParameters = List.nil();List methodThrows = List.nil();JCExpression printlnMethod = JavacHandlerUtil.chainDots(treeMaker, type, "System", "out", "println"); List printlnArgs = List.of(treeMaker.Literal("hello world"));JCMethodInvocation printlnInvocation = treeMaker.Apply(List.nil(), printlnMethod, printlnArgs);JCBlock methodBody = treeMaker.Block(0, List.of(treeMaker.Exec(printlnInvocation)));JCExpression defaultValue = null;return treeMaker.MethodDef(modifiers, methodName, methodType,methodGenericTypes, methodParameters, methodThrows, methodBody, defaultValue );}

不知你看的怎么样,反正我已经看的头晕了。如果想要使用最简洁的方法来通过注解实现拦截,看起来非lombok不可了。如果你有兴趣,可以去了解一下。使用lombok现有的注解不难,难的是利用它来开发新的注解。

参考资料

[1]http://www.javatronic.fr/articles/2014/10/08/how_does_annotation_processing_work_in_java.htmlJava注解处理器的工作原理

[2] http://www.ibm.com/developerworks/library/j-lombok/ lombok的介绍

[3] http://notatube.blogspot.com/2010/12/project-lombok-creating-custom.html lombok的原理,需要翻墙

[4] http://openjdk.java.net/groups/compiler/doc/compilation-overview/ Java编译过程


这篇关于Java注解实现拦截的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1049600

相关文章

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001

Java面试题:通过实例说明内连接、左外连接和右外连接的区别

在 SQL 中,连接(JOIN)用于在多个表之间组合行。最常用的连接类型是内连接(INNER JOIN)、左外连接(LEFT OUTER JOIN)和右外连接(RIGHT OUTER JOIN)。它们的主要区别在于它们如何处理表之间的匹配和不匹配行。下面是每种连接的详细说明和示例。 表示例 假设有两个表:Customers 和 Orders。 Customers CustomerIDCus