java基础和springboot
java基础
直接看Boogipop大佬的文章,贼全面 ,我就记录一些不会的
JVM 角度说进程与线程之间的关系
一个运行的exe就是一个进程,进程可以有多个线程
同一个进程中的多个线程共享堆和方法区
每个线程有独立的自己的程序计数器、虚拟机栈和本地方法栈
多线程并不能提高运行速度,只能提高CPU使用效率
JVM
在线程中执行一个方法时,会创建一个栈帧入栈并执行,不管该栈帧是正常结束还是异常,栈帧都会销毁
Scanner
SICTF看到一个很秀的操作
(#a=new java.util.Scanner(new java.io.File("/flag")).next())
//“#a”是因为使用OGNL表达式
读取内容
两种方式读取next(),nextline()
next()
以空格为分隔符,nextline()
以回车为分隔符
Scanner test = new Scanner(System.in);
if(test.hasNext()){
String a = test.next();
System.out.println(a);
}
验证输入有hasNextInt
、hasNextDouble
等,然后使用对应的nextInt
、nextDouble
等接收参数
命令行传参
是在src目录下运行,所以加上路径
java com.boogipop.www.base.DemoReload i am boogipop
可变参数
简单来说就是函数的参数数量不限
(参数类型 ...
变量名) (加三个点)
package org.example.basic;
public class test {
public static void main(String[] args) {
print(1,2,3,4,5,6,78,32,32,432,423,423,423);
}
public static void print(double ... number){
if (number.length==0){
System.out.println("no args");
return;
}
else {
for(int i=0;i<=number.length;i++){
System.out.println(number[i]);
}
}
}
}
数组
定义一个数组,int[] num = {1,2,3,4,5,6}
然后输入num.for
就会出来增强for循环
for (int i : num) {
}
这里的i
相当于python
中的for i in a
,直接就是数组内容
Arrays
Arrays.toString()
输出数组内容
Arrays.sort()
给数组排序
Arrays.fill()
特定值填充数据
int[] nm = {12,2,34,15,06};
System.out.println(nm);
System.out.println(Arrays.toString(nm));
Arrays.sort(nm);
System.out.println(Arrays.toString(nm));
Arrays.fill(nm,2);
System.out.println(Arrays.toString(nm));
Arrays.fill(nm,2,4,0); #数组下标2-4,左闭右开,填充0
System.out.println(Arrays.toString(nm));
##
[I@677327b6 #数组地址
[12, 2, 34, 15, 6]
[2, 6, 12, 15, 34]
[2, 2, 2, 2, 2]
[2, 2, 0, 0, 2]
构造函数、this、this()、super、super()、this动态绑定
this :代表“当前对象”
this():就是调用“当前对象”的无参构造方法
this、this(),new的谁就指向谁。父类里有this也是指向的new出来的对象。this的动态绑定:包括静态方法和静态成员变量是前期绑定、访问方法是后期绑定
super:指向父类
super():就是调用父类的无参构造方法
super、super(),仅指向上一级父类。
A
为父类,当前对象就是new B
的bb
,s3()
的super.s2()
即调用父类的s2()
,A
中this.s1()
调用当前对象的s1()
,即bb
的s1()
,也就是B
中的s1()
所以输出B中的s1
A
不加this
的话也是输出B中的s1
,因为B
为子类,相当于重写了s1
方法
两个案例理解一下这句话
instanceof
判断两个类之间有无父子关系
多态
静态代码块、匿名代码块、构造代码块
抽象类
类似占位符,只能定义不能写
接口
跟抽象类一样
内部类
套娃
捕获异常
try catch finally throw throws
finally不管怎样都会执行
java io
字节流
FileOutputStream
FileOutputStream out = new FileOutputStream("flag"); //创建flag文件
String hello = "Hello World!";
out.write(97);
out.write('a');
out.write(hello.getBytes()); //转为字节数组
out.close();
System.out.println("finish");
FileInputStream
FileInputStream in = new FileInputStream("flag");
while ((read=in.read())!=-1) {
System.out.printf(read+",");
System.out.printf((char)read+",");
}
in.close();
每经过一次in.read()
就会指向下一个内容,所以先把值赋给一个变量
in.read()
返回int
类型
FileInputStream in = new FileInputStream("flag");
byte[] buf = new byte[3]; //
System.out.println(in.read(buf)); //把内容读入数组(缓冲区),输出的是数组长度
System.out.println(new String(buf,0,1));
//0为偏移,1为读取的长度,不能超过数组的长度
in.close();
BufferedInputStream
读入缓冲区,作用跟byte[] buf = new byte[3];
一样
FileInputStream in = new FileInputStream("flag");
BufferedInputStream bif = new BufferedInputStream(in);
while ((read=bif.read())!=-1){
System.out.print((char)read);
}
bif.close(); //也会关闭in
BufferedOutputStream
FileOutputStream out=new FileOutputStream("1.txt");
BufferedOutputStream bos=new BufferedOutputStream(out);
for(int i=0;i<=10;i++){
bos.write("helloworld,".getBytes()); //到这一步不会写入文件,只是写入了缓冲区
bos.flush();//刷新缓冲区写入文件
}
bos.close();//关闭的时候也会调用flush,同时关闭out
ObjectInputStream和ObjectOutputStream
序列化与反序列化
重写方法
private void writeObject(ObjectOutputStream s)throws java.io.IOException
private void readObject(ObjectInputStream oos)throws java.io.IOException, ClassNotFoundException
{
oos.defaultReadObject();
}
字符流
使用字节流读取中文会出现乱码
FileReader
FileReader fr=new FileReader("resource/test.txt");
char[] buf=new char[1024]; //创建一个缓冲区
int count=0;
while((count=fr.read(buf))!=-1){
System.out.println(new String(buf,0,count));
FileWriter
FileWriter fw=new FileWriter("writer.txt");
//写入
for(int i=0;i<10;i++){
fw.write("java is the best");
fw.flush();
}
fw.close();
BufferReader
转换流
InputStreamReader/OutputStreamWriter
- 可将字节流变为转换流
- 可设置字符的编码方式
FileInputStream fs= new FileInputStream("resource/test.txt");
InputStreamReader isr=new InputStreamReader(fs,"utf-8");//传入字节流,转换流指定utf-8编码
//读取文件
int data=0;
while((data=isr.read())!=-1){
System.out.println((char)data);
}
isr.close();
FileOutputStream fo=new FileOutputStream("fo.txt");
OutputStreamWriter osw=new OutputStreamWriter(fo,"GBK"); //gbk编码写入
for(int i=1;i<=10;i++){
osw.write("我是傻逼");
osw.flush();
}
osw.close();
类
Object类
equals()
== 基础类型对比的是值是否相同,引用类型对比的是引用是否相同;
equals只是单单的比较值
String s1 = "string";
String s2 = "string";
String s3 = new String("string");
System.out.println(s1==s2);
System.out.println(s1==s3);
System.out.println(s1.equals(s2));
System.out.println(s1.equals(s3));
/*
true
false
true
true
*/
String类
str.charAt(str.length()-1) //里面的数字相当于数组下标
//charAt();返回对应下标的字符
//toCharArray();返回字符串对应数组
//indexOf('asd');返回字符串首次出现位置
//lastIndexOf('asd');返回字符串最后一次出现的位置
集合
Collection接口
Collection col = new Arraylist();
col.add(s1);
col.add(s2);
col.add(s3);
Iterator it = col.iterator(); //迭代器
while(it.hasNext()){
System.out.println(it.next());
}
泛型
实际上就是常见的
Map集合
public class test {
public static void main(String[] args) throws IOException {
Map<String,String> map = new HashMap<String, String>();
map.put("cn","中国");
map.put("usa","美国");
map.put("uk","英国");
//遍历map
Set<String> keyset = map.keySet();
for (String s : map.keySet()) {
System.out.println(s+"--------"+map.get(s));
}
System.out.println("----------entryset---------");
Set<Map.Entry<String,String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
System.out.println(entry.getKey()+"------"+entry.getValue());
}
}
}
WOW,entryset
,跟CC1的有一步骤有关
注解与反射
Class.forName(className, true, currentLoader)
第⼆个参数表示是否初始化,在 forName 的时候,构造函数并不会执⾏,而是执⾏类初始化。他会执行static{}
静态块里面的内容
newInstance() 对类进行实例化
但需要注意的是,invoke 方法第一个参数并不是固定的:
如果调用这个方法是普通方法,第一个参数就是类对象;
如果调用这个方法是静态方法,第一个参数就是类;
如果调用的方法是静态方法。那么invoke方法传入的第一个参数永远为
null
动态代理
newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
loader
– 类加载器来定义代理类interfaces
– 代理类实现的接口列表h
– 调度方法调用的调用处理函数
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
Proxy.newProxyInstance(ordinaryStudents.getClass().getClassLoader(), ordinaryStudents.getClass().getInterfaces(), handler)
通常是这两句,参数调整一下
InvocationHandler instance = (InvocationHandler) constructor.newInstance(Target.class,lazymap);
Map proxyInstance = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},instance);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler instance = (InvocationHandler) constructor.newInstance(Target.class,lazymap);
Map proxyInstance = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},instance);
Object o = constructor.newInstance(Override.class,proxyInstance);
类加载
BootstrapClassLoader ExtensionsClassLoader AppClassLoader
URLClassLoader
①:URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader
来寻找类,即为在Jar包中寻找.class文件
②:URL以斜杠 / 结尾,且协议名是 file
,则使用 FileLoader
来寻找类,即为在本地文件系统中寻找.class文件
③:URL以斜杠 / 结尾,且协议名不是 file
,则使用最基础的 Loader
来寻找类。
package com.ctf.bcel;
import java.io.IOException;
public class calc {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
file:// 路径为.class
文件所在路径
public class BCELDemo {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file://C:\\Users\\86136\\Desktop\\rmi\\out\\production\\rmi\\com\\ctf\\bcel")});
urlClassLoader.loadClass("com.ctf.bcel.calc").newInstance();
} //包名.类名
}
http:// python起一个http服务
public class BCELDemo {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:8000")});
urlClassLoader.loadClass("com.ctf.bcel.calc").newInstance();
} //包名.类名
}
将我们之前的 class 文件打包一下,打包为 jar 文件。
jar -cvf calc.jar clac.class
file + jar协议
public class BCELDemo {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///E:\\Calc.jar!/")});
urlClassLoader.loadClass("com.ctf.bcel.calc").newInstance();
} //包名.类名
}
http + jar协议
public class BCELDemo {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://127.0.0.1:9999/Calc.jar!/")});
urlClassLoader.loadClass("com.ctf.bcel.calc").newInstance();
} //包名.类名
}
加载字节码
ClassLoader#defineClass
defineClass(String name,byte[] b,int off,int len)
//name为类名,b为字节码数组,off为偏移量,len为字节码数组的长度。
public class BCELDemo {
public static void main(String[] args) throws Exception {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\rmi\\out\\production\\rmi\\com\\ctf\\bcel\\calc.class"));
Class c = (Class) method.invoke(systemClassLoader,"com.ctf.bcel.calc",code,0,code.length);
c.newInstance();
}
}
Unsafe 的defineClass
public class BCELDemo {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<Unsafe> unsafeClass = Unsafe.class;
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe classUnsafe = (Unsafe) unsafeField.get(null);
Method defineClassMethod = unsafeClass.getMethod("defineClass", String.class, byte[].class,
int.class, int.class, ClassLoader.class, ProtectionDomain.class);
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\rmi\\out\\production\\rmi\\com\\ctf\\bcel\\calc.class"));
Class calc = (Class) defineClassMethod.invoke(classUnsafe, "com.ctf.bcel.calc", code, 0, code.length, classLoader, null);
calc.newInstance();
}
}
TemplatesImpl 加载字节码 !!!!
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()
TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer()
可以利用这两个去构造poc
首先先构造字节码,注意,这里的字节码必须继承AbstractTranslet
,因为继承了这一抽象类,所以必须要重写一下里面的方法
package com.ctf.bcel;
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 calc extends AbstractTranslet {
public calc() throws IOException {
super();
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
public class TemplatesRce {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("E:\\JavaClass\\TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
BCEL 加载字节码
通过 BCEL 提供的两个类 Repository
和 Utility
来利用: Repository
用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码; Utility
用于将原生的字节码转换成BCEL格式的字节码:
JavaClass cls = Repository.lookupClass(Evil.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println("$$BCEL$$"+code);
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
import java.io.IOException;
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}
读p神文章
编译.xsl生成.class
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,它在defineClass
中需要的字节码所对应的基类,就是这里的com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
XSLT在使用时会先编译成Java字节码,这也就是为什么TemplatesImpl
会使用defineClass
的原因
springboot
创建程序
首先安装好maven,我修改了maven的repository的位置,所以要在settings.xml中修改,修改完后关键是mvn help system
下载repository
里的文件(自行上网查吧)
高版本springboot需要java的高版本,我这java8用的是2.2.0(生成后手动改的)
只选择spring web就够了
算了,不太会讲,就记录知识吧
@Configuration //标明这是配置类
@Bean //给容器中添加组件
@Component //注册
@Controller、@Service、@Repository
@ComponentScan、@Import
@ConditionalOnxxxxx满足Conditional指定的条件,则进行组件注入
@ImportResource("classpath:beans.xml")
@ConfigurationProperties(prefix="") //能够在配置文件中赋值
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定
生效的配置类就会给容器中装配很多组件
设置静态资源访问目录
spring:
mvc:
static-path-pattern: /res/**
启动程序主要是这三个东西@SpringBootApplication
、SpringApplication
和run
充当配置文件的application.properties
也可以是application.yml
在启动程序Application的同级目录下创建controller文件夹,在这写有关路由的东西
@Controller (返回页面) @RestController(返回内容)
在application.properties
中能配置端口server.port=8081
自动装配原理
yaml
大小写敏感
使用缩进表示层级关系
缩进不允许使用tab,只允许空格
缩进的空格数不重要,只要同级元素左对齐即可
#表示注释
Person:
name: zero${random.uuid}}
age: ${random.int}
birth: 2018/11/02
dog:
name: wow
age: 12
server:
port: 8082
dog:
name: pop
age: 321
#Human: ["myname","myage","nothing"]
#Hulman: ["myname","youage","everything",[1,2,3]]
Hulman:
- myname
- youage
- everything
-
- 1
- 2
- 3
#companies: [{id: 1,name: asd},{id: 2,name: sdf}]
companies:
-
id: 1
name: asd
-
id: 2
name: sdf
boolean:
- true
- false
float:
- 3.14
- 1.14
int:
- 1
- 2
赋值
使用Spring的@Value
注解进行赋值,并且配合@Component
注解注册bean
yaml配置文件赋值
@ConfigurationProperties(prefix = "dog")
dog:
name: pop
age: 321
#(注意空格)
properties配置文件
@PropertySource("classpath:user.properties")
松散绑定
yml中写last-name
与lastName
是一样的,-
后面跟着的字母默认是大写的
JSR303校验
@Validated
@Email()
private String name;
一般使用Pattern() 正则匹配
多个配置文件位置
默认选择顺序
主动选择激活配置 spring.profiles.active=
application-名字.properties
选择application-test.properties的配置
#application.yml
spring:
profiles:
active: dev //选择执行名字为dev的配置
server:
port: 8081
#(---不是为了好看画的、起到分隔的作用)
---
server:
port: 8082
spring:
profiles: dev //名字
---
server:
port: 8083
spring:
profiles: test
自动装配
//标注这是一个配置类
@Configuration()
@EnableConfigurationProperties()
//根据不同的条件来判断当前配置是否生效
@ConditionalOnWebApplication()
@ConditionalOnClass()
@ConditionalOnProperty()
xxxxAutoConfiguration:自动配置类;给容器中添加组件
xxxxProperties:封装配置文件中相关属性
而相关属性我们可以使用配置文件进行修改
web开发
webjars
获取静态资源resources下放public、static、resources
优先级 resources > static > public
首页:在三个目录下寻找index.html
图标:2.2版本没有,2.1版本放在三个任意一个目录下favicon.ico
@Controller .html文件放在template目录下
thymeleaf 模板
*{…}也可和${…}混用,星号语法对选定对象而不是整个上下文评估表达式,也就是说,只要没有选定的对象,美元(${…})和星号(*{...})的语法就完全一样。
<div th:object="${user}">
<p>Name: <span th:text="*{name}">赛</span>.</p>
<p>Age: <span th:text="*{age}">18</span>.</p>
<p>Detail: <span th:text="*{detail}">好好学习</span>.</p>
</div>
//等价于
<div >
<p>Name: <span th:text="*{user.name}">赛</span>.</p>
<p>Age: <span th:text="${user.age}">18</span>.</p>
<p>Detail: <span th:text="${user.detail}">好好学习</span>.</p>
</div>
th:utext=”${msg}” <h1>123</h1>
能够被成功解析
${对象.get方法名}
th:each=”user:${users}” th:text=”${user}”
MVC配置
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Bean
public ViewResolver myview(){
return new MyViewResolver();
}
public static class MyViewResolver implements ViewResolver{
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
}
}
员工管理系统
所有页面的静态资源都需要使用thymeleaf接管()
标上xmlns:th=”http://www.thymeleaf.org“ ,以及修改href和src….等本地链接
th:href th:src th:link @{/}
国际化 #{}
即页面的中英文转化
先创建login.properties
,再创建login_zh_CN.properties
然后设置信息
设置完后spring.messages.basename=i18n.login
然后在html中对应的显示位置 添加 th:xx=”#{}”
th:href="@{/index.html(l=zh_CN)}"
th:href="@{/index.html(l=en_US)}"
点击按钮转换中英文,设置组件LocaleResolver
public class myLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String language = request.getParameter("l");
Locale locale = Locale.getDefault();
if (!StringUtils.isEmpty(language)){
//zh_CN
String[] s = language.split("_");
//语言,国家
locale = new Locale(s[0],s[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
然后在自定义的myMvcConfig中放入@Bean中
@Bean
public LocaleResolver localeResolver(){
return new myLocaleResolver();
}
登录
@Controller
public class loginController {
@RequestMapping("/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model){
if (!StringUtils.isEmpty(username) && "admin".equals(password)){
return "index";
//先在mvc中添加映射,然后跳转
//return "redirect:/main.html";
}
else {
//提示信息
model.addAttribute("msg","用户名或密码错误");
return "login";
}
}
}
<p th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
拦截器
public class loginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取session
Object loginuser = request.getSession().getAttribute("loginuser");
if (loginuser==null){
request.setAttribute("msg","请先登录");
request.getRequestDispatcher("/login.html").forward(request,response);
//跳转
return false;
}else {
return true;
}
}
}
mvc中添加
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new loginInterceptor()).addPathPatterns("/**").excludePathPatterns("/","/login.html","/about.html","/contact.html","/js/**","/css/*","/img/*");
}
@RequestMapping("/login.html")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model, HttpSession session){
if (!StringUtils.isEmpty(username) && "admin".equals(password)){
session.setAttribute("loginuser",username);
return "redirect:index.html";
员工列表
提取公共页面
把相同的代码添加一个属性 th:fragment=”sidebar”
调用 th:replace=”~{commons/commons::topbar}” //定义属性的位置
高亮
()传递参数 ~{commons/commons::topbar(active=”index.html”)}
th:xx="${active=='index.html'?'nav-link active':'nav-link'}"
三元运算符判断是否高亮
展示员工界面
<tr th:each="user:${users}">
<td th:text="${user.getId()}"></td>
<td th:text="${user.getName()}"></td>
<td th:text="${user.getAge()}"></td>
<td th:text="${user.getId()}"></td>
<td th:text="${user.getId()}"></td>
</tr>